370 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			370 lines
		
	
	
	
		
			9.6 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 "ui/effects/animation_value_f.h"
 | |
| #include "data/data_statistics.h"
 | |
| #include "ui/painter.h"
 | |
| #include "styles/style_statistics.h"
 | |
| 
 | |
| namespace Statistic {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kAlphaDuration = float64(200);
 | |
| 
 | |
| struct LeftStartAndStep final {
 | |
| 	float64 start = 0.;
 | |
| 	float64 step = 0.;
 | |
| };
 | |
| 
 | |
| [[nodiscard]] LeftStartAndStep ComputeLeftStartAndStep(
 | |
| 		const Data::StatisticalChart &chartData,
 | |
| 		const Limits &xPercentageLimits,
 | |
| 		const QRect &rect,
 | |
| 		float64 xIndexStart) {
 | |
| 	const auto fullWidth = rect.width()
 | |
| 		/ (xPercentageLimits.max - xPercentageLimits.min);
 | |
| 	const auto offset = fullWidth * xPercentageLimits.min;
 | |
| 	const auto p = (chartData.xPercentage.size() < 2)
 | |
| 		? 1.
 | |
| 		: chartData.xPercentage[1] * fullWidth;
 | |
| 	const auto w = chartData.xPercentage[1] * (fullWidth - p);
 | |
| 	const auto leftStart = rect.x()
 | |
| 		+ chartData.xPercentage[xIndexStart] * (fullWidth - p)
 | |
| 		- offset;
 | |
| 	return { leftStart, w };
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| StackLinearChartView::StackLinearChartView() = default;
 | |
| 
 | |
| StackLinearChartView::~StackLinearChartView() = default;
 | |
| 
 | |
| void StackLinearChartView::paint(
 | |
| 		QPainter &p,
 | |
| 		const Data::StatisticalChart &chartData,
 | |
| 		const Limits &xIndices,
 | |
| 		const Limits &xPercentageLimits,
 | |
| 		const Limits &heightLimits,
 | |
| 		const QRect &rect,
 | |
| 		bool footer) {
 | |
| 	constexpr auto kOffset = float64(2);
 | |
| 	_lastPaintedXIndices = {
 | |
| 		float64(std::max(0., xIndices.min - kOffset)),
 | |
| 		float64(std::min(
 | |
| 			float64(chartData.xPercentage.size() - 1),
 | |
| 			xIndices.max + kOffset)),
 | |
| 	};
 | |
| 
 | |
| 	StackLinearChartView::paint(
 | |
| 		p,
 | |
| 		chartData,
 | |
| 		xPercentageLimits,
 | |
| 		heightLimits,
 | |
| 		rect,
 | |
| 		footer);
 | |
| }
 | |
| 
 | |
| void StackLinearChartView::paint(
 | |
| 		QPainter &p,
 | |
| 		const Data::StatisticalChart &chartData,
 | |
| 		const Limits &xPercentageLimits,
 | |
| 		const Limits &heightLimits,
 | |
| 		const QRect &rect,
 | |
| 		bool footer) {
 | |
| 	const auto &[localStart, localEnd] = _lastPaintedXIndices;
 | |
| 	const auto &[leftStart, w] = ComputeLeftStartAndStep(
 | |
| 		chartData,
 | |
| 		xPercentageLimits,
 | |
| 		rect,
 | |
| 		localStart);
 | |
| 
 | |
| 	auto skipPoints = std::vector<bool>(chartData.lines.size(), false);
 | |
| 	auto paths = std::vector<QPainterPath>(chartData.lines.size(), QPainterPath());
 | |
| 
 | |
| 	for (auto i = localStart; i <= localEnd; i++) {
 | |
| 		auto stackOffset = 0.;
 | |
| 		auto sum = 0.;
 | |
| 		auto lastEnabled = int(0);
 | |
| 
 | |
| 		auto drawingLinesCount = int(0);
 | |
| 
 | |
| 		for (auto k = 0; k < chartData.lines.size(); k++) {
 | |
| 			const auto &line = chartData.lines[k];
 | |
| 			if (!isEnabled(line.id)) {
 | |
| 				continue;
 | |
| 			}
 | |
| 			if (line.y[i] > 0) {
 | |
| 				sum += line.y[i] * alpha(line.id);
 | |
| 				drawingLinesCount++;
 | |
| 			}
 | |
| 			lastEnabled = k;
 | |
| 		}
 | |
| 
 | |
| 		for (auto k = 0; k < chartData.lines.size(); k++) {
 | |
| 			const auto &line = chartData.lines[k];
 | |
| 			if (!isEnabled(line.id)) {
 | |
| 				continue;
 | |
| 			}
 | |
| 			const auto &y = line.y;
 | |
| 			const auto lineAlpha = alpha(line.id);
 | |
| 
 | |
| 			auto &chartPath = paths[k];
 | |
| 
 | |
| 			auto yPercentage = 0.;
 | |
| 
 | |
| 			if (drawingLinesCount == 1) {
 | |
| 				if (y[i] == 0) {
 | |
| 					yPercentage = 0;
 | |
| 				} else {
 | |
| 					yPercentage = lineAlpha;
 | |
| 				}
 | |
| 			} else {
 | |
| 				if (sum == 0) {
 | |
| 					yPercentage = 0;
 | |
| 				} else {
 | |
| 					yPercentage = y[i] * lineAlpha / sum;
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			const auto xPoint = rect.width()
 | |
| 				* ((chartData.xPercentage[i] - xPercentageLimits.min)
 | |
| 					/ (xPercentageLimits.max - xPercentageLimits.min));
 | |
| 			const auto nextXPoint = (i == localEnd)
 | |
| 				? rect.width()
 | |
| 				: rect.width()
 | |
| 					* ((chartData.xPercentage[i + 1] - xPercentageLimits.min)
 | |
| 						/ (xPercentageLimits.max - xPercentageLimits.min));
 | |
| 
 | |
| 			const auto height = (yPercentage) * rect.height();
 | |
| 			const auto yPoint = rect.y() + rect.height() - height - stackOffset;
 | |
| 
 | |
| 			auto yPointZero = rect.y() + rect.height();
 | |
| 			auto xPointZero = xPoint;
 | |
| 
 | |
| 			if (i == localStart) {
 | |
| 				auto localX = rect.x();
 | |
| 				auto localY = rect.y() + rect.height();
 | |
| 				chartPath.moveTo(localX, localY);
 | |
| 				skipPoints[k] = false;
 | |
| 			}
 | |
| 
 | |
| 			const auto transitionProgress = 0.;
 | |
| 			if ((yPercentage == 0)
 | |
| 				&& (i > 0 && (y[i - 1] == 0))
 | |
| 				&& (i < localEnd && (y[i + 1] == 0))) {
 | |
| 				if (!skipPoints[k]) {
 | |
| 					if (k == lastEnabled) {
 | |
| 						chartPath.lineTo(xPointZero, yPointZero * (1. - transitionProgress));
 | |
| 					} else {
 | |
| 						chartPath.lineTo(xPointZero, yPointZero);
 | |
| 					}
 | |
| 				}
 | |
| 				skipPoints[k] = true;
 | |
| 			} else {
 | |
| 				if (skipPoints[k]) {
 | |
| 					if (k == lastEnabled) {
 | |
| 						chartPath.lineTo(xPointZero, yPointZero * (1. - transitionProgress));
 | |
| 					} else {
 | |
| 						chartPath.lineTo(xPointZero, yPointZero);
 | |
| 					}
 | |
| 				}
 | |
| 				if (k == lastEnabled) {
 | |
| 					chartPath.lineTo(xPoint, yPoint * (1. - transitionProgress));
 | |
| 				} else {
 | |
| 					chartPath.lineTo(xPoint, yPoint);
 | |
| 				}
 | |
| 				skipPoints[k] = false;
 | |
| 			}
 | |
| 
 | |
| 			if (i == localEnd) {
 | |
| 				auto localX = rect.x() + rect.width();
 | |
| 				auto localY = rect.y() + rect.height();
 | |
| 					chartPath.lineTo(localX, localY);
 | |
| 			}
 | |
| 
 | |
| 			stackOffset += height;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	auto hq = PainterHighQualityEnabler(p);
 | |
| 
 | |
| 	for (auto k = int(chartData.lines.size() - 1); k >= 0; k--) {
 | |
| 		if (paths[k].isEmpty()) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		const auto &line = chartData.lines[k];
 | |
| 		p.setOpacity(alpha(line.id));
 | |
| 		p.setPen(Qt::NoPen);
 | |
| 		p.fillPath(paths[k], line.color);
 | |
| 	}
 | |
| 	p.setOpacity(1.);
 | |
| }
 | |
| 
 | |
| void StackLinearChartView::paintSelectedXIndex(
 | |
| 		QPainter &p,
 | |
| 		const Data::StatisticalChart &chartData,
 | |
| 		const Limits &xPercentageLimits,
 | |
| 		const Limits &heightLimits,
 | |
| 		const QRect &rect,
 | |
| 		int selectedXIndex,
 | |
| 		float64 progress) {
 | |
| 	if (selectedXIndex < 0) {
 | |
| 		return;
 | |
| 	}
 | |
| 	p.setBrush(st::boxBg);
 | |
| 	const auto r = st::statisticsDetailsDotRadius;
 | |
| 	const auto i = selectedXIndex;
 | |
| 	const auto isSameToken = (_selectedPoints.lastXIndex == selectedXIndex)
 | |
| 		&& (_selectedPoints.lastHeightLimits.min == heightLimits.min)
 | |
| 		&& (_selectedPoints.lastHeightLimits.max == heightLimits.max)
 | |
| 		&& (_selectedPoints.lastXLimits.min == xPercentageLimits.min)
 | |
| 		&& (_selectedPoints.lastXLimits.max == xPercentageLimits.max);
 | |
| 	for (const auto &line : chartData.lines) {
 | |
| 		const auto lineAlpha = alpha(line.id);
 | |
| 		const auto useCache = isSameToken
 | |
| 			|| (lineAlpha < 1. && !isEnabled(line.id));
 | |
| 		if (!useCache) {
 | |
| 			// Calculate.
 | |
| 			const auto xPoint = rect.width()
 | |
| 				* ((chartData.xPercentage[i] - xPercentageLimits.min)
 | |
| 					/ (xPercentageLimits.max - xPercentageLimits.min));
 | |
| 			const auto yPercentage = (line.y[i] - heightLimits.min)
 | |
| 				/ float64(heightLimits.max - heightLimits.min);
 | |
| 			_selectedPoints.points[line.id] = QPointF(xPoint, 0)
 | |
| 				+ rect.topLeft();
 | |
| 		}
 | |
| 
 | |
| 		{
 | |
| 			const auto lineRect = QRectF(
 | |
| 				rect.x()
 | |
| 					+ begin(_selectedPoints.points)->second.x()
 | |
| 					- (st::lineWidth / 2.),
 | |
| 				rect.y(),
 | |
| 				st::lineWidth,
 | |
| 				rect.height());
 | |
| 			p.fillRect(lineRect, st::windowSubTextFg);
 | |
| 		}
 | |
| 	}
 | |
| 	_selectedPoints.lastXIndex = selectedXIndex;
 | |
| 	_selectedPoints.lastHeightLimits = heightLimits;
 | |
| 	_selectedPoints.lastXLimits = xPercentageLimits;
 | |
| }
 | |
| 
 | |
| int StackLinearChartView::findXIndexByPosition(
 | |
| 		const Data::StatisticalChart &chartData,
 | |
| 		const Limits &xPercentageLimits,
 | |
| 		const QRect &rect,
 | |
| 		float64 x) {
 | |
| 	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 rawXPercentage = anim::interpolateF(
 | |
| 		xPercentageLimits.min,
 | |
| 		xPercentageLimits.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::distance(
 | |
| 		begin(chartData.xPercentage),
 | |
| 		nearestXPercentageIt);
 | |
| }
 | |
| 
 | |
| void StackLinearChartView::setEnabled(int id, bool enabled, crl::time now) {
 | |
| 	const auto it = _entries.find(id);
 | |
| 	if (it == end(_entries)) {
 | |
| 		_entries[id] = Entry{
 | |
| 			.enabled = enabled,
 | |
| 			.startedAt = now,
 | |
| 			.anim = anim::value(enabled ? 0. : 1., enabled ? 1. : 0.),
 | |
| 		};
 | |
| 	} else if (it->second.enabled != enabled) {
 | |
| 		auto &entry = it->second;
 | |
| 		entry.enabled = enabled;
 | |
| 		entry.startedAt = now;
 | |
| 		entry.anim.start(enabled ? 1. : 0.);
 | |
| 	}
 | |
| 	_isFinished = false;
 | |
| 	_cachedHeightLimits = {};
 | |
| }
 | |
| 
 | |
| bool StackLinearChartView::isFinished() const {
 | |
| 	return _isFinished;
 | |
| }
 | |
| 
 | |
| bool StackLinearChartView::isEnabled(int id) const {
 | |
| 	const auto it = _entries.find(id);
 | |
| 	return (it == end(_entries)) ? true : it->second.enabled;
 | |
| }
 | |
| 
 | |
| float64 StackLinearChartView::alpha(int id) const {
 | |
| 	const auto it = _entries.find(id);
 | |
| 	return (it == end(_entries)) ? 1. : it->second.alpha;
 | |
| }
 | |
| 
 | |
| AbstractChartView::HeightLimits StackLinearChartView::heightLimits(
 | |
| 		Data::StatisticalChart &chartData,
 | |
| 		Limits xIndices) {
 | |
| 	constexpr auto kMaxStackLinear = 100.;
 | |
| 	return {
 | |
| 		.full = { 0, kMaxStackLinear },
 | |
| 		.ranged = { 0., kMaxStackLinear },
 | |
| 	};
 | |
| }
 | |
| 
 | |
| void StackLinearChartView::tick(crl::time now) {
 | |
| 	for (auto &[id, entry] : _entries) {
 | |
| 		const auto dt = std::min(
 | |
| 			(now - entry.startedAt) / kAlphaDuration,
 | |
| 			1.);
 | |
| 		if (dt > 1.) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		return update(dt);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void StackLinearChartView::update(float64 dt) {
 | |
| 	auto finishedCount = 0;
 | |
| 	auto idsToRemove = std::vector<int>();
 | |
| 	for (auto &[id, entry] : _entries) {
 | |
| 		if (!entry.startedAt) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		entry.anim.update(dt, anim::linear);
 | |
| 		const auto progress = entry.anim.current();
 | |
| 		entry.alpha = std::clamp(
 | |
| 			progress,
 | |
| 			0.,
 | |
| 			1.);
 | |
| 		if (entry.alpha == 1.) {
 | |
| 			idsToRemove.push_back(id);
 | |
| 		}
 | |
| 		if (entry.anim.current() == entry.anim.to()) {
 | |
| 			finishedCount++;
 | |
| 			entry.anim.finish();
 | |
| 		}
 | |
| 	}
 | |
| 	_isFinished = (finishedCount == _entries.size());
 | |
| 	for (const auto &id : idsToRemove) {
 | |
| 		_entries.remove(id);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| } // namespace Statistic
 | 
