2023-05-24 15:30:30 +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-08-27 20:47:04 +00:00
|
|
|
#include "statistics/view/linear_chart_view.h"
|
2023-05-24 15:30:30 +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-05-26 13:04:33 +00:00
|
|
|
#include "statistics/statistics_common.h"
|
2023-05-26 13:04:49 +00:00
|
|
|
#include "ui/effects/animation_value_f.h"
|
2023-07-07 08:28:50 +00:00
|
|
|
#include "ui/painter.h"
|
2023-05-24 15:30:30 +00:00
|
|
|
#include "styles/style_boxes.h"
|
2023-07-07 08:28:50 +00:00
|
|
|
#include "styles/style_statistics.h"
|
2023-05-24 15:30:30 +00:00
|
|
|
|
|
|
|
namespace Statistic {
|
2023-07-26 22:03:52 +00:00
|
|
|
namespace {
|
|
|
|
|
2023-09-29 14:21:53 +00:00
|
|
|
[[nodiscard]] float64 Ratio(
|
|
|
|
const LinearChartView::CachedLineRatios &ratios,
|
|
|
|
int id) {
|
2023-09-11 13:19:46 +00:00
|
|
|
return (id == 1) ? ratios.first : ratios.second;
|
|
|
|
}
|
|
|
|
|
2023-07-26 22:03:52 +00:00
|
|
|
void PaintChartLine(
|
|
|
|
QPainter &p,
|
|
|
|
int lineIndex,
|
2023-09-19 10:02:50 +00:00
|
|
|
const PaintContext &c,
|
2023-09-11 13:19:46 +00:00
|
|
|
const LinearChartView::CachedLineRatios &ratios) {
|
2023-09-19 10:02:50 +00:00
|
|
|
const auto &line = c.chartData.lines[lineIndex];
|
2023-07-26 22:03:52 +00:00
|
|
|
|
|
|
|
auto chartPoints = QPolygonF();
|
|
|
|
|
2023-09-28 00:22:25 +00:00
|
|
|
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));
|
2023-07-26 22:03:52 +00:00
|
|
|
|
2023-09-11 13:19:46 +00:00
|
|
|
const auto ratio = Ratio(ratios, line.id);
|
|
|
|
|
2023-07-26 22:03:52 +00:00
|
|
|
for (auto i = localStart; i <= localEnd; i++) {
|
|
|
|
if (line.y[i] < 0) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-09-19 10:02:50 +00:00
|
|
|
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();
|
2023-07-26 22:03:52 +00:00
|
|
|
chartPoints << QPointF(xPoint, yPoint);
|
|
|
|
}
|
2023-10-03 16:09:34 +00:00
|
|
|
p.setPen(QPen(
|
|
|
|
line.color,
|
|
|
|
c.footer ? st::lineWidth : st::statisticsChartLineWidth));
|
2023-07-26 22:03:52 +00:00
|
|
|
p.setBrush(Qt::NoBrush);
|
|
|
|
p.drawPolyline(chartPoints);
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
2023-05-24 15:30:30 +00:00
|
|
|
|
2023-09-11 12:22:24 +00:00
|
|
|
LinearChartView::LinearChartView(bool isDouble)
|
2023-09-11 13:19:46 +00:00
|
|
|
: _cachedLineRatios(CachedLineRatios{ isDouble ? 0 : 1, isDouble ? 0 : 1 }) {
|
2023-09-11 12:22:24 +00:00
|
|
|
}
|
2023-07-26 22:03:52 +00:00
|
|
|
|
2023-08-27 21:16:48 +00:00
|
|
|
LinearChartView::~LinearChartView() = default;
|
|
|
|
|
2023-09-19 10:02:50 +00:00
|
|
|
void LinearChartView::paint(QPainter &p, const PaintContext &c) {
|
2023-07-26 22:10:24 +00:00
|
|
|
const auto cacheToken = LinearChartView::CacheToken(
|
2023-09-19 10:02:50 +00:00
|
|
|
c.xIndices,
|
|
|
|
c.xPercentageLimits,
|
|
|
|
c.heightLimits,
|
|
|
|
c.rect.size());
|
2023-07-26 22:03:52 +00:00
|
|
|
|
2023-09-19 14:35:39 +00:00
|
|
|
const auto opacity = p.opacity();
|
2023-09-27 11:43:37 +00:00
|
|
|
const auto linesFilter = linesFilterController();
|
2023-09-19 10:02:50 +00:00
|
|
|
const auto imageSize = c.rect.size() * style::DevicePixelRatio();
|
2023-07-26 22:49:53 +00:00
|
|
|
const auto cacheScale = 1. / style::DevicePixelRatio();
|
2023-09-19 10:02:50 +00:00
|
|
|
auto &caches = (c.footer ? _footerCaches : _mainCaches);
|
2023-07-26 22:49:53 +00:00
|
|
|
|
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-27 11:43:37 +00:00
|
|
|
p.setOpacity(linesFilter->alpha(line.id));
|
2023-07-26 22:03:52 +00:00
|
|
|
if (!p.opacity()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-07-26 22:49:53 +00:00
|
|
|
auto &cache = caches[line.id];
|
2023-07-26 22:03:52 +00:00
|
|
|
|
|
|
|
const auto isSameToken = (cache.lastToken == cacheToken);
|
2023-07-26 22:49:53 +00:00
|
|
|
if ((isSameToken && cache.hq)
|
2023-09-27 11:43:37 +00:00
|
|
|
|| (p.opacity() < 1. && !linesFilter->isEnabled(line.id))) {
|
2023-09-19 10:02:50 +00:00
|
|
|
p.drawImage(c.rect.topLeft(), cache.image);
|
2023-07-26 22:03:52 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
cache.hq = isSameToken;
|
|
|
|
auto image = QImage();
|
|
|
|
image = QImage(
|
2023-07-26 22:49:53 +00:00
|
|
|
imageSize * (isSameToken ? 1. : cacheScale),
|
2023-07-26 22:03:52 +00:00
|
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
|
|
image.setDevicePixelRatio(style::DevicePixelRatio());
|
|
|
|
image.fill(Qt::transparent);
|
|
|
|
{
|
|
|
|
auto imagePainter = QPainter(&image);
|
2023-07-26 22:49:53 +00:00
|
|
|
auto hq = PainterHighQualityEnabler(imagePainter);
|
|
|
|
if (!isSameToken) {
|
|
|
|
imagePainter.scale(cacheScale, cacheScale);
|
2023-07-26 22:03:52 +00:00
|
|
|
}
|
|
|
|
|
2023-09-19 10:02:50 +00:00
|
|
|
PaintChartLine(imagePainter, i, c, _cachedLineRatios);
|
2023-07-26 22:03:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!isSameToken) {
|
2023-07-26 22:49:53 +00:00
|
|
|
image = image.scaled(
|
|
|
|
imageSize,
|
|
|
|
Qt::IgnoreAspectRatio,
|
|
|
|
Qt::FastTransformation);
|
2023-07-26 22:03:52 +00:00
|
|
|
}
|
2023-09-19 10:02:50 +00:00
|
|
|
p.drawImage(c.rect.topLeft(), image);
|
2023-07-26 22:03:52 +00:00
|
|
|
cache.lastToken = cacheToken;
|
|
|
|
cache.image = std::move(image);
|
|
|
|
}
|
2023-09-19 14:35:39 +00:00
|
|
|
p.setOpacity(opacity);
|
2023-07-26 22:03:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 00:13:32 +00:00
|
|
|
void LinearChartView::paintSelectedXIndex(
|
|
|
|
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-07-27 00:13:32 +00:00
|
|
|
if (selectedXIndex < 0) {
|
|
|
|
return;
|
|
|
|
}
|
2023-09-27 11:43:37 +00:00
|
|
|
const auto linesFilter = linesFilterController();
|
2023-09-05 14:03:08 +00:00
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
|
|
auto o = ScopedPainterOpacity(p, progress);
|
2023-07-27 00:13:32 +00:00
|
|
|
p.setBrush(st::boxBg);
|
|
|
|
const auto r = st::statisticsDetailsDotRadius;
|
|
|
|
const auto i = selectedXIndex;
|
|
|
|
const auto isSameToken = (_selectedPoints.lastXIndex == selectedXIndex)
|
2023-09-19 10:02:50 +00:00
|
|
|
&& (_selectedPoints.lastHeightLimits.min == c.heightLimits.min)
|
|
|
|
&& (_selectedPoints.lastHeightLimits.max == c.heightLimits.max)
|
|
|
|
&& (_selectedPoints.lastXLimits.min == c.xPercentageLimits.min)
|
|
|
|
&& (_selectedPoints.lastXLimits.max == c.xPercentageLimits.max);
|
2023-09-05 14:03:08 +00:00
|
|
|
auto linePainted = false;
|
2023-09-19 10:02:50 +00:00
|
|
|
for (const auto &line : c.chartData.lines) {
|
2023-09-27 11:43:37 +00:00
|
|
|
const auto lineAlpha = linesFilter->alpha(line.id);
|
2023-07-27 00:13:32 +00:00
|
|
|
const auto useCache = isSameToken
|
2023-09-27 11:43:37 +00:00
|
|
|
|| (lineAlpha < 1. && !linesFilter->isEnabled(line.id));
|
2023-07-27 00:13:32 +00:00
|
|
|
if (!useCache) {
|
|
|
|
// Calculate.
|
2023-09-11 13:19:46 +00:00
|
|
|
const auto r = Ratio(_cachedLineRatios, line.id);
|
2023-09-19 10:02:50 +00:00
|
|
|
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();
|
2023-07-27 00:13:32 +00:00
|
|
|
_selectedPoints.points[line.id] = QPointF(xPoint, yPoint)
|
2023-09-19 10:02:50 +00:00
|
|
|
+ c.rect.topLeft();
|
2023-07-27 00:13:32 +00:00
|
|
|
}
|
|
|
|
|
2023-09-05 14:03:08 +00:00
|
|
|
if (!linePainted) {
|
2023-10-02 23:07:05 +00:00
|
|
|
[[maybe_unused]] const auto o = ScopedPainterOpacity(
|
|
|
|
p,
|
|
|
|
p.opacity() * progress * kRulerLineAlpha);
|
2023-09-05 14:03:08 +00:00
|
|
|
const auto lineRect = QRectF(
|
2023-10-02 23:07:05 +00:00
|
|
|
begin(_selectedPoints.points)->second.x()
|
2023-09-05 14:03:08 +00:00
|
|
|
- (st::lineWidth / 2.),
|
2023-09-19 10:02:50 +00:00
|
|
|
c.rect.y(),
|
2023-09-05 14:03:08 +00:00
|
|
|
st::lineWidth,
|
2023-09-19 10:02:50 +00:00
|
|
|
c.rect.height());
|
2023-10-02 23:07:05 +00:00
|
|
|
p.fillRect(lineRect, st::boxTextFg);
|
2023-09-05 14:03:08 +00:00
|
|
|
linePainted = true;
|
|
|
|
}
|
|
|
|
|
2023-07-27 00:13:32 +00:00
|
|
|
// 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;
|
2023-09-19 10:02:50 +00:00
|
|
|
_selectedPoints.lastHeightLimits = c.heightLimits;
|
|
|
|
_selectedPoints.lastXLimits = c.xPercentageLimits;
|
2023-07-27 00:13:32 +00:00
|
|
|
}
|
|
|
|
|
2023-09-05 17:04:37 +00:00
|
|
|
int LinearChartView::findXIndexByPosition(
|
|
|
|
const Data::StatisticalChart &chartData,
|
|
|
|
const Limits &xPercentageLimits,
|
|
|
|
const QRect &rect,
|
|
|
|
float64 x) {
|
2023-09-28 00:22:25 +00:00
|
|
|
if ((x < rect.x()) || (x > (rect.x() + rect.width()))) {
|
|
|
|
return -1;
|
2023-09-05 17:04:37 +00:00
|
|
|
}
|
|
|
|
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;
|
2023-09-28 00:22:25 +00:00
|
|
|
const auto nearest = ((right) > (left)) ? (it - 1) : it;
|
|
|
|
const auto resultXPercentageIt = ((*nearest) > xPercentageLimits.max)
|
|
|
|
? (nearest - 1)
|
|
|
|
: ((*nearest) < xPercentageLimits.min)
|
|
|
|
? (nearest + 1)
|
|
|
|
: nearest;
|
|
|
|
return std::distance(begin(chartData.xPercentage), resultXPercentageIt);
|
2023-09-05 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
2023-08-27 21:39:17 +00:00
|
|
|
AbstractChartView::HeightLimits LinearChartView::heightLimits(
|
|
|
|
Data::StatisticalChart &chartData,
|
|
|
|
Limits xIndices) {
|
2023-09-11 13:19:46 +00:00
|
|
|
if (!_cachedLineRatios.first) {
|
|
|
|
// Double Linear calculation.
|
|
|
|
if (chartData.lines.size() != 2) {
|
|
|
|
_cachedLineRatios.first = 1.;
|
|
|
|
_cachedLineRatios.second = 1.;
|
|
|
|
} else {
|
|
|
|
const auto firstMax = chartData.lines.front().maxValue;
|
|
|
|
const auto secondMax = chartData.lines.back().maxValue;
|
|
|
|
if (firstMax > secondMax) {
|
|
|
|
_cachedLineRatios.first = 1.;
|
|
|
|
_cachedLineRatios.second = firstMax / float64(secondMax);
|
|
|
|
} else {
|
|
|
|
_cachedLineRatios.first = secondMax / float64(firstMax);
|
|
|
|
_cachedLineRatios.second = 1.;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:39:17 +00:00
|
|
|
auto minValue = std::numeric_limits<int>::max();
|
|
|
|
auto maxValue = 0;
|
|
|
|
|
|
|
|
auto minValueFull = std::numeric_limits<int>::max();
|
|
|
|
auto maxValueFull = 0;
|
|
|
|
for (auto &l : chartData.lines) {
|
2023-09-27 11:43:37 +00:00
|
|
|
if (!linesFilterController()->isEnabled(l.id)) {
|
2023-08-27 21:39:17 +00:00
|
|
|
continue;
|
|
|
|
}
|
2023-09-11 13:19:46 +00:00
|
|
|
const auto r = Ratio(_cachedLineRatios, l.id);
|
2023-08-27 21:39:17 +00:00
|
|
|
const auto lineMax = l.segmentTree.rMaxQ(xIndices.min, xIndices.max);
|
|
|
|
const auto lineMin = l.segmentTree.rMinQ(xIndices.min, xIndices.max);
|
2023-09-11 13:19:46 +00:00
|
|
|
maxValue = std::max(int(lineMax * r), maxValue);
|
|
|
|
minValue = std::min(int(lineMin * r), minValue);
|
2023-08-27 21:39:17 +00:00
|
|
|
|
2023-09-11 13:19:46 +00:00
|
|
|
maxValueFull = std::max(int(l.maxValue * r), maxValueFull);
|
|
|
|
minValueFull = std::min(int(l.minValue * r), minValueFull);
|
2023-08-27 21:39:17 +00:00
|
|
|
}
|
2023-10-09 14:59:59 +00:00
|
|
|
if (maxValue == minValue) {
|
|
|
|
maxValue = chartData.maxValue;
|
|
|
|
minValue = chartData.minValue;
|
|
|
|
}
|
2023-08-27 21:39:17 +00:00
|
|
|
return {
|
|
|
|
.full = Limits{ float64(minValueFull), float64(maxValueFull) },
|
|
|
|
.ranged = Limits{ float64(minValue), float64(maxValue) },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-05-24 15:30:30 +00:00
|
|
|
} // namespace Statistic
|