2023-08-27 21:44:50 +00:00
|
|
|
/*
|
|
|
|
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
|
|
|
|
*/
|
2023-11-16 23:41:36 +00:00
|
|
|
#include "statistics/view/bar_chart_view.h"
|
2023-08-27 21:44:50 +00:00
|
|
|
|
2023-09-29 13:41:18 +00:00
|
|
|
#include "data/data_statistics_chart.h"
|
2023-09-27 11:43:37 +00:00
|
|
|
#include "statistics/chart_lines_filter_controller.h"
|
2023-09-18 18:07:02 +00:00
|
|
|
#include "statistics/view/stack_chart_common.h"
|
|
|
|
#include "ui/effects/animation_value_f.h"
|
2023-09-08 09:15:35 +00:00
|
|
|
#include "ui/painter.h"
|
2023-11-17 00:28:33 +00:00
|
|
|
#include "ui/rect.h"
|
|
|
|
#include "styles/style_statistics.h"
|
2023-08-27 22:17:30 +00:00
|
|
|
|
2023-08-27 21:44:50 +00:00
|
|
|
namespace Statistic {
|
|
|
|
|
2023-11-17 00:28:33 +00:00
|
|
|
BarChartView::BarChartView(bool isStack)
|
|
|
|
: _isStack(isStack)
|
|
|
|
, _cachedLineRatios(false) {
|
|
|
|
}
|
|
|
|
|
2023-11-16 23:41:36 +00:00
|
|
|
BarChartView::~BarChartView() = default;
|
2023-08-27 21:44:50 +00:00
|
|
|
|
2023-11-16 23:41:36 +00:00
|
|
|
void BarChartView::paint(QPainter &p, const PaintContext &c) {
|
2023-09-08 09:15:35 +00:00
|
|
|
constexpr auto kOffset = float64(2);
|
|
|
|
_lastPaintedXIndices = {
|
2023-09-19 10:02:50 +00:00
|
|
|
float64(std::max(0., c.xIndices.min - kOffset)),
|
2023-09-08 09:15:35 +00:00
|
|
|
float64(std::min(
|
2023-09-19 10:02:50 +00:00
|
|
|
float64(c.chartData.xPercentage.size() - 1),
|
|
|
|
c.xIndices.max + kOffset)),
|
2023-09-08 09:15:35 +00:00
|
|
|
};
|
2023-08-27 23:40:59 +00:00
|
|
|
|
2023-11-16 23:41:36 +00:00
|
|
|
BarChartView::paintChartAndSelected(p, c);
|
2023-09-08 09:15:35 +00:00
|
|
|
}
|
|
|
|
|
2023-11-16 23:41:36 +00:00
|
|
|
void BarChartView::paintChartAndSelected(
|
2023-09-08 09:15:35 +00:00
|
|
|
QPainter &p,
|
2023-09-19 10:02:50 +00:00
|
|
|
const PaintContext &c) {
|
2023-09-08 09:15:35 +00:00
|
|
|
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
|
|
|
const auto &[leftStart, w] = ComputeLeftStartAndStep(
|
2023-09-19 10:02:50 +00:00
|
|
|
c.chartData,
|
|
|
|
c.xPercentageLimits,
|
|
|
|
c.rect,
|
2023-09-08 09:15:35 +00:00
|
|
|
localStart);
|
|
|
|
|
2023-11-17 00:28:33 +00:00
|
|
|
p.setClipRect(0, 0, c.rect.width() * 2, rect::bottom(c.rect));
|
|
|
|
|
2023-09-08 09:15:35 +00:00
|
|
|
const auto opacity = p.opacity();
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
|
|
|
2023-09-19 10:02:50 +00:00
|
|
|
auto bottoms = std::vector<float64>(
|
|
|
|
localEnd - localStart + 1,
|
|
|
|
-c.rect.y());
|
2023-09-08 09:15:35 +00:00
|
|
|
auto selectedBottoms = std::vector<float64>();
|
2023-11-17 00:28:33 +00:00
|
|
|
const auto hasSelectedXIndex = _isStack
|
|
|
|
&& !c.footer
|
|
|
|
&& (_lastSelectedXIndex >= 0);
|
2023-09-08 09:15:35 +00:00
|
|
|
if (hasSelectedXIndex) {
|
2023-09-19 10:02:50 +00:00
|
|
|
selectedBottoms = std::vector<float64>(c.chartData.lines.size(), 0);
|
2023-09-08 09:15:35 +00:00
|
|
|
constexpr auto kSelectedAlpha = 0.5;
|
|
|
|
p.setOpacity(
|
|
|
|
anim::interpolateF(1.0, kSelectedAlpha, _lastSelectedXProgress));
|
|
|
|
}
|
|
|
|
|
2023-09-19 10:02:50 +00:00
|
|
|
for (auto i = 0; i < c.chartData.lines.size(); i++) {
|
|
|
|
const auto &line = c.chartData.lines[i];
|
2023-09-08 09:15:35 +00:00
|
|
|
auto path = QPainterPath();
|
|
|
|
for (auto x = localStart; x <= localEnd; x++) {
|
|
|
|
if (line.y[x] <= 0) {
|
2023-08-27 23:40:59 +00:00
|
|
|
continue;
|
|
|
|
}
|
2023-09-19 10:02:50 +00:00
|
|
|
const auto yPercentage = (line.y[x] - c.heightLimits.min)
|
|
|
|
/ float64(c.heightLimits.max - c.heightLimits.min);
|
|
|
|
const auto yPoint = yPercentage
|
|
|
|
* c.rect.height()
|
2023-09-27 11:43:37 +00:00
|
|
|
* linesFilterController()->alpha(line.id);
|
2023-09-08 09:15:35 +00:00
|
|
|
|
|
|
|
const auto bottomIndex = x - localStart;
|
2023-08-27 23:40:59 +00:00
|
|
|
const auto column = QRectF(
|
2023-09-08 09:15:35 +00:00
|
|
|
leftStart + (x - localStart) * w,
|
2023-09-19 10:02:50 +00:00
|
|
|
c.rect.height() - bottoms[bottomIndex] - yPoint,
|
2023-08-27 23:40:59 +00:00
|
|
|
w,
|
|
|
|
yPoint);
|
2023-09-08 09:15:35 +00:00
|
|
|
if (hasSelectedXIndex && (x == _lastSelectedXIndex)) {
|
|
|
|
selectedBottoms[i] = column.y();
|
|
|
|
}
|
2023-11-17 00:28:33 +00:00
|
|
|
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);
|
2023-08-27 23:40:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-08 09:15:35 +00:00
|
|
|
for (auto i = 0; i < selectedBottoms.size(); i++) {
|
|
|
|
p.setOpacity(opacity);
|
|
|
|
if (selectedBottoms[i] <= 0) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-09-19 10:02:50 +00:00
|
|
|
const auto &line = c.chartData.lines[i];
|
2023-09-27 11:43:37 +00:00
|
|
|
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);
|
2023-09-08 09:15:35 +00:00
|
|
|
|
|
|
|
const auto column = QRectF(
|
|
|
|
leftStart + (_lastSelectedXIndex - localStart) * w,
|
|
|
|
selectedBottoms[i],
|
|
|
|
w,
|
|
|
|
yPoint);
|
|
|
|
p.fillRect(column, line.color);
|
2023-08-27 23:40:59 +00:00
|
|
|
}
|
2023-11-17 00:28:33 +00:00
|
|
|
|
|
|
|
p.setClipping(false);
|
2023-08-27 21:44:50 +00:00
|
|
|
}
|
|
|
|
|
2023-11-16 23:41:36 +00:00
|
|
|
void BarChartView::paintSelectedXIndex(
|
2023-08-27 21:44:50 +00:00
|
|
|
QPainter &p,
|
2023-09-19 10:02:50 +00:00
|
|
|
const PaintContext &c,
|
2023-09-05 14:03:08 +00:00
|
|
|
int selectedXIndex,
|
|
|
|
float64 progress) {
|
2023-10-05 10:22:17 +00:00
|
|
|
const auto was = _lastSelectedXIndex;
|
2023-09-08 09:15:35 +00:00
|
|
|
_lastSelectedXIndex = selectedXIndex;
|
|
|
|
_lastSelectedXProgress = progress;
|
2023-11-17 00:28:33 +00:00
|
|
|
|
2023-11-22 21:08:28 +00:00
|
|
|
if ((_lastSelectedXIndex < 0) && (was < 0)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-11-17 00:28:33 +00:00
|
|
|
if (_isStack) {
|
2023-11-22 21:08:28 +00:00
|
|
|
BarChartView::paintChartAndSelected(p, c);
|
2023-11-17 00:28:33 +00:00
|
|
|
} else {
|
|
|
|
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;
|
2023-10-05 10:22:17 +00:00
|
|
|
}
|
2023-08-27 21:44:50 +00:00
|
|
|
}
|
|
|
|
|
2023-11-16 23:41:36 +00:00
|
|
|
int BarChartView::findXIndexByPosition(
|
2023-09-05 17:04:37 +00:00
|
|
|
const Data::StatisticalChart &chartData,
|
|
|
|
const Limits &xPercentageLimits,
|
|
|
|
const QRect &rect,
|
|
|
|
float64 xPos) {
|
2023-10-05 10:22:17 +00:00
|
|
|
if ((xPos < rect.x()) || (xPos > (rect.x() + rect.width()))) {
|
|
|
|
return _lastSelectedXIndex = -1;
|
2023-09-08 09:15:35 +00:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-05 10:22:17 +00:00
|
|
|
return _lastSelectedXIndex = -1;
|
2023-09-05 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
2023-11-16 23:41:36 +00:00
|
|
|
AbstractChartView::HeightLimits BarChartView::heightLimits(
|
2023-08-27 21:44:50 +00:00
|
|
|
Data::StatisticalChart &chartData,
|
|
|
|
Limits xIndices) {
|
2023-11-17 00:28:33 +00:00
|
|
|
if (!_isStack) {
|
|
|
|
if (!_cachedLineRatios) {
|
|
|
|
_cachedLineRatios.init(chartData);
|
|
|
|
}
|
|
|
|
|
|
|
|
return DefaultHeightLimits(
|
|
|
|
_cachedLineRatios,
|
|
|
|
linesFilterController(),
|
|
|
|
chartData,
|
|
|
|
xIndices);
|
|
|
|
}
|
2023-09-27 11:43:37 +00:00
|
|
|
_cachedHeightLimits = {};
|
2023-08-27 22:17:30 +00:00
|
|
|
if (_cachedHeightLimits.ySum.empty()) {
|
|
|
|
_cachedHeightLimits.ySum.reserve(chartData.x.size());
|
|
|
|
|
|
|
|
auto maxValueFull = 0;
|
|
|
|
for (auto i = 0; i < chartData.x.size(); i++) {
|
|
|
|
auto sum = 0;
|
|
|
|
for (const auto &line : chartData.lines) {
|
2023-09-27 11:43:37 +00:00
|
|
|
if (linesFilterController()->isEnabled(line.id)) {
|
2023-08-27 22:17:30 +00:00
|
|
|
sum += line.y[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_cachedHeightLimits.ySum.push_back(sum);
|
|
|
|
maxValueFull = std::max(sum, maxValueFull);
|
|
|
|
}
|
|
|
|
|
|
|
|
_cachedHeightLimits.ySumSegmentTree = SegmentTree(
|
|
|
|
_cachedHeightLimits.ySum);
|
|
|
|
_cachedHeightLimits.full = { 0., float64(maxValueFull) };
|
|
|
|
}
|
2023-10-09 14:59:59 +00:00
|
|
|
const auto max = std::max(
|
|
|
|
_cachedHeightLimits.ySumSegmentTree.rMaxQ(
|
|
|
|
xIndices.min,
|
|
|
|
xIndices.max),
|
|
|
|
1);
|
2023-08-27 22:17:30 +00:00
|
|
|
return {
|
|
|
|
.full = _cachedHeightLimits.full,
|
|
|
|
.ranged = { 0., float64(max) },
|
|
|
|
};
|
2023-08-27 21:44:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Statistic
|