mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-19 15:47:11 +02:00
Added initial support of sync zoom of charts to stack linear chart view.
This commit is contained in:
parent
32cd454554
commit
71b6a58683
4 changed files with 270 additions and 53 deletions
|
@ -21,4 +21,19 @@ enum class ChartViewType {
|
|||
StackLinear,
|
||||
};
|
||||
|
||||
[[nodiscard]] inline Limits FindNearestElements(
|
||||
const std::vector<float64> &vector,
|
||||
const Limits &limit) {
|
||||
const auto find = [&](float64 raw) -> float64 {
|
||||
const auto it = ranges::lower_bound(vector, raw);
|
||||
const auto left = raw - (*(it - 1));
|
||||
const auto right = (*it) - raw;
|
||||
const auto nearestXPercentageIt = ((right) > (left)) ? (it - 1) : it;
|
||||
return std::distance(
|
||||
begin(vector),
|
||||
nearestXPercentageIt);
|
||||
};
|
||||
return { find(limit.min), find(limit.max) };
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
|
|
|
@ -60,6 +60,30 @@ public:
|
|||
virtual void update(float64 dt) {
|
||||
};
|
||||
|
||||
struct LocalZoomResult final {
|
||||
bool hasZoom = false;
|
||||
Limits limit;
|
||||
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 {};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
|
|
|
@ -42,6 +42,10 @@ constexpr auto kLeftTop = short(3);
|
|||
}
|
||||
}
|
||||
|
||||
inline float64 InterpolationRatio(float64 from, float64 to, float64 result) {
|
||||
return (result - from) / (to - from);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
StackLinearChartView::StackLinearChartView() = default;
|
||||
|
@ -49,24 +53,42 @@ StackLinearChartView::StackLinearChartView() = default;
|
|||
StackLinearChartView::~StackLinearChartView() = default;
|
||||
|
||||
void StackLinearChartView::paint(QPainter &p, const PaintContext &c) {
|
||||
constexpr auto kOffset = float64(2);
|
||||
const auto wasXIndices = _lastPaintedXIndices;
|
||||
_lastPaintedXIndices = {
|
||||
float64(std::max(0., c.xIndices.min - kOffset)),
|
||||
float64(std::min(
|
||||
float64(c.chartData.xPercentage.size() - 1),
|
||||
c.xIndices.max + kOffset)),
|
||||
};
|
||||
if ((wasXIndices.min != _lastPaintedXIndices.min)
|
||||
|| (wasXIndices.max != _lastPaintedXIndices.max)) {
|
||||
if (!_transitionProgress && !c.footer) {
|
||||
prepareZoom(c, TransitionStep::ZoomedOut);
|
||||
}
|
||||
if (_pendingPrepareCachedTransition) {
|
||||
_pendingPrepareCachedTransition = false;
|
||||
prepareZoom(c, TransitionStep::PrepareToZoomIn);
|
||||
}
|
||||
|
||||
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
||||
StackLinearChartView::paintChartOrZoomAnimation(p, c);
|
||||
}
|
||||
|
||||
void StackLinearChartView::prepareZoom(
|
||||
const PaintContext &c,
|
||||
TransitionStep step) {
|
||||
if (step == TransitionStep::ZoomedOut) {
|
||||
constexpr auto kOffset = float64(2);
|
||||
_cachedTransition.zoomedOutXIndices = {
|
||||
float64(std::max(0., c.xIndices.min - kOffset)),
|
||||
float64(std::min(
|
||||
float64(c.chartData.xPercentage.size() - 1),
|
||||
c.xIndices.max + kOffset)),
|
||||
};
|
||||
} else if (step == TransitionStep::PrepareToZoomIn) {
|
||||
const auto &[zoomedStart, zoomedEnd] =
|
||||
_cachedTransition.zoomedOutXIndices;
|
||||
_cachedTransition.lines = std::vector<Transition::TransitionLine>(
|
||||
c.chartData.lines.size(),
|
||||
Transition::TransitionLine());
|
||||
|
||||
const auto xPercentageLimits = Limits{
|
||||
c.chartData.xPercentage[_cachedTransition.zoomedOutXIndices.min],
|
||||
c.chartData.xPercentage[_cachedTransition.zoomedOutXIndices.max],
|
||||
};
|
||||
|
||||
for (auto j = 0; j < 2; j++) {
|
||||
const auto i = int((j == 1) ? localEnd : localStart);
|
||||
const auto i = int((j == 1) ? zoomedEnd : zoomedStart);
|
||||
auto stackOffset = 0;
|
||||
auto sum = 0.;
|
||||
auto drawingLinesCount = 0;
|
||||
|
@ -93,36 +115,77 @@ void StackLinearChartView::paint(QPainter &p, const PaintContext &c) {
|
|||
: (sum ? (line.y[i] * alpha(line.id) / sum) : 0);
|
||||
|
||||
const auto xPoint = c.rect.width()
|
||||
* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)
|
||||
/ (c.xPercentageLimits.max - c.xPercentageLimits.min));
|
||||
* ((c.chartData.xPercentage[i] - xPercentageLimits.min)
|
||||
/ (xPercentageLimits.max - xPercentageLimits.min));
|
||||
const auto height = yPercentage * c.rect.height();
|
||||
const auto yPoint = rect::bottom(c.rect) - height - stackOffset;
|
||||
const auto yPoint = rect::bottom(c.rect)
|
||||
- height
|
||||
- stackOffset;
|
||||
linePoint = { xPoint, yPoint };
|
||||
stackOffset += height;
|
||||
}
|
||||
}
|
||||
|
||||
auto sums = std::vector<float64>();
|
||||
sums.reserve(c.chartData.lines.size());
|
||||
auto totalSum = 0;
|
||||
for (const auto &line : c.chartData.lines) {
|
||||
auto sum = 0;
|
||||
for (auto i = c.xIndices.min; i <= c.xIndices.max; i++) {
|
||||
sum += line.y[i];
|
||||
}
|
||||
sum *= alpha(line.id);
|
||||
totalSum += sum;
|
||||
sums.push_back(sum);
|
||||
}
|
||||
auto stackedPercentage = 0.;
|
||||
for (auto k = 0; k < sums.size(); k++) {
|
||||
const auto percentage = (sums[k] / float64(totalSum));
|
||||
stackedPercentage += percentage;
|
||||
_cachedTransition.lines[k].angle = stackedPercentage * 360 - 180.;
|
||||
}
|
||||
savePieTextParts(c);
|
||||
applyParts(_cachedTransition.textParts);
|
||||
}
|
||||
}
|
||||
|
||||
StackLinearChartView::paintChartOrZoomAnimation(p, c);
|
||||
void StackLinearChartView::applyParts(const std::vector<PiePartData> &parts) {
|
||||
for (auto k = 0; k < parts.size(); k++) {
|
||||
_cachedTransition.lines[k].angle = parts[k].stackedAngle;
|
||||
}
|
||||
}
|
||||
|
||||
void StackLinearChartView::saveZoomRange(const PaintContext &c) {
|
||||
const auto zoomedXPercentage = Limits{
|
||||
anim::interpolateF(
|
||||
_localZoom.limit.min,
|
||||
_localZoom.limit.max,
|
||||
c.xPercentageLimits.min),
|
||||
anim::interpolateF(
|
||||
_localZoom.limit.min,
|
||||
_localZoom.limit.max,
|
||||
c.xPercentageLimits.max),
|
||||
};
|
||||
const auto zoomedXIndices = FindNearestElements(
|
||||
c.chartData.xPercentage,
|
||||
zoomedXPercentage);
|
||||
_localZoom.rangeIndices = zoomedXIndices;
|
||||
_localZoom.range = zoomedXPercentage;
|
||||
}
|
||||
|
||||
void StackLinearChartView::savePieTextParts(const PaintContext &c) {
|
||||
_cachedTransition.textParts = partsPercentage(
|
||||
c.chartData,
|
||||
_localZoom.rangeIndices);
|
||||
}
|
||||
|
||||
auto StackLinearChartView::partsPercentage(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xIndices) -> std::vector<PiePartData> {
|
||||
auto result = std::vector<PiePartData>();
|
||||
result.reserve(chartData.lines.size());
|
||||
auto sums = std::vector<float64>();
|
||||
sums.reserve(chartData.lines.size());
|
||||
auto totalSum = 0;
|
||||
for (const auto &line : chartData.lines) {
|
||||
auto sum = 0;
|
||||
for (auto i = xIndices.min; i <= xIndices.max; i++) {
|
||||
sum += line.y[i];
|
||||
}
|
||||
sum *= alpha(line.id);
|
||||
totalSum += sum;
|
||||
sums.push_back(sum);
|
||||
}
|
||||
auto stackedPercentage = 0.;
|
||||
for (auto k = 0; k < sums.size(); k++) {
|
||||
const auto percentage = 0.01
|
||||
* std::round((sums[k] / float64(totalSum)) * 100.);
|
||||
stackedPercentage += percentage;
|
||||
result.push_back({ percentage, stackedPercentage * 360. - 180. });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void StackLinearChartView::paintChartOrZoomAnimation(
|
||||
|
@ -136,7 +199,10 @@ void StackLinearChartView::paintChartOrZoomAnimation(
|
|||
}
|
||||
return p.setOpacity(0.);
|
||||
}
|
||||
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
||||
const auto hasTransitionAnimation = _transitionProgress && !c.footer;
|
||||
const auto &[localStart, localEnd] = c.footer
|
||||
? Limits{ 0., float64(c.chartData.xPercentage.size() - 1) }
|
||||
: _cachedTransition.zoomedOutXIndices;
|
||||
_skipPoints = std::vector<bool>(c.chartData.lines.size(), false);
|
||||
auto paths = std::vector<QPainterPath>(
|
||||
c.chartData.lines.size(),
|
||||
|
@ -152,7 +218,10 @@ void StackLinearChartView::paintChartOrZoomAnimation(
|
|||
.map(p);
|
||||
};
|
||||
|
||||
const auto hasTransitionAnimation = _transitionProgress && !c.footer;
|
||||
const auto xPercentageLimits = Limits{
|
||||
c.chartData.xPercentage[localStart],
|
||||
c.chartData.xPercentage[localEnd],
|
||||
};
|
||||
|
||||
auto straightLineProgress = 0.;
|
||||
auto hasEmptyPoint = false;
|
||||
|
@ -215,8 +284,8 @@ void StackLinearChartView::paintChartOrZoomAnimation(
|
|||
: float64(sum ? (y[i] * lineAlpha / sum) : 0.);
|
||||
|
||||
const auto xPoint = c.rect.width()
|
||||
* ((c.chartData.xPercentage[i] - c.xPercentageLimits.min)
|
||||
/ (c.xPercentageLimits.max - c.xPercentageLimits.min));
|
||||
* ((c.chartData.xPercentage[i] - xPercentageLimits.min)
|
||||
/ (xPercentageLimits.max - xPercentageLimits.min));
|
||||
|
||||
if (!yPercentage && isLastLine) {
|
||||
hasEmptyPoint = true;
|
||||
|
@ -266,7 +335,7 @@ void StackLinearChartView::paintChartOrZoomAnimation(
|
|||
std::max(pointZero.x(), center.x()),
|
||||
rotate(resultAngle, pointZero).y());
|
||||
} else {
|
||||
const auto &xLimits = c.xPercentageLimits;
|
||||
const auto &xLimits = xPercentageLimits;
|
||||
const auto isNextXPointAfterCenter = false
|
||||
|| center.x() < (c.rect.width() * ((i == localEnd)
|
||||
? 1.
|
||||
|
@ -413,6 +482,11 @@ void StackLinearChartView::paintZoomed(QPainter &p, const PaintContext &c) {
|
|||
if (c.footer) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveZoomRange(c);
|
||||
const auto parts = partsPercentage(c.chartData, _localZoom.rangeIndices);
|
||||
applyParts(parts);
|
||||
|
||||
p.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg);
|
||||
const auto center = QPointF(c.rect.center());
|
||||
const auto side = (c.rect.width() / 2.) * kCircleSizeRatio;
|
||||
|
@ -425,9 +499,9 @@ void StackLinearChartView::paintZoomed(QPainter &p, const PaintContext &c) {
|
|||
const auto skipTranslation = skipSelectedTranslation();
|
||||
for (auto k = 0; k < c.chartData.lines.size(); k++) {
|
||||
const auto previous = k
|
||||
? _cachedTransition.lines[k - 1].angle
|
||||
? parts[k - 1].stackedAngle
|
||||
: -180;
|
||||
const auto now = _cachedTransition.lines[k].angle;
|
||||
const auto now = parts[k].stackedAngle;
|
||||
|
||||
const auto &line = c.chartData.lines[k];
|
||||
p.setBrush(line.color);
|
||||
|
@ -453,10 +527,10 @@ void StackLinearChartView::paintZoomed(QPainter &p, const PaintContext &c) {
|
|||
paintPieText(p, c);
|
||||
|
||||
if (selectedLineIndex >= 0) {
|
||||
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
||||
const auto &[zoomedStart, zoomedEnd] = _localZoom.rangeIndices;
|
||||
const auto &line = c.chartData.lines[selectedLineIndex];
|
||||
auto sum = 0;
|
||||
for (auto i = localStart; i <= localEnd; i++) {
|
||||
for (auto i = zoomedStart; i <= zoomedEnd; i++) {
|
||||
sum += line.y[i];
|
||||
}
|
||||
sum *= alpha(line.id);
|
||||
|
@ -474,13 +548,16 @@ void StackLinearChartView::paintZoomedFooter(
|
|||
}
|
||||
auto o = ScopedPainterOpacity(p, _transitionProgress);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto &[localStart, localEnd] = _lastPaintedXIndices;
|
||||
const auto &[zoomedStart, zoomedEnd] = _localZoom.limitIndices;
|
||||
const auto &[leftStart, w] = ComputeLeftStartAndStep(
|
||||
c.chartData,
|
||||
c.xPercentageLimits,
|
||||
{
|
||||
c.chartData.xPercentage[zoomedStart],
|
||||
c.chartData.xPercentage[zoomedEnd],
|
||||
},
|
||||
c.rect,
|
||||
localStart);
|
||||
for (auto i = localStart; i <= localEnd; i++) {
|
||||
zoomedStart);
|
||||
for (auto i = zoomedStart; i <= zoomedEnd; i++) {
|
||||
auto sum = 0.;
|
||||
auto lastEnabledId = int(0);
|
||||
for (const auto &line : c.chartData.lines) {
|
||||
|
@ -503,7 +580,7 @@ void StackLinearChartView::paintZoomedFooter(
|
|||
: visibleHeight;
|
||||
|
||||
const auto column = QRectF(
|
||||
leftStart + (i - localStart) * w,
|
||||
leftStart + (i - zoomedStart) * w,
|
||||
stack,
|
||||
w,
|
||||
height);
|
||||
|
@ -516,6 +593,11 @@ void StackLinearChartView::paintZoomedFooter(
|
|||
}
|
||||
|
||||
void StackLinearChartView::paintPieText(QPainter &p, const PaintContext &c) {
|
||||
if (_transitionProgress == 1.) {
|
||||
savePieTextParts(c);
|
||||
}
|
||||
const auto &parts = _cachedTransition.textParts;
|
||||
|
||||
const auto center = QPointF(c.rect.center());
|
||||
const auto side = (c.rect.width() / 2.) * kCircleSizeRatio;
|
||||
const auto rectF = QRectF(
|
||||
|
@ -531,10 +613,10 @@ void StackLinearChartView::paintPieText(QPainter &p, const PaintContext &c) {
|
|||
const auto skipTranslation = skipSelectedTranslation();
|
||||
for (auto k = 0; k < c.chartData.lines.size(); k++) {
|
||||
const auto previous = k
|
||||
? _cachedTransition.lines[k - 1].angle
|
||||
? parts[k - 1].stackedAngle
|
||||
: -180;
|
||||
const auto now = _cachedTransition.lines[k].angle;
|
||||
const auto percentage = (now - previous) / 360.;
|
||||
const auto now = parts[k].stackedAngle;
|
||||
const auto percentage = parts[k].roundedPercentage;
|
||||
|
||||
const auto rText = side * std::sqrt(1. - percentage);
|
||||
const auto textAngle = (previous + kPieAngleOffset)
|
||||
|
@ -784,6 +866,73 @@ AbstractChartView::HeightLimits StackLinearChartView::heightLimits(
|
|||
};
|
||||
}
|
||||
|
||||
auto StackLinearChartView::maybeLocalZoom(
|
||||
const LocalZoomArgs &args) -> LocalZoomResult {
|
||||
// 7 days.
|
||||
constexpr auto kLimitLength = int(27);
|
||||
// 1 day in middle of limits.
|
||||
constexpr auto kRangeLength = int(1);
|
||||
constexpr auto kLeftSide = int(kLimitLength / 2);
|
||||
constexpr auto kRightSide = int(kLimitLength / 2 + kRangeLength);
|
||||
|
||||
_transitionProgress = args.progress;
|
||||
if (args.type == LocalZoomArgs::Type::SkipCalculation) {
|
||||
return { true, _localZoom.limit, _localZoom.range };
|
||||
} else if (args.type == LocalZoomArgs::Type::CheckAvailability) {
|
||||
return { .hasZoom = true };
|
||||
} else if (args.type == LocalZoomArgs::Type::Prepare) {
|
||||
_pendingPrepareCachedTransition = true;
|
||||
}
|
||||
const auto xIndex = args.xIndex;
|
||||
const auto &xPercentage = args.chartData.xPercentage;
|
||||
const auto backIndex = (xPercentage.size() - 1);
|
||||
const auto localRangeIndex = (xIndex == backIndex)
|
||||
? (backIndex - kRangeLength)
|
||||
: xIndex;
|
||||
_localZoom.range = {
|
||||
xPercentage[localRangeIndex],
|
||||
xPercentage[localRangeIndex + kRangeLength],
|
||||
};
|
||||
_localZoom.rangeIndices = {
|
||||
float64(localRangeIndex),
|
||||
float64(localRangeIndex + kRangeLength),
|
||||
};
|
||||
if (xIndex < kLeftSide) {
|
||||
_localZoom.limitIndices = { 0, kLimitLength };
|
||||
} else if (xIndex > (backIndex - kRightSide)) {
|
||||
_localZoom.limitIndices = {
|
||||
float64(backIndex - kLimitLength),
|
||||
float64(backIndex),
|
||||
};
|
||||
} else {
|
||||
_localZoom.limitIndices = {
|
||||
float64(xIndex - kLeftSide),
|
||||
float64(xIndex + kRightSide),
|
||||
};
|
||||
}
|
||||
_localZoom.limit = {
|
||||
anim::interpolateF(
|
||||
0.,
|
||||
xPercentage[_localZoom.limitIndices.min],
|
||||
args.progress),
|
||||
anim::interpolateF(
|
||||
1.,
|
||||
xPercentage[_localZoom.limitIndices.max],
|
||||
args.progress),
|
||||
};
|
||||
const auto resultRange = Limits{
|
||||
InterpolationRatio(
|
||||
_localZoom.limit.min,
|
||||
_localZoom.limit.max,
|
||||
_localZoom.range.min),
|
||||
InterpolationRatio(
|
||||
_localZoom.limit.min,
|
||||
_localZoom.limit.max,
|
||||
_localZoom.range.max),
|
||||
};
|
||||
return { true, _localZoom.limit, resultRange };
|
||||
}
|
||||
|
||||
void StackLinearChartView::tick(crl::time now) {
|
||||
for (auto &[id, entry] : _entries) {
|
||||
const auto dt = std::min(
|
||||
|
|
|
@ -52,6 +52,8 @@ public:
|
|||
void tick(crl::time now) override;
|
||||
void update(float64 dt) override;
|
||||
|
||||
LocalZoomResult maybeLocalZoom(const LocalZoomArgs &args) override final;
|
||||
|
||||
void setUpdateCallback(Fn<void()> callback);
|
||||
void handleMouseMove(
|
||||
const Data::StatisticalChart &chartData,
|
||||
|
@ -59,6 +61,11 @@ public:
|
|||
const QPoint &p);
|
||||
|
||||
private:
|
||||
enum class TransitionStep {
|
||||
PrepareToZoomIn,
|
||||
PrepareToZoomOut,
|
||||
ZoomedOut,
|
||||
};
|
||||
void paintChartOrZoomAnimation(QPainter &p, const PaintContext &c);
|
||||
|
||||
void paintZoomed(QPainter &p, const PaintContext &c);
|
||||
|
@ -67,6 +74,20 @@ private:
|
|||
|
||||
[[nodiscard]] bool skipSelectedTranslation() const;
|
||||
|
||||
struct PiePartData {
|
||||
float64 roundedPercentage = 0; // 0.XX.
|
||||
float64 stackedAngle = 0.;
|
||||
};
|
||||
|
||||
void prepareZoom(const PaintContext &c, TransitionStep step);
|
||||
|
||||
void saveZoomRange(const PaintContext &c);
|
||||
void savePieTextParts(const PaintContext &c);
|
||||
void applyParts(const std::vector<PiePartData> &parts);
|
||||
[[nodiscard]] std::vector<PiePartData> partsPercentage(
|
||||
const Data::StatisticalChart &chartData,
|
||||
const Limits &xIndices);
|
||||
|
||||
struct SelectedPoints final {
|
||||
int lastXIndex = -1;
|
||||
Limits lastHeightLimits;
|
||||
|
@ -95,12 +116,20 @@ private:
|
|||
float64 sum = 0.;
|
||||
};
|
||||
std::vector<TransitionLine> lines;
|
||||
Limits zoomedOutXIndices;
|
||||
std::vector<PiePartData> textParts;
|
||||
} _cachedTransition;
|
||||
|
||||
Limits _lastPaintedXIndices;
|
||||
|
||||
std::vector<bool> _skipPoints;
|
||||
|
||||
struct {
|
||||
Limits limit;
|
||||
Limits limitIndices;
|
||||
Limits range;
|
||||
Limits rangeIndices;
|
||||
} _localZoom;
|
||||
bool _pendingPrepareCachedTransition = false;
|
||||
|
||||
class PiePartController final {
|
||||
public:
|
||||
using LineId = int;
|
||||
|
|
Loading…
Add table
Reference in a new issue