tdesktop/Telegram/SourceFiles/statistics/view/bar_chart_view.cpp

289 lines
7.9 KiB
C++

/*
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) {
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 {
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;
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 bottomIndex = x - localStart;
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 = 0;
for (auto i = 0; i < chartData.x.size(); i++) {
auto sum = 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),
1);
return {
.full = _cachedHeightLimits.full,
.ranged = { 0., float64(max) },
};
}
} // namespace Statistic