stack_linear_chart_view.cpp 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "statistics/view/stack_linear_chart_view.h"
  8. #include "data/data_statistics_chart.h"
  9. #include "statistics/chart_lines_filter_controller.h"
  10. #include "statistics/view/stack_chart_common.h"
  11. #include "statistics/widgets/point_details_widget.h"
  12. #include "ui/effects/animation_value_f.h"
  13. #include "ui/painter.h"
  14. #include "ui/rect.h"
  15. #include "styles/style_statistics.h"
  16. #include <QtCore/QtMath>
  17. namespace Statistic {
  18. namespace {
  19. constexpr auto kCircleSizeRatio = 0.42;
  20. constexpr auto kMinTextScaleRatio = 0.3;
  21. constexpr auto kPieAngleOffset = 90;
  22. constexpr auto kRightTop = short(0);
  23. constexpr auto kRightBottom = short(1);
  24. constexpr auto kLeftBottom = short(2);
  25. constexpr auto kLeftTop = short(3);
  26. [[nodiscard]] short QuarterForPoint(const QRect &r, const QPointF &p) {
  27. if (p.x() >= r.center().x() && p.y() <= r.center().y()) {
  28. return kRightTop;
  29. } else if (p.x() >= r.center().x() && p.y() >= r.center().y()) {
  30. return kRightBottom;
  31. } else if (p.x() < r.center().x() && p.y() >= r.center().y()) {
  32. return kLeftBottom;
  33. } else {
  34. return kLeftTop;
  35. }
  36. }
  37. inline float64 InterpolationRatio(float64 from, float64 to, float64 result) {
  38. return (result - from) / (to - from);
  39. };
  40. [[nodiscard]] Limits FindAdditionalZoomedOutXIndices(const PaintContext &c) {
  41. constexpr auto kOffset = int(1);
  42. auto &xPercentage = c.chartData.xPercentage;
  43. auto leftResult = 0.;
  44. {
  45. auto i = std::max(int(c.xIndices.min) - kOffset, 0);
  46. if (xPercentage[i] > c.xPercentageLimits.min) {
  47. while (true) {
  48. i--;
  49. if (i < 0) {
  50. leftResult = 0;
  51. break;
  52. } else if (!(xPercentage[i] > c.xPercentageLimits.min)) {
  53. leftResult = i;
  54. break;
  55. }
  56. }
  57. } else {
  58. leftResult = i;
  59. }
  60. }
  61. {
  62. const auto lastIndex = float64(xPercentage.size() - 1);
  63. auto i = std::min(lastIndex, float64(c.xIndices.max) + kOffset);
  64. if (xPercentage[i] < c.xPercentageLimits.max) {
  65. while (true) {
  66. i++;
  67. if (i > lastIndex) {
  68. return { leftResult, lastIndex };
  69. } else if (!(xPercentage[i] < c.xPercentageLimits.max)) {
  70. return { leftResult, i };
  71. }
  72. }
  73. } else {
  74. return { leftResult, i };
  75. }
  76. }
  77. }
  78. } // namespace
  79. void StackLinearChartView::ChangingPiePartController::setParts(
  80. const std::vector<PiePartData::Part> &was,
  81. const std::vector<PiePartData::Part> &now) {
  82. if (_animValues.size() != was.size()) {
  83. _animValues = std::vector<anim::value>(was.size(), anim::value());
  84. for (auto i = 0; i < was.size(); i++) {
  85. _animValues[i] = anim::value(
  86. was[i].roundedPercentage,
  87. now[i].roundedPercentage);
  88. }
  89. } else {
  90. for (auto i = 0; i < was.size(); i++) {
  91. _animValues[i] = anim::value(
  92. _animValues[i].current(),
  93. now[i].roundedPercentage);
  94. }
  95. }
  96. _startedAt = crl::now();
  97. _isFinished = false;
  98. }
  99. void StackLinearChartView::ChangingPiePartController::update() {
  100. const auto progress = std::clamp(
  101. (crl::now() - _startedAt) / float64(st::slideWrapDuration),
  102. 0.,
  103. 1.);
  104. auto totalSum = 0.;
  105. auto finished = true;
  106. auto result = std::vector<float64>();
  107. result.reserve(_animValues.size());
  108. for (auto &anim : _animValues) {
  109. anim.update(progress, anim::easeOutCubic);
  110. if (finished && (anim.current() != anim.to())) {
  111. finished = false;
  112. }
  113. const auto value = anim.current();
  114. result.push_back(value);
  115. totalSum += value;
  116. }
  117. _isFinished = finished;
  118. _current = PiePartsPercentage(result, totalSum, false);
  119. }
  120. PiePartData StackLinearChartView::ChangingPiePartController::current() const {
  121. return _current;
  122. }
  123. bool StackLinearChartView::ChangingPiePartController::isFinished() const {
  124. return _isFinished;
  125. }
  126. StackLinearChartView::StackLinearChartView() {
  127. _piePartAnimation.init([=] { AbstractChartView::update(); });
  128. }
  129. StackLinearChartView::~StackLinearChartView() = default;
  130. void StackLinearChartView::paint(QPainter &p, const PaintContext &c) {
  131. if (!_transition.progress && !c.footer) {
  132. prepareZoom(c, TransitionStep::ZoomedOut);
  133. }
  134. if (_transition.pendingPrepareToZoomIn) {
  135. _transition.pendingPrepareToZoomIn = false;
  136. prepareZoom(c, TransitionStep::PrepareToZoomIn);
  137. }
  138. StackLinearChartView::paintChartOrZoomAnimation(p, c);
  139. }
  140. void StackLinearChartView::prepareZoom(
  141. const PaintContext &c,
  142. TransitionStep step) {
  143. if (step == TransitionStep::ZoomedOut) {
  144. _transition.zoomedOutXIndicesAdditional
  145. = FindAdditionalZoomedOutXIndices(c);
  146. _transition.zoomedOutXIndices = c.xIndices;
  147. _transition.zoomedOutXPercentage = c.xPercentageLimits;
  148. } else if (step == TransitionStep::PrepareToZoomIn) {
  149. const auto &[zoomedStart, zoomedEnd]
  150. = _transition.zoomedOutXIndices;
  151. _transition.lines = std::vector<Transition::TransitionLine>(
  152. c.chartData.lines.size(),
  153. Transition::TransitionLine());
  154. const auto xPercentageLimits = _transition.zoomedOutXPercentage;
  155. const auto &linesFilter = linesFilterController();
  156. for (auto j = 0; j < 2; j++) {
  157. const auto i = int((j == 1) ? zoomedEnd : zoomedStart);
  158. auto stackOffset = 0;
  159. auto sum = 0.;
  160. auto drawingLinesCount = 0;
  161. for (const auto &line : c.chartData.lines) {
  162. if (line.y[i] > 0) {
  163. const auto alpha = linesFilter->alpha(line.id);
  164. sum += line.y[i] * alpha;
  165. if (alpha > 0.) {
  166. drawingLinesCount++;
  167. }
  168. }
  169. }
  170. for (auto k = 0; k < c.chartData.lines.size(); k++) {
  171. auto &linePoint = (j
  172. ? _transition.lines[k].end
  173. : _transition.lines[k].start);
  174. const auto &line = c.chartData.lines[k];
  175. const auto yPercentage = (drawingLinesCount == 1)
  176. ? (line.y[i] ? linesFilter->alpha(line.id) : 0)
  177. : (sum
  178. ? (line.y[i] * linesFilter->alpha(line.id) / sum)
  179. : 0);
  180. const auto xPoint = c.rect.width()
  181. * ((c.chartData.xPercentage[i] - xPercentageLimits.min)
  182. / (xPercentageLimits.max - xPercentageLimits.min));
  183. const auto height = yPercentage * c.rect.height();
  184. const auto yPoint = rect::bottom(c.rect)
  185. - height
  186. - stackOffset;
  187. linePoint = { xPoint, yPoint };
  188. stackOffset += height;
  189. }
  190. }
  191. savePieTextParts(c);
  192. applyParts(_transition.textParts);
  193. }
  194. }
  195. void StackLinearChartView::applyParts(
  196. const std::vector<PiePartData::Part> &parts) {
  197. for (auto k = 0; k < parts.size(); k++) {
  198. _transition.lines[k].angle = parts[k].stackedAngle;
  199. }
  200. }
  201. void StackLinearChartView::saveZoomRange(const PaintContext &c) {
  202. _transition.zoomedInRangeXIndices = FindStackXIndicesFromRawXPercentages(
  203. c.chartData,
  204. c.xPercentageLimits,
  205. _transition.zoomedInLimitXIndices);
  206. _transition.zoomedInRange = {
  207. c.chartData.xPercentage[_transition.zoomedInRangeXIndices.min],
  208. c.chartData.xPercentage[_transition.zoomedInRangeXIndices.max],
  209. };
  210. }
  211. void StackLinearChartView::savePieTextParts(const PaintContext &c) {
  212. auto data = PiePartsPercentageByIndices(
  213. c.chartData,
  214. linesFilterController(),
  215. _transition.zoomedInRangeXIndices);
  216. _transition.textParts = std::move(data.parts);
  217. _pieHasSinglePart = data.pieHasSinglePart;
  218. }
  219. void StackLinearChartView::paintChartOrZoomAnimation(
  220. QPainter &p,
  221. const PaintContext &c) {
  222. if (_transition.progress == 1.) {
  223. if (c.footer) {
  224. paintZoomedFooter(p, c);
  225. } else {
  226. paintZoomed(p, c);
  227. }
  228. return p.setOpacity(0.);
  229. }
  230. const auto &linesFilter = linesFilterController();
  231. const auto hasTransitionAnimation = _transition.progress && !c.footer;
  232. const auto &[localStart, localEnd] = c.footer
  233. ? Limits{ 0., float64(c.chartData.xPercentage.size() - 1) }
  234. : _transition.zoomedOutXIndicesAdditional;
  235. _skipPoints = std::vector<bool>(c.chartData.lines.size(), false);
  236. auto paths = std::vector<QPainterPath>(
  237. c.chartData.lines.size(),
  238. QPainterPath());
  239. const auto center = QPointF(c.rect.center());
  240. const auto rotate = [&](float64 ang, const QPointF &p) {
  241. return QTransform()
  242. .translate(center.x(), center.y())
  243. .rotate(ang)
  244. .translate(-center.x(), -center.y())
  245. .map(p);
  246. };
  247. const auto xPercentageLimits = !c.footer
  248. ? _transition.zoomedOutXPercentage
  249. : Limits{
  250. c.chartData.xPercentage[localStart],
  251. c.chartData.xPercentage[localEnd],
  252. };
  253. auto straightLineProgress = 0.;
  254. auto hasEmptyPoint = false;
  255. auto ovalPath = QPainterPath();
  256. if (hasTransitionAnimation) {
  257. constexpr auto kStraightLinePart = 0.6;
  258. straightLineProgress = std::clamp(
  259. _transition.progress / kStraightLinePart,
  260. 0.,
  261. 1.);
  262. auto rectPath = QPainterPath();
  263. rectPath.addRect(c.rect);
  264. const auto r = anim::interpolateF(
  265. 1.,
  266. kCircleSizeRatio,
  267. _transition.progress);
  268. const auto per = anim::interpolateF(0., 100., _transition.progress);
  269. const auto side = (c.rect.width() / 2.) * r;
  270. const auto rectF = QRectF(
  271. center - QPointF(side, side),
  272. center + QPointF(side, side));
  273. ovalPath.addRoundedRect(rectF, per, per, Qt::RelativeSize);
  274. ovalPath = ovalPath.intersected(rectPath);
  275. }
  276. for (auto i = localStart; i <= localEnd; i++) {
  277. auto stackOffset = 0.;
  278. auto sum = 0.;
  279. auto lastEnabled = int(0);
  280. auto drawingLinesCount = int(0);
  281. const auto xPoint = c.rect.width()
  282. * ((c.chartData.xPercentage[i] - xPercentageLimits.min)
  283. / (xPercentageLimits.max - xPercentageLimits.min));
  284. for (auto k = 0; k < c.chartData.lines.size(); k++) {
  285. const auto &line = c.chartData.lines[k];
  286. const auto alpha = linesFilter->alpha(line.id);
  287. if (!alpha) {
  288. continue;
  289. }
  290. if (line.y[i] > 0) {
  291. sum += line.y[i] * alpha;
  292. drawingLinesCount++;
  293. }
  294. lastEnabled = k;
  295. }
  296. for (auto k = 0; k < c.chartData.lines.size(); k++) {
  297. const auto &line = c.chartData.lines[k];
  298. const auto isLastLine = (k == lastEnabled);
  299. const auto lineAlpha = linesFilter->alpha(line.id);
  300. if (isLastLine && (lineAlpha < 1.)) {
  301. hasEmptyPoint = true;
  302. }
  303. if (!lineAlpha) {
  304. continue;
  305. }
  306. const auto &transitionLine = hasTransitionAnimation
  307. ? _transition.lines[k]
  308. : Transition::TransitionLine();
  309. const auto &y = line.y;
  310. auto &chartPath = paths[k];
  311. const auto yPercentage = (drawingLinesCount == 1)
  312. ? float64(y[i] ? lineAlpha : 0.)
  313. : float64(sum ? (y[i] * lineAlpha / sum) : 0.);
  314. if (isLastLine && !yPercentage) {
  315. hasEmptyPoint = true;
  316. }
  317. const auto height = yPercentage * c.rect.height();
  318. const auto yPoint = rect::bottom(c.rect) - height - stackOffset;
  319. // startFromY[k] = yPoint;
  320. auto angle = 0.;
  321. auto resultPoint = QPointF(xPoint, yPoint);
  322. auto pointZero = QPointF(xPoint, c.rect.y() + c.rect.height());
  323. // if (i == localEnd) {
  324. // endXPoint = xPoint;
  325. // } else if (i == localStart) {
  326. // startXPoint = xPoint;
  327. // }
  328. if (hasTransitionAnimation && !isLastLine) {
  329. const auto point1 = (resultPoint.x() < center.x())
  330. ? transitionLine.start
  331. : transitionLine.end;
  332. const auto diff = center - point1;
  333. const auto yTo = point1.y()
  334. + diff.y() * (resultPoint.x() - point1.x()) / diff.x();
  335. const auto yToResult = yTo * straightLineProgress;
  336. const auto revProgress = (1. - straightLineProgress);
  337. resultPoint.setY(resultPoint.y() * revProgress + yToResult);
  338. pointZero.setY(pointZero.y() * revProgress + yToResult);
  339. {
  340. const auto angleK = diff.y() / float64(diff.x());
  341. angle = (angleK > 0)
  342. ? (-std::atan(angleK)) * (180. / M_PI)
  343. : (std::atan(std::abs(angleK))) * (180. / M_PI);
  344. angle -= 90;
  345. }
  346. if (resultPoint.x() >= center.x()) {
  347. const auto resultAngle = _transition.progress * angle;
  348. const auto rotated = rotate(resultAngle, resultPoint);
  349. resultPoint = QPointF(
  350. std::max(rotated.x(), center.x()),
  351. rotated.y());
  352. pointZero = QPointF(
  353. std::max(pointZero.x(), center.x()),
  354. rotate(resultAngle, pointZero).y());
  355. } else {
  356. const auto &xLimits = xPercentageLimits;
  357. const auto isNextXPointAfterCenter = false
  358. || center.x() < (c.rect.width() * ((i == localEnd)
  359. ? 1.
  360. : ((c.chartData.xPercentage[i + 1] - xLimits.min)
  361. / (xLimits.max - xLimits.min))));
  362. if (isNextXPointAfterCenter) {
  363. pointZero = resultPoint = QPointF()
  364. + center * straightLineProgress
  365. + resultPoint * revProgress;
  366. } else {
  367. const auto resultAngle = _transition.progress * angle
  368. + _transition.progress * transitionLine.angle;
  369. resultPoint = rotate(resultAngle, resultPoint);
  370. pointZero = rotate(resultAngle, pointZero);
  371. }
  372. }
  373. }
  374. if (i == localStart) {
  375. const auto bottomLeft = QPointF(
  376. c.rect.x(),
  377. rect::bottom(c.rect));
  378. const auto local = (hasTransitionAnimation && !isLastLine)
  379. ? rotate(
  380. _transition.progress * angle
  381. + _transition.progress * transitionLine.angle,
  382. bottomLeft - QPointF(center.x(), 0))
  383. : bottomLeft;
  384. chartPath.setFillRule(Qt::WindingFill);
  385. chartPath.moveTo(local);
  386. _skipPoints[k] = false;
  387. }
  388. const auto yRatio = 1. - (isLastLine ? _transition.progress : 0.);
  389. if ((!yPercentage)
  390. && (i > 0 && (y[i - 1] == 0))
  391. && (i < localEnd && (y[i + 1] == 0))
  392. && (!hasTransitionAnimation)) {
  393. if (!_skipPoints[k]) {
  394. chartPath.lineTo(pointZero.x(), pointZero.y() * yRatio);
  395. }
  396. _skipPoints[k] = true;
  397. } else {
  398. if (_skipPoints[k]) {
  399. chartPath.lineTo(pointZero.x(), pointZero.y() * yRatio);
  400. }
  401. chartPath.lineTo(resultPoint.x(), resultPoint.y() * yRatio);
  402. _skipPoints[k] = false;
  403. }
  404. if (i == localEnd) {
  405. if (hasTransitionAnimation && !isLastLine) {
  406. {
  407. const auto diff = center - transitionLine.start;
  408. const auto angleK = diff.y() / diff.x();
  409. angle = (angleK > 0)
  410. ? ((-std::atan(angleK)) * (180. / M_PI))
  411. : ((std::atan(std::abs(angleK))) * (180. / M_PI));
  412. angle -= 90;
  413. }
  414. const auto local = rotate(
  415. _transition.progress * angle
  416. + _transition.progress * transitionLine.angle,
  417. transitionLine.start);
  418. const auto ending = true
  419. && (std::abs(resultPoint.x() - local.x()) < 0.001)
  420. && ((local.y() < center.y()
  421. && resultPoint.y() < center.y())
  422. || (local.y() > center.y()
  423. && resultPoint.y() > center.y()));
  424. const auto endQuarter = (!ending)
  425. ? QuarterForPoint(c.rect, resultPoint)
  426. : kRightTop;
  427. const auto startQuarter = (!ending)
  428. ? QuarterForPoint(c.rect, local)
  429. : (transitionLine.angle == -180.)
  430. ? kRightTop
  431. : kLeftTop;
  432. for (auto q = endQuarter; q <= startQuarter; q++) {
  433. chartPath.lineTo(
  434. (q == kLeftTop || q == kLeftBottom)
  435. ? c.rect.x()
  436. : rect::right(c.rect),
  437. (q == kLeftTop || q == kRightTop)
  438. ? c.rect.y()
  439. : rect::right(c.rect));
  440. }
  441. } else {
  442. chartPath.lineTo(
  443. rect::right(c.rect),
  444. rect::bottom(c.rect));
  445. }
  446. }
  447. stackOffset += height;
  448. }
  449. }
  450. auto hq = PainterHighQualityEnabler(p);
  451. p.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg);
  452. if (!ovalPath.isEmpty()) {
  453. p.setClipPath(ovalPath);
  454. }
  455. if (hasEmptyPoint) {
  456. p.fillRect(c.rect, st::boxDividerBg);
  457. }
  458. const auto opacity = c.footer ? (1. - _transition.progress) : 1.;
  459. for (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) {
  460. if (paths[k].isEmpty()) {
  461. continue;
  462. }
  463. const auto &line = c.chartData.lines[k];
  464. p.setPen(Qt::NoPen);
  465. p.fillPath(paths[k], line.color);
  466. }
  467. p.setOpacity(opacity);
  468. if (!c.footer) {
  469. constexpr auto kAlphaTextPart = 0.6;
  470. const auto progress = std::clamp(
  471. (_transition.progress - kAlphaTextPart) / (1. - kAlphaTextPart),
  472. 0.,
  473. 1.);
  474. if (progress > 0) {
  475. auto o = ScopedPainterOpacity(p, progress);
  476. paintPieText(p, c);
  477. }
  478. } else if (_transition.progress) {
  479. paintZoomedFooter(p, c);
  480. }
  481. // Fix ugly outline.
  482. if (!c.footer || !_transition.progress) {
  483. p.setBrush(Qt::transparent);
  484. p.setPen(st::boxBg);
  485. p.drawPath(ovalPath);
  486. }
  487. if (!ovalPath.isEmpty()) {
  488. p.setClipRect(c.rect, Qt::NoClip);
  489. }
  490. p.setOpacity(1. - _transition.progress);
  491. }
  492. void StackLinearChartView::paintZoomed(QPainter &p, const PaintContext &c) {
  493. if (c.footer) {
  494. return;
  495. }
  496. const auto wasZoomedInRangeXIndices = _transition.zoomedInRangeXIndices;
  497. saveZoomRange(c);
  498. const auto &[zoomedStart, zoomedEnd] = _transition.zoomedInRangeXIndices;
  499. const auto partsData = PiePartsPercentageByIndices(
  500. c.chartData,
  501. linesFilterController(),
  502. _transition.zoomedInRangeXIndices);
  503. const auto xIndicesChanged = (wasZoomedInRangeXIndices.min != zoomedStart)
  504. || (wasZoomedInRangeXIndices.max != zoomedEnd);
  505. if (xIndicesChanged) {
  506. const auto wasParts = PiePartsPercentageByIndices(
  507. c.chartData,
  508. linesFilterController(),
  509. wasZoomedInRangeXIndices);
  510. _changingPieController.setParts(wasParts.parts, partsData.parts);
  511. if (!_piePartAnimation.animating()) {
  512. _piePartAnimation.start();
  513. }
  514. }
  515. if (!_changingPieController.isFinished()) {
  516. _changingPieController.update();
  517. }
  518. _pieHasSinglePart = partsData.pieHasSinglePart;
  519. applyParts(partsData.parts);
  520. const auto &parts = _changingPieController.isFinished()
  521. ? partsData.parts
  522. : _changingPieController.current().parts;
  523. p.fillRect(c.rect + QMargins(0, 0, 0, st::lineWidth), st::boxBg);
  524. const auto center = QPointF(c.rect.center());
  525. const auto side = (c.rect.width() / 2.) * kCircleSizeRatio;
  526. const auto rectF = QRectF(
  527. center - QPointF(side, side),
  528. center + QPointF(side, side));
  529. auto hq = PainterHighQualityEnabler(p);
  530. auto selectedLineIndex = -1;
  531. const auto skipTranslation = skipSelectedTranslation();
  532. for (auto k = 0; k < c.chartData.lines.size(); k++) {
  533. const auto previous = k
  534. ? parts[k - 1].stackedAngle
  535. : -180;
  536. const auto now = parts[k].stackedAngle;
  537. const auto &line = c.chartData.lines[k];
  538. p.setBrush(line.color);
  539. p.setPen(Qt::NoPen);
  540. const auto textAngle = (previous + kPieAngleOffset)
  541. + (now - previous) / 2.;
  542. const auto partOffset = skipTranslation
  543. ? QPointF()
  544. : _piePartController.offset(line.id, textAngle);
  545. p.translate(partOffset);
  546. p.drawPie(
  547. rectF,
  548. -(previous + kPieAngleOffset) * 16,
  549. -(now - previous) * 16);
  550. p.translate(-partOffset);
  551. if (_piePartController.selected() == line.id) {
  552. selectedLineIndex = k;
  553. }
  554. }
  555. if (_piePartController.isFinished() && _changingPieController.isFinished()) {
  556. _piePartAnimation.stop();
  557. }
  558. paintPieText(p, c);
  559. if (selectedLineIndex >= 0) {
  560. const auto &line = c.chartData.lines[selectedLineIndex];
  561. auto sum = ChartValue(0);
  562. for (auto i = zoomedStart; i <= zoomedEnd; i++) {
  563. sum += line.y[i];
  564. }
  565. sum *= linesFilterController()->alpha(line.id);
  566. if (sum > 0) {
  567. PaintDetails(p, line, sum, c.rect);
  568. }
  569. }
  570. }
  571. void StackLinearChartView::paintZoomedFooter(
  572. QPainter &p,
  573. const PaintContext &c) {
  574. if (!c.footer) {
  575. return;
  576. }
  577. auto o = ScopedPainterOpacity(p, _transition.progress);
  578. auto hq = PainterHighQualityEnabler(p);
  579. const auto &[zoomedStart, zoomedEnd] = _transition.zoomedInLimitXIndices;
  580. const auto sideW = st::statisticsChartFooterSideWidth;
  581. const auto width = c.rect.width() - sideW * 2.;
  582. const auto leftStart = c.rect.x() + sideW;
  583. const auto &xPercentage = c.chartData.xPercentage;
  584. auto previousX = leftStart;
  585. // Read FindStackXIndicesFromRawXPercentages.
  586. const auto offset = (xPercentage[zoomedEnd] == 1.) ? 0 : 1;
  587. for (auto i = zoomedStart; i <= zoomedEnd; i++) {
  588. auto sum = 0.;
  589. auto lastEnabledId = int(0);
  590. for (const auto &line : c.chartData.lines) {
  591. const auto alpha = linesFilterController()->alpha(line.id);
  592. sum += line.y[i] * alpha;
  593. if (alpha > 0.) {
  594. lastEnabledId = line.id;
  595. }
  596. }
  597. const auto columnMargins = QMarginsF(
  598. (i == zoomedStart) ? sideW : 0,
  599. 0,
  600. (i == zoomedEnd - offset) ? sideW : 0,
  601. 0);
  602. const auto next = std::clamp(i + offset, zoomedStart, zoomedEnd);
  603. const auto xPointPercentage
  604. = (xPercentage[next] - xPercentage[zoomedStart])
  605. / (xPercentage[zoomedEnd] - xPercentage[zoomedStart]);
  606. const auto xPoint = leftStart + width * xPointPercentage;
  607. auto stack = 0.;
  608. for (auto k = int(c.chartData.lines.size() - 1); k >= 0; k--) {
  609. const auto &line = c.chartData.lines[k];
  610. const auto visibleHeight = c.rect.height()
  611. * (line.y[i] * linesFilterController()->alpha(line.id) / sum);
  612. if (!visibleHeight) {
  613. continue;
  614. }
  615. const auto height = (line.id == lastEnabledId)
  616. ? c.rect.height()
  617. : visibleHeight;
  618. const auto column = columnMargins + QRectF(
  619. previousX,
  620. stack,
  621. xPoint - previousX,
  622. height);
  623. p.setPen(Qt::NoPen);
  624. p.fillRect(column, line.color);
  625. stack += visibleHeight;
  626. }
  627. previousX = xPoint;
  628. }
  629. }
  630. void StackLinearChartView::paintPieText(QPainter &p, const PaintContext &c) {
  631. constexpr auto kMinPercentage = 0.039;
  632. if (_transition.progress == 1.) {
  633. savePieTextParts(c);
  634. }
  635. const auto &parts = _changingPieController.isFinished()
  636. ? _transition.textParts
  637. : _changingPieController.current().parts;
  638. const auto center = QPointF(c.rect.center());
  639. const auto side = (c.rect.width() / 2.) * kCircleSizeRatio;
  640. const auto rectF = QRectF(
  641. center - QPointF(side, side),
  642. center + QPointF(side, side));
  643. const auto &font = st::statisticsPieChartFont;
  644. const auto maxScale = side / (font->height * 2);
  645. const auto minScale = maxScale * kMinTextScaleRatio;
  646. p.setBrush(Qt::NoBrush);
  647. p.setPen(st::premiumButtonFg);
  648. p.setFont(font);
  649. const auto opacity = p.opacity();
  650. const auto skipTranslation = skipSelectedTranslation();
  651. for (auto k = 0; k < c.chartData.lines.size(); k++) {
  652. const auto previous = k
  653. ? parts[k - 1].stackedAngle
  654. : -180;
  655. const auto now = parts[k].stackedAngle;
  656. const auto percentage = parts[k].roundedPercentage;
  657. if (percentage <= kMinPercentage) {
  658. continue;
  659. }
  660. const auto rText = side * std::sqrt(1. - percentage);
  661. const auto textAngle = (now == previous)
  662. ? 0.
  663. : ((previous + kPieAngleOffset) + (now - previous) / 2.);
  664. const auto textRadians = textAngle * M_PI / 180.;
  665. const auto scale = (maxScale == minScale)
  666. ? 0.
  667. : (minScale) + percentage * (maxScale - minScale);
  668. const auto text = parts[k].percentageText;
  669. const auto textW = font->width(text);
  670. const auto textXShift = textW / 2.;
  671. const auto textYShift = textW / 2.;
  672. const auto textRectCenter = rectF.center() + QPointF(
  673. (rText - textXShift * (1. - scale)) * std::cos(textRadians),
  674. (rText - textYShift * (1. - scale)) * std::sin(textRadians));
  675. const auto textRect = QRectF(
  676. textRectCenter - QPointF(textXShift, textYShift),
  677. textRectCenter + QPointF(textXShift, textYShift));
  678. const auto partOffset = skipTranslation
  679. ? QPointF()
  680. : _piePartController.offset(c.chartData.lines[k].id, textAngle);
  681. p.setTransform(
  682. QTransform()
  683. .translate(
  684. textRectCenter.x() + partOffset.x(),
  685. textRectCenter.y() + partOffset.y())
  686. .scale(scale, scale)
  687. .translate(-textRectCenter.x(), -textRectCenter.y()));
  688. p.setOpacity(opacity
  689. * linesFilterController()->alpha(c.chartData.lines[k].id));
  690. p.drawText(textRect, text, style::al_center);
  691. }
  692. p.resetTransform();
  693. }
  694. bool StackLinearChartView::PiePartController::set(int id) {
  695. if (_selected != id) {
  696. update(_selected);
  697. _selected = id;
  698. update(_selected);
  699. return true;
  700. }
  701. return false;
  702. }
  703. void StackLinearChartView::PiePartController::update(int id) {
  704. if (id >= 0) {
  705. const auto was = _startedAt[id];
  706. const auto p = (crl::now() - was) / float64(st::slideWrapDuration);
  707. const auto progress = ((p > 0) && (p < 1)) ? (1. - p) : 0.;
  708. _startedAt[id] = crl::now() - (st::slideWrapDuration * progress);
  709. }
  710. }
  711. float64 StackLinearChartView::PiePartController::progress(int id) const {
  712. const auto it = _startedAt.find(id);
  713. if (it == end(_startedAt)) {
  714. return 0.;
  715. }
  716. const auto at = it->second;
  717. const auto show = (_selected == id);
  718. const auto progress = std::clamp(
  719. (crl::now() - at) / float64(st::slideWrapDuration),
  720. 0.,
  721. 1.);
  722. return std::clamp(show ? progress : (1. - progress), 0., 1.);
  723. }
  724. QPointF StackLinearChartView::PiePartController::offset(
  725. LineId id,
  726. float64 angle) const {
  727. const auto offset = st::statisticsPieChartPartOffset * progress(id);
  728. const auto radians = angle * M_PI / 180.;
  729. return { std::cos(radians) * offset, std::sin(radians) * offset };
  730. }
  731. auto StackLinearChartView::PiePartController::selected() const -> LineId {
  732. return _selected;
  733. }
  734. bool StackLinearChartView::PiePartController::isFinished() const {
  735. for (const auto &[id, _] : _startedAt) {
  736. const auto p = progress(id);
  737. if (p > 0 && p < 1) {
  738. return false;
  739. }
  740. }
  741. return true;
  742. }
  743. void StackLinearChartView::handleMouseMove(
  744. const Data::StatisticalChart &chartData,
  745. const QRect &rect,
  746. const QPoint &p) {
  747. if (_transition.progress < 1) {
  748. return;
  749. }
  750. const auto center = rect.center();
  751. const auto theta = std::atan2(center.y() - p.y(), (center.x() - p.x()));
  752. const auto rawAngle = theta * (180. / M_PI) + 90.;
  753. const auto angle = (rawAngle > 180.) ? (rawAngle - 360.) : rawAngle;
  754. for (auto k = 0; k < chartData.lines.size(); k++) {
  755. const auto previous = k
  756. ? _transition.lines[k - 1].angle
  757. : -180;
  758. const auto now = _transition.lines[k].angle;
  759. if (angle > previous && angle <= now) {
  760. const auto id = p.isNull()
  761. ? -1
  762. : chartData.lines[k].id;
  763. if (_piePartController.set(id)) {
  764. if (!_piePartAnimation.animating()) {
  765. _piePartAnimation.start();
  766. }
  767. }
  768. return;
  769. }
  770. }
  771. }
  772. bool StackLinearChartView::skipSelectedTranslation() const {
  773. return _pieHasSinglePart;
  774. }
  775. void StackLinearChartView::paintSelectedXIndex(
  776. QPainter &p,
  777. const PaintContext &c,
  778. int selectedXIndex,
  779. float64 progress) {
  780. if ((selectedXIndex < 0) || c.footer) {
  781. return;
  782. }
  783. const auto xPercentageLimits = _transition.zoomedOutXPercentage;
  784. p.setBrush(st::boxBg);
  785. const auto i = selectedXIndex;
  786. const auto isSameToken = (_selectedPoints.lastXIndex == selectedXIndex)
  787. && (_selectedPoints.lastHeightLimits.min == c.heightLimits.min)
  788. && (_selectedPoints.lastHeightLimits.max == c.heightLimits.max)
  789. && (_selectedPoints.lastXLimits.min == xPercentageLimits.min)
  790. && (_selectedPoints.lastXLimits.max == xPercentageLimits.max);
  791. {
  792. const auto useCache = isSameToken;
  793. if (!useCache) {
  794. // Calculate.
  795. const auto xPoint = c.rect.width()
  796. * ((c.chartData.xPercentage[i] - xPercentageLimits.min)
  797. / (xPercentageLimits.max - xPercentageLimits.min));
  798. _selectedPoints.xPoint = xPoint;
  799. }
  800. {
  801. [[maybe_unused]] const auto o = ScopedPainterOpacity(
  802. p,
  803. p.opacity() * progress * kRulerLineAlpha);
  804. const auto lineRect = QRectF(
  805. _selectedPoints.xPoint - (st::lineWidth / 2.),
  806. c.rect.y(),
  807. st::lineWidth,
  808. c.rect.height());
  809. p.fillRect(lineRect, st::boxTextFg);
  810. }
  811. }
  812. _selectedPoints.lastXIndex = selectedXIndex;
  813. _selectedPoints.lastHeightLimits = c.heightLimits;
  814. _selectedPoints.lastXLimits = xPercentageLimits;
  815. }
  816. int StackLinearChartView::findXIndexByPosition(
  817. const Data::StatisticalChart &chartData,
  818. const Limits &xPercentageLimits,
  819. const QRect &rect,
  820. float64 x) {
  821. if (_transition.progress == 1.) {
  822. return -1;
  823. } else if (x < rect.x()) {
  824. return 0;
  825. } else if (x > (rect.x() + rect.width())) {
  826. return chartData.xPercentage.size() - 1;
  827. }
  828. const auto pointerRatio = std::clamp(
  829. (x - rect.x()) / rect.width(),
  830. 0.,
  831. 1.);
  832. const auto &[localStart, localEnd] = _transition.zoomedOutXIndices;
  833. const auto rawXPercentage = anim::interpolateF(
  834. _transition.zoomedOutXPercentage.min,
  835. _transition.zoomedOutXPercentage.max,
  836. pointerRatio);
  837. const auto it = ranges::lower_bound(
  838. chartData.xPercentage,
  839. rawXPercentage);
  840. const auto left = rawXPercentage - (*(it - 1));
  841. const auto right = (*it) - rawXPercentage;
  842. const auto nearestXPercentageIt = ((right) > (left)) ? (it - 1) : it;
  843. return std::clamp(
  844. std::distance(begin(chartData.xPercentage), nearestXPercentageIt),
  845. std::ptrdiff_t(localStart),
  846. std::ptrdiff_t(localEnd));
  847. }
  848. AbstractChartView::HeightLimits StackLinearChartView::heightLimits(
  849. Data::StatisticalChart &chartData,
  850. Limits xIndices) {
  851. constexpr auto kMaxStackLinear = 100.;
  852. return {
  853. .full = { 0, kMaxStackLinear },
  854. .ranged = { 0., kMaxStackLinear },
  855. };
  856. }
  857. auto StackLinearChartView::maybeLocalZoom(
  858. const LocalZoomArgs &args) -> LocalZoomResult {
  859. // 8 days.
  860. constexpr auto kLimitLength = int(8);
  861. // 1 day in middle of limits.
  862. constexpr auto kRangeLength = int(0);
  863. constexpr auto kLeftSide = int(kLimitLength / 2 + kRangeLength);
  864. constexpr auto kRightSide = int(kLimitLength / 2) + int(1);
  865. _transition.progress = args.progress;
  866. if (args.type == LocalZoomArgs::Type::SkipCalculation) {
  867. return { true, _transition.zoomedInLimit, _transition.zoomedInRange };
  868. } else if (args.type == LocalZoomArgs::Type::CheckAvailability) {
  869. return { .hasZoom = true };
  870. } else if (args.type == LocalZoomArgs::Type::Prepare) {
  871. _transition.pendingPrepareToZoomIn = true;
  872. }
  873. const auto xIndex = args.xIndex;
  874. const auto &xPercentage = args.chartData.xPercentage;
  875. const auto backIndex = (xPercentage.size() - 1);
  876. const auto localRangeIndex = (xIndex == backIndex)
  877. ? (backIndex - kRangeLength)
  878. : xIndex;
  879. _transition.zoomedInRange = {
  880. xPercentage[localRangeIndex],
  881. xPercentage[localRangeIndex + kRangeLength],
  882. };
  883. _transition.zoomedInRangeXIndices = {
  884. float64(localRangeIndex),
  885. float64(localRangeIndex + kRangeLength),
  886. };
  887. _transition.zoomedInLimitXIndices = (xIndex < kLeftSide)
  888. ? Limits{ 0, kLeftSide + kRightSide }
  889. : (xIndex > (backIndex - kRightSide - kRangeLength))
  890. ? Limits{ float64(backIndex - kLimitLength), float64(backIndex) }
  891. : Limits{ float64(xIndex - kLeftSide), float64(xIndex + kRightSide) };
  892. _transition.zoomedInLimit = {
  893. anim::interpolateF(
  894. 0.,
  895. xPercentage[_transition.zoomedInLimitXIndices.min],
  896. args.progress),
  897. anim::interpolateF(
  898. 1.,
  899. xPercentage[_transition.zoomedInLimitXIndices.max],
  900. args.progress),
  901. };
  902. const auto oneDay = std::abs(xPercentage[localRangeIndex]
  903. - xPercentage[localRangeIndex + ((xIndex == backIndex) ? -1 : 1)]);
  904. // Read FindStackXIndicesFromRawXPercentages.
  905. const auto offset = (_transition.zoomedInLimitXIndices.max == backIndex)
  906. ? -oneDay
  907. : 0.;
  908. const auto resultRange = Limits{
  909. InterpolationRatio(
  910. _transition.zoomedInLimit.min,
  911. _transition.zoomedInLimit.max,
  912. _transition.zoomedInRange.min + oneDay * 0.25 + offset),
  913. InterpolationRatio(
  914. _transition.zoomedInLimit.min,
  915. _transition.zoomedInLimit.max,
  916. _transition.zoomedInRange.max + oneDay * 0.75 + offset),
  917. };
  918. return { true, _transition.zoomedInLimitXIndices, resultRange };
  919. }
  920. } // namespace Statistic