/* 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_view.h" #include "data/data_statistics_chart.h" #include "statistics/chart_lines_filter_controller.h" #include "statistics/view/stack_chart_common.h" #include "statistics/widgets/point_details_widget.h" #include "ui/effects/animation_value_f.h" #include "ui/painter.h" #include "ui/rect.h" #include "styles/style_statistics.h" #include namespace Statistic { namespace { constexpr auto kCircleSizeRatio = 0.42; constexpr auto kMinTextScaleRatio = 0.3; constexpr auto kPieAngleOffset = 90; constexpr auto kRightTop = short(0); constexpr auto kRightBottom = short(1); constexpr auto kLeftBottom = short(2); constexpr auto kLeftTop = short(3); [[nodiscard]] short QuarterForPoint(const QRect &r, const QPointF &p) { if (p.x() >= r.center().x() && p.y() <= r.center().y()) { return kRightTop; } else if (p.x() >= r.center().x() && p.y() >= r.center().y()) { return kRightBottom; } else if (p.x() < r.center().x() && p.y() >= r.center().y()) { return kLeftBottom; } else { return kLeftTop; } } inline float64 InterpolationRatio(float64 from, float64 to, float64 result) { return (result - from) / (to - from); }; [[nodiscard]] Limits FindAdditionalZoomedOutXIndices(const PaintContext &c) { constexpr auto kOffset = int(1); auto &xPercentage = c.chartData.xPercentage; auto leftResult = 0.; { auto i = std::max(int(c.xIndices.min) - kOffset, 0); if (xPercentage[i] > c.xPercentageLimits.min) { while (true) { i--; if (i < 0) { leftResult = 0; break; } else if (!(xPercentage[i] > c.xPercentageLimits.min)) { leftResult = i; break; } } } else { leftResult = i; } } { const auto lastIndex = float64(xPercentage.size() - 1); auto i = std::min(lastIndex, float64(c.xIndices.max) + kOffset); if (xPercentage[i] < c.xPercentageLimits.max) { while (true) { i++; if (i > lastIndex) { return { leftResult, lastIndex }; } else if (!(xPercentage[i] < c.xPercentageLimits.max)) { return { leftResult, i }; } } } else { return { leftResult, i }; } } } } // namespace void StackLinearChartView::ChangingPiePartController::setParts( const std::vector &was, const std::vector &now) { if (_animValues.size() != was.size()) { _animValues = std::vector(was.size(), anim::value()); for (auto i = 0; i < was.size(); i++) { _animValues[i] = anim::value( was[i].roundedPercentage, now[i].roundedPercentage); } } else { for (auto i = 0; i < was.size(); i++) { _animValues[i] = anim::value( _animValues[i].current(), now[i].roundedPercentage); } } _startedAt = crl::now(); _isFinished = false; } void StackLinearChartView::ChangingPiePartController::update() { const auto progress = std::clamp( (crl::now() - _startedAt) / float64(st::slideWrapDuration), 0., 1.); auto totalSum = 0.; auto finished = true; auto result = std::vector(); result.reserve(_animValues.size()); for (auto &anim : _animValues) { anim.update(progress, anim::easeOutCubic); if (finished && (anim.current() != anim.to())) { finished = false; } const auto value = anim.current(); result.push_back(value); totalSum += value; } _isFinished = finished; _current = PiePartsPercentage(result, totalSum, false); } PiePartData StackLinearChartView::ChangingPiePartController::current() const { return _current; } bool StackLinearChartView::ChangingPiePartController::isFinished() const { return _isFinished; } StackLinearChartView::StackLinearChartView() { _piePartAnimation.init([=] { AbstractChartView::update(); }); } StackLinearChartView::~StackLinearChartView() = default; void StackLinearChartView::paint(QPainter &p, const PaintContext &c) { if (!_transition.progress && !c.footer) { prepareZoom(c, TransitionStep::ZoomedOut); } if (_transition.pendingPrepareToZoomIn) { _transition.pendingPrepareToZoomIn = false; prepareZoom(c, TransitionStep::PrepareToZoomIn); } StackLinearChartView::paintChartOrZoomAnimation(p, c); } void StackLinearChartView::prepareZoom( const PaintContext &c, TransitionStep step) { if (step == TransitionStep::ZoomedOut) { _transition.zoomedOutXIndicesAdditional = FindAdditionalZoomedOutXIndices(c); _transition.zoomedOutXIndices = c.xIndices; _transition.zoomedOutXPercentage = c.xPercentageLimits; } else if (step == TransitionStep::PrepareToZoomIn) { const auto &[zoomedStart, zoomedEnd] = _transition.zoomedOutXIndices; _transition.lines = std::vector( c.chartData.lines.size(), Transition::TransitionLine()); const auto xPercentageLimits = _transition.zoomedOutXPercentage; const auto &linesFilter = linesFilterController(); for (auto j = 0; j < 2; j++) { const auto i = int((j == 1) ? zoomedEnd : zoomedStart); auto stackOffset = 0; auto sum = 0.; auto drawingLinesCount = 0; for (const auto &line : c.chartData.lines) { if (line.y[i] > 0) { const auto alpha = linesFilter->alpha(line.id); sum += line.y[i] * alpha; if (alpha > 0.) { drawingLinesCount++; } } } for (auto k = 0; k < c.chartData.lines.size(); k++) { auto &linePoint = (j ? _transition.lines[k].end : _transition.lines[k].start); const auto &line = c.chartData.lines[k]; const auto yPercentage = (drawingLinesCount == 1) ? (line.y[i] ? linesFilter->alpha(line.id) : 0) : (sum ? (line.y[i] * linesFilter->alpha(line.id) / sum) : 0); const auto xPoint = c.rect.width() * ((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; linePoint = { xPoint, yPoint }; stackOffset += height; } } savePieTextParts(c); applyParts(_transition.textParts); } } void StackLinearChartView::applyParts( const std::vector &parts) { for (auto k = 0; k < parts.size(); k++) { _transition.lines[k].angle = parts[k].stackedAngle; } } void StackLinearChartView::saveZoomRange(const PaintContext &c) { _transition.zoomedInRangeXIndices = FindStackXIndicesFromRawXPercentages( c.chartData, c.xPercentageLimits, _transition.zoomedInLimitXIndices); _transition.zoomedInRange = { c.chartData.xPercentage[_transition.zoomedInRangeXIndices.min], c.chartData.xPercentage[_transition.zoomedInRangeXIndices.max], }; } void StackLinearChartView::savePieTextParts(const PaintContext &c) { auto data = PiePartsPercentageByIndices( c.chartData, linesFilterController(), _transition.zoomedInRangeXIndices); _transition.textParts = std::move(data.parts); _pieHasSinglePart = data.pieHasSinglePart; } void StackLinearChartView::paintChartOrZoomAnimation( QPainter &p, const PaintContext &c) { if (_transition.progress == 1.) { if (c.footer) { paintZoomedFooter(p, c); } else { paintZoomed(p, c); } return p.setOpacity(0.); } const auto &linesFilter = linesFilterController(); const auto hasTransitionAnimation = _transition.progress && !c.footer; const auto &[localStart, localEnd] = c.footer ? Limits{ 0., float64(c.chartData.xPercentage.size() - 1) } : _transition.zoomedOutXIndicesAdditional; _skipPoints = std::vector(c.chartData.lines.size(), false); auto paths = std::vector( c.chartData.lines.size(), QPainterPath()); const auto center = QPointF(c.rect.center()); const auto rotate = [&](float64 ang, const QPointF &p) { return QTransform() .translate(center.x(), center.y()) .rotate(ang) .translate(-center.x(), -center.y()) .map(p); }; const auto xPercentageLimits = !c.footer ? _transition.zoomedOutXPercentage : Limits{ c.chartData.xPercentage[localStart], c.chartData.xPercentage[localEnd], }; auto straightLineProgress = 0.; auto hasEmptyPoint = false; auto ovalPath = QPainterPath(); if (hasTransitionAnimation) { constexpr auto kStraightLinePart = 0.6; straightLineProgress = std::clamp( _transition.progress / kStraightLinePart, 0., 1.); auto rectPath = QPainterPath(); rectPath.addRect(c.rect); const auto r = anim::interpolateF( 1., kCircleSizeRatio, _transition.progress); const auto per = anim::interpolateF(0., 100., _transition.progress); const auto side = (c.rect.width() / 2.) * r; const auto rectF = QRectF( center - QPointF(side, side), center + QPointF(side, side)); ovalPath.addRoundedRect(rectF, per, per, Qt::RelativeSize); ovalPath = ovalPath.intersected(rectPath); } for (auto i = localStart; i <= localEnd; i++) { auto stackOffset = 0.; auto sum = 0.; auto lastEnabled = int(0); auto drawingLinesCount = int(0); const auto xPoint = c.rect.width() * ((c.chartData.xPercentage[i] - xPercentageLimits.min) / (xPercentageLimits.max - xPercentageLimits.min)); for (auto k = 0; k < c.chartData.lines.size(); k++) { const auto &line = c.chartData.lines[k]; const auto alpha = linesFilter->alpha(line.id); if (!alpha) { continue; } if (line.y[i] > 0) { sum += line.y[i] * alpha; drawingLinesCount++; } lastEnabled = k; } for (auto k = 0; k < c.chartData.lines.size(); k++) { const auto &line = c.chartData.lines[k]; const auto isLastLine = (k == lastEnabled); const auto lineAlpha = linesFilter->alpha(line.id); if (isLastLine && (lineAlpha < 1.)) { hasEmptyPoint = true; } if (!lineAlpha) { continue; } const auto &transitionLine = hasTransitionAnimation ? _transition.lines[k] : Transition::TransitionLine(); const auto &y = line.y; auto &chartPath = paths[k]; const auto yPercentage = (drawingLinesCount == 1) ? float64(y[i] ? lineAlpha : 0.) : float64(sum ? (y[i] * lineAlpha / sum) : 0.); if (isLastLine && !yPercentage) { hasEmptyPoint = true; } const auto height = yPercentage * c.rect.height(); const auto yPoint = rect::bottom(c.rect) - height - stackOffset; // startFromY[k] = yPoint; auto angle = 0.; auto resultPoint = QPointF(xPoint, yPoint); auto pointZero = QPointF(xPoint, c.rect.y() + c.rect.height()); // if (i == localEnd) { // endXPoint = xPoint; // } else if (i == localStart) { // startXPoint = xPoint; // } if (hasTransitionAnimation && !isLastLine) { const auto point1 = (resultPoint.x() < center.x()) ? transitionLine.start : transitionLine.end; const auto diff = center - point1; const auto yTo = point1.y() + diff.y() * (resultPoint.x() - point1.x()) / diff.x(); const auto yToResult = yTo * straightLineProgress; const auto revProgress = (1. - straightLineProgress); resultPoint.setY(resultPoint.y() * revProgress + yToResult); pointZero.setY(pointZero.y() * revProgress + yToResult); { const auto angleK = diff.y() / float64(diff.x()); angle = (angleK > 0) ? (-std::atan(angleK)) * (180. / M_PI) : (std::atan(std::abs(angleK))) * (180. / M_PI); angle -= 90; } if (resultPoint.x() >= center.x()) { const auto resultAngle = _transition.progress * angle; const auto rotated = rotate(resultAngle, resultPoint); resultPoint = QPointF( std::max(rotated.x(), center.x()), rotated.y()); pointZero = QPointF( std::max(pointZero.x(), center.x()), rotate(resultAngle, pointZero).y()); } else { const auto &xLimits = xPercentageLimits; const auto isNextXPointAfterCenter = false || center.x() < (c.rect.width() * ((i == localEnd) ? 1. : ((c.chartData.xPercentage[i + 1] - xLimits.min) / (xLimits.max - xLimits.min)))); if (isNextXPointAfterCenter) { pointZero = resultPoint = QPointF() + center * straightLineProgress + resultPoint * revProgress; } else { const auto resultAngle = _transition.progress * angle + _transition.progress * transitionLine.angle; resultPoint = rotate(resultAngle, resultPoint); pointZero = rotate(resultAngle, pointZero); } } } if (i == localStart) { const auto bottomLeft = QPointF( c.rect.x(), rect::bottom(c.rect)); const auto local = (hasTransitionAnimation && !isLastLine) ? rotate( _transition.progress * angle + _transition.progress * transitionLine.angle, bottomLeft - QPointF(center.x(), 0)) : bottomLeft; chartPath.setFillRule(Qt::WindingFill); chartPath.moveTo(local); _skipPoints[k] = false; } const auto yRatio = 1. - (isLastLine ? _transition.progress : 0.); if ((!yPercentage) && (i > 0 && (y[i - 1] == 0)) && (i < localEnd && (y[i + 1] == 0)) && (!hasTransitionAnimation)) { if (!_skipPoints[k]) { chartPath.lineTo(pointZero.x(), pointZero.y() * yRatio); } _skipPoints[k] = true; } else { if (_skipPoints[k]) { chartPath.lineTo(pointZero.x(), pointZero.y() * yRatio); } chartPath.lineTo(resultPoint.x(), resultPoint.y() * yRatio); _skipPoints[k] = false; } if (i == localEnd) { if (hasTransitionAnimation && !isLastLine) { { const auto diff = center - transitionLine.start; const auto angleK = diff.y() / diff.x(); angle = (angleK > 0) ? ((-std::atan(angleK)) * (180. / M_PI)) : ((std::atan(std::abs(angleK))) * (180. / M_PI)); angle -= 90; } const auto local = rotate( _transition.progress * angle + _transition.progress * transitionLine.angle, transitionLine.start); const auto ending = true && (std::abs(resultPoint.x() - local.x()) < 0.001) && ((local.y() < center.y() && resultPoint.y() < center.y()) || (local.y() > center.y() && resultPoint.y() > center.y())); const auto endQuarter = (!ending) ? QuarterForPoint(c.rect, resultPoint) : kRightTop; const auto startQuarter = (!ending) ? QuarterForPoint(c.rect, local) : (transitionLine.angle == -180.) ? kRightTop : kLeftTop; for (auto q = endQuarter; q <= startQuarter; q++) { chartPath.lineTo( (q == kLeftTop || q == kLeftBottom) ? c.rect.x() : rect::right(c.rect), (q == kLeftTop || q == kRightTop) ? c.rect.y() : rect::right(c.rect)); } } else { chartPath.lineTo( rect::right(c.rect), rect::bottom(c.rect)); } } stackOffset += height; } } auto hq = PainterHighQualityEnabler(p); p.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg); if (!ovalPath.isEmpty()) { p.setClipPath(ovalPath); } if (hasEmptyPoint) { p.fillRect(c.rect, st::boxDividerBg); } const auto opacity = c.footer ? (1. - _transition.progress) : 1.; for (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) { if (paths[k].isEmpty()) { continue; } const auto &line = c.chartData.lines[k]; p.setPen(Qt::NoPen); p.fillPath(paths[k], line.color); } p.setOpacity(opacity); if (!c.footer) { constexpr auto kAlphaTextPart = 0.6; const auto progress = std::clamp( (_transition.progress - kAlphaTextPart) / (1. - kAlphaTextPart), 0., 1.); if (progress > 0) { auto o = ScopedPainterOpacity(p, progress); paintPieText(p, c); } } else if (_transition.progress) { paintZoomedFooter(p, c); } // Fix ugly outline. if (!c.footer || !_transition.progress) { p.setBrush(Qt::transparent); p.setPen(st::boxBg); p.drawPath(ovalPath); } if (!ovalPath.isEmpty()) { p.setClipRect(c.rect, Qt::NoClip); } p.setOpacity(1. - _transition.progress); } void StackLinearChartView::paintZoomed(QPainter &p, const PaintContext &c) { if (c.footer) { return; } const auto wasZoomedInRangeXIndices = _transition.zoomedInRangeXIndices; saveZoomRange(c); const auto &[zoomedStart, zoomedEnd] = _transition.zoomedInRangeXIndices; const auto partsData = PiePartsPercentageByIndices( c.chartData, linesFilterController(), _transition.zoomedInRangeXIndices); const auto xIndicesChanged = (wasZoomedInRangeXIndices.min != zoomedStart) || (wasZoomedInRangeXIndices.max != zoomedEnd); if (xIndicesChanged) { const auto wasParts = PiePartsPercentageByIndices( c.chartData, linesFilterController(), wasZoomedInRangeXIndices); _changingPieController.setParts(wasParts.parts, partsData.parts); if (!_piePartAnimation.animating()) { _piePartAnimation.start(); } } if (!_changingPieController.isFinished()) { _changingPieController.update(); } _pieHasSinglePart = partsData.pieHasSinglePart; applyParts(partsData.parts); const auto &parts = _changingPieController.isFinished() ? partsData.parts : _changingPieController.current().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; const auto rectF = QRectF( center - QPointF(side, side), center + QPointF(side, side)); auto hq = PainterHighQualityEnabler(p); auto selectedLineIndex = -1; const auto skipTranslation = skipSelectedTranslation(); for (auto k = 0; k < c.chartData.lines.size(); k++) { const auto previous = k ? parts[k - 1].stackedAngle : -180; const auto now = parts[k].stackedAngle; const auto &line = c.chartData.lines[k]; p.setBrush(line.color); p.setPen(Qt::NoPen); const auto textAngle = (previous + kPieAngleOffset) + (now - previous) / 2.; const auto partOffset = skipTranslation ? QPointF() : _piePartController.offset(line.id, textAngle); p.translate(partOffset); p.drawPie( rectF, -(previous + kPieAngleOffset) * 16, -(now - previous) * 16); p.translate(-partOffset); if (_piePartController.selected() == line.id) { selectedLineIndex = k; } } if (_piePartController.isFinished() && _changingPieController.isFinished()) { _piePartAnimation.stop(); } paintPieText(p, c); if (selectedLineIndex >= 0) { const auto &line = c.chartData.lines[selectedLineIndex]; auto sum = 0; for (auto i = zoomedStart; i <= zoomedEnd; i++) { sum += line.y[i]; } sum *= linesFilterController()->alpha(line.id); if (sum > 0) { PaintDetails(p, line, sum, c.rect); } } } void StackLinearChartView::paintZoomedFooter( QPainter &p, const PaintContext &c) { if (!c.footer) { return; } auto o = ScopedPainterOpacity(p, _transition.progress); auto hq = PainterHighQualityEnabler(p); const auto &[zoomedStart, zoomedEnd] = _transition.zoomedInLimitXIndices; const auto sideW = st::statisticsChartFooterSideWidth; const auto width = c.rect.width() - sideW * 2.; const auto leftStart = c.rect.x() + sideW; const auto &xPercentage = c.chartData.xPercentage; auto previousX = leftStart; // Read FindStackXIndicesFromRawXPercentages. const auto offset = (xPercentage[zoomedEnd] == 1.) ? 0 : 1; for (auto i = zoomedStart; i <= zoomedEnd; i++) { auto sum = 0.; auto lastEnabledId = int(0); for (const auto &line : c.chartData.lines) { const auto alpha = linesFilterController()->alpha(line.id); sum += line.y[i] * alpha; if (alpha > 0.) { lastEnabledId = line.id; } } const auto columnMargins = QMarginsF( (i == zoomedStart) ? sideW : 0, 0, (i == zoomedEnd - offset) ? sideW : 0, 0); const auto next = std::clamp(i + offset, zoomedStart, zoomedEnd); const auto xPointPercentage = (xPercentage[next] - xPercentage[zoomedStart]) / (xPercentage[zoomedEnd] - xPercentage[zoomedStart]); const auto xPoint = leftStart + width * xPointPercentage; auto stack = 0.; for (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) { const auto &line = c.chartData.lines[k]; const auto visibleHeight = c.rect.height() * (line.y[i] * linesFilterController()->alpha(line.id) / sum); if (!visibleHeight) { continue; } const auto height = (line.id == lastEnabledId) ? c.rect.height() : visibleHeight; const auto column = columnMargins + QRectF( previousX, stack, xPoint - previousX, height); p.setPen(Qt::NoPen); p.fillRect(column, line.color); stack += visibleHeight; } previousX = xPoint; } } void StackLinearChartView::paintPieText(QPainter &p, const PaintContext &c) { constexpr auto kMinPercentage = 0.039; if (_transition.progress == 1.) { savePieTextParts(c); } const auto &parts = _changingPieController.isFinished() ? _transition.textParts : _changingPieController.current().parts; const auto center = QPointF(c.rect.center()); const auto side = (c.rect.width() / 2.) * kCircleSizeRatio; const auto rectF = QRectF( center - QPointF(side, side), center + QPointF(side, side)); const auto &font = st::statisticsPieChartFont; const auto maxScale = side / (font->height * 2); const auto minScale = maxScale * kMinTextScaleRatio; p.setBrush(Qt::NoBrush); p.setPen(st::premiumButtonFg); p.setFont(font); const auto opacity = p.opacity(); const auto skipTranslation = skipSelectedTranslation(); for (auto k = 0; k < c.chartData.lines.size(); k++) { const auto previous = k ? parts[k - 1].stackedAngle : -180; const auto now = parts[k].stackedAngle; const auto percentage = parts[k].roundedPercentage; if (percentage <= kMinPercentage) { continue; } const auto rText = side * std::sqrt(1. - percentage); const auto textAngle = (now == previous) ? 0. : ((previous + kPieAngleOffset) + (now - previous) / 2.); const auto textRadians = textAngle * M_PI / 180.; const auto scale = (maxScale == minScale) ? 0. : (minScale) + percentage * (maxScale - minScale); const auto text = parts[k].percentageText; const auto textW = font->width(text); const auto textXShift = textW / 2.; const auto textYShift = textW / 2.; const auto textRectCenter = rectF.center() + QPointF( (rText - textXShift * (1. - scale)) * std::cos(textRadians), (rText - textYShift * (1. - scale)) * std::sin(textRadians)); const auto textRect = QRectF( textRectCenter - QPointF(textXShift, textYShift), textRectCenter + QPointF(textXShift, textYShift)); const auto partOffset = skipTranslation ? QPointF() : _piePartController.offset(c.chartData.lines[k].id, textAngle); p.setTransform( QTransform() .translate( textRectCenter.x() + partOffset.x(), textRectCenter.y() + partOffset.y()) .scale(scale, scale) .translate(-textRectCenter.x(), -textRectCenter.y())); p.setOpacity(opacity * linesFilterController()->alpha(c.chartData.lines[k].id)); p.drawText(textRect, text, style::al_center); } p.resetTransform(); } bool StackLinearChartView::PiePartController::set(int id) { if (_selected != id) { update(_selected); _selected = id; update(_selected); return true; } return false; } void StackLinearChartView::PiePartController::update(int id) { if (id >= 0) { const auto was = _startedAt[id]; const auto p = (crl::now() - was) / float64(st::slideWrapDuration); const auto progress = ((p > 0) && (p < 1)) ? (1. - p) : 0.; _startedAt[id] = crl::now() - (st::slideWrapDuration * progress); } } float64 StackLinearChartView::PiePartController::progress(int id) const { const auto it = _startedAt.find(id); if (it == end(_startedAt)) { return 0.; } const auto at = it->second; const auto show = (_selected == id); const auto progress = std::clamp( (crl::now() - at) / float64(st::slideWrapDuration), 0., 1.); return std::clamp(show ? progress : (1. - progress), 0., 1.); } QPointF StackLinearChartView::PiePartController::offset( LineId id, float64 angle) const { const auto offset = st::statisticsPieChartPartOffset * progress(id); const auto radians = angle * M_PI / 180.; return { std::cos(radians) * offset, std::sin(radians) * offset }; } auto StackLinearChartView::PiePartController::selected() const -> LineId { return _selected; } bool StackLinearChartView::PiePartController::isFinished() const { for (const auto &[id, _] : _startedAt) { const auto p = progress(id); if (p > 0 && p < 1) { return false; } } return true; } void StackLinearChartView::handleMouseMove( const Data::StatisticalChart &chartData, const QRect &rect, const QPoint &p) { if (_transition.progress < 1) { return; } const auto center = rect.center(); const auto theta = std::atan2(center.y() - p.y(), (center.x() - p.x())); const auto rawAngle = theta * (180. / M_PI) + 90.; const auto angle = (rawAngle > 180.) ? (rawAngle - 360.) : rawAngle; for (auto k = 0; k < chartData.lines.size(); k++) { const auto previous = k ? _transition.lines[k - 1].angle : -180; const auto now = _transition.lines[k].angle; if (angle > previous && angle <= now) { const auto id = p.isNull() ? -1 : chartData.lines[k].id; if (_piePartController.set(id)) { if (!_piePartAnimation.animating()) { _piePartAnimation.start(); } } return; } } } bool StackLinearChartView::skipSelectedTranslation() const { return _pieHasSinglePart; } void StackLinearChartView::paintSelectedXIndex( QPainter &p, const PaintContext &c, int selectedXIndex, float64 progress) { if ((selectedXIndex < 0) || c.footer) { return; } const auto xPercentageLimits = _transition.zoomedOutXPercentage; p.setBrush(st::boxBg); const auto i = selectedXIndex; const auto isSameToken = (_selectedPoints.lastXIndex == selectedXIndex) && (_selectedPoints.lastHeightLimits.min == c.heightLimits.min) && (_selectedPoints.lastHeightLimits.max == c.heightLimits.max) && (_selectedPoints.lastXLimits.min == xPercentageLimits.min) && (_selectedPoints.lastXLimits.max == xPercentageLimits.max); { const auto useCache = isSameToken; if (!useCache) { // Calculate. const auto xPoint = c.rect.width() * ((c.chartData.xPercentage[i] - xPercentageLimits.min) / (xPercentageLimits.max - xPercentageLimits.min)); _selectedPoints.xPoint = xPoint; } { [[maybe_unused]] const auto o = ScopedPainterOpacity( p, p.opacity() * progress * kRulerLineAlpha); const auto lineRect = QRectF( _selectedPoints.xPoint - (st::lineWidth / 2.), c.rect.y(), st::lineWidth, c.rect.height()); p.fillRect(lineRect, st::boxTextFg); } } _selectedPoints.lastXIndex = selectedXIndex; _selectedPoints.lastHeightLimits = c.heightLimits; _selectedPoints.lastXLimits = xPercentageLimits; } int StackLinearChartView::findXIndexByPosition( const Data::StatisticalChart &chartData, const Limits &xPercentageLimits, const QRect &rect, float64 x) { if (_transition.progress == 1.) { return -1; } else if (x < rect.x()) { return 0; } else if (x > (rect.x() + rect.width())) { return chartData.xPercentage.size() - 1; } const auto pointerRatio = std::clamp( (x - rect.x()) / rect.width(), 0., 1.); const auto &[localStart, localEnd] = _transition.zoomedOutXIndices; const auto rawXPercentage = anim::interpolateF( _transition.zoomedOutXPercentage.min, _transition.zoomedOutXPercentage.max, pointerRatio); const auto it = ranges::lower_bound( chartData.xPercentage, rawXPercentage); const auto left = rawXPercentage - (*(it - 1)); const auto right = (*it) - rawXPercentage; const auto nearestXPercentageIt = ((right) > (left)) ? (it - 1) : it; return std::clamp( std::distance(begin(chartData.xPercentage), nearestXPercentageIt), std::ptrdiff_t(localStart), std::ptrdiff_t(localEnd)); } AbstractChartView::HeightLimits StackLinearChartView::heightLimits( Data::StatisticalChart &chartData, Limits xIndices) { constexpr auto kMaxStackLinear = 100.; return { .full = { 0, kMaxStackLinear }, .ranged = { 0., kMaxStackLinear }, }; } auto StackLinearChartView::maybeLocalZoom( const LocalZoomArgs &args) -> LocalZoomResult { // 8 days. constexpr auto kLimitLength = int(8); // 1 day in middle of limits. constexpr auto kRangeLength = int(0); constexpr auto kLeftSide = int(kLimitLength / 2 + kRangeLength); constexpr auto kRightSide = int(kLimitLength / 2) + int(1); _transition.progress = args.progress; if (args.type == LocalZoomArgs::Type::SkipCalculation) { return { true, _transition.zoomedInLimit, _transition.zoomedInRange }; } else if (args.type == LocalZoomArgs::Type::CheckAvailability) { return { .hasZoom = true }; } else if (args.type == LocalZoomArgs::Type::Prepare) { _transition.pendingPrepareToZoomIn = 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; _transition.zoomedInRange = { xPercentage[localRangeIndex], xPercentage[localRangeIndex + kRangeLength], }; _transition.zoomedInRangeXIndices = { float64(localRangeIndex), float64(localRangeIndex + kRangeLength), }; _transition.zoomedInLimitXIndices = (xIndex < kLeftSide) ? Limits{ 0, kLeftSide + kRightSide } : (xIndex > (backIndex - kRightSide - kRangeLength)) ? Limits{ float64(backIndex - kLimitLength), float64(backIndex) } : Limits{ float64(xIndex - kLeftSide), float64(xIndex + kRightSide) }; _transition.zoomedInLimit = { anim::interpolateF( 0., xPercentage[_transition.zoomedInLimitXIndices.min], args.progress), anim::interpolateF( 1., xPercentage[_transition.zoomedInLimitXIndices.max], args.progress), }; const auto oneDay = std::abs(xPercentage[localRangeIndex] - xPercentage[localRangeIndex + ((xIndex == backIndex) ? -1 : 1)]); // Read FindStackXIndicesFromRawXPercentages. const auto offset = (_transition.zoomedInLimitXIndices.max == backIndex) ? -oneDay : 0.; const auto resultRange = Limits{ InterpolationRatio( _transition.zoomedInLimit.min, _transition.zoomedInLimit.max, _transition.zoomedInRange.min + oneDay * 0.25 + offset), InterpolationRatio( _transition.zoomedInLimit.min, _transition.zoomedInLimit.max, _transition.zoomedInRange.max + oneDay * 0.75 + offset), }; return { true, _transition.zoomedInLimitXIndices, resultRange }; } } // namespace Statistic