init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
/*
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/statistics_common.h"
#include "statistics/view/abstract_chart_view.h"
#include "data/data_statistics_chart.h"
#include "statistics/chart_lines_filter_controller.h"
#include "statistics/statistics_types.h"
namespace Statistic {
bool CachedSelectedPoints::isSame(int x, const PaintContext &c) const {
return (lastXIndex == x)
&& (lastHeightLimits.min == c.heightLimits.min)
&& (lastHeightLimits.max == c.heightLimits.max)
&& (lastXLimits.min == c.xPercentageLimits.min)
&& (lastXLimits.max == c.xPercentageLimits.max);
}
DoubleLineRatios::DoubleLineRatios(bool isDouble) {
first = second = (isDouble ? 0 : 1);
}
void DoubleLineRatios::init(const Data::StatisticalChart &chartData) {
if (chartData.lines.size() != 2) {
first = 1.;
second = 1.;
} else {
const auto firstMax = chartData.lines.front().maxValue;
const auto secondMax = chartData.lines.back().maxValue;
if (firstMax > secondMax) {
first = 1.;
second = firstMax / float64(secondMax);
} else {
first = secondMax / float64(firstMax);
second = 1.;
}
}
}
float64 DoubleLineRatios::ratio(int lineId) const {
return (lineId == 1) ? first : second;
}
void AbstractChartView::setUpdateCallback(Fn<void()> callback) {
_updateCallback = std::move(callback);
}
void AbstractChartView::update() {
if (_updateCallback) {
_updateCallback();
}
}
void AbstractChartView::setLinesFilterController(
std::shared_ptr<LinesFilterController> c) {
_linesFilterController = std::move(c);
}
auto AbstractChartView::linesFilterController() const
-> std::shared_ptr<LinesFilterController> {
return _linesFilterController;
}
AbstractChartView::HeightLimits DefaultHeightLimits(
const DoubleLineRatios &ratios,
const std::shared_ptr<LinesFilterController> &linesFilter,
Data::StatisticalChart &chartData,
Limits xIndices) {
auto minValue = std::numeric_limits<ChartValue>::max();
auto maxValue = ChartValue(0);
auto minValueFull = std::numeric_limits<ChartValue>::max();
auto maxValueFull = ChartValue(0);
for (auto &l : chartData.lines) {
if (!linesFilter->isEnabled(l.id)) {
continue;
}
const auto r = ratios.ratio(l.id);
const auto lineMax = l.segmentTree.rMaxQ(xIndices.min, xIndices.max);
const auto lineMin = l.segmentTree.rMinQ(xIndices.min, xIndices.max);
maxValue = std::max(ChartValue(lineMax * r), maxValue);
minValue = std::min(ChartValue(lineMin * r), minValue);
maxValueFull = std::max(ChartValue(l.maxValue * r), maxValueFull);
minValueFull = std::min(ChartValue(l.minValue * r), minValueFull);
}
if (maxValue == minValue) {
maxValue = chartData.maxValue;
minValue = chartData.minValue;
}
return {
.full = Limits{ float64(minValueFull), float64(maxValueFull) },
.ranged = Limits{ float64(minValue), float64(maxValue) },
};
}
} // namespace Statistic

View File

@@ -0,0 +1,127 @@
/*
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 {
struct Limits;
class LinesFilterController;
struct PaintContext final {
const Data::StatisticalChart &chartData;
const Limits xIndices;
const Limits xPercentageLimits;
const Limits heightLimits;
const QRect &rect;
bool footer = false;
};
struct CachedSelectedPoints final {
[[nodiscard]] bool isSame(int x, const PaintContext &c) const;
int lastXIndex = -1;
Limits lastHeightLimits;
Limits lastXLimits;
base::flat_map<int, QPointF> points;
};
class DoubleLineRatios final : std::pair<float64, float64> {
public:
DoubleLineRatios(bool isDouble);
operator bool() const {
return first > 0;
}
void init(const Data::StatisticalChart &chartData);
[[nodiscard]] float64 ratio(int lineId) const;
};
class AbstractChartView {
public:
virtual ~AbstractChartView() = default;
virtual void paint(QPainter &p, const PaintContext &c) = 0;
virtual void paintSelectedXIndex(
QPainter &p,
const PaintContext &c,
int selectedXIndex,
float64 progress) = 0;
[[nodiscard]] virtual int findXIndexByPosition(
const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits,
const QRect &rect,
float64 x) = 0;
struct HeightLimits final {
Limits full;
Limits ranged;
};
[[nodiscard]] virtual HeightLimits heightLimits(
Data::StatisticalChart &chartData,
Limits xIndices) = 0;
struct LocalZoomResult final {
bool hasZoom = false;
Limits limitIndices;
Limits range;
};
struct LocalZoomArgs final {
enum class Type {
Prepare,
SkipCalculation,
CheckAvailability,
Process,
SaveZoomFromFooter,
};
const Data::StatisticalChart &chartData;
Type type;
float64 progress = 0.;
int xIndex = 0;
};
virtual LocalZoomResult maybeLocalZoom(const LocalZoomArgs &args) {
return {};
}
virtual void handleMouseMove(
const Data::StatisticalChart &chartData,
const QRect &rect,
const QPoint &p) {
}
void setUpdateCallback(Fn<void()> callback);
void update();
void setLinesFilterController(std::shared_ptr<LinesFilterController> c);
protected:
using LinesFilterControllerPtr = std::shared_ptr<LinesFilterController>;
[[nodiscard]] LinesFilterControllerPtr linesFilterController() const;
private:
LinesFilterControllerPtr _linesFilterController;
Fn<void()> _updateCallback;
};
AbstractChartView::HeightLimits DefaultHeightLimits(
const DoubleLineRatios &ratios,
const std::shared_ptr<LinesFilterController> &linesFilter,
Data::StatisticalChart &chartData,
Limits xIndices);
} // namespace Statistic

View File

@@ -0,0 +1,286 @@
/*
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/bar_chart_view.h"
#include "data/data_statistics_chart.h"
#include "statistics/chart_lines_filter_controller.h"
#include "statistics/view/stack_chart_common.h"
#include "ui/effects/animation_value_f.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_statistics.h"
namespace Statistic {
BarChartView::BarChartView(bool isStack)
: _isStack(isStack)
, _cachedLineRatios(false) {
}
BarChartView::~BarChartView() = default;
void BarChartView::paint(QPainter &p, const PaintContext &c) {
constexpr auto kOffset = float64(2);
_lastPaintedXIndices = {
float64(std::max(0., c.xIndices.min - kOffset)),
float64(std::min(
float64(c.chartData.xPercentage.size() - 1),
c.xIndices.max + kOffset)),
};
BarChartView::paintChartAndSelected(p, c);
}
void BarChartView::paintChartAndSelected(
QPainter &p,
const PaintContext &c) {
const auto &[localStart, localEnd] = _lastPaintedXIndices;
const auto &[leftStart, w] = ComputeLeftStartAndStep(
c.chartData,
c.xPercentageLimits,
c.rect,
localStart);
p.setClipRect(0, 0, c.rect.width() * 2, rect::bottom(c.rect));
const auto opacity = p.opacity();
auto hq = PainterHighQualityEnabler(p);
auto bottoms = std::vector<float64>(
localEnd - localStart + 1,
-c.rect.y());
auto selectedBottoms = std::vector<float64>();
const auto hasSelectedXIndex = _isStack
&& !c.footer
&& (_lastSelectedXIndex >= 0);
if (hasSelectedXIndex) {
selectedBottoms = std::vector<float64>(c.chartData.lines.size(), 0);
constexpr auto kSelectedAlpha = 0.5;
p.setOpacity(
anim::interpolateF(1.0, kSelectedAlpha, _lastSelectedXProgress));
}
for (auto i = 0; i < c.chartData.lines.size(); i++) {
const auto &line = c.chartData.lines[i];
auto path = QPainterPath();
for (auto x = localStart; x <= localEnd; x++) {
if (line.y[x] <= 0 && _isStack) {
continue;
}
const auto yPercentage = (line.y[x] - c.heightLimits.min)
/ float64(c.heightLimits.max - c.heightLimits.min);
const auto yPoint = yPercentage
* c.rect.height()
* linesFilterController()->alpha(line.id);
const auto bottomIndex = x - localStart;
const auto column = QRectF(
leftStart + (x - localStart) * w,
c.rect.height() - bottoms[bottomIndex] - yPoint,
w,
yPoint);
if (hasSelectedXIndex && (x == _lastSelectedXIndex)) {
selectedBottoms[i] = column.y();
}
if (_isStack) {
path.addRect(column);
bottoms[bottomIndex] += yPoint;
} else {
if (path.isEmpty()) {
path.moveTo(column.topLeft());
} else {
path.lineTo(column.topLeft());
}
if (x == localEnd) {
path.lineTo(c.rect.width(), column.y());
} else {
path.lineTo(rect::right(column), column.y());
}
}
}
if (_isStack) {
p.fillPath(path, line.color);
} else {
p.strokePath(path, line.color);
}
}
for (auto i = 0; i < selectedBottoms.size(); i++) {
p.setOpacity(opacity);
if (selectedBottoms[i] <= 0) {
continue;
}
const auto &line = c.chartData.lines[i];
const auto yPercentage = 0.
+ (line.y[_lastSelectedXIndex] - c.heightLimits.min)
/ float64(c.heightLimits.max - c.heightLimits.min);
const auto yPoint = yPercentage
* c.rect.height()
* linesFilterController()->alpha(line.id);
const auto column = QRectF(
leftStart + (_lastSelectedXIndex - localStart) * w,
selectedBottoms[i],
w,
yPoint);
p.fillRect(column, line.color);
}
p.setClipping(false);
}
void BarChartView::paintSelectedXIndex(
QPainter &p,
const PaintContext &c,
int selectedXIndex,
float64 progress) {
const auto was = _lastSelectedXIndex;
_lastSelectedXIndex = selectedXIndex;
_lastSelectedXProgress = progress;
if ((_lastSelectedXIndex < 0) && (was < 0)) {
return;
}
if (_isStack) {
BarChartView::paintChartAndSelected(p, c);
} else if (selectedXIndex >= 0) {
const auto linesFilter = linesFilterController();
auto hq = PainterHighQualityEnabler(p);
auto o = ScopedPainterOpacity(p, progress);
p.setBrush(st::boxBg);
const auto r = st::statisticsDetailsDotRadius;
const auto isSameToken = _selectedPoints.isSame(selectedXIndex, c);
auto linePainted = false;
const auto &[localStart, localEnd] = _lastPaintedXIndices;
const auto &[leftStart, w] = ComputeLeftStartAndStep(
c.chartData,
c.xPercentageLimits,
c.rect,
localStart);
for (auto i = 0; i < c.chartData.lines.size(); i++) {
const auto &line = c.chartData.lines[i];
const auto lineAlpha = linesFilter->alpha(line.id);
const auto useCache = isSameToken
|| (lineAlpha < 1. && !linesFilter->isEnabled(line.id));
if (!useCache) {
// Calculate.
const auto x = _lastSelectedXIndex;
const auto yPercentage = (line.y[x] - c.heightLimits.min)
/ float64(c.heightLimits.max - c.heightLimits.min);
const auto yPoint = (1. - yPercentage) * c.rect.height();
const auto column = QRectF(
leftStart + (x - localStart) * w,
c.rect.height() - 0 - yPoint,
w,
yPoint);
const auto xPoint = column.left() + column.width() / 2.;
_selectedPoints.points[line.id] = QPointF(xPoint, yPoint)
+ c.rect.topLeft();
}
if (!linePainted && lineAlpha) {
[[maybe_unused]] const auto o = ScopedPainterOpacity(
p,
p.opacity() * progress * kRulerLineAlpha);
const auto lineRect = QRectF(
begin(_selectedPoints.points)->second.x()
- (st::lineWidth / 2.),
c.rect.y(),
st::lineWidth,
c.rect.height());
p.fillRect(lineRect, st::boxTextFg);
linePainted = true;
}
// Paint.
auto o = ScopedPainterOpacity(p, lineAlpha * p.opacity());
p.setPen(QPen(line.color, st::statisticsChartLineWidth));
p.drawEllipse(_selectedPoints.points[line.id], r, r);
}
_selectedPoints.lastXIndex = selectedXIndex;
_selectedPoints.lastHeightLimits = c.heightLimits;
_selectedPoints.lastXLimits = c.xPercentageLimits;
}
}
int BarChartView::findXIndexByPosition(
const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits,
const QRect &rect,
float64 xPos) {
if ((xPos < rect.x()) || (xPos > (rect.x() + rect.width()))) {
return _lastSelectedXIndex = -1;
}
const auto &[localStart, localEnd] = _lastPaintedXIndices;
const auto &[leftStart, w] = ComputeLeftStartAndStep(
chartData,
xPercentageLimits,
rect,
localStart);
for (auto i = 0; i < chartData.lines.size(); 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 = -1;
}
AbstractChartView::HeightLimits BarChartView::heightLimits(
Data::StatisticalChart &chartData,
Limits xIndices) {
if (!_isStack) {
if (!_cachedLineRatios) {
_cachedLineRatios.init(chartData);
}
return DefaultHeightLimits(
_cachedLineRatios,
linesFilterController(),
chartData,
xIndices);
}
_cachedHeightLimits = {};
if (_cachedHeightLimits.ySum.empty()) {
_cachedHeightLimits.ySum.reserve(chartData.x.size());
auto maxValueFull = ChartValue(0);
for (auto i = 0; i < chartData.x.size(); i++) {
auto sum = ChartValue(0);
for (const auto &line : chartData.lines) {
if (linesFilterController()->isEnabled(line.id)) {
sum += line.y[i];
}
}
_cachedHeightLimits.ySum.push_back(sum);
maxValueFull = std::max(sum, maxValueFull);
}
_cachedHeightLimits.ySumSegmentTree = SegmentTree(
_cachedHeightLimits.ySum);
_cachedHeightLimits.full = { 0., float64(maxValueFull) };
}
const auto max = std::max(
_cachedHeightLimits.ySumSegmentTree.rMaxQ(
xIndices.min,
xIndices.max),
ChartValue(1));
return {
.full = _cachedHeightLimits.full,
.ranged = { 0., float64(max) },
};
}
} // namespace Statistic

View File

@@ -0,0 +1,66 @@
/*
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/statistics_types.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 BarChartView final : public AbstractChartView {
public:
BarChartView(bool isStack);
~BarChartView() override final;
void paint(QPainter &p, const PaintContext &c) override;
void paintSelectedXIndex(
QPainter &p,
const PaintContext &c,
int selectedXIndex,
float64 progress) override;
int findXIndexByPosition(
const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits,
const QRect &rect,
float64 x) override;
[[nodiscard]] HeightLimits heightLimits(
Data::StatisticalChart &chartData,
Limits xIndices) override;
private:
void paintChartAndSelected(QPainter &p, const PaintContext &c);
struct {
Limits full;
std::vector<ChartValue> ySum;
SegmentTree ySumSegmentTree;
} _cachedHeightLimits;
const bool _isStack;
DoubleLineRatios _cachedLineRatios; // Non-stack.
Limits _lastPaintedXIndices;
int _lastSelectedXIndex = -1;
float64 _lastSelectedXProgress = 0;
CachedSelectedPoints _selectedPoints; // Non-stack.
};
} // namespace Statistic

View File

@@ -0,0 +1,205 @@
/*
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/chart_rulers_view.h"
#include "info/channel_statistics/earn/earn_format.h"
#include "lang/lang_keys.h"
#include "statistics/chart_lines_filter_controller.h"
#include "statistics/statistics_common.h"
#include "statistics/statistics_graphics.h"
#include "styles/style_basic.h"
#include "styles/style_statistics.h"
#include <QtCore/QLocale>
namespace Statistic {
namespace {
[[nodiscard]] QString FormatF(float64 absoluteValue) {
static constexpr auto kTooMuch = int(10'000);
static constexpr auto kTooSmall = 1e-9;
return (std::abs(absoluteValue) <= kTooSmall)
? u"0"_q
: (absoluteValue >= kTooMuch)
? Lang::FormatCountToShort(absoluteValue).string
: QLocale().toString(absoluteValue);
}
} // namespace
ChartRulersView::ChartRulersView() = default;
void ChartRulersView::setChartData(
const Data::StatisticalChart &chartData,
ChartViewType type,
std::shared_ptr<LinesFilterController> linesFilter) {
_rulers.clear();
_isDouble = (type == ChartViewType::DoubleLinear)
|| chartData.currencyRate;
if (chartData.currencyRate) {
_currencyIcon = ChartCurrencyIcon(chartData, {});
if (chartData.currency == Data::StatisticalCurrency::Ton) {
_leftCustomCaption = [=](float64 value) {
return FormatF(value / float64(kOneStarInNano));
};
_rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) {
return Info::ChannelEarn::ToUsd(v / float64(kOneStarInNano), rate, 0);
};
} else {
_leftCustomCaption = [=](float64 value) {
return FormatF(value);
};
_rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) {
return Info::ChannelEarn::ToUsd(v, rate, 0);
};
}
_rightPen = QPen(st::windowSubTextFg);
}
if (_isDouble && (chartData.lines.size() == 2)) {
_linesFilter = std::move(linesFilter);
_leftPen = QPen(chartData.lines.front().color);
_rightPen = QPen(chartData.lines.back().color);
_leftLineId = chartData.lines.front().id;
_rightLineId = chartData.lines.back().id;
const auto firstMax = chartData.lines.front().maxValue;
const auto secondMax = chartData.lines.back().maxValue;
if (firstMax > secondMax) {
_isLeftLineScaled = false;
_scaledLineRatio = firstMax / float64(secondMax);
} else {
_isLeftLineScaled = true;
_scaledLineRatio = secondMax / float64(firstMax);
}
}
}
void ChartRulersView::paintRulers(
QPainter &p,
const QRect &r) {
const auto alpha = p.opacity();
for (auto &ruler : _rulers) {
p.setOpacity(alpha * ruler.alpha * kRulerLineAlpha);
for (const auto &line : ruler.lines) {
const auto lineRect = QRect(
0,
r.y() + r.height() * line.relativeValue,
r.x() + r.width(),
st::lineWidth);
p.fillRect(lineRect, st::boxTextFg);
}
}
p.setOpacity(alpha);
}
void ChartRulersView::paintCaptionsToRulers(
QPainter &p,
const QRect &r) {
const auto offset = r.y() - st::statisticsChartRulerCaptionSkip;
p.setFont(st::statisticsDetailsBottomCaptionStyle.font);
const auto alpha = p.opacity();
for (auto &ruler : _rulers) {
const auto rulerAlpha = alpha * ruler.alpha;
p.setOpacity(rulerAlpha);
const auto left = _currencyIcon.isNull()
? 0
: _currencyIcon.width() / style::DevicePixelRatio();
for (const auto &line : ruler.lines) {
const auto y = offset + r.height() * line.relativeValue;
const auto hasLinesFilter = _isDouble && _linesFilter;
if (hasLinesFilter) {
p.setPen(_leftPen);
p.setOpacity(rulerAlpha * _linesFilter->alpha(_leftLineId));
} else {
p.setPen(st::windowSubTextFg);
}
if (!_currencyIcon.isNull()) {
const auto iconTop = y
- _currencyIcon.height() / style::DevicePixelRatio()
+ st::statisticsChartRulerCaptionSkip;
p.drawImage(0, iconTop, _currencyIcon);
}
p.drawText(
left,
y,
(!_isDouble)
? line.caption
: _isLeftLineScaled
? line.scaledLineCaption
: line.caption);
if (hasLinesFilter || _rightCustomCaption) {
if (_linesFilter) {
p.setOpacity(rulerAlpha * _linesFilter->alpha(_rightLineId));
}
p.setPen(_rightPen);
p.drawText(
r.width() - line.rightCaptionWidth,
y,
_isLeftLineScaled
? line.caption
: line.scaledLineCaption);
}
}
}
p.setOpacity(alpha);
}
void ChartRulersView::computeRelative(
int newMaxHeight,
int newMinHeight) {
for (auto &ruler : _rulers) {
ruler.computeRelative(newMaxHeight, newMinHeight);
}
}
void ChartRulersView::setAlpha(float64 value) {
for (auto &ruler : _rulers) {
ruler.alpha = ruler.fixedAlpha * (1. - value);
}
_rulers.back().alpha = value;
if (value == 1.) {
while (_rulers.size() > 1) {
const auto startIt = begin(_rulers);
if (!startIt->alpha) {
_rulers.erase(startIt);
} else {
break;
}
}
}
}
void ChartRulersView::add(Limits newHeight, bool animated) {
auto newLinesData = ChartRulersData(
newHeight.max,
newHeight.min,
true,
_isDouble ? _scaledLineRatio : 0.,
_leftCustomCaption,
_rightCustomCaption);
if (_isDouble) {
const auto &font = st::statisticsDetailsBottomCaptionStyle.font;
for (auto &line : newLinesData.lines) {
line.rightCaptionWidth = font->width(_isLeftLineScaled
? line.caption
: line.scaledLineCaption);
}
}
if (!animated) {
_rulers.clear();
}
for (auto &ruler : _rulers) {
ruler.fixedAlpha = ruler.alpha;
}
_rulers.push_back(newLinesData);
if (!animated) {
_rulers.back().alpha = 1.;
}
}
} // namespace Statistic

View File

@@ -0,0 +1,59 @@
/*
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/chart_rulers_data.h"
namespace Data {
struct StatisticalChart;
} // namespace Data
namespace Statistic {
enum class ChartViewType;
struct Limits;
class LinesFilterController;
struct ChartRulersView final {
public:
ChartRulersView();
void setChartData(
const Data::StatisticalChart &chartData,
ChartViewType type,
std::shared_ptr<LinesFilterController> linesFilter);
void paintRulers(QPainter &p, const QRect &r);
void paintCaptionsToRulers(QPainter &p, const QRect &r);
void computeRelative(int newMaxHeight, int newMinHeight);
void setAlpha(float64 value);
void add(Limits newHeight, bool animated);
private:
bool _isDouble = false;
QPen _leftPen;
QPen _rightPen;
int _leftLineId = 0;
int _rightLineId = 0;
QImage _currencyIcon;
Fn<QString(float64)> _leftCustomCaption = nullptr;
Fn<QString(float64)> _rightCustomCaption = nullptr;
std::vector<ChartRulersData> _rulers;
std::shared_ptr<LinesFilterController> _linesFilter;
float64 _scaledLineRatio = 0.;
bool _isLeftLineScaled = false;
};
} // namespace Statistic

View File

@@ -0,0 +1,38 @@
/*
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/chart_view_factory.h"
#include "statistics/statistics_common.h"
#include "statistics/view/linear_chart_view.h"
#include "statistics/view/bar_chart_view.h"
#include "statistics/view/stack_linear_chart_view.h"
namespace Statistic {
std::unique_ptr<AbstractChartView> CreateChartView(ChartViewType type) {
switch (type) {
case ChartViewType::Linear: {
return std::make_unique<LinearChartView>(false);
} break;
case ChartViewType::Bar: {
return std::make_unique<BarChartView>(false);
} break;
case ChartViewType::StackBar: {
return std::make_unique<BarChartView>(true);
} break;
case ChartViewType::DoubleLinear: {
return std::make_unique<LinearChartView>(true);
} break;
case ChartViewType::StackLinear: {
return std::make_unique<StackLinearChartView>();
} break;
default: Unexpected("Type in Statistic::CreateChartView.");
}
}
} // namespace Statistic

View File

@@ -0,0 +1,18 @@
/*
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 Statistic {
class AbstractChartView;
enum class ChartViewType;
[[nodiscard]] std::unique_ptr<AbstractChartView> CreateChartView(
ChartViewType type);
} // namespace Statistic

View File

@@ -0,0 +1,227 @@
/*
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/linear_chart_view.h"
#include "data/data_statistics_chart.h"
#include "statistics/chart_lines_filter_controller.h"
#include "statistics/statistics_common.h"
#include "ui/effects/animation_value_f.h"
#include "ui/painter.h"
#include "styles/style_boxes.h"
#include "styles/style_statistics.h"
namespace Statistic {
namespace {
void PaintChartLine(
QPainter &p,
int lineIndex,
const PaintContext &c,
const DoubleLineRatios &ratios) {
const auto &line = c.chartData.lines[lineIndex];
auto chartPoints = QPolygonF();
constexpr auto kOffset = float64(2);
const auto localStart = int(std::max(0., c.xIndices.min - kOffset));
const auto localEnd = int(std::min(
float64(c.chartData.xPercentage.size() - 1),
c.xIndices.max + kOffset));
const auto ratio = ratios.ratio(line.id);
for (auto i = localStart; i <= localEnd; i++) {
if (line.y[i] < 0) {
continue;
}
const auto xPoint = c.rect.width()
* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)
/ (c.xPercentageLimits.max - c.xPercentageLimits.min));
const auto yPercentage = (line.y[i] * ratio - c.heightLimits.min)
/ float64(c.heightLimits.max - c.heightLimits.min);
const auto yPoint = (1. - yPercentage) * c.rect.height();
chartPoints << QPointF(xPoint, yPoint);
}
p.setPen(QPen(
line.color,
c.footer ? st::lineWidth : st::statisticsChartLineWidth));
p.setBrush(Qt::NoBrush);
p.drawPolyline(chartPoints);
}
} // namespace
LinearChartView::LinearChartView(bool isDouble)
: _cachedLineRatios(isDouble) {
}
LinearChartView::~LinearChartView() = default;
void LinearChartView::paint(QPainter &p, const PaintContext &c) {
const auto cacheToken = LinearChartView::CacheToken(
c.xIndices,
c.xPercentageLimits,
c.heightLimits,
c.rect.size());
const auto opacity = p.opacity();
const auto linesFilter = linesFilterController();
const auto imageSize = c.rect.size() * style::DevicePixelRatio();
const auto cacheScale = 1. / style::DevicePixelRatio();
auto &caches = (c.footer ? _footerCaches : _mainCaches);
for (auto i = 0; i < c.chartData.lines.size(); i++) {
const auto &line = c.chartData.lines[i];
p.setOpacity(linesFilter->alpha(line.id));
if (!p.opacity()) {
continue;
}
auto &cache = caches[line.id];
const auto isSameToken = (cache.lastToken == cacheToken);
if ((isSameToken && cache.hq)
|| (p.opacity() < 1. && !linesFilter->isEnabled(line.id))) {
p.drawImage(c.rect.topLeft(), cache.image);
continue;
}
cache.hq = isSameToken;
auto image = QImage();
image = QImage(
imageSize * (isSameToken ? 1. : cacheScale),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
{
auto imagePainter = QPainter(&image);
auto hq = PainterHighQualityEnabler(imagePainter);
if (!isSameToken) {
imagePainter.scale(cacheScale, cacheScale);
}
PaintChartLine(imagePainter, i, c, _cachedLineRatios);
}
if (!isSameToken) {
image = image.scaled(
imageSize,
Qt::IgnoreAspectRatio,
Qt::FastTransformation);
}
p.drawImage(c.rect.topLeft(), image);
cache.lastToken = cacheToken;
cache.image = std::move(image);
}
p.setOpacity(opacity);
}
void LinearChartView::paintSelectedXIndex(
QPainter &p,
const PaintContext &c,
int selectedXIndex,
float64 progress) {
if (selectedXIndex < 0) {
return;
}
const auto linesFilter = linesFilterController();
auto hq = PainterHighQualityEnabler(p);
auto o = ScopedPainterOpacity(p, progress);
p.setBrush(st::boxBg);
const auto r = st::statisticsDetailsDotRadius;
const auto i = selectedXIndex;
const auto isSameToken = _selectedPoints.isSame(selectedXIndex, c);
auto linePainted = false;
for (const auto &line : c.chartData.lines) {
const auto lineAlpha = linesFilter->alpha(line.id);
const auto useCache = isSameToken
|| (lineAlpha < 1. && !linesFilter->isEnabled(line.id));
if (!useCache) {
// Calculate.
const auto r = _cachedLineRatios.ratio(line.id);
const auto xPoint = c.rect.width()
* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)
/ (c.xPercentageLimits.max - c.xPercentageLimits.min));
const auto yPercentage = (line.y[i] * r - c.heightLimits.min)
/ float64(c.heightLimits.max - c.heightLimits.min);
const auto yPoint = (1. - yPercentage) * c.rect.height();
_selectedPoints.points[line.id] = QPointF(xPoint, yPoint)
+ c.rect.topLeft();
}
if (!linePainted && lineAlpha) {
[[maybe_unused]] const auto o = ScopedPainterOpacity(
p,
p.opacity() * progress * kRulerLineAlpha);
const auto lineRect = QRectF(
begin(_selectedPoints.points)->second.x()
- (st::lineWidth / 2.),
c.rect.y(),
st::lineWidth,
c.rect.height());
p.fillRect(lineRect, st::boxTextFg);
linePainted = true;
}
// Paint.
auto o = ScopedPainterOpacity(p, lineAlpha * p.opacity());
p.setPen(QPen(line.color, st::statisticsChartLineWidth));
p.drawEllipse(_selectedPoints.points[line.id], r, r);
}
_selectedPoints.lastXIndex = selectedXIndex;
_selectedPoints.lastHeightLimits = c.heightLimits;
_selectedPoints.lastXLimits = c.xPercentageLimits;
}
int LinearChartView::findXIndexByPosition(
const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits,
const QRect &rect,
float64 x) {
if ((x < rect.x()) || (x > (rect.x() + rect.width()))) {
return -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 nearest = ((right) > (left)) ? (it - 1) : it;
const auto resultXPercentageIt = ((*nearest) > xPercentageLimits.max)
? (nearest - 1)
: ((*nearest) < xPercentageLimits.min)
? (nearest + 1)
: nearest;
if (resultXPercentageIt == end(chartData.xPercentage)) {
return chartData.xPercentage.size() - 1;
}
return std::distance(begin(chartData.xPercentage), resultXPercentageIt);
}
AbstractChartView::HeightLimits LinearChartView::heightLimits(
Data::StatisticalChart &chartData,
Limits xIndices) {
if (!_cachedLineRatios) {
_cachedLineRatios.init(chartData);
}
return DefaultHeightLimits(
_cachedLineRatios,
linesFilterController(),
chartData,
xIndices);
}
} // namespace Statistic

View File

@@ -0,0 +1,95 @@
/*
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/statistics_common.h"
#include "statistics/view/abstract_chart_view.h"
namespace Data {
struct StatisticalChart;
} // namespace Data
namespace Statistic {
struct Limits;
class LinearChartView final : public AbstractChartView {
public:
LinearChartView(bool isDouble);
~LinearChartView() override final;
void paint(QPainter &p, const PaintContext &c) override;
void paintSelectedXIndex(
QPainter &p,
const PaintContext &c,
int selectedXIndex,
float64 progress) override;
int findXIndexByPosition(
const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits,
const QRect &rect,
float64 x) override;
[[nodiscard]] HeightLimits heightLimits(
Data::StatisticalChart &chartData,
Limits xIndices) override;
private:
DoubleLineRatios _cachedLineRatios;
[[nodiscard]] float64 lineRatio() const;
struct CacheToken final {
explicit CacheToken() = default;
explicit CacheToken(
Limits xIndices,
Limits xPercentageLimits,
Limits heightLimits,
QSize rectSize)
: xIndices(std::move(xIndices))
, xPercentageLimits(std::move(xPercentageLimits))
, heightLimits(std::move(heightLimits))
, rectSize(std::move(rectSize)) {
}
bool operator==(const CacheToken &other) const {
return (rectSize == other.rectSize)
&& (xIndices.min == other.xIndices.min)
&& (xIndices.max == other.xIndices.max)
&& (xPercentageLimits.min == other.xPercentageLimits.min)
&& (xPercentageLimits.max == other.xPercentageLimits.max)
&& (heightLimits.min == other.heightLimits.min)
&& (heightLimits.max == other.heightLimits.max);
}
bool operator!=(const CacheToken &other) const {
return !(*this == other);
}
Limits xIndices;
Limits xPercentageLimits;
Limits heightLimits;
QSize rectSize;
};
struct Cache final {
QImage image;
CacheToken lastToken;
bool hq = false;
};
base::flat_map<int, Cache> _mainCaches;
base::flat_map<int, Cache> _footerCaches;
CachedSelectedPoints _selectedPoints;
};
} // namespace Statistic

View File

@@ -0,0 +1,87 @@
/*
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_chart_common.h"
#include "data/data_statistics_chart.h"
#include "statistics/statistics_common.h"
#include "ui/effects/animation_value_f.h"
namespace Statistic {
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 };
}
Limits FindStackXIndicesFromRawXPercentages(
const Data::StatisticalChart &chartData,
const Limits &rawXPercentageLimits,
const Limits &zoomedInLimitXIndices) {
const auto zoomLimit = Limits{
chartData.xPercentage[zoomedInLimitXIndices.min],
chartData.xPercentage[zoomedInLimitXIndices.max],
};
// Due to a specificity of the stack chart plotting,
// the right edge has a special offset to the left.
// This reduces the number of displayed points by 1,
// but allows the last point to be displayed.
const auto offset = (zoomLimit.max == 1.) ? 0 : -1;
const auto rightShrink = (rawXPercentageLimits.max == 1.)
? ((zoomLimit.max == 1.) ? 0 : 1)
: 0;
const auto n = chartData.xPercentage.size();
auto minIt = -1;
auto maxIt = n;
const auto zoomedIn = Limits{
anim::interpolateF(
zoomLimit.min,
zoomLimit.max,
rawXPercentageLimits.min),
anim::interpolateF(
zoomLimit.min,
zoomLimit.max,
rawXPercentageLimits.max),
};
for (auto i = int(0); i < n; i++) {
if (minIt < 0) {
if (chartData.xPercentage[i] > zoomedIn.min) {
minIt = i;
}
}
if (maxIt >= n) {
if (chartData.xPercentage[i] > zoomedIn.max) {
maxIt = i;
}
}
}
return {
.min = std::clamp(
float64(minIt + offset),
zoomedInLimitXIndices.min,
zoomedInLimitXIndices.max - rightShrink),
.max = std::clamp(
float64(maxIt + offset),
zoomedInLimitXIndices.min,
zoomedInLimitXIndices.max - rightShrink),
};
}
} // namespace Statistic

View File

@@ -0,0 +1,34 @@
/*
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 {
struct Limits;
struct LeftStartAndStep final {
float64 start = 0.;
float64 step = 0.;
};
[[nodiscard]] LeftStartAndStep ComputeLeftStartAndStep(
const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits,
const QRect &rect,
float64 xIndexStart);
[[nodiscard]] Limits FindStackXIndicesFromRawXPercentages(
const Data::StatisticalChart &chartData,
const Limits &rawXPercentageLimits,
const Limits &zoomedInLimitXIndices);
} // namespace Statistic

View File

@@ -0,0 +1,99 @@
/*
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_common.h"
#include "data/data_statistics_chart.h"
#include "statistics/chart_lines_filter_controller.h"
#include "statistics/statistics_common.h"
namespace Statistic {
PiePartData PiePartsPercentage(
const std::vector<float64> &sums,
float64 totalSum,
bool round) {
auto result = PiePartData();
result.parts.reserve(sums.size());
auto stackedPercentage = 0.;
auto sumPercDiffs = 0.;
auto maxPercDiff = 0.;
auto minPercDiff = 0.;
auto maxPercDiffIndex = int(-1);
auto minPercDiffIndex = int(-1);
auto roundedPercentagesSum = 0.;
result.pieHasSinglePart = false;
constexpr auto kPerChar = '%';
for (auto k = 0; k < sums.size(); k++) {
const auto rawPercentage = totalSum ? (sums[k] / totalSum) : 0.;
const auto rounded = round
? (0.01 * std::round(rawPercentage * 100.))
: rawPercentage;
roundedPercentagesSum += rounded;
const auto diff = rawPercentage - rounded;
sumPercDiffs += diff;
const auto diffAbs = std::abs(diff);
if (maxPercDiff < diffAbs) {
maxPercDiff = diffAbs;
maxPercDiffIndex = k;
}
if (minPercDiff < diffAbs) {
minPercDiff = diffAbs;
minPercDiffIndex = k;
}
stackedPercentage += rounded;
result.parts.push_back({
rounded,
stackedPercentage * 360. - 180.,
QString::number(int(rounded * 100)) + kPerChar,
});
result.pieHasSinglePart |= (rounded == 1.);
}
if (round) {
const auto index = (roundedPercentagesSum > 1.)
? maxPercDiffIndex
: minPercDiffIndex;
if (index >= 0) {
result.parts[index].roundedPercentage += sumPercDiffs;
result.parts[index].percentageText = QString::number(
int(result.parts[index].roundedPercentage * 100)) + kPerChar;
const auto angleShrink = (sumPercDiffs) * 360.;
for (auto &part : result.parts) {
part.stackedAngle += angleShrink;
}
}
}
return result;
}
PiePartData PiePartsPercentageByIndices(
const Data::StatisticalChart &chartData,
const std::shared_ptr<LinesFilterController> &linesFilter,
const Limits &xIndices) {
auto sums = std::vector<float64>();
sums.reserve(chartData.lines.size());
auto totalSum = 0.;
for (const auto &line : chartData.lines) {
auto sum = ChartValue(0);
for (auto i = xIndices.min; i <= xIndices.max; i++) {
const auto index = int(base::SafeRound(i));
Assert(index >= 0 && index < line.y.size());
sum += line.y[i];
}
if (linesFilter) {
sum *= linesFilter->alpha(line.id);
}
totalSum += sum;
sums.push_back(sum);
}
return PiePartsPercentage(sums, totalSum, true);
}
} // namespace Statistic

View File

@@ -0,0 +1,39 @@
/*
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 {
struct Limits;
class LinesFilterController;
struct PiePartData final {
struct Part final {
float64 roundedPercentage = 0; // 0.XX.
float64 stackedAngle = 0.;
QString percentageText;
};
std::vector<Part> parts;
bool pieHasSinglePart = false;
};
[[nodiscard]] PiePartData PiePartsPercentage(
const std::vector<float64> &sums,
float64 totalSum,
bool round);
[[nodiscard]] PiePartData PiePartsPercentageByIndices(
const Data::StatisticalChart &chartData,
const std::shared_ptr<LinesFilterController> &linesFilter,
const Limits &xIndices);
} // namespace Statistic

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,150 @@
/*
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 "statistics/view/stack_linear_chart_common.h"
#include "ui/effects/animations.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 PaintContext &c) override;
void paintSelectedXIndex(
QPainter &p,
const PaintContext &c,
int selectedXIndex,
float64 progress) override;
int findXIndexByPosition(
const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits,
const QRect &rect,
float64 x) override;
[[nodiscard]] HeightLimits heightLimits(
Data::StatisticalChart &chartData,
Limits xIndices) override;
LocalZoomResult maybeLocalZoom(const LocalZoomArgs &args) override final;
void handleMouseMove(
const Data::StatisticalChart &chartData,
const QRect &rect,
const QPoint &p) override;
private:
enum class TransitionStep {
PrepareToZoomIn,
PrepareToZoomOut,
ZoomedOut,
};
void paintChartOrZoomAnimation(QPainter &p, const PaintContext &c);
void paintZoomed(QPainter &p, const PaintContext &c);
void paintZoomedFooter(QPainter &p, const PaintContext &c);
void paintPieText(QPainter &p, const PaintContext &c);
[[nodiscard]] bool skipSelectedTranslation() const;
void prepareZoom(const PaintContext &c, TransitionStep step);
void saveZoomRange(const PaintContext &c);
void savePieTextParts(const PaintContext &c);
void applyParts(const std::vector<PiePartData::Part> &parts);
struct SelectedPoints final {
int lastXIndex = -1;
Limits lastHeightLimits;
Limits lastXLimits;
float64 xPoint = 0.;
};
SelectedPoints _selectedPoints;
struct Transition {
struct TransitionLine {
QPointF start;
QPointF end;
float64 angle = 0.;
float64 sum = 0.;
};
std::vector<TransitionLine> lines;
float64 progress = 0;
bool pendingPrepareToZoomIn = false;
Limits zoomedOutXIndices;
Limits zoomedOutXIndicesAdditional;
Limits zoomedOutXPercentage;
Limits zoomedInLimit;
Limits zoomedInLimitXIndices;
Limits zoomedInRange;
Limits zoomedInRangeXIndices;
std::vector<PiePartData::Part> textParts;
} _transition;
std::vector<bool> _skipPoints;
class PiePartController final {
public:
using LineId = int;
bool set(LineId id);
[[nodiscard]] float64 progress(LineId id) const;
[[nodiscard]] QPointF offset(LineId id, float64 angle) const;
[[nodiscard]] LineId selected() const;
[[nodiscard]] bool isFinished() const;
private:
void update(LineId id);
base::flat_map<LineId, crl::time> _startedAt;
LineId _selected = -1;
};
class ChangingPiePartController final {
public:
void setParts(
const std::vector<PiePartData::Part> &was,
const std::vector<PiePartData::Part> &now);
void update();
[[nodiscard]] PiePartData current() const;
[[nodiscard]] bool isFinished() const;
private:
crl::time _startedAt = 0;
std::vector<anim::value> _animValues;
PiePartData _current;
bool _isFinished = true;
};
PiePartController _piePartController;
ChangingPiePartController _changingPieController;
Ui::Animations::Basic _piePartAnimation;
bool _pieHasSinglePart = false;
};
} // namespace Statistic