From f026271436963e86cf19d4573c759ecdebb77b2d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 12 Sep 2023 11:35:09 +0300 Subject: [PATCH] Added initial implementation of stack linear chart. --- Telegram/CMakeLists.txt | 2 + .../statistics/statistics_common.h | 1 + .../statistics/view/chart_view_factory.cpp | 4 + .../view/stack_linear_chart_view.cpp | 370 ++++++++++++++++++ .../statistics/view/stack_linear_chart_view.h | 102 +++++ 5 files changed, 479 insertions(+) create mode 100644 Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp create mode 100644 Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 72371cde2..3e43ec10a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1304,6 +1304,8 @@ PRIVATE statistics/view/linear_chart_view.h statistics/view/stack_chart_view.cpp statistics/view/stack_chart_view.h + statistics/view/stack_linear_chart_view.cpp + statistics/view/stack_linear_chart_view.h storage/details/storage_file_utilities.cpp storage/details/storage_file_utilities.h storage/details/storage_settings_scheme.cpp diff --git a/Telegram/SourceFiles/statistics/statistics_common.h b/Telegram/SourceFiles/statistics/statistics_common.h index 9d0843d5f..e5ea79962 100644 --- a/Telegram/SourceFiles/statistics/statistics_common.h +++ b/Telegram/SourceFiles/statistics/statistics_common.h @@ -18,6 +18,7 @@ enum class ChartViewType { Linear, Stack, DoubleLinear, + StackLinear, }; } // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/view/chart_view_factory.cpp b/Telegram/SourceFiles/statistics/view/chart_view_factory.cpp index f12e89ed1..dd8e4d1ce 100644 --- a/Telegram/SourceFiles/statistics/view/chart_view_factory.cpp +++ b/Telegram/SourceFiles/statistics/view/chart_view_factory.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "statistics/statistics_common.h" #include "statistics/view/linear_chart_view.h" #include "statistics/view/stack_chart_view.h" +#include "statistics/view/stack_linear_chart_view.h" namespace Statistic { @@ -24,6 +25,9 @@ std::unique_ptr CreateChartView(ChartViewType type) { case ChartViewType::DoubleLinear: { return std::make_unique(true); } break; + case ChartViewType::StackLinear: { + return std::make_unique(); + } break; default: Unexpected("Type in Statistic::CreateChartView."); } } diff --git a/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp b/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp new file mode 100644 index 000000000..7561a0930 --- /dev/null +++ b/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp @@ -0,0 +1,370 @@ +/* +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(chartData.lines.size(), false); + auto paths = std::vector(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(); + 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 diff --git a/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h b/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h new file mode 100644 index 000000000..890cec1ae --- /dev/null +++ b/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h @@ -0,0 +1,102 @@ +/* +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 +*/ +#pragma once + +#include "statistics/segment_tree.h" +#include "statistics/statistics_common.h" +#include "statistics/view/abstract_chart_view.h" +#include "ui/effects/animation_value.h" + +namespace Data { +struct StatisticalChart; +} // namespace Data + +namespace Statistic { + +struct Limits; + +class StackLinearChartView final : public AbstractChartView { +public: + StackLinearChartView(); + ~StackLinearChartView() override final; + + void paint( + QPainter &p, + const Data::StatisticalChart &chartData, + const Limits &xIndices, + const Limits &xPercentageLimits, + const Limits &heightLimits, + const QRect &rect, + bool footer) override; + + void paintSelectedXIndex( + QPainter &p, + const Data::StatisticalChart &chartData, + const Limits &xPercentageLimits, + const Limits &heightLimits, + const QRect &rect, + int selectedXIndex, + float64 progress) override; + + int findXIndexByPosition( + const Data::StatisticalChart &chartData, + const Limits &xPercentageLimits, + const QRect &rect, + float64 x) override; + + void setEnabled(int id, bool enabled, crl::time now) override; + [[nodiscard]] bool isEnabled(int id) const override; + [[nodiscard]] bool isFinished() const override; + [[nodiscard]] float64 alpha(int id) const override; + + [[nodiscard]] HeightLimits heightLimits( + Data::StatisticalChart &chartData, + Limits xIndices) override; + + void tick(crl::time now) override; + void update(float64 dt) override; + +private: + void paint( + QPainter &p, + const Data::StatisticalChart &chartData, + const Limits &xPercentageLimits, + const Limits &heightLimits, + const QRect &rect, + bool footer); + + struct { + Limits full; + std::vector ySum; + SegmentTree ySumSegmentTree; + } _cachedHeightLimits; + + Limits _lastPaintedXIndices; + + struct SelectedPoints final { + int lastXIndex = -1; + Limits lastHeightLimits; + Limits lastXLimits; + base::flat_map points; + }; + SelectedPoints _selectedPoints; + + struct Entry final { + bool enabled = false; + crl::time startedAt = 0; + float64 alpha = 1.; + anim::value anim; + bool disabled = false; + }; + + base::flat_map _entries; + bool _isFinished = true; + +}; + +} // namespace Statistic