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
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:
103
Telegram/SourceFiles/statistics/view/abstract_chart_view.cpp
Normal file
103
Telegram/SourceFiles/statistics/view/abstract_chart_view.cpp
Normal 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
|
||||
127
Telegram/SourceFiles/statistics/view/abstract_chart_view.h
Normal file
127
Telegram/SourceFiles/statistics/view/abstract_chart_view.h
Normal 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 ▭
|
||||
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
|
||||
286
Telegram/SourceFiles/statistics/view/bar_chart_view.cpp
Normal file
286
Telegram/SourceFiles/statistics/view/bar_chart_view.cpp
Normal 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
|
||||
66
Telegram/SourceFiles/statistics/view/bar_chart_view.h
Normal file
66
Telegram/SourceFiles/statistics/view/bar_chart_view.h
Normal 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
|
||||
205
Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp
Normal file
205
Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp
Normal 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
|
||||
59
Telegram/SourceFiles/statistics/view/chart_rulers_view.h
Normal file
59
Telegram/SourceFiles/statistics/view/chart_rulers_view.h
Normal 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
|
||||
38
Telegram/SourceFiles/statistics/view/chart_view_factory.cpp
Normal file
38
Telegram/SourceFiles/statistics/view/chart_view_factory.cpp
Normal 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
|
||||
18
Telegram/SourceFiles/statistics/view/chart_view_factory.h
Normal file
18
Telegram/SourceFiles/statistics/view/chart_view_factory.h
Normal 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
|
||||
227
Telegram/SourceFiles/statistics/view/linear_chart_view.cpp
Normal file
227
Telegram/SourceFiles/statistics/view/linear_chart_view.cpp
Normal 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
|
||||
95
Telegram/SourceFiles/statistics/view/linear_chart_view.h
Normal file
95
Telegram/SourceFiles/statistics/view/linear_chart_view.h
Normal 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
|
||||
87
Telegram/SourceFiles/statistics/view/stack_chart_common.cpp
Normal file
87
Telegram/SourceFiles/statistics/view/stack_chart_common.cpp
Normal 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
|
||||
34
Telegram/SourceFiles/statistics/view/stack_chart_common.h
Normal file
34
Telegram/SourceFiles/statistics/view/stack_chart_common.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
1010
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp
Normal file
1010
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp
Normal file
File diff suppressed because it is too large
Load Diff
150
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h
Normal file
150
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h
Normal 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
|
||||
Reference in New Issue
Block a user