diff --git a/Telegram/SourceFiles/statistics/chart_widget.cpp b/Telegram/SourceFiles/statistics/chart_widget.cpp index a7cea3beb3..409775aaa2 100644 --- a/Telegram/SourceFiles/statistics/chart_widget.cpp +++ b/Telegram/SourceFiles/statistics/chart_widget.cpp @@ -706,7 +706,17 @@ void ChartWidget::ChartAnimationController::tick( const auto footerMinFinished = isFinished(_animationValueFooterHeightMin); const auto footerMaxFinished = isFinished(_animationValueFooterHeightMax); - chartView->tick(now); + // chartView->tick(now); + { + constexpr auto kDtHeightSpeed1 = 0.03 * 2; + constexpr auto kDtHeightSpeed2 = 0.03 * 2; + constexpr auto kDtHeightSpeed3 = 0.045 * 2; + if (_dtHeight.current.max > 0 && _dtHeight.current.max < 1) { + chartView->update(_dtHeight.current.max); + } else { + chartView->tick(now); + } + } if (xFinished && yFinished @@ -1262,7 +1272,8 @@ void ChartWidget::setupFilterButtons() { void ChartWidget::setChartData(Data::StatisticalChart chartData) { _chartData = std::move(chartData); - _chartView = CreateChartView(ChartViewType::Linear); + // _chartView = CreateChartView(ChartViewType::Linear); + _chartView = CreateChartView(ChartViewType::Stack); setupDetails(); setupFilterButtons(); diff --git a/Telegram/SourceFiles/statistics/view/abstract_chart_view.h b/Telegram/SourceFiles/statistics/view/abstract_chart_view.h index 1dd0d3ab18..db347b5822 100644 --- a/Telegram/SourceFiles/statistics/view/abstract_chart_view.h +++ b/Telegram/SourceFiles/statistics/view/abstract_chart_view.h @@ -58,6 +58,8 @@ public: Limits xIndices) = 0; virtual void tick(crl::time now) = 0; + virtual void update(float64 dt) { + }; }; diff --git a/Telegram/SourceFiles/statistics/view/stack_chart_view.cpp b/Telegram/SourceFiles/statistics/view/stack_chart_view.cpp index 979b5aee19..96da19619b 100644 --- a/Telegram/SourceFiles/statistics/view/stack_chart_view.cpp +++ b/Telegram/SourceFiles/statistics/view/stack_chart_view.cpp @@ -7,10 +7,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "statistics/view/stack_chart_view.h" +#include "ui/effects/animation_value_f.h" #include "data/data_statistics.h" +#include "ui/painter.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 StackChartView::StackChartView() = default; @@ -25,66 +53,96 @@ void StackChartView::paint( const Limits &heightLimits, const QRect &rect, bool footer) { - const auto localStart = std::max(0, int(xIndices.min) - 2); - const auto localEnd = std::min( - int(chartData.xPercentage.size() - 1), - int(xIndices.max) + 2); + constexpr auto kOffset = float64(2); + _lastPaintedXIndices = { + float64(std::max(0., xIndices.min - kOffset)), + float64(std::min( + float64(chartData.xPercentage.size() - 1), + xIndices.max + kOffset)), + }; - const auto fullWidth = rect.width() / (xPercentageLimits.max - xPercentageLimits.min); - const auto offset = fullWidth * xPercentageLimits.min; - const auto pp = (chartData.xPercentage.size() < 2) - ? 1. - : chartData.xPercentage[1] * fullWidth; - const auto w = chartData.xPercentage[1] * (fullWidth - pp); - // const auto w = rect.width() / float64(localEnd - localStart); - const auto r = w / 2.; - const auto leftStart = chartData.xPercentage[localStart] * (fullWidth - pp) - offset + rect.x(); + StackChartView::paint( + p, + chartData, + xPercentageLimits, + heightLimits, + rect, + footer); +} - auto paths = std::vector(); - paths.resize(chartData.lines.size()); +void StackChartView::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); - for (auto i = localStart; i <= localEnd; i++) { - auto chartPoints = QPolygonF(); + const auto opacity = p.opacity(); + auto hq = PainterHighQualityEnabler(p); - const auto xPoint = rect.width() - * ((chartData.xPercentage[i] - xPercentageLimits.min) - / (xPercentageLimits.max - xPercentageLimits.min)); - auto bottom = float64(-rect.y()); - const auto left = leftStart + (i - localStart) * w; - - for (auto j = 0; j < chartData.lines.size(); j++) { - const auto &line = chartData.lines[j]; - if (line.y[i] <= 0) { - continue; - } - const auto yPercentage = (line.y[i] - heightLimits.min) - / float64(heightLimits.max - heightLimits.min); - const auto yPoint = yPercentage * rect.height() * alpha(line.id); - // const auto column = QRectF( - // xPoint - r, - // rect.height() - bottom - yPoint, - // w, - // yPoint); - const auto column = QRectF( - left, - rect.height() - bottom - yPoint, - w, - yPoint); - paths[j].addRect(column); - - // p.setPen(Qt::NoPen); - // p.setBrush(line.color); - // p.setOpacity(0.3); - // // p.setOpacity(1.); - // p.drawRect(column); - // p.setOpacity(1.); - bottom += yPoint; - } + auto bottoms = std::vector(localEnd - localStart + 1, -rect.y()); + auto selectedBottoms = std::vector(); + const auto hasSelectedXIndex = !footer && (_lastSelectedXIndex >= 0); + if (hasSelectedXIndex) { + selectedBottoms = std::vector(chartData.lines.size(), 0); + constexpr auto kSelectedAlpha = 0.5; + p.setOpacity( + anim::interpolateF(1.0, kSelectedAlpha, _lastSelectedXProgress)); } - p.setPen(Qt::NoPen); - for (auto i = 0; i < paths.size(); i++) { - p.fillPath(paths[i], chartData.lines[i].color); + for (auto i = 0; i < chartData.lines.size(); i++) { + const auto &line = chartData.lines[i]; + auto path = QPainterPath(); + for (auto x = localStart; x <= localEnd; x++) { + if (line.y[x] <= 0) { + continue; + } + const auto xPoint = rect.width() + * ((chartData.xPercentage[x] - xPercentageLimits.min) + / (xPercentageLimits.max - xPercentageLimits.min)); + const auto yPercentage = (line.y[x] - heightLimits.min) + / float64(heightLimits.max - heightLimits.min); + const auto yPoint = yPercentage * rect.height() * alpha(line.id); + + const auto bottomIndex = x - localStart; + const auto column = QRectF( + leftStart + (x - localStart) * w, + rect.height() - bottoms[bottomIndex] - yPoint, + w, + yPoint); + if (hasSelectedXIndex && (x == _lastSelectedXIndex)) { + selectedBottoms[i] = column.y(); + } else { + path.addRect(column); + } + bottoms[bottomIndex] += yPoint; + } + p.fillPath(path, line.color); + } + + for (auto i = 0; i < selectedBottoms.size(); i++) { + p.setOpacity(opacity); + if (selectedBottoms[i] <= 0) { + continue; + } + const auto &line = chartData.lines[i]; + const auto yPercentage = (line.y[_lastSelectedXIndex] - heightLimits.min) + / float64(heightLimits.max - heightLimits.min); + const auto yPoint = yPercentage * rect.height() * alpha(line.id); + + const auto column = QRectF( + leftStart + (_lastSelectedXIndex - localStart) * w, + selectedBottoms[i], + w, + yPoint); + p.fillRect(column, line.color); } } @@ -96,6 +154,16 @@ void StackChartView::paintSelectedXIndex( const QRect &rect, int selectedXIndex, float64 progress) { + _lastSelectedXIndex = selectedXIndex; + _lastSelectedXProgress = progress; + [[maybe_unused]] const auto o = ScopedPainterOpacity(p, progress); + StackChartView::paint( + p, + chartData, + xPercentageLimits, + heightLimits, + rect, + false); } int StackChartView::findXIndexByPosition( @@ -103,22 +171,60 @@ int StackChartView::findXIndexByPosition( const Limits &xPercentageLimits, const QRect &rect, float64 xPos) { - return 0; + if (xPos < rect.x()) { + return 0; + } else if (xPos > (rect.x() + rect.width())) { + return chartData.xPercentage.size() - 1; + } + const auto &[localStart, localEnd] = _lastPaintedXIndices; + const auto &[leftStart, w] = ComputeLeftStartAndStep( + chartData, + xPercentageLimits, + rect, + localStart); + + for (auto i = 0; i < chartData.lines.size(); i++) { + const auto &line = chartData.lines[i]; + for (auto x = localStart; x <= localEnd; x++) { + const auto left = leftStart + (x - localStart) * w; + if ((xPos >= left) && (xPos < (left + w))) { + return _lastSelectedXIndex = x; + } + } + } + return _lastSelectedXIndex = 0; } void StackChartView::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 StackChartView::isFinished() const { - return true; + return _isFinished; } bool StackChartView::isEnabled(int id) const { - return true; + const auto it = _entries.find(id); + return (it == end(_entries)) ? true : it->second.enabled; } float64 StackChartView::alpha(int id) const { - return 1.0; + const auto it = _entries.find(id); + return (it == end(_entries)) ? 1. : it->second.alpha; } AbstractChartView::HeightLimits StackChartView::heightLimits( @@ -153,6 +259,42 @@ AbstractChartView::HeightLimits StackChartView::heightLimits( } void StackChartView::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 StackChartView::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_chart_view.h b/Telegram/SourceFiles/statistics/view/stack_chart_view.h index dc0e84d46f..34cc8e0bcc 100644 --- a/Telegram/SourceFiles/statistics/view/stack_chart_view.h +++ b/Telegram/SourceFiles/statistics/view/stack_chart_view.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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; @@ -58,14 +59,38 @@ public: 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; + int _lastSelectedXIndex = -1; + float64 _lastSelectedXProgress = 0; + + 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