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,71 @@
/*
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/chart_lines_filter_controller.h"
namespace Statistic {
LinesFilterController::LinesFilterController() = default;
void LinesFilterController::setEnabled(int id, bool enabled, crl::time now) {
const auto it = _entries.find(id);
if (it == end(_entries)) {
_entries[id] = Entry{
.enabled = enabled,
.startedAt = now,
.anim = anim::value(enabled ? 0. : 1., enabled ? 1. : 0.),
};
} else if (it->second.enabled != enabled) {
auto &entry = it->second;
entry.enabled = enabled;
entry.startedAt = now;
entry.dtCurrent = 0.;
entry.anim.start(enabled ? 1. : 0.);
}
_isFinished = false;
}
bool LinesFilterController::isFinished() const {
return _isFinished;
}
bool LinesFilterController::isEnabled(int id) const {
const auto it = _entries.find(id);
return (it == end(_entries)) ? true : it->second.enabled;
}
float64 LinesFilterController::alpha(int id) const {
const auto it = _entries.find(id);
return (it == end(_entries)) ? 1. : it->second.alpha;
}
void LinesFilterController::tick(float64 dtSpeed) {
auto finishedCount = 0;
auto idsToRemove = std::vector<int>();
for (auto &[id, entry] : _entries) {
if (!entry.startedAt) {
continue;
}
entry.dtCurrent = std::min(entry.dtCurrent + dtSpeed, 1.);
entry.anim.update(entry.dtCurrent, anim::easeInCubic);
const auto progress = entry.anim.current();
entry.alpha = std::clamp(progress, 0., 1.);
if ((entry.alpha == 1.) && entry.enabled) {
idsToRemove.push_back(id);
}
if (entry.anim.current() == entry.anim.to()) {
finishedCount++;
entry.anim.finish();
}
}
_isFinished = (finishedCount == _entries.size());
for (const auto &id : idsToRemove) {
_entries.remove(id);
}
}
} // 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
#include "ui/effects/animation_value.h"
namespace Statistic {
class LinesFilterController final {
public:
LinesFilterController();
void setEnabled(int id, bool enabled, crl::time now);
[[nodiscard]] bool isEnabled(int id) const;
[[nodiscard]] bool isFinished() const;
[[nodiscard]] float64 alpha(int id) const;
void tick(float64 dtSpeed);
private:
struct Entry final {
bool enabled = false;
crl::time startedAt = 0;
float64 alpha = 1.;
anim::value anim;
float64 dtCurrent = 0.;
};
base::flat_map<int, Entry> _entries;
bool _isFinished = true;
};
} // namespace Statistic

View File

@@ -0,0 +1,135 @@
/*
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/chart_rulers_data.h"
#include "lang/lang_tag.h"
#include <QLocale>
namespace Statistic {
namespace {
constexpr auto kMinLines = ChartValue(2);
constexpr auto kMaxLines = ChartValue(6);
constexpr auto kStep = 5.;
[[nodiscard]] ChartValue Round(ChartValue maxValue) {
const auto k = ChartValue(maxValue / kStep);
return (k % 10 == 0) ? maxValue : ((maxValue / 10 + 1) * 10);
}
[[nodiscard]] QString Format(ChartValue absoluteValue) {
static constexpr auto kTooMuch = ChartValue(10'000);
return (absoluteValue >= kTooMuch)
? Lang::FormatCountToShort(absoluteValue).string
: QLocale().toString(absoluteValue);
}
} // namespace
ChartRulersData::ChartRulersData(
ChartValue newMaxHeight,
ChartValue newMinHeight,
bool useMinHeight,
float64 rightRatio,
Fn<QString(float64)> leftCustomCaption,
Fn<QString(float64)> rightCustomCaption) {
if (!useMinHeight) {
const auto v = (newMaxHeight > 100)
? Round(newMaxHeight)
: newMaxHeight;
const auto step = std::max(
ChartValue(1),
ChartValue(std::ceil(v / kStep)));
auto n = kMaxLines;
if (v < kMaxLines) {
n = std::max(2, int(v + 1));
} else if (v / 2 < kMaxLines) {
n = v / 2 + 1;
if (v % 2 != 0) {
n++;
}
}
lines.resize(n);
for (auto i = 1; i < n; i++) {
auto &line = lines[i];
line.absoluteValue = i * step;
line.caption = Lang::FormatCountToShort(
line.absoluteValue).string;
}
} else {
auto n = int(0);
const auto diff = newMaxHeight - newMinHeight;
auto step = 0.;
if (diff == 0) {
newMinHeight--;
n = kMaxLines / 2;
step = 1.;
} else if (diff < kMaxLines) {
n = std::max(kMinLines, diff + 1);
step = 1.;
} else if (diff / 2 < kMaxLines) {
n = diff / 2 + diff % 2 + 1;
step = 2.;
} else {
step = (newMaxHeight - newMinHeight) / kStep;
if (step <= 0) {
step = 1;
n = std::max(kMinLines, newMaxHeight - newMinHeight + 1);
} else {
n = 6;
}
}
lines.resize(n);
const auto diffAbsoluteValue = ChartValue((n - 1) * step);
const auto skipFloatValues = (step / rightRatio) < 1;
for (auto i = 0; i < n; i++) {
auto &line = lines[i];
const auto value = ChartValue(i * step);
line.absoluteValue = newMinHeight + value;
line.relativeValue = 1. - value / float64(diffAbsoluteValue);
line.caption = leftCustomCaption
? leftCustomCaption(line.absoluteValue)
: Format(line.absoluteValue);
if (rightRatio > 0 || rightCustomCaption) {
const auto v = (newMinHeight + i * step) / rightRatio;
line.scaledLineCaption = rightCustomCaption
? rightCustomCaption(line.absoluteValue)
: (!skipFloatValues)
? Format(v)
: ((v - ChartValue(v)) < 0.01)
? Format(v)
: QString();
}
}
}
}
void ChartRulersData::computeRelative(
ChartValue newMaxHeight,
ChartValue newMinHeight) {
for (auto &line : lines) {
line.relativeValue = 1.
- ((line.absoluteValue - newMinHeight)
/ (newMaxHeight - newMinHeight));
}
}
ChartValue ChartRulersData::LookupHeight(ChartValue maxValue) {
const auto v = (maxValue > 100) ? Round(maxValue) : maxValue;
const auto step = ChartValue(std::ceil(v / kStep));
return step * kStep;
}
} // namespace Statistic

View File

@@ -0,0 +1,44 @@
/*
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_types.h"
namespace Statistic {
struct ChartRulersData final {
public:
ChartRulersData(
ChartValue newMaxHeight,
ChartValue newMinHeight,
bool useMinHeight,
float64 rightRatio,
Fn<QString(float64)> leftCustomCaption = nullptr,
Fn<QString(float64)> rightCustomCaption = nullptr);
void computeRelative(
ChartValue newMaxHeight,
ChartValue newMinHeight);
[[nodiscard]] static ChartValue LookupHeight(ChartValue maxValue);
struct Line final {
float64 absoluteValue = 0.;
float64 relativeValue = 0.;
QString caption;
QString scaledLineCaption;
float64 rightCaptionWidth = 0.;
};
std::vector<Line> lines;
float64 alpha = 0.;
float64 fixedAlpha = 1.;
};
} // namespace Statistic

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
/*
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 "data/data_statistics_chart.h"
#include "statistics/view/chart_rulers_view.h"
#include "statistics/statistics_common.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
namespace Statistic {
class RpMouseWidget;
class PointDetailsWidget;
class ChartLinesFilterWidget;
class AbstractChartView;
class Header;
class LinesFilterController;
class ChartWidget : public Ui::RpWidget {
public:
ChartWidget(not_null<Ui::RpWidget*> parent);
void setChartData(Data::StatisticalChart chartData, ChartViewType type);
void setTitle(rpl::producer<QString> &&title);
void setZoomedChartData(
Data::StatisticalChart chartData,
float64 x,
ChartViewType type);
void addRuler(Limits newHeight, bool animated);
[[nodiscard]] rpl::producer<float64> zoomRequests();
struct BottomCaptionLineData final {
int step = 0;
int stepMax = 0;
int stepMin = 0;
int stepMinFast = 0;
int stepRaw = 0;
float64 alpha = 0.;
float64 fixedAlpha = 0.;
};
protected:
int resizeGetHeight(int newWidth) override;
private:
class Footer;
class ChartAnimationController final {
public:
ChartAnimationController(Fn<void()> &&updateCallback);
void setXPercentageLimits(
Data::StatisticalChart &chartData,
Limits xPercentageLimits,
const std::unique_ptr<AbstractChartView> &chartView,
const std::shared_ptr<LinesFilterController> &linesFilter,
crl::time now);
void start();
void finish();
void resetAlpha();
void restartBottomLineAlpha();
void tick(
crl::time now,
ChartRulersView &rulersView,
std::vector<BottomCaptionLineData> &dateLines,
const std::unique_ptr<AbstractChartView> &chartView,
const std::shared_ptr<LinesFilterController> &linesFilter);
[[nodiscard]] Limits currentXLimits() const;
[[nodiscard]] Limits currentXIndices() const;
[[nodiscard]] Limits finalXLimits() const;
[[nodiscard]] Limits currentHeightLimits() const;
[[nodiscard]] Limits currentFooterHeightLimits() const;
[[nodiscard]] Limits finalHeightLimits() const;
[[nodiscard]] bool animating() const;
[[nodiscard]] bool footerAnimating() const;
[[nodiscard]] rpl::producer<> addRulerRequests() const;
private:
Ui::Animations::Basic _animation;
crl::time _lastUserInteracted = 0;
crl::time _bottomLineAlphaAnimationStartedAt = 0;
anim::value _animationValueXMin;
anim::value _animationValueXMax;
anim::value _animationValueHeightMin;
anim::value _animationValueHeightMax;
anim::value _animationValueFooterHeightMin;
anim::value _animationValueFooterHeightMax;
anim::value _animationValueHeightAlpha;
anim::value _animValueBottomLineAlpha;
Limits _finalHeightLimits;
Limits _currentXIndices;
struct {
float speed = 0.;
Limits current;
float64 currentAlpha = 0.;
} _dtHeight;
Limits _previousFullHeightLimits;
struct {
crl::time lastTickedAt = 0;
bool lastFPSSlow = false;
} _benchmark;
rpl::event_stream<> _addRulerRequests;
};
[[nodiscard]] QRect chartAreaRect() const;
void setupChartArea();
void setupFooter();
void setupDetails();
void setupFilterButtons();
void updateBottomDates();
void updateHeader();
void updateChartFullWidth(int w);
[[nodiscard]] bool hasLocalZoom() const;
void processLocalZoom(int xIndex);
const base::unique_qptr<RpMouseWidget> _chartArea;
const std::unique_ptr<Header> _header;
const std::unique_ptr<Footer> _footer;
base::unique_qptr<ChartLinesFilterWidget> _filterButtons;
Data::StatisticalChart _chartData;
base::unique_qptr<ChartWidget> _zoomedChartWidget;
std::unique_ptr<AbstractChartView> _chartView;
struct {
base::unique_qptr<PointDetailsWidget> widget;
Ui::Animations::Basic animation;
bool hideOnAnimationEnd = false;
} _details;
struct {
BottomCaptionLineData current;
std::vector<BottomCaptionLineData> dates;
int chartFullWidth = 0;
int captionIndicesOffset = 0;
} _bottomLine;
bool _areRulersAbove = false;
std::shared_ptr<LinesFilterController> _linesFilterController;
ChartAnimationController _animationController;
crl::time _lastHeightLimitsChanged = 0;
ChartRulersView _rulersView;
bool _zoomEnabled = false;
rpl::event_stream<float64> _zoomRequests;
rpl::lifetime _waitingSizeLifetime;
rpl::lifetime _localZoomLifetime;
};
void FixCacheForHighDPIChartWidget(not_null<Ui::RpWidget*> container);
} // namespace Statistic

View File

@@ -0,0 +1,151 @@
/*
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/segment_tree.h"
namespace Statistic {
namespace {
constexpr auto kMinArraySize = size_t(30);
} // namespace
SegmentTree::SegmentTree(std::vector<ChartValue> array)
: _array(std::move(array)) {
if (_array.size() < kMinArraySize) {
return;
}
// The max size of this array is about 2 * 2 ^ log2(n) + 1.
const auto size = 2 * std::pow(
2.,
std::floor((std::log(_array.size()) / std::log(2.)) + 1));
_heap.resize(int(size));
build(1, 0, _array.size());
}
void SegmentTree::build(ChartValue v, int from, int size) {
_heap[v].from = from;
_heap[v].to = (from + size - 1);
if (size == 1) {
_heap[v].sum = _array[from];
_heap[v].max = _array[from];
_heap[v].min = _array[from];
} else {
// Build children.
build(2 * v, from, size / 2);
build(2 * v + 1, from + size / 2, size - size / 2);
_heap[v].sum = _heap[2 * v].sum + _heap[2 * v + 1].sum;
// max = max of the children.
_heap[v].max = std::max(_heap[2 * v].max, _heap[2 * v + 1].max);
_heap[v].min = std::min(_heap[2 * v].min, _heap[2 * v + 1].min);
}
}
ChartValue SegmentTree::rMaxQ(int from, int to) {
if (_array.size() < kMinArraySize) {
auto max = ChartValue(0);
from = std::max(from, 0);
to = std::min(to, int(_array.size() - 1));
for (auto i = from; i <= to; i++) {
max = std::max(max, _array[i]);
}
return max;
}
return rMaxQ(1, from, to);
}
ChartValue SegmentTree::rMaxQ(ChartValue v, int from, int to) {
const auto &n = _heap[v];
// If you did a range update that contained this node,
// you can infer the Min value without going down the tree.
if (n.pendingVal && contains(n.from, n.to, from, to)) {
return n.pendingVal.value;
}
if (contains(from, to, n.from, n.to)) {
return _heap[v].max;
}
if (intersects(from, to, n.from, n.to)) {
propagate(v);
const auto leftMin = rMaxQ(2 * v, from, to);
const auto rightMin = rMaxQ(2 * v + 1, from, to);
return std::max(leftMin, rightMin);
}
return 0;
}
ChartValue SegmentTree::rMinQ(int from, int to) {
if (_array.size() < kMinArraySize) {
auto min = std::numeric_limits<ChartValue>::max();
from = std::max(from, 0);
to = std::min(to, int(_array.size() - 1));
for (auto i = from; i <= to; i++) {
min = std::min(min, _array[i]);
}
return min;
}
return rMinQ(1, from, to);
}
ChartValue SegmentTree::rMinQ(ChartValue v, int from, int to) {
const auto &n = _heap[v];
// If you did a range update that contained this node,
// you can infer the Min value without going down the tree.
if (n.pendingVal && contains(n.from, n.to, from, to)) {
return n.pendingVal.value;
}
if (contains(from, to, n.from, n.to)) {
return _heap[v].min;
}
if (intersects(from, to, n.from, n.to)) {
propagate(v);
const auto leftMin = rMinQ(2 * v, from, to);
const auto rightMin = rMinQ(2 * v + 1, from, to);
return std::min(leftMin, rightMin);
}
return std::numeric_limits<ChartValue>::max();
}
void SegmentTree::propagate(ChartValue v) {
auto &n = _heap[v];
if (n.pendingVal) {
const auto value = n.pendingVal.value;
n.pendingVal = {};
change(_heap[2 * v], value);
change(_heap[2 * v + 1], value);
}
}
void SegmentTree::change(SegmentTree::Node &n, ChartValue value) {
n.pendingVal = { value, true };
n.sum = n.size() * value;
n.max = value;
n.min = value;
_array[n.from] = value;
}
bool SegmentTree::contains(int from1, int to1, int from2, int to2) const {
return (from2 >= from1) && (to2 <= to1);
}
bool SegmentTree::intersects(int from1, int to1, int from2, int to2) const {
return ((from1 <= from2) && (to1 >= from2)) // (.[..)..] or (.[...]..)
|| ((from1 >= from2) && (from1 <= to2)); // [.(..]..) or [..(..)..
}
} // namespace Statistic

View File

@@ -0,0 +1,71 @@
/*
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_types.h"
namespace Statistic {
class SegmentTree final {
public:
SegmentTree() = default;
SegmentTree(std::vector<ChartValue> array);
[[nodiscard]] bool empty() const {
return _array.empty();
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
[[nodiscard]] ChartValue rMaxQ(int from, int to);
[[nodiscard]] ChartValue rMinQ(int from, int to);
private:
struct Node final {
ChartValue sum = 0;
ChartValue max = 0;
ChartValue min = 0;
struct PendingVal {
[[nodiscard]] explicit operator bool() const {
return available;
}
ChartValue value = 0;
bool available = false;
};
PendingVal pendingVal;
int from = 0;
int to = 0;
[[nodiscard]] int size() {
return to - from + 1;
}
};
void build(ChartValue v, int from, int size);
void propagate(ChartValue v);
void change(Node &n, ChartValue value);
[[nodiscard]] ChartValue rMaxQ(ChartValue v, int from, int to);
[[nodiscard]] ChartValue rMinQ(ChartValue v, int from, int to);
[[nodiscard]] bool contains(int from1, int to1, int from2, int to2) const;
[[nodiscard]] bool intersects(
int from1,
int to1,
int from2,
int to2) const;
std::vector<ChartValue> _array;
std::vector<Node> _heap;
};
} // namespace Statistic

View File

@@ -0,0 +1,183 @@
/*
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
*/
using "ui/basic.style";
using "window/window.style";
using "ui/widgets/widgets.style";
statisticsLayerOverviewMargins: margins(0px, 17px, 0px, 9px);
statisticsLayerMargins: margins(20px, 0px, 20px, 0px);
statisticsChartHeight: 200px;
statisticsChartEntryPadding: margins(0px, 13px, 0px, 2px);
statisticsDetailsArrowShift: 3px;
statisticsDetailsArrowStroke: 1.5;
statisticsDetailsPopupMargins: margins(12px, 8px, 12px, 11px);
statisticsDetailsPopupPadding: margins(6px, 6px, 6px, 6px);
statisticsDetailsPopupMidLineSpace: 4px;
statisticsDetailsDotRadius: 5px;
statisticsChartLineWidth: 2px;
statisticsChartFooterSkip: 11px;
statisticsChartFooterHeight: 42px;
statisticsChartFooterBetweenSide: 5px;
statisticsChartFooterSideWidth: 10px;
statisticsChartFooterArrowHeight: 10px;
statisticsChartFooterSideRadius: 6px;
statisticsChartRulerCaptionSkip: 4px;
statisticsChartBottomCaptionHeight: 15px;
statisticsChartBottomCaptionSkip: 6px;
statisticsChartFlatCheckboxMargins: margins(4px, 3px, 4px, 5px);
statisticsChartFlatCheckboxCheckWidth: 3px;
statisticsChartFlatCheckboxShrinkkWidth: 4px;
statisticsFilterButtonsPadding: margins(0px, 12px, 0px, 8px);
statisticsDetailsPopupHeaderStyle: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
statisticsDetailsPopupStyle: TextStyle(defaultTextStyle) {
font: font(12px);
}
statisticsDetailsBottomCaptionStyle: TextStyle(defaultTextStyle) {
font: font(10px);
}
statisticsPieChartFont: font(20px);
statisticsPieChartPartOffset: 8px;
statisticsChartHeaderHeight: 36px;
statisticsChartHeaderPadding: margins(2px, 0px, 0px, 10px);
statisticsHeaderTitleTextStyle: TextStyle(defaultTextStyle) {
font: font(boxFontSize semibold);
}
statisticsHeaderDatesTextStyle: TextStyle(defaultTextStyle) {
font: font(11px);
}
statisticsHeaderButton: RoundButton(defaultLightButton) {
width: -14px;
height: 20px;
textTop: 2px;
style: TextStyle(semiboldTextStyle) {
font: font(11px semibold);
}
}
statisticsLoadingSubtext: FlatLabel(changePhoneDescription) {
minWidth: 256px;
}
statisticsOverviewValue: FlatLabel(boxLabel) {
minWidth: 0px;
maxHeight: 60px;
style: TextStyle(defaultTextStyle) {
font: font(14px);
}
}
statisticsOverviewValuePadding: margins(2px, 1px, 0px, 0px);
statisticsOverviewSecondValuePadding: margins(5px, 3px, 0px, 0px);
statisticsOverviewSecondValue: FlatLabel(boxLabel) {
style: TextStyle(defaultTextStyle) {
font: font(11px);
}
}
statisticsOverviewSubtext: FlatLabel(boxLabel) {
minWidth: 152px;
maxHeight: 32px;
style: statisticsHeaderDatesTextStyle;
}
statisticsOverviewMidSkip: 50px;
statisticsOverviewRightSkip: 14px;
statisticsRecentPostRowHeight: 40px;
statisticsRecentPostButton: SettingsButton(defaultSettingsButton) {
height: 56px;
padding: margins(7px, 0px, 24px, 0px);
}
statisticsRecentPostIconSkip: 1px;
statisticsRecentPostShareIcon: icon {{ "statistics/mini_stats_share", windowSubTextFg }};
statisticsRecentPostReactionIcon: icon {{ "statistics/mini_stats_like", windowSubTextFg }};
statisticsRecentPostUserpic: UserpicButton(defaultUserpicButton) {
size: size(contactsPhotoSize, contactsPhotoSize);
photoSize: contactsPhotoSize;
}
statisticsShowMoreButton: SettingsButton(defaultSettingsButton) {
textFg: lightButtonFg;
textFgOver: lightButtonFgOver;
padding: margins(70px, 10px, 22px, 8px);
}
statisticsShowMoreButtonArrowPosition: point(29px, 13px);
statisticsShowMoreButtonArrowSize: 7px;
statisticsLimitsDividerPadding: margins(22px, -26px, 22px, 22px);
statisticsLimitsLinePadding: margins(0px, -2px, 0px, 0px);
boostsLayerOverviewMargins: margins(0px, 12px, 0px, 4px);
boostsOverviewValuePadding: margins(2px, 0px, 0px, 0px);
boostsChartHeaderPadding: margins(2px, 1px, 0px, 1px);
boostsListBox: PeerList(defaultPeerList) {
padding: margins(
0px,
7px,
0px,
7px);
item: PeerListItem(defaultPeerListItem) {
height: 52px;
photoPosition: point(18px, 4px);
namePosition: point(70px, 6px);
statusPosition: point(70px, 26px);
photoSize: 42px;
}
}
boostsLinkSkip: 5px;
boostsLinkFieldPadding: margins(22px, 7px, 22px, 12px);
boostsButton: SettingsButton(defaultSettingsButton) {
textFg: lightButtonFg;
textFgOver: lightButtonFgOver;
}
getBoostsButton: SettingsButton(reportReasonButton) {
textFg: lightButtonFg;
textFgOver: lightButtonFg;
}
getBoostsButtonIcon: icon {{ "menu/gift_premium", lightButtonFg }};
boostsListMiniIcon: icon{{ "boosts/boost_mini2", premiumButtonFg }};
boostsListMiniIconPadding: margins(1px, 0px, 0px, 0px);
boostsListMiniIconSkip: 1px;
boostsListEntryIcon: IconEmoji {
icon: boostsListMiniIcon;
padding: margins(6px, 2px, 0px, 0px);
}
boostsListBadgeTextPadding: margins(16px, 1px, 6px, 0px);
boostsListBadgePadding: margins(4px, 1px, 4px, 0px);
boostsListBadgeHeight: 16px;
boostsListRightBadgeTextStyle: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
boostsListRightBadgeTextPadding: margins(22px, 1px, 8px, 0px);
boostsListRightBadgePadding: margins(4px, 5px, 12px, 0px);
boostsListRightBadgeHeight: 20px;
boostsListGiftMiniIconPadding: margins(4px, 2px, 0px, 0px);
boostsListGiftMiniIcon: icon{{ "boosts/mini_gift", historyPeer8UserpicBg2 }};
boostsListGiveawayMiniIcon: icon{{ "boosts/mini_giveaway", historyPeer4UserpicBg2 }};
boostsListUnclaimedIcon: icon{{ "boosts/boost_unclaimed", premiumButtonFg }};
boostsListUnknownIcon: icon{{ "boosts/boost_unknown", premiumButtonFg }};
boostsListCreditsIconSize: 13px;
statisticsCurrencyIcon: icon {{ "statistics/mini_currency_graph", windowSubTextFg }};

View File

@@ -0,0 +1,27 @@
/*
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 {
constexpr auto kRulerLineAlpha = 0.06;
struct Limits final {
float64 min = 0;
float64 max = 0;
};
enum class ChartViewType {
Linear,
Bar,
StackBar,
DoubleLinear,
StackLinear,
};
} // namespace Statistic

View File

@@ -0,0 +1,176 @@
/*
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_data_deserialize.h"
#include "base/debug_log.h"
#include "data/data_statistics_chart.h"
#include "statistics/statistics_types.h"
#include "ui/text/format_values.h" // kCreditsCurrency.
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
namespace Statistic {
Data::StatisticalChart StatisticalChartFromJSON(const QByteArray &json) {
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
const auto document = QJsonDocument::fromJson(json, &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
LOG(("API Error: Bad stats graph json received."));
return {};
}
const auto root = document.object();
const auto columns = root.value(u"columns"_q).toArray();
if (columns.empty()) {
LOG(("API Error: Empty columns list from stats graph received."));
return {};
}
const auto hiddenLinesRaw = root.value(u"hidden"_q).toArray();
const auto hiddenLines = ranges::views::all(
hiddenLinesRaw
) | ranges::views::transform([](const auto &q) {
return q.toString();
}) | ranges::to_vector;
auto result = Data::StatisticalChart();
{
const auto tickFormatIt = root.constFind(u"yTickFormatter"_q);
if (tickFormatIt != root.constEnd()) {
const auto tickFormat = tickFormatIt->toString();
if (tickFormat.contains(u"TON"_q)) {
result.currency = Data::StatisticalCurrency::Ton;
} else if (tickFormat.contains(Ui::kCreditsCurrency)) {
result.currency = Data::StatisticalCurrency::Credits;
}
}
}
auto columnIdCount = 0;
for (const auto &column : columns) {
const auto array = column.toArray();
if (array.empty()) {
LOG(("API Error: Empty column from stats graph received."));
return {};
}
const auto columnId = array.first().toString();
if (columnId == u"x"_q) {
const auto length = array.size() - 1;
result.x.reserve(length);
for (auto i = 0; i < length; i++) {
result.x.push_back(array.at(i + 1).toDouble());
}
} else {
auto line = Data::StatisticalChart::Line();
const auto length = array.size() - 1;
line.id = (++columnIdCount);
line.idString = columnId;
line.isHiddenOnStart = ranges::contains(hiddenLines, columnId);
line.y.resize(length);
for (auto i = 0; i < length; i++) {
const auto multiplier = 1;
const auto value = ChartValue(
base::SafeRound(array.at(i + 1).toDouble()))
* multiplier;
line.y[i] = value;
if (value > line.maxValue) {
line.maxValue = value;
}
if (value < line.minValue) {
line.minValue = value;
}
}
result.lines.push_back(std::move(line));
}
if (result.x.size() > 1) {
result.timeStep = std::max(1., result.x[1] - result.x[0]);
} else {
constexpr auto kOneDay = 3600 * 24 * 1000;
result.timeStep = kOneDay;
}
result.measure();
}
if (result.maxValue == result.minValue) {
if (result.minValue) {
result.minValue = 0;
} else {
result.maxValue = 1;
}
}
{
const auto subchart = root.value(u"subchart"_q).toObject();
const auto subchartShowIt = subchart.constFind(u"show"_q);
if (subchartShowIt != subchart.constEnd()) {
if (subchartShowIt->isBool()) {
result.isFooterHidden = !(subchartShowIt->toBool());
}
}
const auto defaultZoomIt = subchart.constFind(u"defaultZoom"_q);
auto min = int(0);
auto max = int(result.x.size() - 1);
if (defaultZoomIt != subchart.constEnd()) {
if (const auto array = defaultZoomIt->toArray(); !array.empty()) {
const auto minValue = array.first().toDouble();
const auto maxValue = array.last().toDouble();
for (auto i = 0; i < result.x.size(); i++) {
if (result.x[i] == minValue) {
min = i;
}
if (result.x[i] == maxValue) {
max = i;
}
}
}
}
result.defaultZoomXIndex.min = std::min(min, max);
result.defaultZoomXIndex.max = std::max(min, max);
}
{
const auto percentageShowIt = root.constFind(u"percentage"_q);
if (percentageShowIt != root.constEnd()) {
if (percentageShowIt->isBool()) {
result.hasPercentages = (percentageShowIt->toBool());
}
}
}
{
const auto tooltipFormatIt = root.constFind(u"xTooltipFormatter"_q);
if (tooltipFormatIt != root.constEnd()) {
const auto tooltipFormat = tooltipFormatIt->toString();
result.weekFormat = tooltipFormat.contains(u"'week'"_q);
}
}
const auto colors = root.value(u"colors"_q).toObject();
const auto names = root.value(u"names"_q).toObject();
for (auto &line : result.lines) {
const auto colorIt = colors.constFind(line.idString);
if (colorIt != colors.constEnd() && (*colorIt).isString()) {
static const auto RegExp = QRegularExpression(u"(.*)(#.*)"_q);
const auto match = RegExp.match(
colorIt->toString());
if (match.hasMatch()) {
line.colorKey = match.captured(1);
line.color = QColor(match.captured(2));
}
}
const auto nameIt = names.constFind(line.idString);
if (nameIt != names.constEnd() && (*nameIt).isString()) {
line.name = nameIt->toString().replace('-', QChar(8212));
}
}
return result;
}
} // namespace Statistic

View File

@@ -0,0 +1,21 @@
/*
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
class QByteArray;
namespace Statistic {
[[nodiscard]] Data::StatisticalChart StatisticalChartFromJSON(
const QByteArray &json);
} // namespace Statistic

View File

@@ -0,0 +1,70 @@
/*
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_format_values.h"
#include "base/unixtime.h"
#include "lang/lang_keys.h"
#include <QtCore/QLocale>
namespace Statistic {
QString LangDayMonthYear(crl::time seconds) {
const auto date = base::unixtime::parse(seconds).date();
return tr::lng_stats_day_month_year(
tr::now,
lt_days_count,
QString::number(date.day()),
lt_month,
Lang::MonthSmall(date.month())(tr::now),
lt_year,
QString::number(date.year()));
}
QString LangDayMonth(crl::time seconds) {
const auto date = base::unixtime::parse(seconds).date();
return tr::lng_stats_day_month(
tr::now,
lt_days_count,
QString::number(date.day()),
lt_month,
Lang::MonthSmall(date.month())(tr::now));
}
QString LangDetailedDayMonth(crl::time seconds) {
const auto dateTime = base::unixtime::parse(seconds);
if (dateTime.toUTC().time().hour() || dateTime.toUTC().time().minute()) {
constexpr auto kOneDay = 3600 * 24;
if (seconds < kOneDay) {
return QLocale().toString(dateTime, QLocale::ShortFormat);
}
return tr::lng_stats_weekday_day_month_time(
tr::now,
lt_day,
Lang::Weekday(dateTime.date().dayOfWeek())(tr::now),
lt_days_count,
QString::number(dateTime.date().day()),
lt_month,
Lang::MonthSmall(dateTime.date().month())(tr::now),
lt_time,
QLocale().toString(dateTime.time(), QLocale::ShortFormat));
} else {
return tr::lng_stats_weekday_day_month_year(
tr::now,
lt_day,
Lang::Weekday(dateTime.date().dayOfWeek())(tr::now),
lt_days_count,
QString::number(dateTime.date().day()),
lt_month,
Lang::MonthSmall(dateTime.date().month())(tr::now),
lt_year,
QString::number(dateTime.date().year()));
}
}
} // namespace Statistic

View File

@@ -0,0 +1,16 @@
/*
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 {
[[nodiscard]] QString LangDayMonthYear(crl::time seconds);
[[nodiscard]] QString LangDayMonth(crl::time seconds);
[[nodiscard]] QString LangDetailedDayMonth(crl::time seconds);
} // namespace Statistic

View File

@@ -0,0 +1,43 @@
/*
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_graphics.h"
#include "data/data_statistics_chart.h"
#include "ui/effects/credits_graphics.h" // GenerateStars.
#include "ui/painter.h"
#include "styles/style_basic.h"
#include "styles/style_statistics.h"
namespace Statistic {
QImage ChartCurrencyIcon(
const Data::StatisticalChart &chartData,
std::optional<QColor> color) {
auto result = QImage();
const auto iconSize = st::statisticsCurrencyIcon.size();
if (chartData.currency == Data::StatisticalCurrency::Ton) {
result = QImage(
iconSize * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
{
auto p = Painter(&result);
if (const auto w = iconSize.width(); w && color) {
st::statisticsCurrencyIcon.paint(p, 0, 0, w, *color);
} else {
st::statisticsCurrencyIcon.paint(p, 0, 0, iconSize.width());
}
}
} else if (chartData.currency == Data::StatisticalCurrency::Credits) {
return Ui::GenerateStars(iconSize.height(), 1);
}
return result;
}
} // namespace Statistic

View File

@@ -0,0 +1,20 @@
/*
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 {
[[nodiscard]] QImage ChartCurrencyIcon(
const Data::StatisticalChart &chartData,
std::optional<QColor> color);
} // namespace Statistic

View File

@@ -0,0 +1,14 @@
/*
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 {
using ChartValue = int64;
} // namespace Statistic

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

View File

@@ -0,0 +1,58 @@
/*
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/widgets/chart_header_widget.h"
#include "ui/painter.h"
#include "styles/style_statistics.h"
namespace Statistic {
Header::Header(not_null<Ui::RpWidget*> parent)
: Ui::RpWidget(parent)
, _height(st::statisticsChartHeaderHeight) {
}
QString Header::title() const {
return _title.toString();
}
void Header::setTitle(QString title) {
_title.setText(st::statisticsHeaderTitleTextStyle, std::move(title));
}
int Header::resizeGetHeight(int newWidth) {
return _height;
}
void Header::setSubTitle(QString subTitle) {
_height = subTitle.isEmpty()
? st::statisticsHeaderTitleTextStyle.font->height
: st::statisticsChartHeaderHeight;
_subTitle.setText(
st::statisticsHeaderDatesTextStyle,
std::move(subTitle));
}
void Header::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
p.fillRect(rect(), st::boxBg);
p.setPen(st::windowActiveTextFg);
_title.drawLeftElided(p, 0, 0, width(), width());
p.setPen(st::windowSubTextFg);
_subTitle.drawLeftElided(p, 0, _infoTop, width(), width());
}
void Header::resizeEvent(QResizeEvent *e) {
_infoTop = e->size().height()
- st::statisticsHeaderDatesTextStyle.font->height;
}
} // namespace Statistic

View File

@@ -0,0 +1,36 @@
/*
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 "ui/rp_widget.h"
namespace Statistic {
class Header final : public Ui::RpWidget {
public:
explicit Header(not_null<Ui::RpWidget*> parent);
[[nodiscard]] QString title() const;
void setTitle(QString title);
void setSubTitle(QString subTitle);
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
int resizeGetHeight(int newWidth) override;
private:
Ui::Text::String _title;
Ui::Text::String _subTitle;
int _infoTop = 0;
int _height = 0;
};
} // namespace Statistic

View File

@@ -0,0 +1,230 @@
/*
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/widgets/chart_lines_filter_widget.h"
#include "ui/abstract_button.h"
#include "ui/effects/animations.h"
#include "ui/effects/shake_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_basic.h"
#include "styles/style_statistics.h"
#include "styles/style_widgets.h"
namespace Statistic {
class ChartLinesFilterWidget::FlatCheckbox final : public Ui::AbstractButton {
public:
FlatCheckbox(
not_null<Ui::RpWidget*> parent,
const QString &text,
QColor activeColor);
void shake();
void setChecked(bool value, bool animated);
[[nodiscard]] bool checked() const;
protected:
void paintEvent(QPaintEvent *e) override;
private:
const QColor _inactiveTextColor;
const QColor _activeColor;
const QColor _inactiveColor;
Ui::Text::String _text;
Ui::Animations::Simple _animation;
struct {
Ui::Animations::Simple animation;
int shift = 0;
} _shake;
bool _checked = true;
};
ChartLinesFilterWidget::FlatCheckbox::FlatCheckbox(
not_null<Ui::RpWidget*> parent,
const QString &text,
QColor activeColor)
: Ui::AbstractButton(parent)
, _inactiveTextColor(st::premiumButtonFg->c)
, _activeColor(activeColor)
, _inactiveColor(st::boxBg->c)
, _text(st::statisticsDetailsPopupStyle, text) {
const auto &margins = st::statisticsChartFlatCheckboxMargins;
const auto h = _text.minHeight() + rect::m::sum::v(margins) * 2;
resize(
_text.maxWidth()
+ rect::m::sum::h(margins)
+ h
+ st::statisticsChartFlatCheckboxCheckWidth * 3
- st::statisticsChartFlatCheckboxShrinkkWidth,
h);
}
void ChartLinesFilterWidget::FlatCheckbox::setChecked(
bool value,
bool animated) {
if (_checked == value) {
return;
}
_checked = value;
if (!animated) {
_animation.stop();
} else {
const auto from = value ? 0. : 1.;
const auto to = value ? 1. : 0.;
_animation.start([=] { update(); }, from, to, st::shakeDuration);
}
}
bool ChartLinesFilterWidget::FlatCheckbox::checked() const {
return _checked;
}
void ChartLinesFilterWidget::FlatCheckbox::shake() {
if (_shake.animation.animating()) {
return;
}
_shake.animation.start(Ui::DefaultShakeCallback([=](int shift) {
_shake.shift = shift;
update();
}), 0., 1., st::shakeDuration);
}
void ChartLinesFilterWidget::FlatCheckbox::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto progress = _animation.value(_checked ? 1. : 0.);
p.translate(_shake.shift, 0);
const auto checkWidth = st::statisticsChartFlatCheckboxCheckWidth;
const auto r = rect() - st::statisticsChartFlatCheckboxMargins;
const auto heightHalf = r.height() / 2.;
const auto textX = anim::interpolate(
r.center().x() - _text.maxWidth() / 2.,
r.x() + heightHalf + checkWidth * 5,
progress);
const auto textY = (r - st::statisticsChartFlatCheckboxMargins).y();
p.fillRect(r, Qt::transparent);
constexpr auto kCheckPartProgress = 0.5;
const auto checkProgress = progress / kCheckPartProgress;
const auto textColor = (progress <= kCheckPartProgress)
? anim::color(_activeColor, _inactiveTextColor, checkProgress)
: _inactiveTextColor;
const auto fillColor = (progress <= kCheckPartProgress)
? anim::color(_inactiveColor, _activeColor, checkProgress)
: _activeColor;
p.setPen(QPen(_activeColor, st::statisticsChartLineWidth));
p.setBrush(fillColor);
const auto radius = r.height() / 2.;
{
auto hq = PainterHighQualityEnabler(p);
p.drawRoundedRect(r, radius, radius);
}
p.setPen(textColor);
const auto textContext = Ui::Text::PaintContext{
.position = QPoint(textX, textY),
.availableWidth = width(),
};
_text.draw(p, textContext);
if (progress > kCheckPartProgress) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(QPen(textColor, st::statisticsChartLineWidth));
const auto bounceProgress = checkProgress - 1.;
const auto start = QPoint(
r.x() + heightHalf + checkWidth,
textY + _text.style()->font->ascent);
p.translate(start);
p.drawLine({}, -QPoint(checkWidth, checkWidth) * bounceProgress);
p.drawLine({}, QPoint(checkWidth, -checkWidth) * bounceProgress * 2);
}
}
ChartLinesFilterWidget::ChartLinesFilterWidget(
not_null<Ui::RpWidget*> parent)
: Ui::RpWidget(parent) {
}
void ChartLinesFilterWidget::resizeToWidth(int outerWidth) {
auto maxRight = 0;
for (auto i = 0; i < _buttons.size(); i++) {
const auto raw = _buttons[i].get();
if (!i) {
raw->move(0, 0);
} else {
const auto prevRaw = _buttons[i - 1].get();
const auto prevLeft = rect::right(prevRaw);
const auto isOut = (prevLeft + raw->width() > outerWidth);
const auto left = isOut ? 0 : prevLeft;
const auto top = isOut ? rect::bottom(prevRaw) : prevRaw->y();
raw->move(left, top);
}
maxRight = std::max(maxRight, rect::right(raw));
}
if (!_buttons.empty()) {
resize(maxRight, rect::bottom(_buttons.back().get()));
}
}
void ChartLinesFilterWidget::fillButtons(
const std::vector<ButtonData> &buttonsData) {
_buttons.clear();
_buttons.reserve(buttonsData.size());
for (auto i = 0; i < buttonsData.size(); i++) {
const auto &buttonData = buttonsData[i];
auto button = base::make_unique_q<FlatCheckbox>(
this,
buttonData.text,
buttonData.color);
button->show();
if (buttonData.disabled) {
button->setChecked(false, false);
}
const auto id = buttonData.id;
button->setClickedCallback([=, raw = button.get()] {
const auto checked = !raw->checked();
if (!checked) {
const auto cancel = [&] {
for (const auto &b : _buttons) {
if (b.get() == raw) {
continue;
}
if (b->checked()) {
return false;
}
}
return true;
}();
if (cancel) {
raw->shake();
return;
}
}
raw->setChecked(checked, true);
_buttonEnabledChanges.fire({ .id = id, .enabled = checked });
});
_buttons.push_back(std::move(button));
}
}
auto ChartLinesFilterWidget::buttonEnabledChanges() const
-> rpl::producer<ChartLinesFilterWidget::Entry> {
return _buttonEnabledChanges.events();
}
} // namespace Statistic

View File

@@ -0,0 +1,44 @@
/*
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 "ui/rp_widget.h"
namespace Statistic {
class ChartLinesFilterWidget final : public Ui::RpWidget {
public:
ChartLinesFilterWidget(not_null<Ui::RpWidget*> parent);
struct ButtonData final {
QString text;
QColor color;
int id = 0;
bool disabled = false;
};
void fillButtons(const std::vector<ButtonData> &buttonsData);
void resizeToWidth(int outerWidth);
struct Entry final {
int id = 0;
bool enabled = 0;
};
[[nodiscard]] rpl::producer<Entry> buttonEnabledChanges() const;
private:
class FlatCheckbox;
std::vector<base::unique_qptr<FlatCheckbox>> _buttons;
rpl::event_stream<Entry> _buttonEnabledChanges;
};
} // namespace Statistic

View File

@@ -0,0 +1,527 @@
/*
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/widgets/point_details_widget.h"
#include "base/debug_log.h"
#include "info/channel_statistics/earn/earn_format.h"
#include "lang/lang_keys.h"
#include "statistics/statistics_common.h"
#include "statistics/statistics_format_values.h"
#include "statistics/statistics_graphics.h"
#include "statistics/view/stack_linear_chart_common.h"
#include "ui/cached_round_corners.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_layers.h"
#include "styles/style_statistics.h"
#include <QtCore/QDateTime>
#include <QtCore/QLocale>
namespace Statistic {
namespace {
[[nodiscard]] QString FormatWeek(float64 timestamp) {
constexpr auto kSevenDays = 3600 * 24 * 7;
timestamp /= 1000;
return LangDayMonth(timestamp)
+ ' '
+ QChar(8212)
+ ' '
+ LangDayMonthYear(timestamp + kSevenDays);
}
void PaintShadow(QPainter &p, int radius, const QRect &r) {
constexpr auto kHorizontalOffset = 1;
constexpr auto kHorizontalOffset2 = 2;
constexpr auto kVerticalOffset = 2;
constexpr auto kVerticalOffset2 = 3;
constexpr auto kOpacityStep = 0.2;
constexpr auto kOpacityStep2 = 0.4;
const auto hOffset = style::ConvertScale(kHorizontalOffset);
const auto hOffset2 = style::ConvertScale(kHorizontalOffset2);
const auto vOffset = style::ConvertScale(kVerticalOffset);
const auto vOffset2 = style::ConvertScale(kVerticalOffset2);
const auto opacity = p.opacity();
auto hq = PainterHighQualityEnabler(p);
p.setOpacity(opacity);
p.drawRoundedRect(r + QMarginsF(0, hOffset, 0, hOffset), radius, radius);
p.setOpacity(opacity * kOpacityStep);
p.drawRoundedRect(r + QMarginsF(hOffset, 0, hOffset, 0), radius, radius);
p.setOpacity(opacity * kOpacityStep2);
p.drawRoundedRect(
r + QMarginsF(hOffset2, 0, hOffset2, 0),
radius,
radius);
p.setOpacity(opacity * kOpacityStep);
p.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset), radius, radius);
p.setOpacity(opacity * kOpacityStep2);
p.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset2), radius, radius);
p.setOpacity(opacity);
}
} // namespace
void PaintDetails(
QPainter &p,
const Data::StatisticalChart::Line &line,
int absoluteValue,
const QRect &rect) {
auto name = Ui::Text::String(
st::statisticsDetailsPopupStyle,
line.name);
auto value = Ui::Text::String(
st::statisticsDetailsPopupStyle,
Lang::FormatCountDecimal(absoluteValue));
const auto nameWidth = name.maxWidth();
const auto valueWidth = value.maxWidth();
const auto width = valueWidth
+ rect::m::sum::h(st::statisticsDetailsPopupMargins)
+ rect::m::sum::h(st::statisticsDetailsPopupPadding)
+ st::statisticsDetailsPopupPadding.left() // Between strings.
+ nameWidth;
const auto height = st::statisticsDetailsPopupStyle.font->height
+ rect::m::sum::v(st::statisticsDetailsPopupMargins)
+ rect::m::sum::v(st::statisticsDetailsPopupPadding);
const auto fullRect = QRect(
rect.x() + rect.width() - width,
rect.y(),
width,
height);
const auto innerRect = fullRect - st::statisticsDetailsPopupPadding;
const auto textRect = innerRect - st::statisticsDetailsPopupMargins;
p.setBrush(st::shadowFg);
p.setPen(Qt::NoPen);
PaintShadow(p, st::boxRadius, innerRect);
Ui::FillRoundRect(p, innerRect, st::boxBg, Ui::BoxCorners);
const auto lineY = textRect.y();
const auto valueContext = Ui::Text::PaintContext{
.position = QPoint(rect::right(textRect) - valueWidth, lineY),
.outerWidth = textRect.width(),
.availableWidth = valueWidth,
};
const auto nameContext = Ui::Text::PaintContext{
.position = QPoint(textRect.x(), lineY),
.outerWidth = textRect.width(),
.availableWidth = textRect.width() - valueWidth,
};
p.setPen(st::boxTextFg);
name.draw(p, nameContext);
p.setPen(line.color);
value.draw(p, valueContext);
}
PointDetailsWidget::PointDetailsWidget(
not_null<Ui::RpWidget*> parent,
const Data::StatisticalChart &chartData,
bool zoomEnabled)
: Ui::AbstractButton(parent)
, _zoomEnabled(zoomEnabled)
, _chartData(chartData)
, _textStyle(st::statisticsDetailsPopupStyle)
, _headerStyle(st::statisticsDetailsPopupHeaderStyle) {
if (zoomEnabled) {
rpl::single(rpl::empty_value()) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
const auto w = st::statisticsDetailsArrowShift;
const auto stroke = style::ConvertScaleExact(
st::statisticsDetailsArrowStroke);
_arrow = QImage(
QSize(w + stroke, w * 2 + stroke) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_arrow.setDevicePixelRatio(style::DevicePixelRatio());
_arrow.fill(Qt::transparent);
{
auto p = QPainter(&_arrow);
const auto hq = PainterHighQualityEnabler(p);
const auto s = stroke / 2.;
p.setPen(QPen(st::windowSubTextFg, stroke));
p.drawLine(QLineF(s, s, w, w + s));
p.drawLine(QLineF(s, s + w * 2, w, w + s));
}
invalidateCache();
}, lifetime());
}
_maxPercentageWidth = [&] {
if (_chartData.hasPercentages) {
const auto maxPercentageText = Ui::Text::String(
_textStyle,
u"10000%"_q);
return maxPercentageText.maxWidth();
}
return 0;
}();
const auto hasUsdLine = (_chartData.currencyRate != 0)
&& (_chartData.currency != Data::StatisticalCurrency::None)
&& (_chartData.lines.size() == 1);
const auto maxValueTextWidth = [&] {
if (hasUsdLine) {
auto maxValueWidth = 0;
const auto multiplier = float64(kOneStarInNano);
for (const auto &value : _chartData.lines.front().y) {
const auto valueText = Ui::Text::String(
_textStyle,
Lang::FormatExactCountDecimal(value / multiplier));
const auto usdText = Ui::Text::String(
_textStyle,
Info::ChannelEarn::ToUsd(
value / multiplier,
_chartData.currencyRate,
0));
const auto width = std::max(
usdText.maxWidth(),
valueText.maxWidth());
if (width > maxValueWidth) {
maxValueWidth = width;
}
}
return maxValueWidth;
}
const auto maxAbsoluteValue = [&] {
auto maxValue = ChartValue(0);
for (const auto &l : _chartData.lines) {
maxValue = std::max(l.maxValue, maxValue);
}
return maxValue;
}();
const auto maxValueText = Ui::Text::String(
_textStyle,
Lang::FormatCountDecimal(maxAbsoluteValue));
return maxValueText.maxWidth();
}();
const auto calculatedWidth = [&]{
auto maxNameTextWidth = 0;
const auto isCredits
= _chartData.currency == Data::StatisticalCurrency::Credits;
for (const auto &dataLine : _chartData.lines) {
const auto maxNameText = Ui::Text::String(
_textStyle,
dataLine.name);
maxNameTextWidth = std::max(
maxNameText.maxWidth(),
maxNameTextWidth);
if (hasUsdLine) {
const auto text = isCredits
? tr::lng_channel_earn_chart_overriden_detail_credits
: tr::lng_channel_earn_chart_overriden_detail_currency;
const auto currency = Ui::Text::String(
_textStyle,
text(tr::now));
const auto usd = Ui::Text::String(
_textStyle,
tr::lng_channel_earn_chart_overriden_detail_usd(
tr::now));
maxNameTextWidth = std::max(
std::max(currency.maxWidth(), usd.maxWidth()),
maxNameTextWidth);
}
}
{
const auto maxHeaderText = Ui::Text::String(
_headerStyle,
_chartData.weekFormat
? FormatWeek(_chartData.x.front())
: LangDetailedDayMonth(_chartData.x.front() / 1000));
maxNameTextWidth = std::max(
maxHeaderText.maxWidth()
+ st::statisticsDetailsPopupPadding.left(),
maxNameTextWidth);
}
return maxValueTextWidth
+ rect::m::sum::h(st::statisticsDetailsPopupMargins)
+ rect::m::sum::h(st::statisticsDetailsPopupPadding)
+ st::statisticsDetailsPopupPadding.left() // Between strings.
+ maxNameTextWidth
+ (_valueIcon.isNull()
? 0
: _valueIcon.width() / style::DevicePixelRatio())
+ _maxPercentageWidth;
}();
sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto fullRect = s.isNull()
? Rect(Size(calculatedWidth))
: Rect(s);
_innerRect = fullRect - st::statisticsDetailsPopupPadding;
_textRect = _innerRect - st::statisticsDetailsPopupMargins;
invalidateCache();
}, lifetime());
resize(calculatedWidth, height());
resizeHeight();
}
void PointDetailsWidget::setLineAlpha(int lineId, float64 alpha) {
for (auto &line : _lines) {
if (line.id == lineId) {
if (line.alpha != alpha) {
line.alpha = alpha;
resizeHeight();
invalidateCache();
update();
}
return;
}
}
}
void PointDetailsWidget::resizeHeight() {
resize(
width(),
lineYAt(_chartData.lines.size() + (_chartData.currencyRate ? 1 : 0))
+ st::statisticsDetailsPopupMargins.bottom());
}
int PointDetailsWidget::xIndex() const {
return _xIndex;
}
void PointDetailsWidget::setXIndex(int xIndex) {
_xIndex = xIndex;
if (xIndex < 0) {
return;
}
if (xIndex >= _chartData.x.size()) {
LOG((u"xIndex out of bounds: %1, max: %2"_q)
.arg(xIndex)
.arg(_chartData.x.size() - 1));
xIndex = _chartData.x.size() - 1;
}
{
constexpr auto kOneDay = 3600 * 24 * 1000;
const auto timestamp = _chartData.x[xIndex];
_header.setText(
_headerStyle,
(timestamp < kOneDay)
? _chartData.getDayString(xIndex)
: _chartData.weekFormat
? FormatWeek(timestamp)
: LangDetailedDayMonth(timestamp / 1000));
}
_lines.clear();
_lines.reserve(_chartData.lines.size());
auto hasPositiveValues = false;
const auto parts = _maxPercentageWidth
? PiePartsPercentageByIndices(
_chartData,
nullptr,
{ float64(xIndex), float64(xIndex) }).parts
: std::vector<PiePartData::Part>();
const auto isCredits
= (_chartData.currency == Data::StatisticalCurrency::Credits);
for (auto i = 0; i < _chartData.lines.size(); i++) {
const auto &dataLine = _chartData.lines[i];
Assert(xIndex < dataLine.y.size());
auto textLine = Line();
textLine.id = dataLine.id;
if (_maxPercentageWidth) {
textLine.percentage.setText(_textStyle, parts[i].percentageText);
}
textLine.name.setText(_textStyle, dataLine.name);
textLine.value.setText(
_textStyle,
Lang::FormatCountDecimal(dataLine.y[xIndex]));
hasPositiveValues |= (dataLine.y[xIndex] > 0);
textLine.valueColor = QColor(dataLine.color);
if (_chartData.currencyRate) {
auto copy = Line();
copy.id = dataLine.id * 100;
copy.valueColor = QColor(dataLine.color);
copy.name.setText(
_textStyle,
(isCredits
? tr::lng_channel_earn_chart_overriden_detail_credits
: tr::lng_channel_earn_chart_overriden_detail_currency)(
tr::now));
const auto provided = dataLine.y[xIndex];
const auto value = isCredits
? CreditsAmount(provided, CreditsType::Stars)
: CreditsAmount(
provided / kOneStarInNano,
provided % kOneStarInNano,
CreditsType::Ton);
copy.value.setText(
_textStyle,
Lang::FormatCreditsAmountDecimal(value));
_lines.push_back(std::move(copy));
textLine.name.setText(
_textStyle,
tr::lng_channel_earn_chart_overriden_detail_usd(tr::now));
textLine.value.setText(
_textStyle,
Info::ChannelEarn::ToUsd(value, _chartData.currencyRate, 0));
}
_lines.push_back(std::move(textLine));
}
if (_chartData.currencyRate && _valueIcon.isNull()) {
_valueIcon = ChartCurrencyIcon(_chartData, _lines.front().valueColor);
}
const auto clickable = _zoomEnabled && hasPositiveValues;
_hasPositiveValues = hasPositiveValues;
QWidget::setAttribute(
Qt::WA_TransparentForMouseEvents,
!clickable);
invalidateCache();
}
void PointDetailsWidget::setAlpha(float64 alpha) {
_alpha = alpha;
update();
}
float64 PointDetailsWidget::alpha() const {
return _alpha;
}
int PointDetailsWidget::lineYAt(int index) const {
auto linesHeight = 0.;
for (auto i = 0; i < index; i++) {
const auto alpha = (i >= _lines.size()) ? 1. : _lines[i].alpha;
linesHeight += alpha
* (_textStyle.font->height
+ st::statisticsDetailsPopupMidLineSpace);
}
return _textRect.y()
+ _headerStyle.font->height
+ st::statisticsDetailsPopupMargins.bottom() / 2
+ std::ceil(linesHeight);
}
void PointDetailsWidget::invalidateCache() {
_cache = QImage();
}
void PointDetailsWidget::mousePressEvent(QMouseEvent *e) {
AbstractButton::mousePressEvent(e);
const auto position = e->pos() - _innerRect.topLeft();
if (!_ripple) {
_ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
_innerRect.size(),
st::boxRadius),
[=] { update(); });
}
_ripple->add(position);
}
void PointDetailsWidget::mouseReleaseEvent(QMouseEvent *e) {
AbstractButton::mouseReleaseEvent(e);
if (_ripple) {
_ripple->lastStop();
}
}
void PointDetailsWidget::paintEvent(QPaintEvent *e) {
auto painter = QPainter(this);
if (_cache.isNull()) {
_cache = QImage(
size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_cache.setDevicePixelRatio(style::DevicePixelRatio());
_cache.fill(Qt::transparent);
auto p = QPainter(&_cache);
p.setBrush(st::shadowFg);
p.setPen(Qt::NoPen);
PaintShadow(p, st::boxRadius, _innerRect);
Ui::FillRoundRect(p, _innerRect, st::boxBg, Ui::BoxCorners);
if (_ripple) {
_ripple->paint(p, _innerRect.left(), _innerRect.top(), width());
if (_ripple->empty()) {
_ripple.reset();
}
}
p.setPen(st::boxTextFg);
const auto headerContext = Ui::Text::PaintContext{
.position = _textRect.topLeft(),
.availableWidth = _textRect.width(),
};
_header.draw(p, headerContext);
for (auto i = 0; i < _lines.size(); i++) {
const auto &line = _lines[i];
const auto lineY = lineYAt(i);
const auto valueWidth = line.value.maxWidth();
const auto valueContext = Ui::Text::PaintContext{
.position = QPoint(
rect::right(_textRect) - valueWidth,
lineY),
.outerWidth = _textRect.width(),
.availableWidth = valueWidth,
};
if (!i && !_valueIcon.isNull()) {
p.drawImage(
valueContext.position.x()
- _valueIcon.width() / style::DevicePixelRatio(),
lineY + st::lineWidth,
_valueIcon);
}
const auto nameContext = Ui::Text::PaintContext{
.position = QPoint(
_textRect.x() + _maxPercentageWidth,
lineY),
.outerWidth = _textRect.width(),
.availableWidth = _textRect.width() - valueWidth,
};
p.setOpacity(line.alpha * line.alpha);
p.setPen(st::boxTextFg);
if (_maxPercentageWidth) {
const auto percentageContext = Ui::Text::PaintContext{
.position = QPoint(_textRect.x(), lineY),
.outerWidth = _textRect.width(),
.availableWidth = _textRect.width() - valueWidth,
};
line.percentage.draw(p, percentageContext);
}
line.name.draw(p, nameContext);
p.setPen(line.valueColor);
line.value.draw(p, valueContext);
}
if (_zoomEnabled && _hasPositiveValues) {
const auto s = _arrow.size() / style::DevicePixelRatio();
const auto x = rect::right(_textRect) - s.width();
const auto y = _textRect.y()
+ (_headerStyle.font->ascent - s.height());
p.drawImage(x, y, _arrow);
}
}
if (_alpha < 1.) {
painter.setOpacity(_alpha);
}
painter.drawImage(0, 0, _cache);
if (_ripple) {
invalidateCache();
}
}
} // namespace Statistic

View File

@@ -0,0 +1,85 @@
/*
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 "data/data_statistics_chart.h"
#include "ui/abstract_button.h"
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace Statistic {
void PaintDetails(
QPainter &p,
const Data::StatisticalChart::Line &line,
int absoluteValue,
const QRect &rect);
class PointDetailsWidget : public Ui::AbstractButton {
public:
PointDetailsWidget(
not_null<Ui::RpWidget*> parent,
const Data::StatisticalChart &chartData,
bool zoomEnabled);
[[nodiscard]] int xIndex() const;
void setXIndex(int xIndex);
void setAlpha(float64 alpha);
[[nodiscard]] float64 alpha() const;
void setLineAlpha(int lineId, float64 alpha);
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
private:
const bool _zoomEnabled;
const Data::StatisticalChart &_chartData;
const style::TextStyle &_textStyle;
const style::TextStyle &_headerStyle;
Ui::Text::String _header;
QImage _valueIcon;
void invalidateCache();
[[nodiscard]] int lineYAt(int index) const;
void resizeHeight();
struct Line final {
int id = 0;
Ui::Text::String name;
Ui::Text::String value;
Ui::Text::String percentage;
QColor valueColor;
float64 alpha = 1.;
};
bool _hasPositiveValues = true;
int _maxPercentageWidth = 0;
QRect _innerRect;
QRect _textRect;
QImage _arrow;
QImage _cache;
int _xIndex = -1;
float64 _alpha = 1.;
std::vector<Line> _lines;
std::unique_ptr<Ui::RippleAnimation> _ripple;
};
} // namespace Statistic