init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
135
Telegram/SourceFiles/statistics/chart_rulers_data.cpp
Normal file
135
Telegram/SourceFiles/statistics/chart_rulers_data.cpp
Normal 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
|
||||
44
Telegram/SourceFiles/statistics/chart_rulers_data.h
Normal file
44
Telegram/SourceFiles/statistics/chart_rulers_data.h
Normal 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
|
||||
1571
Telegram/SourceFiles/statistics/chart_widget.cpp
Normal file
1571
Telegram/SourceFiles/statistics/chart_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
184
Telegram/SourceFiles/statistics/chart_widget.h
Normal file
184
Telegram/SourceFiles/statistics/chart_widget.h
Normal 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
|
||||
151
Telegram/SourceFiles/statistics/segment_tree.cpp
Normal file
151
Telegram/SourceFiles/statistics/segment_tree.cpp
Normal 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
|
||||
71
Telegram/SourceFiles/statistics/segment_tree.h
Normal file
71
Telegram/SourceFiles/statistics/segment_tree.h
Normal 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
|
||||
183
Telegram/SourceFiles/statistics/statistics.style
Normal file
183
Telegram/SourceFiles/statistics/statistics.style
Normal 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 }};
|
||||
27
Telegram/SourceFiles/statistics/statistics_common.h
Normal file
27
Telegram/SourceFiles/statistics/statistics_common.h
Normal 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
|
||||
176
Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp
Normal file
176
Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp
Normal 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
|
||||
@@ -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
|
||||
70
Telegram/SourceFiles/statistics/statistics_format_values.cpp
Normal file
70
Telegram/SourceFiles/statistics/statistics_format_values.cpp
Normal 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
|
||||
16
Telegram/SourceFiles/statistics/statistics_format_values.h
Normal file
16
Telegram/SourceFiles/statistics/statistics_format_values.h
Normal 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
|
||||
43
Telegram/SourceFiles/statistics/statistics_graphics.cpp
Normal file
43
Telegram/SourceFiles/statistics/statistics_graphics.cpp
Normal 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
|
||||
20
Telegram/SourceFiles/statistics/statistics_graphics.h
Normal file
20
Telegram/SourceFiles/statistics/statistics_graphics.h
Normal 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
|
||||
14
Telegram/SourceFiles/statistics/statistics_types.h
Normal file
14
Telegram/SourceFiles/statistics/statistics_types.h
Normal 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
|
||||
103
Telegram/SourceFiles/statistics/view/abstract_chart_view.cpp
Normal file
103
Telegram/SourceFiles/statistics/view/abstract_chart_view.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "statistics/view/abstract_chart_view.h"
|
||||
|
||||
#include "data/data_statistics_chart.h"
|
||||
#include "statistics/chart_lines_filter_controller.h"
|
||||
#include "statistics/statistics_types.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
bool CachedSelectedPoints::isSame(int x, const PaintContext &c) const {
|
||||
return (lastXIndex == x)
|
||||
&& (lastHeightLimits.min == c.heightLimits.min)
|
||||
&& (lastHeightLimits.max == c.heightLimits.max)
|
||||
&& (lastXLimits.min == c.xPercentageLimits.min)
|
||||
&& (lastXLimits.max == c.xPercentageLimits.max);
|
||||
}
|
||||
|
||||
DoubleLineRatios::DoubleLineRatios(bool isDouble) {
|
||||
first = second = (isDouble ? 0 : 1);
|
||||
}
|
||||
|
||||
void DoubleLineRatios::init(const Data::StatisticalChart &chartData) {
|
||||
if (chartData.lines.size() != 2) {
|
||||
first = 1.;
|
||||
second = 1.;
|
||||
} else {
|
||||
const auto firstMax = chartData.lines.front().maxValue;
|
||||
const auto secondMax = chartData.lines.back().maxValue;
|
||||
if (firstMax > secondMax) {
|
||||
first = 1.;
|
||||
second = firstMax / float64(secondMax);
|
||||
} else {
|
||||
first = secondMax / float64(firstMax);
|
||||
second = 1.;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float64 DoubleLineRatios::ratio(int lineId) const {
|
||||
return (lineId == 1) ? first : second;
|
||||
}
|
||||
|
||||
void AbstractChartView::setUpdateCallback(Fn<void()> callback) {
|
||||
_updateCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void AbstractChartView::update() {
|
||||
if (_updateCallback) {
|
||||
_updateCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractChartView::setLinesFilterController(
|
||||
std::shared_ptr<LinesFilterController> c) {
|
||||
_linesFilterController = std::move(c);
|
||||
}
|
||||
|
||||
auto AbstractChartView::linesFilterController() const
|
||||
-> std::shared_ptr<LinesFilterController> {
|
||||
return _linesFilterController;
|
||||
}
|
||||
|
||||
AbstractChartView::HeightLimits DefaultHeightLimits(
|
||||
const DoubleLineRatios &ratios,
|
||||
const std::shared_ptr<LinesFilterController> &linesFilter,
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices) {
|
||||
auto minValue = std::numeric_limits<ChartValue>::max();
|
||||
auto maxValue = ChartValue(0);
|
||||
|
||||
auto minValueFull = std::numeric_limits<ChartValue>::max();
|
||||
auto maxValueFull = ChartValue(0);
|
||||
for (auto &l : chartData.lines) {
|
||||
if (!linesFilter->isEnabled(l.id)) {
|
||||
continue;
|
||||
}
|
||||
const auto r = ratios.ratio(l.id);
|
||||
const auto lineMax = l.segmentTree.rMaxQ(xIndices.min, xIndices.max);
|
||||
const auto lineMin = l.segmentTree.rMinQ(xIndices.min, xIndices.max);
|
||||
maxValue = std::max(ChartValue(lineMax * r), maxValue);
|
||||
minValue = std::min(ChartValue(lineMin * r), minValue);
|
||||
|
||||
maxValueFull = std::max(ChartValue(l.maxValue * r), maxValueFull);
|
||||
minValueFull = std::min(ChartValue(l.minValue * r), minValueFull);
|
||||
}
|
||||
if (maxValue == minValue) {
|
||||
maxValue = chartData.maxValue;
|
||||
minValue = chartData.minValue;
|
||||
}
|
||||
return {
|
||||
.full = Limits{ float64(minValueFull), float64(maxValueFull) },
|
||||
.ranged = Limits{ float64(minValue), float64(maxValue) },
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
127
Telegram/SourceFiles/statistics/view/abstract_chart_view.h
Normal file
127
Telegram/SourceFiles/statistics/view/abstract_chart_view.h
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
struct StatisticalChart;
|
||||
} // namespace Data
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
struct Limits;
|
||||
class LinesFilterController;
|
||||
|
||||
struct PaintContext final {
|
||||
const Data::StatisticalChart &chartData;
|
||||
const Limits xIndices;
|
||||
const Limits xPercentageLimits;
|
||||
const Limits heightLimits;
|
||||
const QRect ▭
|
||||
bool footer = false;
|
||||
};
|
||||
|
||||
struct CachedSelectedPoints final {
|
||||
[[nodiscard]] bool isSame(int x, const PaintContext &c) const;
|
||||
|
||||
int lastXIndex = -1;
|
||||
Limits lastHeightLimits;
|
||||
Limits lastXLimits;
|
||||
base::flat_map<int, QPointF> points;
|
||||
};
|
||||
|
||||
class DoubleLineRatios final : std::pair<float64, float64> {
|
||||
public:
|
||||
DoubleLineRatios(bool isDouble);
|
||||
|
||||
operator bool() const {
|
||||
return first > 0;
|
||||
}
|
||||
|
||||
void init(const Data::StatisticalChart &chartData);
|
||||
[[nodiscard]] float64 ratio(int lineId) const;
|
||||
};
|
||||
|
||||
class AbstractChartView {
|
||||
public:
|
||||
virtual ~AbstractChartView() = default;
|
||||
|
||||
virtual void paint(QPainter &p, const PaintContext &c) = 0;
|
||||
|
||||
virtual void paintSelectedXIndex(
|
||||
QPainter &p,
|
||||
const PaintContext &c,
|
||||
int selectedXIndex,
|
||||
float64 progress) = 0;
|
||||
|
||||
[[nodiscard]] virtual int findXIndexByPosition(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 x) = 0;
|
||||
|
||||
struct HeightLimits final {
|
||||
Limits full;
|
||||
Limits ranged;
|
||||
};
|
||||
|
||||
[[nodiscard]] virtual HeightLimits heightLimits(
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices) = 0;
|
||||
|
||||
struct LocalZoomResult final {
|
||||
bool hasZoom = false;
|
||||
Limits limitIndices;
|
||||
Limits range;
|
||||
};
|
||||
|
||||
struct LocalZoomArgs final {
|
||||
enum class Type {
|
||||
Prepare,
|
||||
SkipCalculation,
|
||||
CheckAvailability,
|
||||
Process,
|
||||
SaveZoomFromFooter,
|
||||
};
|
||||
const Data::StatisticalChart &chartData;
|
||||
Type type;
|
||||
float64 progress = 0.;
|
||||
int xIndex = 0;
|
||||
};
|
||||
|
||||
virtual LocalZoomResult maybeLocalZoom(const LocalZoomArgs &args) {
|
||||
return {};
|
||||
}
|
||||
|
||||
virtual void handleMouseMove(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const QRect &rect,
|
||||
const QPoint &p) {
|
||||
}
|
||||
|
||||
void setUpdateCallback(Fn<void()> callback);
|
||||
void update();
|
||||
|
||||
void setLinesFilterController(std::shared_ptr<LinesFilterController> c);
|
||||
|
||||
protected:
|
||||
using LinesFilterControllerPtr = std::shared_ptr<LinesFilterController>;
|
||||
[[nodiscard]] LinesFilterControllerPtr linesFilterController() const;
|
||||
|
||||
private:
|
||||
LinesFilterControllerPtr _linesFilterController;
|
||||
Fn<void()> _updateCallback;
|
||||
|
||||
};
|
||||
|
||||
AbstractChartView::HeightLimits DefaultHeightLimits(
|
||||
const DoubleLineRatios &ratios,
|
||||
const std::shared_ptr<LinesFilterController> &linesFilter,
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices);
|
||||
|
||||
} // namespace Statistic
|
||||
286
Telegram/SourceFiles/statistics/view/bar_chart_view.cpp
Normal file
286
Telegram/SourceFiles/statistics/view/bar_chart_view.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "statistics/view/bar_chart_view.h"
|
||||
|
||||
#include "data/data_statistics_chart.h"
|
||||
#include "statistics/chart_lines_filter_controller.h"
|
||||
#include "statistics/view/stack_chart_common.h"
|
||||
#include "ui/effects/animation_value_f.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_statistics.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
BarChartView::BarChartView(bool isStack)
|
||||
: _isStack(isStack)
|
||||
, _cachedLineRatios(false) {
|
||||
}
|
||||
|
||||
BarChartView::~BarChartView() = default;
|
||||
|
||||
void BarChartView::paint(QPainter &p, const PaintContext &c) {
|
||||
constexpr auto kOffset = float64(2);
|
||||
_lastPaintedXIndices = {
|
||||
float64(std::max(0., c.xIndices.min - kOffset)),
|
||||
float64(std::min(
|
||||
float64(c.chartData.xPercentage.size() - 1),
|
||||
c.xIndices.max + kOffset)),
|
||||
};
|
||||
|
||||
BarChartView::paintChartAndSelected(p, c);
|
||||
}
|
||||
|
||||
void BarChartView::paintChartAndSelected(
|
||||
QPainter &p,
|
||||
const PaintContext &c) {
|
||||
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
||||
const auto &[leftStart, w] = ComputeLeftStartAndStep(
|
||||
c.chartData,
|
||||
c.xPercentageLimits,
|
||||
c.rect,
|
||||
localStart);
|
||||
|
||||
p.setClipRect(0, 0, c.rect.width() * 2, rect::bottom(c.rect));
|
||||
|
||||
const auto opacity = p.opacity();
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
auto bottoms = std::vector<float64>(
|
||||
localEnd - localStart + 1,
|
||||
-c.rect.y());
|
||||
auto selectedBottoms = std::vector<float64>();
|
||||
const auto hasSelectedXIndex = _isStack
|
||||
&& !c.footer
|
||||
&& (_lastSelectedXIndex >= 0);
|
||||
if (hasSelectedXIndex) {
|
||||
selectedBottoms = std::vector<float64>(c.chartData.lines.size(), 0);
|
||||
constexpr auto kSelectedAlpha = 0.5;
|
||||
p.setOpacity(
|
||||
anim::interpolateF(1.0, kSelectedAlpha, _lastSelectedXProgress));
|
||||
}
|
||||
|
||||
for (auto i = 0; i < c.chartData.lines.size(); i++) {
|
||||
const auto &line = c.chartData.lines[i];
|
||||
auto path = QPainterPath();
|
||||
for (auto x = localStart; x <= localEnd; x++) {
|
||||
if (line.y[x] <= 0 && _isStack) {
|
||||
continue;
|
||||
}
|
||||
const auto yPercentage = (line.y[x] - c.heightLimits.min)
|
||||
/ float64(c.heightLimits.max - c.heightLimits.min);
|
||||
const auto yPoint = yPercentage
|
||||
* c.rect.height()
|
||||
* linesFilterController()->alpha(line.id);
|
||||
|
||||
const auto bottomIndex = x - localStart;
|
||||
const auto column = QRectF(
|
||||
leftStart + (x - localStart) * w,
|
||||
c.rect.height() - bottoms[bottomIndex] - yPoint,
|
||||
w,
|
||||
yPoint);
|
||||
if (hasSelectedXIndex && (x == _lastSelectedXIndex)) {
|
||||
selectedBottoms[i] = column.y();
|
||||
}
|
||||
if (_isStack) {
|
||||
path.addRect(column);
|
||||
bottoms[bottomIndex] += yPoint;
|
||||
} else {
|
||||
if (path.isEmpty()) {
|
||||
path.moveTo(column.topLeft());
|
||||
} else {
|
||||
path.lineTo(column.topLeft());
|
||||
}
|
||||
if (x == localEnd) {
|
||||
path.lineTo(c.rect.width(), column.y());
|
||||
} else {
|
||||
path.lineTo(rect::right(column), column.y());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_isStack) {
|
||||
p.fillPath(path, line.color);
|
||||
} else {
|
||||
p.strokePath(path, line.color);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto i = 0; i < selectedBottoms.size(); i++) {
|
||||
p.setOpacity(opacity);
|
||||
if (selectedBottoms[i] <= 0) {
|
||||
continue;
|
||||
}
|
||||
const auto &line = c.chartData.lines[i];
|
||||
const auto yPercentage = 0.
|
||||
+ (line.y[_lastSelectedXIndex] - c.heightLimits.min)
|
||||
/ float64(c.heightLimits.max - c.heightLimits.min);
|
||||
const auto yPoint = yPercentage
|
||||
* c.rect.height()
|
||||
* linesFilterController()->alpha(line.id);
|
||||
|
||||
const auto column = QRectF(
|
||||
leftStart + (_lastSelectedXIndex - localStart) * w,
|
||||
selectedBottoms[i],
|
||||
w,
|
||||
yPoint);
|
||||
p.fillRect(column, line.color);
|
||||
}
|
||||
|
||||
p.setClipping(false);
|
||||
}
|
||||
|
||||
void BarChartView::paintSelectedXIndex(
|
||||
QPainter &p,
|
||||
const PaintContext &c,
|
||||
int selectedXIndex,
|
||||
float64 progress) {
|
||||
const auto was = _lastSelectedXIndex;
|
||||
_lastSelectedXIndex = selectedXIndex;
|
||||
_lastSelectedXProgress = progress;
|
||||
|
||||
if ((_lastSelectedXIndex < 0) && (was < 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isStack) {
|
||||
BarChartView::paintChartAndSelected(p, c);
|
||||
} else if (selectedXIndex >= 0) {
|
||||
const auto linesFilter = linesFilterController();
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto o = ScopedPainterOpacity(p, progress);
|
||||
p.setBrush(st::boxBg);
|
||||
const auto r = st::statisticsDetailsDotRadius;
|
||||
const auto isSameToken = _selectedPoints.isSame(selectedXIndex, c);
|
||||
auto linePainted = false;
|
||||
|
||||
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
||||
const auto &[leftStart, w] = ComputeLeftStartAndStep(
|
||||
c.chartData,
|
||||
c.xPercentageLimits,
|
||||
c.rect,
|
||||
localStart);
|
||||
|
||||
for (auto i = 0; i < c.chartData.lines.size(); i++) {
|
||||
const auto &line = c.chartData.lines[i];
|
||||
const auto lineAlpha = linesFilter->alpha(line.id);
|
||||
const auto useCache = isSameToken
|
||||
|| (lineAlpha < 1. && !linesFilter->isEnabled(line.id));
|
||||
if (!useCache) {
|
||||
// Calculate.
|
||||
const auto x = _lastSelectedXIndex;
|
||||
const auto yPercentage = (line.y[x] - c.heightLimits.min)
|
||||
/ float64(c.heightLimits.max - c.heightLimits.min);
|
||||
const auto yPoint = (1. - yPercentage) * c.rect.height();
|
||||
|
||||
const auto column = QRectF(
|
||||
leftStart + (x - localStart) * w,
|
||||
c.rect.height() - 0 - yPoint,
|
||||
w,
|
||||
yPoint);
|
||||
const auto xPoint = column.left() + column.width() / 2.;
|
||||
_selectedPoints.points[line.id] = QPointF(xPoint, yPoint)
|
||||
+ c.rect.topLeft();
|
||||
}
|
||||
|
||||
if (!linePainted && lineAlpha) {
|
||||
[[maybe_unused]] const auto o = ScopedPainterOpacity(
|
||||
p,
|
||||
p.opacity() * progress * kRulerLineAlpha);
|
||||
const auto lineRect = QRectF(
|
||||
begin(_selectedPoints.points)->second.x()
|
||||
- (st::lineWidth / 2.),
|
||||
c.rect.y(),
|
||||
st::lineWidth,
|
||||
c.rect.height());
|
||||
p.fillRect(lineRect, st::boxTextFg);
|
||||
linePainted = true;
|
||||
}
|
||||
|
||||
// Paint.
|
||||
auto o = ScopedPainterOpacity(p, lineAlpha * p.opacity());
|
||||
p.setPen(QPen(line.color, st::statisticsChartLineWidth));
|
||||
p.drawEllipse(_selectedPoints.points[line.id], r, r);
|
||||
}
|
||||
_selectedPoints.lastXIndex = selectedXIndex;
|
||||
_selectedPoints.lastHeightLimits = c.heightLimits;
|
||||
_selectedPoints.lastXLimits = c.xPercentageLimits;
|
||||
}
|
||||
}
|
||||
|
||||
int BarChartView::findXIndexByPosition(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 xPos) {
|
||||
if ((xPos < rect.x()) || (xPos > (rect.x() + rect.width()))) {
|
||||
return _lastSelectedXIndex = -1;
|
||||
}
|
||||
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
||||
const auto &[leftStart, w] = ComputeLeftStartAndStep(
|
||||
chartData,
|
||||
xPercentageLimits,
|
||||
rect,
|
||||
localStart);
|
||||
|
||||
for (auto i = 0; i < chartData.lines.size(); i++) {
|
||||
for (auto x = localStart; x <= localEnd; x++) {
|
||||
const auto left = leftStart + (x - localStart) * w;
|
||||
if ((xPos >= left) && (xPos < (left + w))) {
|
||||
return _lastSelectedXIndex = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _lastSelectedXIndex = -1;
|
||||
}
|
||||
|
||||
AbstractChartView::HeightLimits BarChartView::heightLimits(
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices) {
|
||||
if (!_isStack) {
|
||||
if (!_cachedLineRatios) {
|
||||
_cachedLineRatios.init(chartData);
|
||||
}
|
||||
|
||||
return DefaultHeightLimits(
|
||||
_cachedLineRatios,
|
||||
linesFilterController(),
|
||||
chartData,
|
||||
xIndices);
|
||||
}
|
||||
_cachedHeightLimits = {};
|
||||
if (_cachedHeightLimits.ySum.empty()) {
|
||||
_cachedHeightLimits.ySum.reserve(chartData.x.size());
|
||||
|
||||
auto maxValueFull = ChartValue(0);
|
||||
for (auto i = 0; i < chartData.x.size(); i++) {
|
||||
auto sum = ChartValue(0);
|
||||
for (const auto &line : chartData.lines) {
|
||||
if (linesFilterController()->isEnabled(line.id)) {
|
||||
sum += line.y[i];
|
||||
}
|
||||
}
|
||||
_cachedHeightLimits.ySum.push_back(sum);
|
||||
maxValueFull = std::max(sum, maxValueFull);
|
||||
}
|
||||
|
||||
_cachedHeightLimits.ySumSegmentTree = SegmentTree(
|
||||
_cachedHeightLimits.ySum);
|
||||
_cachedHeightLimits.full = { 0., float64(maxValueFull) };
|
||||
}
|
||||
const auto max = std::max(
|
||||
_cachedHeightLimits.ySumSegmentTree.rMaxQ(
|
||||
xIndices.min,
|
||||
xIndices.max),
|
||||
ChartValue(1));
|
||||
return {
|
||||
.full = _cachedHeightLimits.full,
|
||||
.ranged = { 0., float64(max) },
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
66
Telegram/SourceFiles/statistics/view/bar_chart_view.h
Normal file
66
Telegram/SourceFiles/statistics/view/bar_chart_view.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "statistics/segment_tree.h"
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "statistics/statistics_types.h"
|
||||
#include "statistics/view/abstract_chart_view.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
|
||||
namespace Data {
|
||||
struct StatisticalChart;
|
||||
} // namespace Data
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
struct Limits;
|
||||
|
||||
class BarChartView final : public AbstractChartView {
|
||||
public:
|
||||
BarChartView(bool isStack);
|
||||
~BarChartView() override final;
|
||||
|
||||
void paint(QPainter &p, const PaintContext &c) override;
|
||||
|
||||
void paintSelectedXIndex(
|
||||
QPainter &p,
|
||||
const PaintContext &c,
|
||||
int selectedXIndex,
|
||||
float64 progress) override;
|
||||
|
||||
int findXIndexByPosition(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 x) override;
|
||||
|
||||
[[nodiscard]] HeightLimits heightLimits(
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices) override;
|
||||
|
||||
private:
|
||||
void paintChartAndSelected(QPainter &p, const PaintContext &c);
|
||||
|
||||
struct {
|
||||
Limits full;
|
||||
std::vector<ChartValue> ySum;
|
||||
SegmentTree ySumSegmentTree;
|
||||
} _cachedHeightLimits;
|
||||
|
||||
const bool _isStack;
|
||||
DoubleLineRatios _cachedLineRatios; // Non-stack.
|
||||
Limits _lastPaintedXIndices;
|
||||
int _lastSelectedXIndex = -1;
|
||||
float64 _lastSelectedXProgress = 0;
|
||||
|
||||
CachedSelectedPoints _selectedPoints; // Non-stack.
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
205
Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp
Normal file
205
Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "statistics/view/chart_rulers_view.h"
|
||||
|
||||
#include "info/channel_statistics/earn/earn_format.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "statistics/chart_lines_filter_controller.h"
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "statistics/statistics_graphics.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_statistics.h"
|
||||
|
||||
#include <QtCore/QLocale>
|
||||
|
||||
namespace Statistic {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QString FormatF(float64 absoluteValue) {
|
||||
static constexpr auto kTooMuch = int(10'000);
|
||||
static constexpr auto kTooSmall = 1e-9;
|
||||
return (std::abs(absoluteValue) <= kTooSmall)
|
||||
? u"0"_q
|
||||
: (absoluteValue >= kTooMuch)
|
||||
? Lang::FormatCountToShort(absoluteValue).string
|
||||
: QLocale().toString(absoluteValue);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChartRulersView::ChartRulersView() = default;
|
||||
|
||||
void ChartRulersView::setChartData(
|
||||
const Data::StatisticalChart &chartData,
|
||||
ChartViewType type,
|
||||
std::shared_ptr<LinesFilterController> linesFilter) {
|
||||
_rulers.clear();
|
||||
_isDouble = (type == ChartViewType::DoubleLinear)
|
||||
|| chartData.currencyRate;
|
||||
if (chartData.currencyRate) {
|
||||
_currencyIcon = ChartCurrencyIcon(chartData, {});
|
||||
if (chartData.currency == Data::StatisticalCurrency::Ton) {
|
||||
_leftCustomCaption = [=](float64 value) {
|
||||
return FormatF(value / float64(kOneStarInNano));
|
||||
};
|
||||
_rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) {
|
||||
return Info::ChannelEarn::ToUsd(v / float64(kOneStarInNano), rate, 0);
|
||||
};
|
||||
} else {
|
||||
_leftCustomCaption = [=](float64 value) {
|
||||
return FormatF(value);
|
||||
};
|
||||
_rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) {
|
||||
return Info::ChannelEarn::ToUsd(v, rate, 0);
|
||||
};
|
||||
}
|
||||
_rightPen = QPen(st::windowSubTextFg);
|
||||
}
|
||||
if (_isDouble && (chartData.lines.size() == 2)) {
|
||||
_linesFilter = std::move(linesFilter);
|
||||
_leftPen = QPen(chartData.lines.front().color);
|
||||
_rightPen = QPen(chartData.lines.back().color);
|
||||
_leftLineId = chartData.lines.front().id;
|
||||
_rightLineId = chartData.lines.back().id;
|
||||
|
||||
const auto firstMax = chartData.lines.front().maxValue;
|
||||
const auto secondMax = chartData.lines.back().maxValue;
|
||||
if (firstMax > secondMax) {
|
||||
_isLeftLineScaled = false;
|
||||
_scaledLineRatio = firstMax / float64(secondMax);
|
||||
} else {
|
||||
_isLeftLineScaled = true;
|
||||
_scaledLineRatio = secondMax / float64(firstMax);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartRulersView::paintRulers(
|
||||
QPainter &p,
|
||||
const QRect &r) {
|
||||
const auto alpha = p.opacity();
|
||||
for (auto &ruler : _rulers) {
|
||||
p.setOpacity(alpha * ruler.alpha * kRulerLineAlpha);
|
||||
for (const auto &line : ruler.lines) {
|
||||
const auto lineRect = QRect(
|
||||
0,
|
||||
r.y() + r.height() * line.relativeValue,
|
||||
r.x() + r.width(),
|
||||
st::lineWidth);
|
||||
p.fillRect(lineRect, st::boxTextFg);
|
||||
}
|
||||
}
|
||||
p.setOpacity(alpha);
|
||||
}
|
||||
|
||||
void ChartRulersView::paintCaptionsToRulers(
|
||||
QPainter &p,
|
||||
const QRect &r) {
|
||||
const auto offset = r.y() - st::statisticsChartRulerCaptionSkip;
|
||||
p.setFont(st::statisticsDetailsBottomCaptionStyle.font);
|
||||
const auto alpha = p.opacity();
|
||||
for (auto &ruler : _rulers) {
|
||||
const auto rulerAlpha = alpha * ruler.alpha;
|
||||
p.setOpacity(rulerAlpha);
|
||||
const auto left = _currencyIcon.isNull()
|
||||
? 0
|
||||
: _currencyIcon.width() / style::DevicePixelRatio();
|
||||
for (const auto &line : ruler.lines) {
|
||||
const auto y = offset + r.height() * line.relativeValue;
|
||||
const auto hasLinesFilter = _isDouble && _linesFilter;
|
||||
if (hasLinesFilter) {
|
||||
p.setPen(_leftPen);
|
||||
p.setOpacity(rulerAlpha * _linesFilter->alpha(_leftLineId));
|
||||
} else {
|
||||
p.setPen(st::windowSubTextFg);
|
||||
}
|
||||
if (!_currencyIcon.isNull()) {
|
||||
const auto iconTop = y
|
||||
- _currencyIcon.height() / style::DevicePixelRatio()
|
||||
+ st::statisticsChartRulerCaptionSkip;
|
||||
p.drawImage(0, iconTop, _currencyIcon);
|
||||
}
|
||||
p.drawText(
|
||||
left,
|
||||
y,
|
||||
(!_isDouble)
|
||||
? line.caption
|
||||
: _isLeftLineScaled
|
||||
? line.scaledLineCaption
|
||||
: line.caption);
|
||||
if (hasLinesFilter || _rightCustomCaption) {
|
||||
if (_linesFilter) {
|
||||
p.setOpacity(rulerAlpha * _linesFilter->alpha(_rightLineId));
|
||||
}
|
||||
p.setPen(_rightPen);
|
||||
p.drawText(
|
||||
r.width() - line.rightCaptionWidth,
|
||||
y,
|
||||
_isLeftLineScaled
|
||||
? line.caption
|
||||
: line.scaledLineCaption);
|
||||
}
|
||||
}
|
||||
}
|
||||
p.setOpacity(alpha);
|
||||
}
|
||||
|
||||
void ChartRulersView::computeRelative(
|
||||
int newMaxHeight,
|
||||
int newMinHeight) {
|
||||
for (auto &ruler : _rulers) {
|
||||
ruler.computeRelative(newMaxHeight, newMinHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartRulersView::setAlpha(float64 value) {
|
||||
for (auto &ruler : _rulers) {
|
||||
ruler.alpha = ruler.fixedAlpha * (1. - value);
|
||||
}
|
||||
_rulers.back().alpha = value;
|
||||
if (value == 1.) {
|
||||
while (_rulers.size() > 1) {
|
||||
const auto startIt = begin(_rulers);
|
||||
if (!startIt->alpha) {
|
||||
_rulers.erase(startIt);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartRulersView::add(Limits newHeight, bool animated) {
|
||||
auto newLinesData = ChartRulersData(
|
||||
newHeight.max,
|
||||
newHeight.min,
|
||||
true,
|
||||
_isDouble ? _scaledLineRatio : 0.,
|
||||
_leftCustomCaption,
|
||||
_rightCustomCaption);
|
||||
if (_isDouble) {
|
||||
const auto &font = st::statisticsDetailsBottomCaptionStyle.font;
|
||||
for (auto &line : newLinesData.lines) {
|
||||
line.rightCaptionWidth = font->width(_isLeftLineScaled
|
||||
? line.caption
|
||||
: line.scaledLineCaption);
|
||||
}
|
||||
}
|
||||
if (!animated) {
|
||||
_rulers.clear();
|
||||
}
|
||||
for (auto &ruler : _rulers) {
|
||||
ruler.fixedAlpha = ruler.alpha;
|
||||
}
|
||||
_rulers.push_back(newLinesData);
|
||||
if (!animated) {
|
||||
_rulers.back().alpha = 1.;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
59
Telegram/SourceFiles/statistics/view/chart_rulers_view.h
Normal file
59
Telegram/SourceFiles/statistics/view/chart_rulers_view.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "statistics/chart_rulers_data.h"
|
||||
|
||||
namespace Data {
|
||||
struct StatisticalChart;
|
||||
} // namespace Data
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
enum class ChartViewType;
|
||||
struct Limits;
|
||||
class LinesFilterController;
|
||||
|
||||
struct ChartRulersView final {
|
||||
public:
|
||||
ChartRulersView();
|
||||
|
||||
void setChartData(
|
||||
const Data::StatisticalChart &chartData,
|
||||
ChartViewType type,
|
||||
std::shared_ptr<LinesFilterController> linesFilter);
|
||||
|
||||
void paintRulers(QPainter &p, const QRect &r);
|
||||
|
||||
void paintCaptionsToRulers(QPainter &p, const QRect &r);
|
||||
|
||||
void computeRelative(int newMaxHeight, int newMinHeight);
|
||||
void setAlpha(float64 value);
|
||||
void add(Limits newHeight, bool animated);
|
||||
|
||||
private:
|
||||
bool _isDouble = false;
|
||||
QPen _leftPen;
|
||||
QPen _rightPen;
|
||||
int _leftLineId = 0;
|
||||
int _rightLineId = 0;
|
||||
QImage _currencyIcon;
|
||||
|
||||
Fn<QString(float64)> _leftCustomCaption = nullptr;
|
||||
Fn<QString(float64)> _rightCustomCaption = nullptr;
|
||||
|
||||
std::vector<ChartRulersData> _rulers;
|
||||
|
||||
std::shared_ptr<LinesFilterController> _linesFilter;
|
||||
|
||||
float64 _scaledLineRatio = 0.;
|
||||
bool _isLeftLineScaled = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
38
Telegram/SourceFiles/statistics/view/chart_view_factory.cpp
Normal file
38
Telegram/SourceFiles/statistics/view/chart_view_factory.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "statistics/view/chart_view_factory.h"
|
||||
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "statistics/view/linear_chart_view.h"
|
||||
#include "statistics/view/bar_chart_view.h"
|
||||
#include "statistics/view/stack_linear_chart_view.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
std::unique_ptr<AbstractChartView> CreateChartView(ChartViewType type) {
|
||||
switch (type) {
|
||||
case ChartViewType::Linear: {
|
||||
return std::make_unique<LinearChartView>(false);
|
||||
} break;
|
||||
case ChartViewType::Bar: {
|
||||
return std::make_unique<BarChartView>(false);
|
||||
} break;
|
||||
case ChartViewType::StackBar: {
|
||||
return std::make_unique<BarChartView>(true);
|
||||
} break;
|
||||
case ChartViewType::DoubleLinear: {
|
||||
return std::make_unique<LinearChartView>(true);
|
||||
} break;
|
||||
case ChartViewType::StackLinear: {
|
||||
return std::make_unique<StackLinearChartView>();
|
||||
} break;
|
||||
default: Unexpected("Type in Statistic::CreateChartView.");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
18
Telegram/SourceFiles/statistics/view/chart_view_factory.h
Normal file
18
Telegram/SourceFiles/statistics/view/chart_view_factory.h
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
class AbstractChartView;
|
||||
enum class ChartViewType;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<AbstractChartView> CreateChartView(
|
||||
ChartViewType type);
|
||||
|
||||
} // namespace Statistic
|
||||
227
Telegram/SourceFiles/statistics/view/linear_chart_view.cpp
Normal file
227
Telegram/SourceFiles/statistics/view/linear_chart_view.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "statistics/view/linear_chart_view.h"
|
||||
|
||||
#include "data/data_statistics_chart.h"
|
||||
#include "statistics/chart_lines_filter_controller.h"
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "ui/effects/animation_value_f.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_statistics.h"
|
||||
|
||||
namespace Statistic {
|
||||
namespace {
|
||||
|
||||
void PaintChartLine(
|
||||
QPainter &p,
|
||||
int lineIndex,
|
||||
const PaintContext &c,
|
||||
const DoubleLineRatios &ratios) {
|
||||
const auto &line = c.chartData.lines[lineIndex];
|
||||
|
||||
auto chartPoints = QPolygonF();
|
||||
|
||||
constexpr auto kOffset = float64(2);
|
||||
const auto localStart = int(std::max(0., c.xIndices.min - kOffset));
|
||||
const auto localEnd = int(std::min(
|
||||
float64(c.chartData.xPercentage.size() - 1),
|
||||
c.xIndices.max + kOffset));
|
||||
|
||||
const auto ratio = ratios.ratio(line.id);
|
||||
|
||||
for (auto i = localStart; i <= localEnd; i++) {
|
||||
if (line.y[i] < 0) {
|
||||
continue;
|
||||
}
|
||||
const auto xPoint = c.rect.width()
|
||||
* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)
|
||||
/ (c.xPercentageLimits.max - c.xPercentageLimits.min));
|
||||
const auto yPercentage = (line.y[i] * ratio - c.heightLimits.min)
|
||||
/ float64(c.heightLimits.max - c.heightLimits.min);
|
||||
const auto yPoint = (1. - yPercentage) * c.rect.height();
|
||||
chartPoints << QPointF(xPoint, yPoint);
|
||||
}
|
||||
p.setPen(QPen(
|
||||
line.color,
|
||||
c.footer ? st::lineWidth : st::statisticsChartLineWidth));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawPolyline(chartPoints);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LinearChartView::LinearChartView(bool isDouble)
|
||||
: _cachedLineRatios(isDouble) {
|
||||
}
|
||||
|
||||
LinearChartView::~LinearChartView() = default;
|
||||
|
||||
void LinearChartView::paint(QPainter &p, const PaintContext &c) {
|
||||
const auto cacheToken = LinearChartView::CacheToken(
|
||||
c.xIndices,
|
||||
c.xPercentageLimits,
|
||||
c.heightLimits,
|
||||
c.rect.size());
|
||||
|
||||
const auto opacity = p.opacity();
|
||||
const auto linesFilter = linesFilterController();
|
||||
const auto imageSize = c.rect.size() * style::DevicePixelRatio();
|
||||
const auto cacheScale = 1. / style::DevicePixelRatio();
|
||||
auto &caches = (c.footer ? _footerCaches : _mainCaches);
|
||||
|
||||
for (auto i = 0; i < c.chartData.lines.size(); i++) {
|
||||
const auto &line = c.chartData.lines[i];
|
||||
p.setOpacity(linesFilter->alpha(line.id));
|
||||
if (!p.opacity()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto &cache = caches[line.id];
|
||||
|
||||
const auto isSameToken = (cache.lastToken == cacheToken);
|
||||
if ((isSameToken && cache.hq)
|
||||
|| (p.opacity() < 1. && !linesFilter->isEnabled(line.id))) {
|
||||
p.drawImage(c.rect.topLeft(), cache.image);
|
||||
continue;
|
||||
}
|
||||
cache.hq = isSameToken;
|
||||
auto image = QImage();
|
||||
image = QImage(
|
||||
imageSize * (isSameToken ? 1. : cacheScale),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
auto imagePainter = QPainter(&image);
|
||||
auto hq = PainterHighQualityEnabler(imagePainter);
|
||||
if (!isSameToken) {
|
||||
imagePainter.scale(cacheScale, cacheScale);
|
||||
}
|
||||
|
||||
PaintChartLine(imagePainter, i, c, _cachedLineRatios);
|
||||
}
|
||||
|
||||
if (!isSameToken) {
|
||||
image = image.scaled(
|
||||
imageSize,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::FastTransformation);
|
||||
}
|
||||
p.drawImage(c.rect.topLeft(), image);
|
||||
cache.lastToken = cacheToken;
|
||||
cache.image = std::move(image);
|
||||
}
|
||||
p.setOpacity(opacity);
|
||||
}
|
||||
|
||||
void LinearChartView::paintSelectedXIndex(
|
||||
QPainter &p,
|
||||
const PaintContext &c,
|
||||
int selectedXIndex,
|
||||
float64 progress) {
|
||||
if (selectedXIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const auto linesFilter = linesFilterController();
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto o = ScopedPainterOpacity(p, progress);
|
||||
p.setBrush(st::boxBg);
|
||||
const auto r = st::statisticsDetailsDotRadius;
|
||||
const auto i = selectedXIndex;
|
||||
const auto isSameToken = _selectedPoints.isSame(selectedXIndex, c);
|
||||
auto linePainted = false;
|
||||
for (const auto &line : c.chartData.lines) {
|
||||
const auto lineAlpha = linesFilter->alpha(line.id);
|
||||
const auto useCache = isSameToken
|
||||
|| (lineAlpha < 1. && !linesFilter->isEnabled(line.id));
|
||||
if (!useCache) {
|
||||
// Calculate.
|
||||
const auto r = _cachedLineRatios.ratio(line.id);
|
||||
const auto xPoint = c.rect.width()
|
||||
* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)
|
||||
/ (c.xPercentageLimits.max - c.xPercentageLimits.min));
|
||||
const auto yPercentage = (line.y[i] * r - c.heightLimits.min)
|
||||
/ float64(c.heightLimits.max - c.heightLimits.min);
|
||||
const auto yPoint = (1. - yPercentage) * c.rect.height();
|
||||
_selectedPoints.points[line.id] = QPointF(xPoint, yPoint)
|
||||
+ c.rect.topLeft();
|
||||
}
|
||||
|
||||
if (!linePainted && lineAlpha) {
|
||||
[[maybe_unused]] const auto o = ScopedPainterOpacity(
|
||||
p,
|
||||
p.opacity() * progress * kRulerLineAlpha);
|
||||
const auto lineRect = QRectF(
|
||||
begin(_selectedPoints.points)->second.x()
|
||||
- (st::lineWidth / 2.),
|
||||
c.rect.y(),
|
||||
st::lineWidth,
|
||||
c.rect.height());
|
||||
p.fillRect(lineRect, st::boxTextFg);
|
||||
linePainted = true;
|
||||
}
|
||||
|
||||
// Paint.
|
||||
auto o = ScopedPainterOpacity(p, lineAlpha * p.opacity());
|
||||
p.setPen(QPen(line.color, st::statisticsChartLineWidth));
|
||||
p.drawEllipse(_selectedPoints.points[line.id], r, r);
|
||||
}
|
||||
_selectedPoints.lastXIndex = selectedXIndex;
|
||||
_selectedPoints.lastHeightLimits = c.heightLimits;
|
||||
_selectedPoints.lastXLimits = c.xPercentageLimits;
|
||||
}
|
||||
|
||||
int LinearChartView::findXIndexByPosition(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 x) {
|
||||
if ((x < rect.x()) || (x > (rect.x() + rect.width()))) {
|
||||
return -1;
|
||||
}
|
||||
const auto pointerRatio = std::clamp(
|
||||
(x - rect.x()) / rect.width(),
|
||||
0.,
|
||||
1.);
|
||||
const auto rawXPercentage = anim::interpolateF(
|
||||
xPercentageLimits.min,
|
||||
xPercentageLimits.max,
|
||||
pointerRatio);
|
||||
const auto it = ranges::lower_bound(
|
||||
chartData.xPercentage,
|
||||
rawXPercentage);
|
||||
const auto left = rawXPercentage - (*(it - 1));
|
||||
const auto right = (*it) - rawXPercentage;
|
||||
const auto nearest = ((right) > (left)) ? (it - 1) : it;
|
||||
const auto resultXPercentageIt = ((*nearest) > xPercentageLimits.max)
|
||||
? (nearest - 1)
|
||||
: ((*nearest) < xPercentageLimits.min)
|
||||
? (nearest + 1)
|
||||
: nearest;
|
||||
if (resultXPercentageIt == end(chartData.xPercentage)) {
|
||||
return chartData.xPercentage.size() - 1;
|
||||
}
|
||||
return std::distance(begin(chartData.xPercentage), resultXPercentageIt);
|
||||
}
|
||||
|
||||
AbstractChartView::HeightLimits LinearChartView::heightLimits(
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices) {
|
||||
if (!_cachedLineRatios) {
|
||||
_cachedLineRatios.init(chartData);
|
||||
}
|
||||
|
||||
return DefaultHeightLimits(
|
||||
_cachedLineRatios,
|
||||
linesFilterController(),
|
||||
chartData,
|
||||
xIndices);
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
95
Telegram/SourceFiles/statistics/view/linear_chart_view.h
Normal file
95
Telegram/SourceFiles/statistics/view/linear_chart_view.h
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "statistics/view/abstract_chart_view.h"
|
||||
|
||||
namespace Data {
|
||||
struct StatisticalChart;
|
||||
} // namespace Data
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
struct Limits;
|
||||
|
||||
class LinearChartView final : public AbstractChartView {
|
||||
public:
|
||||
LinearChartView(bool isDouble);
|
||||
~LinearChartView() override final;
|
||||
|
||||
void paint(QPainter &p, const PaintContext &c) override;
|
||||
|
||||
void paintSelectedXIndex(
|
||||
QPainter &p,
|
||||
const PaintContext &c,
|
||||
int selectedXIndex,
|
||||
float64 progress) override;
|
||||
|
||||
int findXIndexByPosition(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 x) override;
|
||||
|
||||
[[nodiscard]] HeightLimits heightLimits(
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices) override;
|
||||
|
||||
private:
|
||||
DoubleLineRatios _cachedLineRatios;
|
||||
|
||||
[[nodiscard]] float64 lineRatio() const;
|
||||
|
||||
struct CacheToken final {
|
||||
explicit CacheToken() = default;
|
||||
explicit CacheToken(
|
||||
Limits xIndices,
|
||||
Limits xPercentageLimits,
|
||||
Limits heightLimits,
|
||||
QSize rectSize)
|
||||
: xIndices(std::move(xIndices))
|
||||
, xPercentageLimits(std::move(xPercentageLimits))
|
||||
, heightLimits(std::move(heightLimits))
|
||||
, rectSize(std::move(rectSize)) {
|
||||
}
|
||||
|
||||
bool operator==(const CacheToken &other) const {
|
||||
return (rectSize == other.rectSize)
|
||||
&& (xIndices.min == other.xIndices.min)
|
||||
&& (xIndices.max == other.xIndices.max)
|
||||
&& (xPercentageLimits.min == other.xPercentageLimits.min)
|
||||
&& (xPercentageLimits.max == other.xPercentageLimits.max)
|
||||
&& (heightLimits.min == other.heightLimits.min)
|
||||
&& (heightLimits.max == other.heightLimits.max);
|
||||
}
|
||||
|
||||
bool operator!=(const CacheToken &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
Limits xIndices;
|
||||
Limits xPercentageLimits;
|
||||
Limits heightLimits;
|
||||
QSize rectSize;
|
||||
};
|
||||
|
||||
struct Cache final {
|
||||
QImage image;
|
||||
CacheToken lastToken;
|
||||
bool hq = false;
|
||||
};
|
||||
|
||||
base::flat_map<int, Cache> _mainCaches;
|
||||
base::flat_map<int, Cache> _footerCaches;
|
||||
|
||||
CachedSelectedPoints _selectedPoints;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
87
Telegram/SourceFiles/statistics/view/stack_chart_common.cpp
Normal file
87
Telegram/SourceFiles/statistics/view/stack_chart_common.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "statistics/view/stack_chart_common.h"
|
||||
|
||||
#include "data/data_statistics_chart.h"
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "ui/effects/animation_value_f.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
LeftStartAndStep ComputeLeftStartAndStep(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 xIndexStart) {
|
||||
const auto fullWidth = rect.width()
|
||||
/ (xPercentageLimits.max - xPercentageLimits.min);
|
||||
const auto offset = fullWidth * xPercentageLimits.min;
|
||||
const auto p = (chartData.xPercentage.size() < 2)
|
||||
? 1.
|
||||
: chartData.xPercentage[1] * fullWidth;
|
||||
const auto w = chartData.xPercentage[1] * (fullWidth - p);
|
||||
const auto leftStart = rect.x()
|
||||
+ chartData.xPercentage[xIndexStart] * (fullWidth - p)
|
||||
- offset;
|
||||
return { leftStart, w };
|
||||
}
|
||||
|
||||
Limits FindStackXIndicesFromRawXPercentages(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &rawXPercentageLimits,
|
||||
const Limits &zoomedInLimitXIndices) {
|
||||
const auto zoomLimit = Limits{
|
||||
chartData.xPercentage[zoomedInLimitXIndices.min],
|
||||
chartData.xPercentage[zoomedInLimitXIndices.max],
|
||||
};
|
||||
// Due to a specificity of the stack chart plotting,
|
||||
// the right edge has a special offset to the left.
|
||||
// This reduces the number of displayed points by 1,
|
||||
// but allows the last point to be displayed.
|
||||
const auto offset = (zoomLimit.max == 1.) ? 0 : -1;
|
||||
const auto rightShrink = (rawXPercentageLimits.max == 1.)
|
||||
? ((zoomLimit.max == 1.) ? 0 : 1)
|
||||
: 0;
|
||||
const auto n = chartData.xPercentage.size();
|
||||
auto minIt = -1;
|
||||
auto maxIt = n;
|
||||
const auto zoomedIn = Limits{
|
||||
anim::interpolateF(
|
||||
zoomLimit.min,
|
||||
zoomLimit.max,
|
||||
rawXPercentageLimits.min),
|
||||
anim::interpolateF(
|
||||
zoomLimit.min,
|
||||
zoomLimit.max,
|
||||
rawXPercentageLimits.max),
|
||||
};
|
||||
for (auto i = int(0); i < n; i++) {
|
||||
if (minIt < 0) {
|
||||
if (chartData.xPercentage[i] > zoomedIn.min) {
|
||||
minIt = i;
|
||||
}
|
||||
}
|
||||
if (maxIt >= n) {
|
||||
if (chartData.xPercentage[i] > zoomedIn.max) {
|
||||
maxIt = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
.min = std::clamp(
|
||||
float64(minIt + offset),
|
||||
zoomedInLimitXIndices.min,
|
||||
zoomedInLimitXIndices.max - rightShrink),
|
||||
.max = std::clamp(
|
||||
float64(maxIt + offset),
|
||||
zoomedInLimitXIndices.min,
|
||||
zoomedInLimitXIndices.max - rightShrink),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
34
Telegram/SourceFiles/statistics/view/stack_chart_common.h
Normal file
34
Telegram/SourceFiles/statistics/view/stack_chart_common.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
struct StatisticalChart;
|
||||
} // namespace Data
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
struct Limits;
|
||||
|
||||
struct LeftStartAndStep final {
|
||||
float64 start = 0.;
|
||||
float64 step = 0.;
|
||||
};
|
||||
|
||||
[[nodiscard]] LeftStartAndStep ComputeLeftStartAndStep(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 xIndexStart);
|
||||
|
||||
[[nodiscard]] Limits FindStackXIndicesFromRawXPercentages(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &rawXPercentageLimits,
|
||||
const Limits &zoomedInLimitXIndices);
|
||||
|
||||
} // namespace Statistic
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "statistics/view/stack_linear_chart_common.h"
|
||||
|
||||
#include "data/data_statistics_chart.h"
|
||||
#include "statistics/chart_lines_filter_controller.h"
|
||||
#include "statistics/statistics_common.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
PiePartData PiePartsPercentage(
|
||||
const std::vector<float64> &sums,
|
||||
float64 totalSum,
|
||||
bool round) {
|
||||
auto result = PiePartData();
|
||||
result.parts.reserve(sums.size());
|
||||
auto stackedPercentage = 0.;
|
||||
|
||||
auto sumPercDiffs = 0.;
|
||||
auto maxPercDiff = 0.;
|
||||
auto minPercDiff = 0.;
|
||||
auto maxPercDiffIndex = int(-1);
|
||||
auto minPercDiffIndex = int(-1);
|
||||
auto roundedPercentagesSum = 0.;
|
||||
|
||||
result.pieHasSinglePart = false;
|
||||
constexpr auto kPerChar = '%';
|
||||
for (auto k = 0; k < sums.size(); k++) {
|
||||
const auto rawPercentage = totalSum ? (sums[k] / totalSum) : 0.;
|
||||
const auto rounded = round
|
||||
? (0.01 * std::round(rawPercentage * 100.))
|
||||
: rawPercentage;
|
||||
roundedPercentagesSum += rounded;
|
||||
const auto diff = rawPercentage - rounded;
|
||||
sumPercDiffs += diff;
|
||||
const auto diffAbs = std::abs(diff);
|
||||
if (maxPercDiff < diffAbs) {
|
||||
maxPercDiff = diffAbs;
|
||||
maxPercDiffIndex = k;
|
||||
}
|
||||
if (minPercDiff < diffAbs) {
|
||||
minPercDiff = diffAbs;
|
||||
minPercDiffIndex = k;
|
||||
}
|
||||
|
||||
stackedPercentage += rounded;
|
||||
result.parts.push_back({
|
||||
rounded,
|
||||
stackedPercentage * 360. - 180.,
|
||||
QString::number(int(rounded * 100)) + kPerChar,
|
||||
});
|
||||
result.pieHasSinglePart |= (rounded == 1.);
|
||||
}
|
||||
if (round) {
|
||||
const auto index = (roundedPercentagesSum > 1.)
|
||||
? maxPercDiffIndex
|
||||
: minPercDiffIndex;
|
||||
if (index >= 0) {
|
||||
result.parts[index].roundedPercentage += sumPercDiffs;
|
||||
result.parts[index].percentageText = QString::number(
|
||||
int(result.parts[index].roundedPercentage * 100)) + kPerChar;
|
||||
const auto angleShrink = (sumPercDiffs) * 360.;
|
||||
for (auto &part : result.parts) {
|
||||
part.stackedAngle += angleShrink;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
PiePartData PiePartsPercentageByIndices(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const std::shared_ptr<LinesFilterController> &linesFilter,
|
||||
const Limits &xIndices) {
|
||||
auto sums = std::vector<float64>();
|
||||
sums.reserve(chartData.lines.size());
|
||||
auto totalSum = 0.;
|
||||
for (const auto &line : chartData.lines) {
|
||||
auto sum = ChartValue(0);
|
||||
for (auto i = xIndices.min; i <= xIndices.max; i++) {
|
||||
const auto index = int(base::SafeRound(i));
|
||||
Assert(index >= 0 && index < line.y.size());
|
||||
sum += line.y[i];
|
||||
}
|
||||
if (linesFilter) {
|
||||
sum *= linesFilter->alpha(line.id);
|
||||
}
|
||||
totalSum += sum;
|
||||
sums.push_back(sum);
|
||||
}
|
||||
return PiePartsPercentage(sums, totalSum, true);
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
struct StatisticalChart;
|
||||
} // namespace Data
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
struct Limits;
|
||||
class LinesFilterController;
|
||||
|
||||
struct PiePartData final {
|
||||
struct Part final {
|
||||
float64 roundedPercentage = 0; // 0.XX.
|
||||
float64 stackedAngle = 0.;
|
||||
QString percentageText;
|
||||
};
|
||||
std::vector<Part> parts;
|
||||
bool pieHasSinglePart = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] PiePartData PiePartsPercentage(
|
||||
const std::vector<float64> &sums,
|
||||
float64 totalSum,
|
||||
bool round);
|
||||
|
||||
[[nodiscard]] PiePartData PiePartsPercentageByIndices(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const std::shared_ptr<LinesFilterController> &linesFilter,
|
||||
const Limits &xIndices);
|
||||
|
||||
} // namespace Statistic
|
||||
1010
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp
Normal file
1010
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.cpp
Normal file
File diff suppressed because it is too large
Load Diff
150
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h
Normal file
150
Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "statistics/segment_tree.h"
|
||||
#include "statistics/statistics_common.h"
|
||||
#include "statistics/view/abstract_chart_view.h"
|
||||
#include "statistics/view/stack_linear_chart_common.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
|
||||
namespace Data {
|
||||
struct StatisticalChart;
|
||||
} // namespace Data
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
struct Limits;
|
||||
|
||||
class StackLinearChartView final : public AbstractChartView {
|
||||
public:
|
||||
StackLinearChartView();
|
||||
~StackLinearChartView() override final;
|
||||
|
||||
void paint(QPainter &p, const PaintContext &c) override;
|
||||
|
||||
void paintSelectedXIndex(
|
||||
QPainter &p,
|
||||
const PaintContext &c,
|
||||
int selectedXIndex,
|
||||
float64 progress) override;
|
||||
|
||||
int findXIndexByPosition(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xPercentageLimits,
|
||||
const QRect &rect,
|
||||
float64 x) override;
|
||||
|
||||
[[nodiscard]] HeightLimits heightLimits(
|
||||
Data::StatisticalChart &chartData,
|
||||
Limits xIndices) override;
|
||||
|
||||
LocalZoomResult maybeLocalZoom(const LocalZoomArgs &args) override final;
|
||||
|
||||
void handleMouseMove(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const QRect &rect,
|
||||
const QPoint &p) override;
|
||||
|
||||
private:
|
||||
enum class TransitionStep {
|
||||
PrepareToZoomIn,
|
||||
PrepareToZoomOut,
|
||||
ZoomedOut,
|
||||
};
|
||||
void paintChartOrZoomAnimation(QPainter &p, const PaintContext &c);
|
||||
|
||||
void paintZoomed(QPainter &p, const PaintContext &c);
|
||||
void paintZoomedFooter(QPainter &p, const PaintContext &c);
|
||||
void paintPieText(QPainter &p, const PaintContext &c);
|
||||
|
||||
[[nodiscard]] bool skipSelectedTranslation() const;
|
||||
|
||||
void prepareZoom(const PaintContext &c, TransitionStep step);
|
||||
|
||||
void saveZoomRange(const PaintContext &c);
|
||||
void savePieTextParts(const PaintContext &c);
|
||||
void applyParts(const std::vector<PiePartData::Part> &parts);
|
||||
|
||||
struct SelectedPoints final {
|
||||
int lastXIndex = -1;
|
||||
Limits lastHeightLimits;
|
||||
Limits lastXLimits;
|
||||
float64 xPoint = 0.;
|
||||
};
|
||||
SelectedPoints _selectedPoints;
|
||||
|
||||
struct Transition {
|
||||
struct TransitionLine {
|
||||
QPointF start;
|
||||
QPointF end;
|
||||
float64 angle = 0.;
|
||||
float64 sum = 0.;
|
||||
};
|
||||
std::vector<TransitionLine> lines;
|
||||
float64 progress = 0;
|
||||
|
||||
bool pendingPrepareToZoomIn = false;
|
||||
|
||||
Limits zoomedOutXIndices;
|
||||
Limits zoomedOutXIndicesAdditional;
|
||||
Limits zoomedOutXPercentage;
|
||||
Limits zoomedInLimit;
|
||||
Limits zoomedInLimitXIndices;
|
||||
Limits zoomedInRange;
|
||||
Limits zoomedInRangeXIndices;
|
||||
|
||||
std::vector<PiePartData::Part> textParts;
|
||||
} _transition;
|
||||
|
||||
std::vector<bool> _skipPoints;
|
||||
|
||||
class PiePartController final {
|
||||
public:
|
||||
using LineId = int;
|
||||
bool set(LineId id);
|
||||
[[nodiscard]] float64 progress(LineId id) const;
|
||||
[[nodiscard]] QPointF offset(LineId id, float64 angle) const;
|
||||
[[nodiscard]] LineId selected() const;
|
||||
[[nodiscard]] bool isFinished() const;
|
||||
|
||||
private:
|
||||
void update(LineId id);
|
||||
|
||||
base::flat_map<LineId, crl::time> _startedAt;
|
||||
LineId _selected = -1;
|
||||
|
||||
};
|
||||
|
||||
class ChangingPiePartController final {
|
||||
public:
|
||||
void setParts(
|
||||
const std::vector<PiePartData::Part> &was,
|
||||
const std::vector<PiePartData::Part> &now);
|
||||
void update();
|
||||
[[nodiscard]] PiePartData current() const;
|
||||
[[nodiscard]] bool isFinished() const;
|
||||
|
||||
private:
|
||||
crl::time _startedAt = 0;
|
||||
std::vector<anim::value> _animValues;
|
||||
PiePartData _current;
|
||||
bool _isFinished = true;
|
||||
|
||||
};
|
||||
|
||||
PiePartController _piePartController;
|
||||
ChangingPiePartController _changingPieController;
|
||||
Ui::Animations::Basic _piePartAnimation;
|
||||
|
||||
bool _pieHasSinglePart = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
527
Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp
Normal file
527
Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp
Normal 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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user