diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2118efdca..8be69b082 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1284,6 +1284,10 @@ PRIVATE settings/settings_websites.h statistics/chart_horizontal_lines_data.cpp statistics/chart_horizontal_lines_data.h + statistics/chart_widget.cpp + statistics/chart_widget.h + statistics/linear_chart_view.cpp + statistics/linear_chart_view.h statistics/segment_tree.cpp statistics/segment_tree.h statistics/statistics_box.cpp diff --git a/Telegram/SourceFiles/statistics/chart_widget.cpp b/Telegram/SourceFiles/statistics/chart_widget.cpp new file mode 100644 index 000000000..84165775e --- /dev/null +++ b/Telegram/SourceFiles/statistics/chart_widget.cpp @@ -0,0 +1,213 @@ +/* +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/chart_widget.h" + +#include "statistics/linear_chart_view.h" +#include "ui/rect.h" +#include "styles/style_boxes.h" + +namespace Statistic { + +namespace { + +constexpr auto kHeightLimitsUpdateTimeout = crl::time(320); + +[[nodiscard]] int FindMaxValue( + Data::StatisticalChart &chartData, + int startXIndex, + int endXIndex) { + auto maxValue = 0; + for (auto &l : chartData.lines) { + const auto lineMax = l.segmentTree.rMaxQ(startXIndex, endXIndex); + maxValue = std::max(lineMax, maxValue); + } + return maxValue; +} + +[[nodiscard]] int FindMinValue( + Data::StatisticalChart &chartData, + int startXIndex, + int endXIndex) { + auto minValue = std::numeric_limits::max(); + for (auto &l : chartData.lines) { + const auto lineMin = l.segmentTree.rMinQ(startXIndex, endXIndex); + minValue = std::min(lineMin, minValue); + } + return minValue; +} + +void PaintHorizontalLines( + QPainter &p, + const ChartHorizontalLinesData &horizontalLine, + const QRect &r) { + for (const auto &line : horizontalLine.lines) { + const auto lineRect = QRect( + 0, + r.y() + r.height() * line.relativeValue, + r.x() + r.width(), + st::lineWidth); + p.fillRect(lineRect, st::boxTextFg); + } +} + +void PaintCaptionsToHorizontalLines( + QPainter &p, + const ChartHorizontalLinesData &horizontalLine, + const QRect &r) { + p.setFont(st::boxTextFont->f); + p.setPen(st::boxTextFg); + for (const auto &line : horizontalLine.lines) { + p.drawText(10, r.y() + r.height() * line.relativeValue, line.caption); + } +} + +} // namespace + +ChartWidget::ChartWidget(not_null parent) +: Ui::RpWidget(parent) { + resize(width(), st::confirmMaxHeight); +} + +void ChartWidget::setChartData(Data::StatisticalChart chartData) { + _chartData = chartData; + + { + const auto startXIndex = _chartData.findStartIndex( + _chartData.xPercentage.front()); + const auto endXIndex = _chartData.findEndIndex( + startXIndex, + _chartData.xPercentage.back()); + setHeightLimits( + { + float64(FindMinValue(_chartData, startXIndex, endXIndex)), + float64(FindMaxValue(_chartData, startXIndex, endXIndex)), + }, + true); + update(); + } +} + +void ChartWidget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto r = rect(); + const auto captionRect = r; + const auto chartRect = r + - QMargins{ 0, st::boxTextFont->height, 0, st::lineWidth }; + + for (const auto &horizontalLine : _horizontalLines) { + PaintHorizontalLines(p, horizontalLine, chartRect); + } + + if (_chartData) { + Statistic::PaintLinearChartView( + p, + _chartData, + chartRect); + } + + for (const auto &horizontalLine : _horizontalLines) { + PaintCaptionsToHorizontalLines(p, horizontalLine, chartRect); + } +} + +void ChartWidget::setHeightLimits(Limits newHeight, bool animated) { + { + const auto lineMaxHeight = ChartHorizontalLinesData::LookupHeight( + newHeight.max); + const auto diff = std::abs(lineMaxHeight - _animateToHeight.max); + const auto heightChanged = (!newHeight.max) + || (diff < _thresholdHeight.max); + if (!heightChanged && (newHeight.max == _animateToHeight.min)) { + return; + } + } + + const auto newLinesData = ChartHorizontalLinesData( + newHeight.max, + newHeight.min, + true); + newHeight = Limits{ + .min = newLinesData.lines.front().absoluteValue, + .max = newLinesData.lines.back().absoluteValue, + }; + + { + auto k = (_currentHeight.max - _currentHeight.min) + / float64(newHeight.max - newHeight.min); + if (k > 1.) { + k = 1. / k; + } + constexpr auto kUpdateStep1 = 0.1; + constexpr auto kUpdateStep2 = 0.03; + constexpr auto kUpdateStep3 = 0.045; + constexpr auto kUpdateStepThreshold1 = 0.7; + constexpr auto kUpdateStepThreshold2 = 0.1; + const auto s = (k > kUpdateStepThreshold1) + ? kUpdateStep1 + : (k < kUpdateStepThreshold2) + ? kUpdateStep2 + : kUpdateStep3; + + const auto refresh = (newHeight.max != _animateToHeight.max) + || (_useMinHeight && (newHeight.min != _animateToHeight.min)); + if (refresh) { + _startFromH = _currentHeight; + _startFrom = {}; + _minMaxUpdateStep = s; + } + } + + _animateToHeight = newHeight; + measureHeightThreshold(); + + { + const auto now = crl::now(); + if ((now - _lastHeightLimitsChanged) < kHeightLimitsUpdateTimeout) { + return; + } + _lastHeightLimitsChanged = now; + } + + if (!animated) { + _currentHeight = newHeight; + _horizontalLines.clear(); + _horizontalLines.push_back(newLinesData); + _horizontalLines.back().alpha = 1.; + return; + } + + for (auto &horizontalLine : _horizontalLines) { + horizontalLine.fixedAlpha = horizontalLine.alpha; + } + _horizontalLines.push_back(newLinesData); + + const auto callback = [=](float64 value) { + _horizontalLines.back().alpha = value; + + const auto startIt = begin(_horizontalLines); + const auto endIt = end(_horizontalLines); + for (auto it = startIt; it != (endIt - 1); it++) { + it->alpha = it->fixedAlpha * (1. - (endIt - 1)->alpha); + } + update(); + }; + _heightLimitsAnimation.start(callback, 0., 1., st::slideDuration); + +} + +void ChartWidget::measureHeightThreshold() { + const auto chartHeight = height(); + if (!_animateToHeight.max || !chartHeight) { + return; + } + _thresholdHeight.max = (_animateToHeight.max / float64(chartHeight)) + * st::boxTextFont->height; +} + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/chart_widget.h b/Telegram/SourceFiles/statistics/chart_widget.h new file mode 100644 index 000000000..162f8fe20 --- /dev/null +++ b/Telegram/SourceFiles/statistics/chart_widget.h @@ -0,0 +1,55 @@ +/* +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 "data/data_statistics.h" +#include "statistics/chart_horizontal_lines_data.h" +#include "ui/effects/animations.h" +#include "ui/rp_widget.h" + +namespace Statistic { + +class ChartWidget : public Ui::RpWidget { +public: + ChartWidget(not_null parent); + + struct Limits final { + float64 min = 0; + float64 max = 0; + }; + + void setChartData(Data::StatisticalChart chartData); + void setHeightLimits(Limits newHeight, bool animated); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + void measureHeightThreshold(); + + Data::StatisticalChart _chartData; + + bool _useMinHeight = false; + + Limits _currentHeight; + Limits _animateToHeight; + Limits _thresholdHeight = { -1, 0 }; + Limits _startFrom; + Limits _startFromH; + + float64 _minMaxUpdateStep = 0.; + + crl::time _lastHeightLimitsChanged = 0; + + std::vector _horizontalLines; + + Ui::Animations::Simple _heightLimitsAnimation; + +}; + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/linear_chart_view.cpp b/Telegram/SourceFiles/statistics/linear_chart_view.cpp new file mode 100644 index 000000000..2b0e832c0 --- /dev/null +++ b/Telegram/SourceFiles/statistics/linear_chart_view.cpp @@ -0,0 +1,65 @@ +/* +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/linear_chart_view.h" + +#include "data/data_statistics.h" +#include "styles/style_boxes.h" + +namespace Statistic { + +void PaintLinearChartView( + QPainter &p, + const Data::StatisticalChart &chartData, + const QRect &rect) { + const auto offset = 0; + const auto currentMinHeight = rect.y(); // + const auto currentMaxHeight = rect.height() + rect.y(); // + + for (const auto &line : chartData.lines) { + const auto additionalP = (chartData.xPercentage.size() < 2) + ? 0. + : (chartData.xPercentage.front() * rect.width()); + const auto additionalPoints = 0; + + auto first = true; + auto chartPath = QPainterPath(); + + const auto startXIndex = chartData.findStartIndex( + chartData.xPercentage.front()); + const auto endXIndex = chartData.findEndIndex( + startXIndex, + chartData.xPercentage.back()); + + const auto localStart = std::max(0, startXIndex - additionalPoints); + const auto localEnd = std::min( + int(chartData.xPercentage.size() - 1), + endXIndex + additionalPoints); + + for (auto i = localStart; i <= localEnd; i++) { + if (line.y[i] < 0) { + continue; + } + const auto xPoint = chartData.xPercentage[i] * rect.width() + - offset; + const auto yPercentage = (line.y[i] - line.minValue) + / float64(line.maxValue - line.minValue); + const auto yPoint = rect.y() + (1. - yPercentage) * rect.height(); + if (first) { + first = false; + chartPath.moveTo(xPoint, yPoint); + } + chartPath.lineTo(xPoint, yPoint); + } + p.setPen(line.color); + p.setBrush(Qt::NoBrush); + p.drawPath(chartPath); + } + p.setPen(st::boxTextFg); +} + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/linear_chart_view.h b/Telegram/SourceFiles/statistics/linear_chart_view.h new file mode 100644 index 000000000..d23067a51 --- /dev/null +++ b/Telegram/SourceFiles/statistics/linear_chart_view.h @@ -0,0 +1,21 @@ +/* +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 + +namespace Data { +struct StatisticalChart; +} // namespace Data + +namespace Statistic { + +void PaintLinearChartView( + QPainter &p, + const Data::StatisticalChart &chartData, + const QRect &rect); + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/statistics_box.cpp b/Telegram/SourceFiles/statistics/statistics_box.cpp index 3c6457813..3476220d7 100644 --- a/Telegram/SourceFiles/statistics/statistics_box.cpp +++ b/Telegram/SourceFiles/statistics/statistics_box.cpp @@ -7,13 +7,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "statistics/statistics_box.h" +#include "api/api_statistics.h" #include "data/data_peer.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "statistics/chart_widget.h" #include "ui/layers/generic_box.h" namespace { } // namespace void StatisticsBox(not_null box, not_null peer) { + const auto chartWidget = box->addRow( + object_ptr(box)); + const auto api = chartWidget->lifetime().make_state( + &peer->session().api()); + + api->request( + peer + ) | rpl::start_with_done([=] { + if (const auto stats = api->channelStats()) { + chartWidget->setChartData(stats.memberCountGraph.chart); + } + }, chartWidget->lifetime()); box->setTitle(tr::lng_stats_title()); }