1010 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1010 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
This file is part of Telegram Desktop,
 | 
						|
the official desktop application for the Telegram messaging service.
 | 
						|
 | 
						|
For license and copyright information please follow this link:
 | 
						|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | 
						|
*/
 | 
						|
#include "statistics/view/stack_linear_chart_view.h"
 | 
						|
 | 
						|
#include "data/data_statistics_chart.h"
 | 
						|
#include "statistics/chart_lines_filter_controller.h"
 | 
						|
#include "statistics/view/stack_chart_common.h"
 | 
						|
#include "statistics/widgets/point_details_widget.h"
 | 
						|
#include "ui/effects/animation_value_f.h"
 | 
						|
#include "ui/painter.h"
 | 
						|
#include "ui/rect.h"
 | 
						|
#include "styles/style_statistics.h"
 | 
						|
 | 
						|
#include <QtCore/QtMath>
 | 
						|
 | 
						|
namespace Statistic {
 | 
						|
namespace {
 | 
						|
 | 
						|
constexpr auto kCircleSizeRatio = 0.42;
 | 
						|
constexpr auto kMinTextScaleRatio = 0.3;
 | 
						|
constexpr auto kPieAngleOffset = 90;
 | 
						|
 | 
						|
constexpr auto kRightTop = short(0);
 | 
						|
constexpr auto kRightBottom = short(1);
 | 
						|
constexpr auto kLeftBottom = short(2);
 | 
						|
constexpr auto kLeftTop = short(3);
 | 
						|
 | 
						|
[[nodiscard]] short QuarterForPoint(const QRect &r, const QPointF &p) {
 | 
						|
	if (p.x() >= r.center().x() && p.y() <= r.center().y()) {
 | 
						|
		return kRightTop;
 | 
						|
	} else if (p.x() >= r.center().x() && p.y() >= r.center().y()) {
 | 
						|
		return kRightBottom;
 | 
						|
	} else if (p.x() < r.center().x() && p.y() >= r.center().y()) {
 | 
						|
		return kLeftBottom;
 | 
						|
	} else {
 | 
						|
		return kLeftTop;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
inline float64 InterpolationRatio(float64 from, float64 to, float64 result) {
 | 
						|
	return (result - from) / (to - from);
 | 
						|
};
 | 
						|
 | 
						|
[[nodiscard]] Limits FindAdditionalZoomedOutXIndices(const PaintContext &c) {
 | 
						|
	constexpr auto kOffset = int(1);
 | 
						|
	auto &xPercentage = c.chartData.xPercentage;
 | 
						|
	auto leftResult = 0.;
 | 
						|
	{
 | 
						|
		auto i = std::max(int(c.xIndices.min) - kOffset, 0);
 | 
						|
		if (xPercentage[i] > c.xPercentageLimits.min) {
 | 
						|
			while (true) {
 | 
						|
				i--;
 | 
						|
				if (i < 0) {
 | 
						|
					leftResult = 0;
 | 
						|
					break;
 | 
						|
				} else if (!(xPercentage[i] > c.xPercentageLimits.min)) {
 | 
						|
					leftResult = i;
 | 
						|
					break;
 | 
						|
				}
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			leftResult = i;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	{
 | 
						|
		const auto lastIndex = float64(xPercentage.size() - 1);
 | 
						|
		auto i = std::min(lastIndex, float64(c.xIndices.max) + kOffset);
 | 
						|
		if (xPercentage[i] < c.xPercentageLimits.max) {
 | 
						|
			while (true) {
 | 
						|
				i++;
 | 
						|
				if (i > lastIndex) {
 | 
						|
					return { leftResult, lastIndex };
 | 
						|
				} else if (!(xPercentage[i] < c.xPercentageLimits.max)) {
 | 
						|
					return { leftResult, i };
 | 
						|
				}
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			return { leftResult, i };
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
void StackLinearChartView::ChangingPiePartController::setParts(
 | 
						|
		const std::vector<PiePartData::Part> &was,
 | 
						|
		const std::vector<PiePartData::Part> &now) {
 | 
						|
	if (_animValues.size() != was.size()) {
 | 
						|
		_animValues = std::vector<anim::value>(was.size(), anim::value());
 | 
						|
		for (auto i = 0; i < was.size(); i++) {
 | 
						|
			_animValues[i] = anim::value(
 | 
						|
				was[i].roundedPercentage,
 | 
						|
				now[i].roundedPercentage);
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		for (auto i = 0; i < was.size(); i++) {
 | 
						|
			_animValues[i] = anim::value(
 | 
						|
				_animValues[i].current(),
 | 
						|
				now[i].roundedPercentage);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	_startedAt = crl::now();
 | 
						|
	_isFinished = false;
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::ChangingPiePartController::update() {
 | 
						|
	const auto progress = std::clamp(
 | 
						|
		(crl::now() - _startedAt) / float64(st::slideWrapDuration),
 | 
						|
		0.,
 | 
						|
		1.);
 | 
						|
	auto totalSum = 0.;
 | 
						|
	auto finished = true;
 | 
						|
	auto result = std::vector<float64>();
 | 
						|
	result.reserve(_animValues.size());
 | 
						|
	for (auto &anim : _animValues) {
 | 
						|
		anim.update(progress, anim::easeOutCubic);
 | 
						|
		if (finished && (anim.current() != anim.to())) {
 | 
						|
			finished = false;
 | 
						|
		}
 | 
						|
		const auto value = anim.current();
 | 
						|
		result.push_back(value);
 | 
						|
		totalSum += value;
 | 
						|
	}
 | 
						|
	_isFinished = finished;
 | 
						|
	_current = PiePartsPercentage(result, totalSum, false);
 | 
						|
}
 | 
						|
 | 
						|
PiePartData StackLinearChartView::ChangingPiePartController::current() const {
 | 
						|
	return _current;
 | 
						|
}
 | 
						|
 | 
						|
bool StackLinearChartView::ChangingPiePartController::isFinished() const {
 | 
						|
	return _isFinished;
 | 
						|
}
 | 
						|
 | 
						|
StackLinearChartView::StackLinearChartView() {
 | 
						|
	_piePartAnimation.init([=] { AbstractChartView::update(); });
 | 
						|
}
 | 
						|
 | 
						|
StackLinearChartView::~StackLinearChartView() = default;
 | 
						|
 | 
						|
void StackLinearChartView::paint(QPainter &p, const PaintContext &c) {
 | 
						|
	if (!_transition.progress && !c.footer) {
 | 
						|
		prepareZoom(c, TransitionStep::ZoomedOut);
 | 
						|
	}
 | 
						|
	if (_transition.pendingPrepareToZoomIn) {
 | 
						|
		_transition.pendingPrepareToZoomIn = false;
 | 
						|
		prepareZoom(c, TransitionStep::PrepareToZoomIn);
 | 
						|
	}
 | 
						|
 | 
						|
	StackLinearChartView::paintChartOrZoomAnimation(p, c);
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::prepareZoom(
 | 
						|
		const PaintContext &c,
 | 
						|
		TransitionStep step) {
 | 
						|
	if (step == TransitionStep::ZoomedOut) {
 | 
						|
		_transition.zoomedOutXIndicesAdditional
 | 
						|
			= FindAdditionalZoomedOutXIndices(c);
 | 
						|
		_transition.zoomedOutXIndices = c.xIndices;
 | 
						|
		_transition.zoomedOutXPercentage = c.xPercentageLimits;
 | 
						|
	} else if (step == TransitionStep::PrepareToZoomIn) {
 | 
						|
		const auto &[zoomedStart, zoomedEnd]
 | 
						|
			= _transition.zoomedOutXIndices;
 | 
						|
		_transition.lines = std::vector<Transition::TransitionLine>(
 | 
						|
			c.chartData.lines.size(),
 | 
						|
			Transition::TransitionLine());
 | 
						|
 | 
						|
		const auto xPercentageLimits = _transition.zoomedOutXPercentage;
 | 
						|
		const auto &linesFilter = linesFilterController();
 | 
						|
 | 
						|
		for (auto j = 0; j < 2; j++) {
 | 
						|
			const auto i = int((j == 1) ? zoomedEnd : zoomedStart);
 | 
						|
			auto stackOffset = 0;
 | 
						|
			auto sum = 0.;
 | 
						|
			auto drawingLinesCount = 0;
 | 
						|
			for (const auto &line : c.chartData.lines) {
 | 
						|
				if (line.y[i] > 0) {
 | 
						|
					const auto alpha = linesFilter->alpha(line.id);
 | 
						|
					sum += line.y[i] * alpha;
 | 
						|
					if (alpha > 0.) {
 | 
						|
						drawingLinesCount++;
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			for (auto k = 0; k < c.chartData.lines.size(); k++) {
 | 
						|
				auto &linePoint = (j
 | 
						|
					? _transition.lines[k].end
 | 
						|
					: _transition.lines[k].start);
 | 
						|
				const auto &line = c.chartData.lines[k];
 | 
						|
				const auto yPercentage = (drawingLinesCount == 1)
 | 
						|
					? (line.y[i] ? linesFilter->alpha(line.id) : 0)
 | 
						|
					: (sum
 | 
						|
						? (line.y[i] * linesFilter->alpha(line.id) / sum)
 | 
						|
						: 0);
 | 
						|
 | 
						|
				const auto xPoint = c.rect.width()
 | 
						|
					* ((c.chartData.xPercentage[i] - xPercentageLimits.min)
 | 
						|
						/ (xPercentageLimits.max - xPercentageLimits.min));
 | 
						|
				const auto height = yPercentage * c.rect.height();
 | 
						|
				const auto yPoint = rect::bottom(c.rect)
 | 
						|
					- height
 | 
						|
					- stackOffset;
 | 
						|
				linePoint = { xPoint, yPoint };
 | 
						|
				stackOffset += height;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		savePieTextParts(c);
 | 
						|
		applyParts(_transition.textParts);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::applyParts(
 | 
						|
		const std::vector<PiePartData::Part> &parts) {
 | 
						|
	for (auto k = 0; k < parts.size(); k++) {
 | 
						|
		_transition.lines[k].angle = parts[k].stackedAngle;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::saveZoomRange(const PaintContext &c) {
 | 
						|
	_transition.zoomedInRangeXIndices = FindStackXIndicesFromRawXPercentages(
 | 
						|
		c.chartData,
 | 
						|
		c.xPercentageLimits,
 | 
						|
		_transition.zoomedInLimitXIndices);
 | 
						|
	_transition.zoomedInRange = {
 | 
						|
		c.chartData.xPercentage[_transition.zoomedInRangeXIndices.min],
 | 
						|
		c.chartData.xPercentage[_transition.zoomedInRangeXIndices.max],
 | 
						|
	};
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::savePieTextParts(const PaintContext &c) {
 | 
						|
	auto data = PiePartsPercentageByIndices(
 | 
						|
		c.chartData,
 | 
						|
		linesFilterController(),
 | 
						|
		_transition.zoomedInRangeXIndices);
 | 
						|
	_transition.textParts = std::move(data.parts);
 | 
						|
	_pieHasSinglePart = data.pieHasSinglePart;
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::paintChartOrZoomAnimation(
 | 
						|
		QPainter &p,
 | 
						|
		const PaintContext &c) {
 | 
						|
	if (_transition.progress == 1.) {
 | 
						|
		if (c.footer) {
 | 
						|
			paintZoomedFooter(p, c);
 | 
						|
		} else {
 | 
						|
			paintZoomed(p, c);
 | 
						|
		}
 | 
						|
		return p.setOpacity(0.);
 | 
						|
	}
 | 
						|
	const auto &linesFilter = linesFilterController();
 | 
						|
	const auto hasTransitionAnimation = _transition.progress && !c.footer;
 | 
						|
	const auto &[localStart, localEnd] = c.footer
 | 
						|
		? Limits{ 0., float64(c.chartData.xPercentage.size() - 1) }
 | 
						|
		: _transition.zoomedOutXIndicesAdditional;
 | 
						|
	_skipPoints = std::vector<bool>(c.chartData.lines.size(), false);
 | 
						|
	auto paths = std::vector<QPainterPath>(
 | 
						|
		c.chartData.lines.size(),
 | 
						|
		QPainterPath());
 | 
						|
 | 
						|
	const auto center = QPointF(c.rect.center());
 | 
						|
 | 
						|
	const auto rotate = [&](float64 ang, const QPointF &p) {
 | 
						|
		return QTransform()
 | 
						|
			.translate(center.x(), center.y())
 | 
						|
			.rotate(ang)
 | 
						|
			.translate(-center.x(), -center.y())
 | 
						|
			.map(p);
 | 
						|
	};
 | 
						|
 | 
						|
	const auto xPercentageLimits = !c.footer
 | 
						|
		? _transition.zoomedOutXPercentage
 | 
						|
		: Limits{
 | 
						|
			c.chartData.xPercentage[localStart],
 | 
						|
			c.chartData.xPercentage[localEnd],
 | 
						|
		};
 | 
						|
 | 
						|
	auto straightLineProgress = 0.;
 | 
						|
	auto hasEmptyPoint = false;
 | 
						|
 | 
						|
	auto ovalPath = QPainterPath();
 | 
						|
	if (hasTransitionAnimation) {
 | 
						|
		constexpr auto kStraightLinePart = 0.6;
 | 
						|
		straightLineProgress = std::clamp(
 | 
						|
			_transition.progress / kStraightLinePart,
 | 
						|
			0.,
 | 
						|
			1.);
 | 
						|
		auto rectPath = QPainterPath();
 | 
						|
		rectPath.addRect(c.rect);
 | 
						|
		const auto r = anim::interpolateF(
 | 
						|
			1.,
 | 
						|
			kCircleSizeRatio,
 | 
						|
			_transition.progress);
 | 
						|
		const auto per = anim::interpolateF(0., 100., _transition.progress);
 | 
						|
		const auto side = (c.rect.width() / 2.) * r;
 | 
						|
		const auto rectF = QRectF(
 | 
						|
			center - QPointF(side, side),
 | 
						|
			center + QPointF(side, side));
 | 
						|
		ovalPath.addRoundedRect(rectF, per, per, Qt::RelativeSize);
 | 
						|
		ovalPath = ovalPath.intersected(rectPath);
 | 
						|
	}
 | 
						|
 | 
						|
	for (auto i = localStart; i <= localEnd; i++) {
 | 
						|
		auto stackOffset = 0.;
 | 
						|
		auto sum = 0.;
 | 
						|
		auto lastEnabled = int(0);
 | 
						|
 | 
						|
		auto drawingLinesCount = int(0);
 | 
						|
 | 
						|
		const auto xPoint = c.rect.width()
 | 
						|
			* ((c.chartData.xPercentage[i] - xPercentageLimits.min)
 | 
						|
				/ (xPercentageLimits.max - xPercentageLimits.min));
 | 
						|
 | 
						|
		for (auto k = 0; k < c.chartData.lines.size(); k++) {
 | 
						|
			const auto &line = c.chartData.lines[k];
 | 
						|
			const auto alpha = linesFilter->alpha(line.id);
 | 
						|
			if (!alpha) {
 | 
						|
				continue;
 | 
						|
			}
 | 
						|
			if (line.y[i] > 0) {
 | 
						|
				sum += line.y[i] * alpha;
 | 
						|
				drawingLinesCount++;
 | 
						|
			}
 | 
						|
			lastEnabled = k;
 | 
						|
		}
 | 
						|
 | 
						|
		for (auto k = 0; k < c.chartData.lines.size(); k++) {
 | 
						|
			const auto &line = c.chartData.lines[k];
 | 
						|
			const auto isLastLine = (k == lastEnabled);
 | 
						|
			const auto lineAlpha = linesFilter->alpha(line.id);
 | 
						|
			if (isLastLine && (lineAlpha < 1.)) {
 | 
						|
				hasEmptyPoint = true;
 | 
						|
			}
 | 
						|
			if (!lineAlpha) {
 | 
						|
				continue;
 | 
						|
			}
 | 
						|
			const auto &transitionLine = hasTransitionAnimation
 | 
						|
				? _transition.lines[k]
 | 
						|
				: Transition::TransitionLine();
 | 
						|
			const auto &y = line.y;
 | 
						|
 | 
						|
			auto &chartPath = paths[k];
 | 
						|
 | 
						|
			const auto yPercentage = (drawingLinesCount == 1)
 | 
						|
				? float64(y[i] ? lineAlpha : 0.)
 | 
						|
				: float64(sum ? (y[i] * lineAlpha / sum) : 0.);
 | 
						|
 | 
						|
			if (isLastLine && !yPercentage) {
 | 
						|
				hasEmptyPoint = true;
 | 
						|
			}
 | 
						|
			const auto height = yPercentage * c.rect.height();
 | 
						|
			const auto yPoint = rect::bottom(c.rect) - height - stackOffset;
 | 
						|
			// startFromY[k] = yPoint;
 | 
						|
 | 
						|
			auto angle = 0.;
 | 
						|
			auto resultPoint = QPointF(xPoint, yPoint);
 | 
						|
			auto pointZero = QPointF(xPoint, c.rect.y() + c.rect.height());
 | 
						|
			// if (i == localEnd) {
 | 
						|
			// 	endXPoint = xPoint;
 | 
						|
			// } else if (i == localStart) {
 | 
						|
			// 	startXPoint = xPoint;
 | 
						|
			// }
 | 
						|
			if (hasTransitionAnimation && !isLastLine) {
 | 
						|
				const auto point1 = (resultPoint.x() < center.x())
 | 
						|
					? transitionLine.start
 | 
						|
					: transitionLine.end;
 | 
						|
 | 
						|
				const auto diff = center - point1;
 | 
						|
				const auto yTo = point1.y()
 | 
						|
					+ diff.y() * (resultPoint.x() - point1.x()) / diff.x();
 | 
						|
				const auto yToResult = yTo * straightLineProgress;
 | 
						|
				const auto revProgress = (1. - straightLineProgress);
 | 
						|
 | 
						|
				resultPoint.setY(resultPoint.y() * revProgress + yToResult);
 | 
						|
				pointZero.setY(pointZero.y() * revProgress + yToResult);
 | 
						|
 | 
						|
				{
 | 
						|
					const auto angleK = diff.y() / float64(diff.x());
 | 
						|
					angle = (angleK > 0)
 | 
						|
						? (-std::atan(angleK)) * (180. / M_PI)
 | 
						|
						: (std::atan(std::abs(angleK))) * (180. / M_PI);
 | 
						|
					angle -= 90;
 | 
						|
				}
 | 
						|
 | 
						|
				if (resultPoint.x() >= center.x()) {
 | 
						|
					const auto resultAngle = _transition.progress * angle;
 | 
						|
					const auto rotated = rotate(resultAngle, resultPoint);
 | 
						|
					resultPoint = QPointF(
 | 
						|
						std::max(rotated.x(), center.x()),
 | 
						|
						rotated.y());
 | 
						|
 | 
						|
					pointZero = QPointF(
 | 
						|
						std::max(pointZero.x(), center.x()),
 | 
						|
						rotate(resultAngle, pointZero).y());
 | 
						|
				} else {
 | 
						|
					const auto &xLimits = xPercentageLimits;
 | 
						|
					const auto isNextXPointAfterCenter = false
 | 
						|
						|| center.x() < (c.rect.width() * ((i == localEnd)
 | 
						|
							? 1.
 | 
						|
							: ((c.chartData.xPercentage[i + 1] - xLimits.min)
 | 
						|
								/ (xLimits.max - xLimits.min))));
 | 
						|
					if (isNextXPointAfterCenter) {
 | 
						|
						pointZero = resultPoint = QPointF()
 | 
						|
							+ center * straightLineProgress
 | 
						|
							+ resultPoint * revProgress;
 | 
						|
					} else {
 | 
						|
						const auto resultAngle = _transition.progress * angle
 | 
						|
							+ _transition.progress * transitionLine.angle;
 | 
						|
						resultPoint = rotate(resultAngle, resultPoint);
 | 
						|
						pointZero = rotate(resultAngle, pointZero);
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			if (i == localStart) {
 | 
						|
				const auto bottomLeft = QPointF(
 | 
						|
					c.rect.x(),
 | 
						|
					rect::bottom(c.rect));
 | 
						|
				const auto local = (hasTransitionAnimation && !isLastLine)
 | 
						|
					? rotate(
 | 
						|
						_transition.progress * angle
 | 
						|
							+ _transition.progress * transitionLine.angle,
 | 
						|
						bottomLeft - QPointF(center.x(), 0))
 | 
						|
					: bottomLeft;
 | 
						|
				chartPath.setFillRule(Qt::WindingFill);
 | 
						|
				chartPath.moveTo(local);
 | 
						|
				_skipPoints[k] = false;
 | 
						|
			}
 | 
						|
 | 
						|
			const auto yRatio = 1. - (isLastLine ? _transition.progress : 0.);
 | 
						|
			if ((!yPercentage)
 | 
						|
				&& (i > 0 && (y[i - 1] == 0))
 | 
						|
				&& (i < localEnd && (y[i + 1] == 0))
 | 
						|
				&& (!hasTransitionAnimation)) {
 | 
						|
				if (!_skipPoints[k]) {
 | 
						|
					chartPath.lineTo(pointZero.x(), pointZero.y() * yRatio);
 | 
						|
				}
 | 
						|
				_skipPoints[k] = true;
 | 
						|
			} else {
 | 
						|
				if (_skipPoints[k]) {
 | 
						|
					chartPath.lineTo(pointZero.x(), pointZero.y() * yRatio);
 | 
						|
				}
 | 
						|
				chartPath.lineTo(resultPoint.x(), resultPoint.y() * yRatio);
 | 
						|
				_skipPoints[k] = false;
 | 
						|
			}
 | 
						|
 | 
						|
			if (i == localEnd) {
 | 
						|
				if (hasTransitionAnimation && !isLastLine) {
 | 
						|
					{
 | 
						|
						const auto diff = center - transitionLine.start;
 | 
						|
						const auto angleK = diff.y() / diff.x();
 | 
						|
						angle = (angleK > 0)
 | 
						|
							? ((-std::atan(angleK)) * (180. / M_PI))
 | 
						|
							: ((std::atan(std::abs(angleK))) * (180. / M_PI));
 | 
						|
						angle -= 90;
 | 
						|
					}
 | 
						|
 | 
						|
					const auto local = rotate(
 | 
						|
						_transition.progress * angle
 | 
						|
							+ _transition.progress * transitionLine.angle,
 | 
						|
						transitionLine.start);
 | 
						|
 | 
						|
					const auto ending = true
 | 
						|
						&& (std::abs(resultPoint.x() - local.x()) < 0.001)
 | 
						|
						&& ((local.y() < center.y()
 | 
						|
								&& resultPoint.y() < center.y())
 | 
						|
							|| (local.y() > center.y()
 | 
						|
								&& resultPoint.y() > center.y()));
 | 
						|
					const auto endQuarter = (!ending)
 | 
						|
						? QuarterForPoint(c.rect, resultPoint)
 | 
						|
						: kRightTop;
 | 
						|
					const auto startQuarter = (!ending)
 | 
						|
						? QuarterForPoint(c.rect, local)
 | 
						|
						: (transitionLine.angle == -180.)
 | 
						|
						? kRightTop
 | 
						|
						: kLeftTop;
 | 
						|
 | 
						|
					for (auto q = endQuarter; q <= startQuarter; q++) {
 | 
						|
						chartPath.lineTo(
 | 
						|
							(q == kLeftTop || q == kLeftBottom)
 | 
						|
								? c.rect.x()
 | 
						|
								: rect::right(c.rect),
 | 
						|
							(q == kLeftTop || q == kRightTop)
 | 
						|
								? c.rect.y()
 | 
						|
								: rect::right(c.rect));
 | 
						|
					}
 | 
						|
				} else {
 | 
						|
					chartPath.lineTo(
 | 
						|
						rect::right(c.rect),
 | 
						|
						rect::bottom(c.rect));
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			stackOffset += height;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	auto hq = PainterHighQualityEnabler(p);
 | 
						|
 | 
						|
	p.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg);
 | 
						|
	if (!ovalPath.isEmpty()) {
 | 
						|
		p.setClipPath(ovalPath);
 | 
						|
	}
 | 
						|
 | 
						|
	if (hasEmptyPoint) {
 | 
						|
		p.fillRect(c.rect, st::boxDividerBg);
 | 
						|
	}
 | 
						|
 | 
						|
	const auto opacity = c.footer ? (1. - _transition.progress) : 1.;
 | 
						|
	for (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) {
 | 
						|
		if (paths[k].isEmpty()) {
 | 
						|
			continue;
 | 
						|
		}
 | 
						|
		const auto &line = c.chartData.lines[k];
 | 
						|
		p.setPen(Qt::NoPen);
 | 
						|
		p.fillPath(paths[k], line.color);
 | 
						|
	}
 | 
						|
	p.setOpacity(opacity);
 | 
						|
	if (!c.footer) {
 | 
						|
		constexpr auto kAlphaTextPart = 0.6;
 | 
						|
		const auto progress = std::clamp(
 | 
						|
			(_transition.progress - kAlphaTextPart) / (1. - kAlphaTextPart),
 | 
						|
			0.,
 | 
						|
			1.);
 | 
						|
		if (progress > 0) {
 | 
						|
			auto o = ScopedPainterOpacity(p, progress);
 | 
						|
			paintPieText(p, c);
 | 
						|
		}
 | 
						|
	} else if (_transition.progress) {
 | 
						|
		paintZoomedFooter(p, c);
 | 
						|
	}
 | 
						|
 | 
						|
	// Fix ugly outline.
 | 
						|
	if (!c.footer || !_transition.progress) {
 | 
						|
		p.setBrush(Qt::transparent);
 | 
						|
		p.setPen(st::boxBg);
 | 
						|
		p.drawPath(ovalPath);
 | 
						|
	}
 | 
						|
 | 
						|
	if (!ovalPath.isEmpty()) {
 | 
						|
		p.setClipRect(c.rect, Qt::NoClip);
 | 
						|
	}
 | 
						|
	p.setOpacity(1. - _transition.progress);
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::paintZoomed(QPainter &p, const PaintContext &c) {
 | 
						|
	if (c.footer) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto wasZoomedInRangeXIndices = _transition.zoomedInRangeXIndices;
 | 
						|
	saveZoomRange(c);
 | 
						|
	const auto &[zoomedStart, zoomedEnd] = _transition.zoomedInRangeXIndices;
 | 
						|
	const auto partsData = PiePartsPercentageByIndices(
 | 
						|
		c.chartData,
 | 
						|
		linesFilterController(),
 | 
						|
		_transition.zoomedInRangeXIndices);
 | 
						|
	const auto xIndicesChanged = (wasZoomedInRangeXIndices.min != zoomedStart)
 | 
						|
		|| (wasZoomedInRangeXIndices.max != zoomedEnd);
 | 
						|
	if (xIndicesChanged) {
 | 
						|
		const auto wasParts = PiePartsPercentageByIndices(
 | 
						|
			c.chartData,
 | 
						|
			linesFilterController(),
 | 
						|
			wasZoomedInRangeXIndices);
 | 
						|
		_changingPieController.setParts(wasParts.parts, partsData.parts);
 | 
						|
		if (!_piePartAnimation.animating()) {
 | 
						|
			_piePartAnimation.start();
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (!_changingPieController.isFinished()) {
 | 
						|
		_changingPieController.update();
 | 
						|
	}
 | 
						|
	_pieHasSinglePart = partsData.pieHasSinglePart;
 | 
						|
	applyParts(partsData.parts);
 | 
						|
	const auto &parts = _changingPieController.isFinished()
 | 
						|
		? partsData.parts
 | 
						|
		: _changingPieController.current().parts;
 | 
						|
 | 
						|
	p.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg);
 | 
						|
	const auto center = QPointF(c.rect.center());
 | 
						|
	const auto side = (c.rect.width() / 2.) * kCircleSizeRatio;
 | 
						|
	const auto rectF = QRectF(
 | 
						|
		center - QPointF(side, side),
 | 
						|
		center + QPointF(side, side));
 | 
						|
 | 
						|
	auto hq = PainterHighQualityEnabler(p);
 | 
						|
	auto selectedLineIndex = -1;
 | 
						|
	const auto skipTranslation = skipSelectedTranslation();
 | 
						|
	for (auto k = 0; k < c.chartData.lines.size(); k++) {
 | 
						|
		const auto previous = k
 | 
						|
			? parts[k - 1].stackedAngle
 | 
						|
			: -180;
 | 
						|
		const auto now = parts[k].stackedAngle;
 | 
						|
 | 
						|
		const auto &line = c.chartData.lines[k];
 | 
						|
		p.setBrush(line.color);
 | 
						|
		p.setPen(Qt::NoPen);
 | 
						|
		const auto textAngle = (previous + kPieAngleOffset)
 | 
						|
			+ (now - previous) / 2.;
 | 
						|
		const auto partOffset = skipTranslation
 | 
						|
			? QPointF()
 | 
						|
			: _piePartController.offset(line.id, textAngle);
 | 
						|
		p.translate(partOffset);
 | 
						|
		p.drawPie(
 | 
						|
			rectF,
 | 
						|
			-(previous + kPieAngleOffset) * 16,
 | 
						|
			-(now - previous) * 16);
 | 
						|
		p.translate(-partOffset);
 | 
						|
		if (_piePartController.selected() == line.id) {
 | 
						|
			selectedLineIndex = k;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (_piePartController.isFinished() && _changingPieController.isFinished()) {
 | 
						|
		_piePartAnimation.stop();
 | 
						|
	}
 | 
						|
	paintPieText(p, c);
 | 
						|
 | 
						|
	if (selectedLineIndex >= 0) {
 | 
						|
		const auto &line = c.chartData.lines[selectedLineIndex];
 | 
						|
		auto sum = ChartValue(0);
 | 
						|
		for (auto i = zoomedStart; i <= zoomedEnd; i++) {
 | 
						|
			sum += line.y[i];
 | 
						|
		}
 | 
						|
		sum *= linesFilterController()->alpha(line.id);
 | 
						|
		if (sum > 0) {
 | 
						|
			PaintDetails(p, line, sum, c.rect);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::paintZoomedFooter(
 | 
						|
		QPainter &p,
 | 
						|
		const PaintContext &c) {
 | 
						|
	if (!c.footer) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	auto o = ScopedPainterOpacity(p, _transition.progress);
 | 
						|
	auto hq = PainterHighQualityEnabler(p);
 | 
						|
	const auto &[zoomedStart, zoomedEnd] = _transition.zoomedInLimitXIndices;
 | 
						|
	const auto sideW = st::statisticsChartFooterSideWidth;
 | 
						|
	const auto width = c.rect.width() - sideW * 2.;
 | 
						|
	const auto leftStart = c.rect.x() + sideW;
 | 
						|
	const auto &xPercentage = c.chartData.xPercentage;
 | 
						|
	auto previousX = leftStart;
 | 
						|
	// Read FindStackXIndicesFromRawXPercentages.
 | 
						|
	const auto offset = (xPercentage[zoomedEnd] == 1.) ? 0 : 1;
 | 
						|
	for (auto i = zoomedStart; i <= zoomedEnd; i++) {
 | 
						|
		auto sum = 0.;
 | 
						|
		auto lastEnabledId = int(0);
 | 
						|
		for (const auto &line : c.chartData.lines) {
 | 
						|
			const auto alpha = linesFilterController()->alpha(line.id);
 | 
						|
			sum += line.y[i] * alpha;
 | 
						|
			if (alpha > 0.) {
 | 
						|
				lastEnabledId = line.id;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		const auto columnMargins = QMarginsF(
 | 
						|
			(i == zoomedStart) ? sideW : 0,
 | 
						|
			0,
 | 
						|
			(i == zoomedEnd - offset) ? sideW : 0,
 | 
						|
			0);
 | 
						|
 | 
						|
		const auto next = std::clamp(i + offset, zoomedStart, zoomedEnd);
 | 
						|
		const auto xPointPercentage
 | 
						|
			= (xPercentage[next] - xPercentage[zoomedStart])
 | 
						|
				/ (xPercentage[zoomedEnd] - xPercentage[zoomedStart]);
 | 
						|
		const auto xPoint = leftStart + width * xPointPercentage;
 | 
						|
 | 
						|
		auto stack = 0.;
 | 
						|
		for (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) {
 | 
						|
			const auto &line = c.chartData.lines[k];
 | 
						|
			const auto visibleHeight = c.rect.height()
 | 
						|
				* (line.y[i] * linesFilterController()->alpha(line.id) / sum);
 | 
						|
			if (!visibleHeight) {
 | 
						|
				continue;
 | 
						|
			}
 | 
						|
			const auto height = (line.id == lastEnabledId)
 | 
						|
				? c.rect.height()
 | 
						|
				: visibleHeight;
 | 
						|
 | 
						|
			const auto column = columnMargins + QRectF(
 | 
						|
				previousX,
 | 
						|
				stack,
 | 
						|
				xPoint - previousX,
 | 
						|
				height);
 | 
						|
 | 
						|
			p.setPen(Qt::NoPen);
 | 
						|
			p.fillRect(column, line.color);
 | 
						|
			stack += visibleHeight;
 | 
						|
		}
 | 
						|
		previousX = xPoint;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::paintPieText(QPainter &p, const PaintContext &c) {
 | 
						|
	constexpr auto kMinPercentage = 0.039;
 | 
						|
	if (_transition.progress == 1.) {
 | 
						|
		savePieTextParts(c);
 | 
						|
	}
 | 
						|
	const auto &parts = _changingPieController.isFinished()
 | 
						|
		? _transition.textParts
 | 
						|
		: _changingPieController.current().parts;
 | 
						|
 | 
						|
	const auto center = QPointF(c.rect.center());
 | 
						|
	const auto side = (c.rect.width() / 2.) * kCircleSizeRatio;
 | 
						|
	const auto rectF = QRectF(
 | 
						|
		center - QPointF(side, side),
 | 
						|
		center + QPointF(side, side));
 | 
						|
	const auto &font = st::statisticsPieChartFont;
 | 
						|
	const auto maxScale = side / (font->height * 2);
 | 
						|
	const auto minScale = maxScale * kMinTextScaleRatio;
 | 
						|
	p.setBrush(Qt::NoBrush);
 | 
						|
	p.setPen(st::premiumButtonFg);
 | 
						|
	p.setFont(font);
 | 
						|
	const auto opacity = p.opacity();
 | 
						|
	const auto skipTranslation = skipSelectedTranslation();
 | 
						|
	for (auto k = 0; k < c.chartData.lines.size(); k++) {
 | 
						|
		const auto previous = k
 | 
						|
			? parts[k - 1].stackedAngle
 | 
						|
			: -180;
 | 
						|
		const auto now = parts[k].stackedAngle;
 | 
						|
		const auto percentage = parts[k].roundedPercentage;
 | 
						|
		if (percentage <= kMinPercentage) {
 | 
						|
			continue;
 | 
						|
		}
 | 
						|
 | 
						|
		const auto rText = side * std::sqrt(1. - percentage);
 | 
						|
		const auto textAngle = (now == previous)
 | 
						|
			? 0.
 | 
						|
			: ((previous + kPieAngleOffset) + (now - previous) / 2.);
 | 
						|
		const auto textRadians = textAngle * M_PI / 180.;
 | 
						|
		const auto scale = (maxScale == minScale)
 | 
						|
			? 0.
 | 
						|
			: (minScale) + percentage * (maxScale - minScale);
 | 
						|
		const auto text = parts[k].percentageText;
 | 
						|
		const auto textW = font->width(text);
 | 
						|
		const auto textXShift = textW / 2.;
 | 
						|
		const auto textYShift = textW / 2.;
 | 
						|
		const auto textRectCenter = rectF.center() + QPointF(
 | 
						|
			(rText - textXShift * (1. - scale)) * std::cos(textRadians),
 | 
						|
			(rText - textYShift * (1. - scale)) * std::sin(textRadians));
 | 
						|
		const auto textRect = QRectF(
 | 
						|
			textRectCenter - QPointF(textXShift, textYShift),
 | 
						|
			textRectCenter + QPointF(textXShift, textYShift));
 | 
						|
		const auto partOffset = skipTranslation
 | 
						|
			? QPointF()
 | 
						|
			: _piePartController.offset(c.chartData.lines[k].id, textAngle);
 | 
						|
		p.setTransform(
 | 
						|
			QTransform()
 | 
						|
				.translate(
 | 
						|
					textRectCenter.x() + partOffset.x(),
 | 
						|
					textRectCenter.y() + partOffset.y())
 | 
						|
				.scale(scale, scale)
 | 
						|
				.translate(-textRectCenter.x(), -textRectCenter.y()));
 | 
						|
		p.setOpacity(opacity
 | 
						|
			* linesFilterController()->alpha(c.chartData.lines[k].id));
 | 
						|
		p.drawText(textRect, text, style::al_center);
 | 
						|
	}
 | 
						|
	p.resetTransform();
 | 
						|
}
 | 
						|
 | 
						|
bool StackLinearChartView::PiePartController::set(int id) {
 | 
						|
	if (_selected != id) {
 | 
						|
		update(_selected);
 | 
						|
		_selected = id;
 | 
						|
		update(_selected);
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::PiePartController::update(int id) {
 | 
						|
	if (id >= 0) {
 | 
						|
		const auto was = _startedAt[id];
 | 
						|
		const auto p = (crl::now() - was) / float64(st::slideWrapDuration);
 | 
						|
		const auto progress = ((p > 0) && (p < 1)) ? (1. - p) : 0.;
 | 
						|
		_startedAt[id] = crl::now() - (st::slideWrapDuration * progress);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
float64 StackLinearChartView::PiePartController::progress(int id) const {
 | 
						|
	const auto it = _startedAt.find(id);
 | 
						|
	if (it == end(_startedAt)) {
 | 
						|
		return 0.;
 | 
						|
	}
 | 
						|
	const auto at = it->second;
 | 
						|
	const auto show = (_selected == id);
 | 
						|
	const auto progress = std::clamp(
 | 
						|
		(crl::now() - at) / float64(st::slideWrapDuration),
 | 
						|
		0.,
 | 
						|
		1.);
 | 
						|
	return std::clamp(show ? progress : (1. - progress), 0., 1.);
 | 
						|
}
 | 
						|
 | 
						|
QPointF StackLinearChartView::PiePartController::offset(
 | 
						|
		LineId id,
 | 
						|
		float64 angle) const {
 | 
						|
	const auto offset = st::statisticsPieChartPartOffset * progress(id);
 | 
						|
	const auto radians = angle * M_PI / 180.;
 | 
						|
	return { std::cos(radians) * offset, std::sin(radians) * offset };
 | 
						|
}
 | 
						|
 | 
						|
auto StackLinearChartView::PiePartController::selected() const -> LineId {
 | 
						|
	return _selected;
 | 
						|
}
 | 
						|
 | 
						|
bool StackLinearChartView::PiePartController::isFinished() const {
 | 
						|
	for (const auto &[id, _] : _startedAt) {
 | 
						|
		const auto p = progress(id);
 | 
						|
		if (p > 0 && p < 1) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::handleMouseMove(
 | 
						|
		const Data::StatisticalChart &chartData,
 | 
						|
		const QRect &rect,
 | 
						|
		const QPoint &p) {
 | 
						|
	if (_transition.progress < 1) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto center = rect.center();
 | 
						|
	const auto theta = std::atan2(center.y() - p.y(), (center.x() - p.x()));
 | 
						|
	const auto rawAngle = theta * (180. / M_PI) + 90.;
 | 
						|
	const auto angle = (rawAngle > 180.) ? (rawAngle - 360.) : rawAngle;
 | 
						|
	for (auto k = 0; k < chartData.lines.size(); k++) {
 | 
						|
		const auto previous = k
 | 
						|
			? _transition.lines[k - 1].angle
 | 
						|
			: -180;
 | 
						|
		const auto now = _transition.lines[k].angle;
 | 
						|
		if (angle > previous && angle <= now) {
 | 
						|
			const auto id = p.isNull()
 | 
						|
				? -1
 | 
						|
				: chartData.lines[k].id;
 | 
						|
			if (_piePartController.set(id)) {
 | 
						|
				if (!_piePartAnimation.animating()) {
 | 
						|
					_piePartAnimation.start();
 | 
						|
				}
 | 
						|
			}
 | 
						|
			return;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool StackLinearChartView::skipSelectedTranslation() const {
 | 
						|
	return _pieHasSinglePart;
 | 
						|
}
 | 
						|
 | 
						|
void StackLinearChartView::paintSelectedXIndex(
 | 
						|
		QPainter &p,
 | 
						|
		const PaintContext &c,
 | 
						|
		int selectedXIndex,
 | 
						|
		float64 progress) {
 | 
						|
	if ((selectedXIndex < 0) || c.footer) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto xPercentageLimits = _transition.zoomedOutXPercentage;
 | 
						|
	p.setBrush(st::boxBg);
 | 
						|
	const auto i = selectedXIndex;
 | 
						|
	const auto isSameToken = (_selectedPoints.lastXIndex == selectedXIndex)
 | 
						|
		&& (_selectedPoints.lastHeightLimits.min == c.heightLimits.min)
 | 
						|
		&& (_selectedPoints.lastHeightLimits.max == c.heightLimits.max)
 | 
						|
		&& (_selectedPoints.lastXLimits.min == xPercentageLimits.min)
 | 
						|
		&& (_selectedPoints.lastXLimits.max == xPercentageLimits.max);
 | 
						|
	{
 | 
						|
		const auto useCache = isSameToken;
 | 
						|
		if (!useCache) {
 | 
						|
			// Calculate.
 | 
						|
			const auto xPoint = c.rect.width()
 | 
						|
				* ((c.chartData.xPercentage[i] - xPercentageLimits.min)
 | 
						|
					/ (xPercentageLimits.max - xPercentageLimits.min));
 | 
						|
			_selectedPoints.xPoint = xPoint;
 | 
						|
		}
 | 
						|
 | 
						|
		{
 | 
						|
			[[maybe_unused]] const auto o = ScopedPainterOpacity(
 | 
						|
				p,
 | 
						|
				p.opacity() * progress * kRulerLineAlpha);
 | 
						|
			const auto lineRect = QRectF(
 | 
						|
				_selectedPoints.xPoint - (st::lineWidth / 2.),
 | 
						|
				c.rect.y(),
 | 
						|
				st::lineWidth,
 | 
						|
				c.rect.height());
 | 
						|
			p.fillRect(lineRect, st::boxTextFg);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	_selectedPoints.lastXIndex = selectedXIndex;
 | 
						|
	_selectedPoints.lastHeightLimits = c.heightLimits;
 | 
						|
	_selectedPoints.lastXLimits = xPercentageLimits;
 | 
						|
}
 | 
						|
 | 
						|
int StackLinearChartView::findXIndexByPosition(
 | 
						|
		const Data::StatisticalChart &chartData,
 | 
						|
		const Limits &xPercentageLimits,
 | 
						|
		const QRect &rect,
 | 
						|
		float64 x) {
 | 
						|
	if (_transition.progress == 1.) {
 | 
						|
		return -1;
 | 
						|
	} else if (x < rect.x()) {
 | 
						|
		return 0;
 | 
						|
	} else if (x > (rect.x() + rect.width())) {
 | 
						|
		return chartData.xPercentage.size() - 1;
 | 
						|
	}
 | 
						|
	const auto pointerRatio = std::clamp(
 | 
						|
		(x - rect.x()) / rect.width(),
 | 
						|
		0.,
 | 
						|
		1.);
 | 
						|
	const auto &[localStart, localEnd] = _transition.zoomedOutXIndices;
 | 
						|
	const auto rawXPercentage = anim::interpolateF(
 | 
						|
		_transition.zoomedOutXPercentage.min,
 | 
						|
		_transition.zoomedOutXPercentage.max,
 | 
						|
		pointerRatio);
 | 
						|
	const auto it = ranges::lower_bound(
 | 
						|
		chartData.xPercentage,
 | 
						|
		rawXPercentage);
 | 
						|
	const auto left = rawXPercentage - (*(it - 1));
 | 
						|
	const auto right = (*it) - rawXPercentage;
 | 
						|
	const auto nearestXPercentageIt = ((right) > (left)) ? (it - 1) : it;
 | 
						|
	return std::clamp(
 | 
						|
		std::distance(begin(chartData.xPercentage), nearestXPercentageIt),
 | 
						|
		std::ptrdiff_t(localStart),
 | 
						|
		std::ptrdiff_t(localEnd));
 | 
						|
}
 | 
						|
 | 
						|
AbstractChartView::HeightLimits StackLinearChartView::heightLimits(
 | 
						|
		Data::StatisticalChart &chartData,
 | 
						|
		Limits xIndices) {
 | 
						|
	constexpr auto kMaxStackLinear = 100.;
 | 
						|
	return {
 | 
						|
		.full = { 0, kMaxStackLinear },
 | 
						|
		.ranged = { 0., kMaxStackLinear },
 | 
						|
	};
 | 
						|
}
 | 
						|
 | 
						|
auto StackLinearChartView::maybeLocalZoom(
 | 
						|
		const LocalZoomArgs &args) -> LocalZoomResult {
 | 
						|
	// 8 days.
 | 
						|
	constexpr auto kLimitLength = int(8);
 | 
						|
	// 1 day in middle of limits.
 | 
						|
	constexpr auto kRangeLength = int(0);
 | 
						|
	constexpr auto kLeftSide = int(kLimitLength / 2 + kRangeLength);
 | 
						|
	constexpr auto kRightSide = int(kLimitLength / 2) + int(1);
 | 
						|
 | 
						|
	_transition.progress = args.progress;
 | 
						|
	if (args.type == LocalZoomArgs::Type::SkipCalculation) {
 | 
						|
		return { true, _transition.zoomedInLimit, _transition.zoomedInRange };
 | 
						|
	} else if (args.type == LocalZoomArgs::Type::CheckAvailability) {
 | 
						|
		return { .hasZoom = true };
 | 
						|
	} else if (args.type == LocalZoomArgs::Type::Prepare) {
 | 
						|
		_transition.pendingPrepareToZoomIn = true;
 | 
						|
	}
 | 
						|
	const auto xIndex = args.xIndex;
 | 
						|
	const auto &xPercentage = args.chartData.xPercentage;
 | 
						|
	const auto backIndex = (xPercentage.size() - 1);
 | 
						|
	const auto localRangeIndex = (xIndex == backIndex)
 | 
						|
		? (backIndex - kRangeLength)
 | 
						|
		: xIndex;
 | 
						|
	_transition.zoomedInRange = {
 | 
						|
		xPercentage[localRangeIndex],
 | 
						|
		xPercentage[localRangeIndex + kRangeLength],
 | 
						|
	};
 | 
						|
	_transition.zoomedInRangeXIndices = {
 | 
						|
		float64(localRangeIndex),
 | 
						|
		float64(localRangeIndex + kRangeLength),
 | 
						|
	};
 | 
						|
	_transition.zoomedInLimitXIndices = (xIndex < kLeftSide)
 | 
						|
		? Limits{ 0, kLeftSide + kRightSide }
 | 
						|
		: (xIndex > (backIndex - kRightSide - kRangeLength))
 | 
						|
		? Limits{ float64(backIndex - kLimitLength), float64(backIndex) }
 | 
						|
		: Limits{ float64(xIndex - kLeftSide), float64(xIndex + kRightSide) };
 | 
						|
	_transition.zoomedInLimit = {
 | 
						|
		anim::interpolateF(
 | 
						|
			0.,
 | 
						|
			xPercentage[_transition.zoomedInLimitXIndices.min],
 | 
						|
			args.progress),
 | 
						|
		anim::interpolateF(
 | 
						|
			1.,
 | 
						|
			xPercentage[_transition.zoomedInLimitXIndices.max],
 | 
						|
			args.progress),
 | 
						|
	};
 | 
						|
	const auto oneDay = std::abs(xPercentage[localRangeIndex]
 | 
						|
		- xPercentage[localRangeIndex + ((xIndex == backIndex) ? -1 : 1)]);
 | 
						|
	// Read FindStackXIndicesFromRawXPercentages.
 | 
						|
	const auto offset = (_transition.zoomedInLimitXIndices.max == backIndex)
 | 
						|
		? -oneDay
 | 
						|
		: 0.;
 | 
						|
	const auto resultRange = Limits{
 | 
						|
		InterpolationRatio(
 | 
						|
			_transition.zoomedInLimit.min,
 | 
						|
			_transition.zoomedInLimit.max,
 | 
						|
			_transition.zoomedInRange.min + oneDay * 0.25 + offset),
 | 
						|
		InterpolationRatio(
 | 
						|
			_transition.zoomedInLimit.min,
 | 
						|
			_transition.zoomedInLimit.max,
 | 
						|
			_transition.zoomedInRange.max + oneDay * 0.75 + offset),
 | 
						|
	};
 | 
						|
	return { true, _transition.zoomedInLimitXIndices, resultRange };
 | 
						|
}
 | 
						|
 | 
						|
} // namespace Statistic
 |