chart_widget.cpp 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558
  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/chart_widget.h"
  8. #include "base/qt/qt_key_modifiers.h"
  9. #include "lang/lang_keys.h"
  10. #include "statistics/chart_lines_filter_controller.h"
  11. #include "statistics/statistics_format_values.h"
  12. #include "statistics/view/abstract_chart_view.h"
  13. #include "statistics/view/chart_view_factory.h"
  14. #include "statistics/view/stack_chart_common.h"
  15. #include "statistics/widgets/chart_header_widget.h"
  16. #include "statistics/widgets/chart_lines_filter_widget.h"
  17. #include "statistics/widgets/point_details_widget.h"
  18. #include "ui/abstract_button.h"
  19. #include "ui/effects/animation_value_f.h"
  20. #include "ui/effects/ripple_animation.h"
  21. #include "ui/effects/show_animation.h"
  22. #include "ui/image/image_prepare.h"
  23. #include "ui/painter.h"
  24. #include "ui/rect.h"
  25. #include "ui/widgets/buttons.h"
  26. #include "styles/style_layers.h"
  27. #include "styles/style_statistics.h"
  28. namespace Statistic {
  29. namespace {
  30. constexpr auto kHeightLimitsUpdateTimeout = crl::time(320);
  31. inline float64 InterpolationRatio(float64 from, float64 to, float64 result) {
  32. return (result - from) / (to - from);
  33. };
  34. void FillLineColorsByKey(Data::StatisticalChart &chartData) {
  35. for (auto &line : chartData.lines) {
  36. if (line.colorKey == u"BLUE"_q) {
  37. line.color = st::statisticsChartLineBlue->c;
  38. } else if (line.colorKey == u"GREEN"_q) {
  39. line.color = st::statisticsChartLineGreen->c;
  40. } else if (line.colorKey == u"RED"_q) {
  41. line.color = st::statisticsChartLineRed->c;
  42. } else if (line.colorKey == u"GOLDEN"_q) {
  43. line.color = st::statisticsChartLineGolden->c;
  44. } else if (line.colorKey == u"LIGHTBLUE"_q) {
  45. line.color = st::statisticsChartLineLightblue->c;
  46. } else if (line.colorKey == u"LIGHTGREEN"_q) {
  47. line.color = st::statisticsChartLineLightgreen->c;
  48. } else if (line.colorKey == u"ORANGE"_q) {
  49. line.color = st::statisticsChartLineOrange->c;
  50. } else if (line.colorKey == u"INDIGO"_q) {
  51. line.color = st::statisticsChartLineIndigo->c;
  52. } else if (line.colorKey == u"PURPLE"_q) {
  53. line.color = st::statisticsChartLinePurple->c;
  54. } else if (line.colorKey == u"CYAN"_q) {
  55. line.color = st::statisticsChartLineCyan->c;
  56. }
  57. }
  58. }
  59. [[nodiscard]] QString HeaderSubTitle(
  60. const Data::StatisticalChart &chartData,
  61. int xIndexMin,
  62. int xIndexMax) {
  63. constexpr auto kOneDay = 3600 * 24 * 1000;
  64. const auto leftTimestamp = chartData.x[xIndexMin];
  65. if (leftTimestamp < kOneDay) {
  66. return {};
  67. }
  68. const auto leftText = LangDayMonthYear(leftTimestamp / 1000);
  69. if ((xIndexMin == xIndexMax) && !chartData.weekFormat) {
  70. return leftText;
  71. } else {
  72. constexpr auto kSevenDays = 3600 * 24 * 7;
  73. const auto rightTimestamp = 0
  74. + (chartData.x[xIndexMax] / 1000)
  75. + (chartData.weekFormat ? kSevenDays : 0);
  76. return leftText
  77. + ' '
  78. + QChar(8212)
  79. + ' '
  80. + LangDayMonthYear(rightTimestamp);
  81. }
  82. }
  83. void PaintBottomLine(
  84. QPainter &p,
  85. const std::vector<ChartWidget::BottomCaptionLineData> &dates,
  86. Data::StatisticalChart &chartData,
  87. const Limits &xPercentageLimits,
  88. int fullWidth,
  89. int chartWidth,
  90. int y,
  91. int captionIndicesOffset) {
  92. p.setFont(st::statisticsDetailsBottomCaptionStyle.font);
  93. const auto opacity = p.opacity();
  94. const auto startXIndex = chartData.findStartIndex(
  95. xPercentageLimits.min);
  96. const auto endXIndex = chartData.findEndIndex(
  97. startXIndex,
  98. xPercentageLimits.max);
  99. const auto captionMaxWidth = chartData.dayStringMaxWidth;
  100. const auto edgeAlphaSize = captionMaxWidth / 4.;
  101. for (auto k = 0; k < dates.size(); k++) {
  102. const auto &date = dates[k];
  103. const auto isLast = (k == dates.size() - 1);
  104. const auto resultAlpha = date.alpha;
  105. const auto step = std::max(date.step, 1);
  106. auto start = startXIndex - captionIndicesOffset;
  107. while (start % step != 0) {
  108. start--;
  109. }
  110. auto end = endXIndex - captionIndicesOffset;
  111. while ((end % step != 0) || end < (chartData.x.size() - 1)) {
  112. end++;
  113. }
  114. start += captionIndicesOffset;
  115. end += captionIndicesOffset;
  116. const auto offset = fullWidth * xPercentageLimits.min;
  117. // 30 ms / 200 ms = 0.15.
  118. constexpr auto kFastAlphaSpeed = 0.85;
  119. const auto hasFastAlpha = (date.stepRaw < dates.back().stepMinFast);
  120. const auto fastAlpha = isLast
  121. ? 1.
  122. : std::max(resultAlpha - kFastAlphaSpeed, 0.);
  123. for (auto i = start; i < end; i += step) {
  124. if ((i < 0) || (i >= (chartData.x.size() - 1))) {
  125. continue;
  126. }
  127. const auto xPercentage = (chartData.x[i] - chartData.x.front())
  128. / float64(chartData.x.back() - chartData.x.front());
  129. const auto xPoint = xPercentage * fullWidth - offset;
  130. const auto r = QRectF(
  131. xPoint - captionMaxWidth / 2.,
  132. y,
  133. captionMaxWidth,
  134. st::statisticsChartBottomCaptionHeight);
  135. const auto edgeAlpha = (r.x() < 0)
  136. ? std::max(
  137. 0.,
  138. 1. + (r.x() / edgeAlphaSize))
  139. : (rect::right(r) > chartWidth)
  140. ? std::max(
  141. 0.,
  142. 1. + ((chartWidth - rect::right(r)) / edgeAlphaSize))
  143. : 1.;
  144. p.setOpacity(opacity
  145. * edgeAlpha
  146. * (hasFastAlpha ? fastAlpha : resultAlpha));
  147. p.drawText(r, chartData.getDayString(i), style::al_center);
  148. }
  149. }
  150. }
  151. } // namespace
  152. class RpMouseWidget : public Ui::AbstractButton {
  153. public:
  154. using Ui::AbstractButton::AbstractButton;
  155. struct State {
  156. QPoint point;
  157. QEvent::Type mouseState;
  158. };
  159. [[nodiscard]] const QPoint &start() const;
  160. [[nodiscard]] rpl::producer<State> mouseStateChanged() const;
  161. protected:
  162. void mousePressEvent(QMouseEvent *e) override;
  163. void mouseMoveEvent(QMouseEvent *e) override;
  164. void mouseReleaseEvent(QMouseEvent *e) override;
  165. private:
  166. QPoint _start = QPoint(-1, -1);
  167. rpl::event_stream<State> _mouseStateChanged;
  168. };
  169. const QPoint &RpMouseWidget::start() const {
  170. return _start;
  171. }
  172. rpl::producer<RpMouseWidget::State> RpMouseWidget::mouseStateChanged() const {
  173. return _mouseStateChanged.events();
  174. }
  175. void RpMouseWidget::mousePressEvent(QMouseEvent *e) {
  176. _start = e->pos();
  177. _mouseStateChanged.fire({ e->pos(), QEvent::MouseButtonPress });
  178. }
  179. void RpMouseWidget::mouseMoveEvent(QMouseEvent *e) {
  180. if (_start.x() >= 0 || _start.y() >= 0) {
  181. _mouseStateChanged.fire({ e->pos(), QEvent::MouseMove });
  182. }
  183. }
  184. void RpMouseWidget::mouseReleaseEvent(QMouseEvent *e) {
  185. _start = { -1, -1 };
  186. _mouseStateChanged.fire({ e->pos(), QEvent::MouseButtonRelease });
  187. }
  188. class ChartWidget::Footer final : public RpMouseWidget {
  189. public:
  190. using PaintCallback = Fn<void(QPainter &, const QRect &)>;
  191. explicit Footer(not_null<Ui::RpWidget*> parent);
  192. void setXPercentageLimits(const Limits &xLimits);
  193. [[nodiscard]] Limits xPercentageLimits() const;
  194. [[nodiscard]] rpl::producer<Limits> xPercentageLimitsChange() const;
  195. void setPaintChartCallback(PaintCallback paintChartCallback);
  196. protected:
  197. void paintEvent(QPaintEvent *e) override;
  198. int resizeGetHeight(int newWidth) override;
  199. private:
  200. void moveSide(bool left, float64 x);
  201. void moveCenter(
  202. bool isDirectionToLeft,
  203. float64 x,
  204. float64 diffBetweenStartAndLeft);
  205. void fire() const;
  206. enum class DragArea {
  207. None,
  208. Middle,
  209. Left,
  210. Right,
  211. };
  212. DragArea _dragArea = DragArea::None;
  213. float64 _diffBetweenStartAndSide = 0;
  214. Ui::Animations::Simple _moveCenterAnimation;
  215. bool _draggedAfterPress = false;
  216. const QPen _sidePen;
  217. float64 _width = 0.;
  218. float64 _widthBetweenSides = 0.;
  219. PaintCallback _paintChartCallback;
  220. QImage _frame;
  221. QImage _mask;
  222. Limits _leftSide;
  223. Limits _rightSide;
  224. rpl::event_stream<Limits> _xPercentageLimitsChange;
  225. };
  226. ChartWidget::Footer::Footer(not_null<Ui::RpWidget*> parent)
  227. : RpMouseWidget(parent)
  228. , _sidePen(
  229. st::premiumButtonFg,
  230. st::statisticsChartLineWidth,
  231. Qt::SolidLine,
  232. Qt::RoundCap) {
  233. sizeValue(
  234. ) | rpl::take(2) | rpl::start_with_next([=](const QSize &s) {
  235. const auto current = xPercentageLimits();
  236. if (current.min == current.max) {
  237. setXPercentageLimits({ 0., 1. });
  238. }
  239. }, lifetime());
  240. mouseStateChanged(
  241. ) | rpl::start_with_next([=](const RpMouseWidget::State &state) {
  242. if (_moveCenterAnimation.animating()) {
  243. return;
  244. }
  245. const auto posX = state.point.x();
  246. const auto isLeftSide = (posX >= _leftSide.min)
  247. && (posX <= _leftSide.max);
  248. const auto isRightSide = !isLeftSide
  249. && (posX >= _rightSide.min)
  250. && (posX <= _rightSide.max);
  251. switch (state.mouseState) {
  252. case QEvent::MouseMove: {
  253. _draggedAfterPress = true;
  254. if (_dragArea == DragArea::None) {
  255. return;
  256. }
  257. const auto resultX = posX - _diffBetweenStartAndSide;
  258. if (_dragArea == DragArea::Right) {
  259. moveSide(false, resultX);
  260. } else if (_dragArea == DragArea::Left) {
  261. moveSide(true, resultX);
  262. } else if (_dragArea == DragArea::Middle) {
  263. const auto toLeft = (posX
  264. - _diffBetweenStartAndSide
  265. - _leftSide.min) <= 0;
  266. moveCenter(toLeft, posX, _diffBetweenStartAndSide);
  267. }
  268. fire();
  269. } break;
  270. case QEvent::MouseButtonPress: {
  271. _draggedAfterPress = false;
  272. _dragArea = isLeftSide
  273. ? DragArea::Left
  274. : isRightSide
  275. ? DragArea::Right
  276. : ((posX < _leftSide.min) || (posX > _rightSide.max))
  277. ? DragArea::None
  278. : DragArea::Middle;
  279. _diffBetweenStartAndSide = isRightSide
  280. ? (start().x() - _rightSide.min)
  281. : (start().x() - _leftSide.min);
  282. } break;
  283. case QEvent::MouseButtonRelease: {
  284. const auto finish = [=] {
  285. _dragArea = DragArea::None;
  286. fire();
  287. };
  288. if ((_dragArea == DragArea::None) && !_draggedAfterPress) {
  289. const auto startX = _leftSide.min
  290. + (_rightSide.max - _leftSide.min) / 2;
  291. const auto finishX = posX;
  292. const auto toLeft = (finishX <= startX);
  293. const auto diffBetweenStartAndLeft = startX - _leftSide.min;
  294. _moveCenterAnimation.stop();
  295. _moveCenterAnimation.start([=](float64 value) {
  296. moveCenter(toLeft, value, diffBetweenStartAndLeft);
  297. fire();
  298. update();
  299. if (value == finishX) {
  300. finish();
  301. }
  302. },
  303. startX,
  304. finishX,
  305. st::slideWrapDuration,
  306. anim::sineInOut);
  307. } else {
  308. finish();
  309. }
  310. } break;
  311. }
  312. update();
  313. }, lifetime());
  314. }
  315. int ChartWidget::Footer::resizeGetHeight(int newWidth) {
  316. const auto h = st::statisticsChartFooterHeight;
  317. if (!newWidth) {
  318. return h;
  319. }
  320. const auto was = xPercentageLimits();
  321. const auto w = float64(st::statisticsChartFooterSideWidth);
  322. _width = newWidth - w;
  323. _widthBetweenSides = newWidth - w * 2.;
  324. _mask = Ui::RippleAnimation::RoundRectMask(
  325. QSize(newWidth, h - st::lineWidth * 2),
  326. st::boxRadius);
  327. _frame = _mask;
  328. if (_widthBetweenSides && was.max) {
  329. setXPercentageLimits(was);
  330. }
  331. return h;
  332. }
  333. Limits ChartWidget::Footer::xPercentageLimits() const {
  334. return {
  335. .min = _widthBetweenSides ? _leftSide.min / _widthBetweenSides : 0.,
  336. .max = _widthBetweenSides
  337. ? (_rightSide.min - st::statisticsChartFooterSideWidth)
  338. / _widthBetweenSides
  339. : 0.,
  340. };
  341. }
  342. void ChartWidget::Footer::fire() const {
  343. _xPercentageLimitsChange.fire(xPercentageLimits());
  344. }
  345. void ChartWidget::Footer::moveCenter(
  346. bool isDirectionToLeft,
  347. float64 x,
  348. float64 diffBetweenStartAndLeft) {
  349. const auto resultX = x - diffBetweenStartAndLeft;
  350. const auto diffBetweenSides = std::max(
  351. _rightSide.min - _leftSide.min,
  352. float64(st::statisticsChartFooterBetweenSide));
  353. if (isDirectionToLeft) {
  354. moveSide(true, resultX);
  355. moveSide(false, _leftSide.min + diffBetweenSides);
  356. } else {
  357. moveSide(false, resultX + diffBetweenSides);
  358. moveSide(true, _rightSide.min - diffBetweenSides);
  359. }
  360. }
  361. void ChartWidget::Footer::moveSide(bool left, float64 x) {
  362. const auto w = float64(st::statisticsChartFooterSideWidth);
  363. const auto mid = float64(st::statisticsChartFooterBetweenSide);
  364. if (_width < (2 * w + mid)) {
  365. return;
  366. } else if (left) {
  367. const auto rightLimit = _rightSide.min - w - mid;
  368. const auto min = std::clamp(
  369. x,
  370. 0.,
  371. (rightLimit <= 0) ? _widthBetweenSides : rightLimit);
  372. _leftSide = Limits{ .min = min, .max = min + w };
  373. } else if (!left) {
  374. const auto min = std::clamp(x, _leftSide.max + mid, _width);
  375. _rightSide = Limits{ .min = min, .max = min + w };
  376. }
  377. }
  378. void ChartWidget::Footer::setPaintChartCallback(
  379. PaintCallback paintChartCallback) {
  380. _paintChartCallback = std::move(paintChartCallback);
  381. }
  382. void ChartWidget::Footer::paintEvent(QPaintEvent *e) {
  383. auto p = QPainter(this);
  384. auto hq = PainterHighQualityEnabler(p);
  385. const auto lineWidth = st::lineWidth;
  386. const auto innerMargins = QMargins{ 0, lineWidth, 0, lineWidth };
  387. const auto r = rect();
  388. const auto innerRect = r - innerMargins;
  389. const auto &inactiveColor = st::statisticsChartInactive;
  390. _frame.fill(Qt::transparent);
  391. if (_paintChartCallback) {
  392. auto q = QPainter(&_frame);
  393. {
  394. const auto opacity = q.opacity();
  395. _paintChartCallback(q, Rect(innerRect.size()));
  396. q.setOpacity(opacity);
  397. }
  398. q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
  399. q.drawImage(0, 0, _mask);
  400. }
  401. p.drawImage(0, lineWidth, _frame);
  402. auto inactivePath = QPainterPath();
  403. inactivePath.addRoundedRect(
  404. innerRect,
  405. st::statisticsChartFooterSideRadius,
  406. st::statisticsChartFooterSideRadius);
  407. auto sidesPath = QPainterPath();
  408. sidesPath.addRoundedRect(
  409. _leftSide.min,
  410. 0,
  411. _rightSide.max - _leftSide.min,
  412. r.height(),
  413. st::statisticsChartFooterSideRadius,
  414. st::statisticsChartFooterSideRadius);
  415. inactivePath = inactivePath.subtracted(sidesPath);
  416. sidesPath.addRect(
  417. _leftSide.max,
  418. lineWidth,
  419. _rightSide.min - _leftSide.max,
  420. r.height() - lineWidth * 2);
  421. p.setBrush(st::statisticsChartActive);
  422. p.setPen(Qt::NoPen);
  423. p.drawPath(sidesPath);
  424. p.setBrush(inactiveColor);
  425. p.drawPath(inactivePath);
  426. {
  427. p.setPen(_sidePen);
  428. const auto halfWidth = st::statisticsChartLineWidth / 2.;
  429. const auto left = _leftSide.min
  430. + (_leftSide.max - _leftSide.min) / 2.
  431. + halfWidth;
  432. const auto right = _rightSide.min
  433. + (_rightSide.max - _rightSide.min) / 2.;
  434. const auto halfHeight = st::statisticsChartFooterArrowHeight / 2.
  435. - halfWidth;
  436. const auto center = r.height() / 2.;
  437. const auto top = center - halfHeight;
  438. const auto bottom = center + halfHeight;
  439. p.drawLine(left, top, left, bottom);
  440. p.drawLine(right, top, right, bottom);
  441. }
  442. }
  443. void ChartWidget::Footer::setXPercentageLimits(const Limits &xLimits) {
  444. const auto left = xLimits.min * _widthBetweenSides;
  445. const auto right = xLimits.max * _widthBetweenSides
  446. + st::statisticsChartFooterSideWidth;
  447. moveSide(true, left);
  448. moveSide(false, right);
  449. fire();
  450. update();
  451. }
  452. rpl::producer<Limits> ChartWidget::Footer::xPercentageLimitsChange() const {
  453. return _xPercentageLimitsChange.events();
  454. }
  455. ChartWidget::ChartAnimationController::ChartAnimationController(
  456. Fn<void()> &&updateCallback)
  457. : _animation(std::move(updateCallback)) {
  458. }
  459. void ChartWidget::ChartAnimationController::setXPercentageLimits(
  460. Data::StatisticalChart &chartData,
  461. Limits xPercentageLimits,
  462. const std::unique_ptr<AbstractChartView> &chartView,
  463. const std::shared_ptr<LinesFilterController> &linesFilter,
  464. crl::time now) {
  465. if ((_animationValueXMin.to() == xPercentageLimits.min)
  466. && (_animationValueXMax.to() == xPercentageLimits.max)
  467. && linesFilter->isFinished()) {
  468. return;
  469. }
  470. start();
  471. _animationValueXMin.start(xPercentageLimits.min);
  472. _animationValueXMax.start(xPercentageLimits.max);
  473. _lastUserInteracted = now;
  474. const auto startXIndex = chartData.findStartIndex(
  475. _animationValueXMin.to());
  476. const auto endXIndex = chartData.findEndIndex(
  477. startXIndex,
  478. _animationValueXMax.to());
  479. _currentXIndices = { float64(startXIndex), float64(endXIndex) };
  480. {
  481. const auto heightLimits = chartView->heightLimits(
  482. chartData,
  483. _currentXIndices);
  484. if (heightLimits.ranged.min == heightLimits.ranged.max) {
  485. return;
  486. }
  487. _previousFullHeightLimits = _finalHeightLimits;
  488. _finalHeightLimits = heightLimits.ranged;
  489. if (!_previousFullHeightLimits.max) {
  490. _previousFullHeightLimits = _finalHeightLimits;
  491. }
  492. if (!linesFilter->isFinished()) {
  493. _animationValueFooterHeightMin = anim::value(
  494. _animationValueFooterHeightMin.current(),
  495. heightLimits.full.min);
  496. _animationValueFooterHeightMax = anim::value(
  497. _animationValueFooterHeightMax.current(),
  498. heightLimits.full.max);
  499. } else if (!_animationValueFooterHeightMax.to()) {
  500. // Will be finished in setChartData.
  501. _animationValueFooterHeightMin = anim::value(
  502. 0,
  503. heightLimits.full.min);
  504. _animationValueFooterHeightMax = anim::value(
  505. 0,
  506. heightLimits.full.max);
  507. }
  508. }
  509. _animationValueHeightMin = anim::value(
  510. _animationValueHeightMin.current(),
  511. _finalHeightLimits.min);
  512. _animationValueHeightMax = anim::value(
  513. _animationValueHeightMax.current(),
  514. _finalHeightLimits.max);
  515. {
  516. const auto previousDelta = _previousFullHeightLimits.max
  517. - _previousFullHeightLimits.min;
  518. auto k = previousDelta
  519. / float64(_finalHeightLimits.max - _finalHeightLimits.min);
  520. if (k > 1.) {
  521. k = 1. / k;
  522. }
  523. constexpr auto kDtHeightSpeed1 = 0.03 * 2;
  524. constexpr auto kDtHeightSpeed2 = 0.03 * 2;
  525. constexpr auto kDtHeightSpeed3 = 0.045 * 2;
  526. constexpr auto kDtHeightSpeedFilter = kDtHeightSpeed1 / 1.2;
  527. constexpr auto kDtHeightSpeedThreshold1 = 0.7;
  528. constexpr auto kDtHeightSpeedThreshold2 = 0.1;
  529. constexpr auto kDtHeightInstantThreshold = 0.97;
  530. if (k < 1.) {
  531. auto &alpha = _animationValueHeightAlpha;
  532. alpha = anim::value(
  533. (alpha.current() == alpha.to()) ? 0. : alpha.current(),
  534. 1.);
  535. _dtHeight.currentAlpha = 0.;
  536. _addRulerRequests.fire({});
  537. }
  538. _dtHeight.speed = (!linesFilter->isFinished())
  539. ? kDtHeightSpeedFilter
  540. : (k > kDtHeightSpeedThreshold1)
  541. ? kDtHeightSpeed1
  542. : (k < kDtHeightSpeedThreshold2)
  543. ? kDtHeightSpeed2
  544. : kDtHeightSpeed3;
  545. if (k < kDtHeightInstantThreshold) {
  546. _dtHeight.current = { 0., 0. };
  547. }
  548. }
  549. }
  550. auto ChartWidget::ChartAnimationController::addRulerRequests() const
  551. -> rpl::producer<> {
  552. return _addRulerRequests.events();
  553. }
  554. void ChartWidget::ChartAnimationController::start() {
  555. if (!_animation.animating()) {
  556. _animation.start();
  557. }
  558. }
  559. void ChartWidget::ChartAnimationController::finish() {
  560. _animation.stop();
  561. _animationValueXMin.finish();
  562. _animationValueXMax.finish();
  563. _animationValueHeightMin.finish();
  564. _animationValueHeightMax.finish();
  565. _animationValueFooterHeightMin.finish();
  566. _animationValueFooterHeightMax.finish();
  567. _animationValueHeightAlpha.finish();
  568. _benchmark = {};
  569. }
  570. void ChartWidget::ChartAnimationController::restartBottomLineAlpha() {
  571. _bottomLineAlphaAnimationStartedAt = crl::now();
  572. _animValueBottomLineAlpha = anim::value(0., 1.);
  573. start();
  574. }
  575. void ChartWidget::ChartAnimationController::tick(
  576. crl::time now,
  577. ChartRulersView &rulersView,
  578. std::vector<BottomCaptionLineData> &dateLines,
  579. const std::unique_ptr<AbstractChartView> &chartView,
  580. const std::shared_ptr<LinesFilterController> &linesFilter) {
  581. if (!_animation.animating()) {
  582. return;
  583. }
  584. constexpr auto kXExpandingDuration = 200.;
  585. constexpr auto kAlphaExpandingDuration = 200.;
  586. {
  587. constexpr auto kIdealFPS = float64(60);
  588. const auto currentFPS = _benchmark.lastTickedAt
  589. ? (1000. / (now - _benchmark.lastTickedAt))
  590. : kIdealFPS;
  591. if (!_benchmark.lastFPSSlow) {
  592. constexpr auto kAcceptableFPS = int(30);
  593. _benchmark.lastFPSSlow = (currentFPS < kAcceptableFPS);
  594. }
  595. _benchmark.lastTickedAt = now;
  596. const auto k = (kIdealFPS / currentFPS)
  597. // Speed up to reduce ugly frames count.
  598. * (_benchmark.lastFPSSlow ? 2. : 1.);
  599. const auto speed = _dtHeight.speed * k;
  600. linesFilter->tick(speed);
  601. _dtHeight.current.min = std::min(_dtHeight.current.min + speed, 1.);
  602. _dtHeight.current.max = std::min(_dtHeight.current.max + speed, 1.);
  603. _dtHeight.currentAlpha = std::min(_dtHeight.currentAlpha + speed, 1.);
  604. }
  605. const auto dtX = std::min(
  606. (now - _animation.started()) / kXExpandingDuration,
  607. 1.);
  608. const auto dtBottomLineAlpha = std::min(
  609. (now - _bottomLineAlphaAnimationStartedAt) / kAlphaExpandingDuration,
  610. 1.);
  611. const auto isFinished = [](const anim::value &anim) {
  612. return anim.current() == anim.to();
  613. };
  614. const auto xFinished = isFinished(_animationValueXMin)
  615. && isFinished(_animationValueXMax);
  616. const auto yFinished = isFinished(_animationValueHeightMin)
  617. && isFinished(_animationValueHeightMax);
  618. const auto alphaFinished = isFinished(_animationValueHeightAlpha)
  619. && isFinished(_animationValueHeightMax);
  620. const auto bottomLineAlphaFinished = isFinished(
  621. _animValueBottomLineAlpha);
  622. const auto footerMinFinished = isFinished(_animationValueFooterHeightMin);
  623. const auto footerMaxFinished = isFinished(_animationValueFooterHeightMax);
  624. if (xFinished
  625. && yFinished
  626. && alphaFinished
  627. && bottomLineAlphaFinished
  628. && footerMinFinished
  629. && footerMaxFinished
  630. && linesFilter->isFinished()) {
  631. if ((_finalHeightLimits.min == _animationValueHeightMin.to())
  632. && _finalHeightLimits.max == _animationValueHeightMax.to()) {
  633. _animation.stop();
  634. _benchmark = {};
  635. }
  636. }
  637. if (xFinished) {
  638. _animationValueXMin.finish();
  639. _animationValueXMax.finish();
  640. } else {
  641. _animationValueXMin.update(dtX, anim::linear);
  642. _animationValueXMax.update(dtX, anim::linear);
  643. }
  644. if (bottomLineAlphaFinished) {
  645. _animValueBottomLineAlpha.finish();
  646. _bottomLineAlphaAnimationStartedAt = 0;
  647. } else {
  648. _animValueBottomLineAlpha.update(
  649. dtBottomLineAlpha,
  650. anim::easeInCubic);
  651. }
  652. if (!yFinished) {
  653. _animationValueHeightMin.update(
  654. _dtHeight.current.min,
  655. anim::easeInCubic);
  656. _animationValueHeightMax.update(
  657. _dtHeight.current.max,
  658. anim::easeInCubic);
  659. rulersView.computeRelative(
  660. _animationValueHeightMax.current(),
  661. _animationValueHeightMin.current());
  662. }
  663. if (!footerMinFinished) {
  664. _animationValueFooterHeightMin.update(
  665. _dtHeight.current.min,
  666. anim::easeInCubic);
  667. }
  668. if (!footerMaxFinished) {
  669. _animationValueFooterHeightMax.update(
  670. _dtHeight.current.max,
  671. anim::easeInCubic);
  672. }
  673. if (!alphaFinished) {
  674. _animationValueHeightAlpha.update(
  675. _dtHeight.currentAlpha,
  676. anim::easeInCubic);
  677. rulersView.setAlpha(_animationValueHeightAlpha.current());
  678. }
  679. if (!bottomLineAlphaFinished) {
  680. const auto value = _animValueBottomLineAlpha.current();
  681. for (auto &date : dateLines) {
  682. date.alpha = (1. - value) * date.fixedAlpha;
  683. }
  684. dateLines.back().alpha = value;
  685. } else {
  686. if (dateLines.size() > 1) {
  687. const auto data = dateLines.back();
  688. dateLines.clear();
  689. dateLines.push_back(data);
  690. }
  691. }
  692. }
  693. Limits ChartWidget::ChartAnimationController::currentXLimits() const {
  694. return { _animationValueXMin.current(), _animationValueXMax.current() };
  695. }
  696. Limits ChartWidget::ChartAnimationController::currentXIndices() const {
  697. return _currentXIndices;
  698. }
  699. Limits ChartWidget::ChartAnimationController::finalXLimits() const {
  700. return { _animationValueXMin.to(), _animationValueXMax.to() };
  701. }
  702. Limits ChartWidget::ChartAnimationController::currentHeightLimits() const {
  703. return {
  704. _animationValueHeightMin.current(),
  705. _animationValueHeightMax.current(),
  706. };
  707. }
  708. auto ChartWidget::ChartAnimationController::currentFooterHeightLimits() const
  709. -> Limits {
  710. return {
  711. _animationValueFooterHeightMin.current(),
  712. _animationValueFooterHeightMax.current(),
  713. };
  714. }
  715. Limits ChartWidget::ChartAnimationController::finalHeightLimits() const {
  716. return _finalHeightLimits;
  717. }
  718. bool ChartWidget::ChartAnimationController::animating() const {
  719. return _animation.animating();
  720. }
  721. bool ChartWidget::ChartAnimationController::footerAnimating() const {
  722. return (_animationValueFooterHeightMin.current()
  723. != _animationValueFooterHeightMin.to())
  724. || (_animationValueFooterHeightMax.current()
  725. != _animationValueFooterHeightMax.to());
  726. }
  727. ChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent)
  728. : Ui::RpWidget(parent)
  729. , _chartArea(base::make_unique_q<RpMouseWidget>(this))
  730. , _header(std::make_unique<Header>(this))
  731. , _footer(std::make_unique<Footer>(this))
  732. , _linesFilterController(std::make_shared<LinesFilterController>())
  733. , _animationController([=] {
  734. _chartArea->update();
  735. if (_animationController.footerAnimating()
  736. || !_linesFilterController->isFinished()) {
  737. _footer->update();
  738. }
  739. }) {
  740. style::PaletteChanged(
  741. ) | rpl::start_with_next([=] {
  742. if (_chartData) {
  743. FillLineColorsByKey(_chartData);
  744. }
  745. }, lifetime());
  746. setupChartArea();
  747. // We have to create the footer,
  748. // even if it has to be hidden, otherwise it breaks some things.
  749. setupFooter();
  750. }
  751. int ChartWidget::resizeGetHeight(int newWidth) {
  752. if (newWidth <= 0) {
  753. return 0;
  754. }
  755. if (_filterButtons) {
  756. _filterButtons->resizeToWidth(newWidth);
  757. }
  758. const auto filtersTopSkip = st::statisticsFilterButtonsPadding.top();
  759. const auto filtersHeight = _filterButtons
  760. ? (_filterButtons->height()
  761. + st::statisticsFilterButtonsPadding.bottom())
  762. : 0;
  763. const auto &headerPadding = st::statisticsChartHeaderPadding;
  764. {
  765. _header->moveToLeft(headerPadding.left(), headerPadding.top());
  766. _header->resizeToWidth(newWidth - rect::m::sum::h(headerPadding));
  767. }
  768. const auto headerArea = rect::m::sum::v(headerPadding)
  769. + _header->height();
  770. const auto footerArea = (!_footer->isHidden())
  771. ? (st::statisticsChartFooterHeight + st::statisticsChartFooterSkip)
  772. : 0;
  773. const auto resultHeight = headerArea
  774. + st::statisticsChartHeight
  775. + footerArea
  776. + filtersTopSkip
  777. + filtersHeight;
  778. {
  779. if (footerArea) {
  780. _footer->resizeToWidth(newWidth);
  781. _footer->moveToLeft(
  782. 0,
  783. resultHeight
  784. - st::statisticsChartFooterHeight
  785. - filtersTopSkip
  786. - filtersHeight);
  787. }
  788. if (_filterButtons) {
  789. _filterButtons->moveToLeft(0, resultHeight - filtersHeight);
  790. }
  791. _chartArea->setGeometry(
  792. 0,
  793. headerArea,
  794. newWidth,
  795. resultHeight
  796. - headerArea
  797. - footerArea
  798. - filtersTopSkip
  799. - filtersHeight);
  800. {
  801. updateChartFullWidth(newWidth);
  802. updateBottomDates();
  803. }
  804. }
  805. return resultHeight;
  806. }
  807. void ChartWidget::updateChartFullWidth(int w) {
  808. const auto finalXLimits = _animationController.finalXLimits();
  809. _bottomLine.chartFullWidth = (finalXLimits.max == finalXLimits.min)
  810. ? 0
  811. : (w / (finalXLimits.max - finalXLimits.min));
  812. }
  813. QRect ChartWidget::chartAreaRect() const {
  814. return _chartArea->rect()
  815. - QMargins(
  816. st::lineWidth,
  817. st::boxTextFont->height,
  818. st::lineWidth,
  819. st::lineWidth
  820. + st::statisticsChartBottomCaptionHeight
  821. + st::statisticsChartBottomCaptionSkip);
  822. }
  823. void ChartWidget::setupChartArea() {
  824. _chartArea->paintRequest(
  825. ) | rpl::start_with_next([=](const QRect &r) {
  826. auto p = QPainter(_chartArea.get());
  827. const auto now = crl::now();
  828. _animationController.tick(
  829. now,
  830. _rulersView,
  831. _bottomLine.dates,
  832. _chartView,
  833. _linesFilterController);
  834. const auto chartRect = chartAreaRect();
  835. p.fillRect(r, st::boxBg);
  836. if (!_chartData) {
  837. return;
  838. }
  839. if (!_areRulersAbove) {
  840. _rulersView.paintRulers(p, chartRect);
  841. }
  842. const auto context = PaintContext{
  843. _chartData,
  844. _animationController.currentXIndices(),
  845. _animationController.currentXLimits(),
  846. _animationController.currentHeightLimits(),
  847. chartRect,
  848. false,
  849. };
  850. {
  851. PainterHighQualityEnabler hp(p);
  852. _chartView->paint(p, context);
  853. }
  854. if (!_areRulersAbove) {
  855. _rulersView.paintCaptionsToRulers(p, chartRect);
  856. }
  857. {
  858. [[maybe_unused]] const auto o = ScopedPainterOpacity(
  859. p,
  860. p.opacity() * kRulerLineAlpha);
  861. const auto bottom = rect()
  862. - QMargins{ 0, rect::bottom(chartRect), 0, 0 };
  863. p.fillRect(bottom, st::boxBg);
  864. p.fillRect(
  865. QRect(bottom.x(), bottom.y(), bottom.width(), st::lineWidth),
  866. st::boxTextFg);
  867. }
  868. if (_details.widget) {
  869. const auto detailsAlpha = _details.widget->alpha();
  870. for (const auto &line : _chartData.lines) {
  871. _details.widget->setLineAlpha(
  872. line.id,
  873. _linesFilterController->alpha(line.id));
  874. }
  875. _chartView->paintSelectedXIndex(
  876. p,
  877. context,
  878. _details.widget->xIndex(),
  879. detailsAlpha);
  880. }
  881. if (_areRulersAbove) {
  882. _rulersView.paintRulers(p, chartRect);
  883. _rulersView.paintCaptionsToRulers(p, chartRect);
  884. }
  885. p.setPen(st::windowSubTextFg);
  886. PaintBottomLine(
  887. p,
  888. _bottomLine.dates,
  889. _chartData,
  890. _animationController.finalXLimits(),
  891. _bottomLine.chartFullWidth,
  892. _chartArea->width(),
  893. rect::bottom(chartRect) + st::statisticsChartBottomCaptionSkip,
  894. _bottomLine.captionIndicesOffset);
  895. }, _footer->lifetime());
  896. }
  897. void ChartWidget::updateBottomDates() {
  898. if (!_chartData || !_bottomLine.chartFullWidth) {
  899. return;
  900. }
  901. const auto d = _bottomLine.chartFullWidth * _chartData.oneDayPercentage;
  902. const auto k = _chartArea->width() / d;
  903. const auto stepRaw = int(k / 6);
  904. const auto by = int(_chartArea->width() / float64(_chartData.x.size()));
  905. _bottomLine.captionIndicesOffset = 0
  906. + _chartData.dayStringMaxWidth / std::max(by, 1);
  907. const auto isCurrentNull = (_bottomLine.current.stepMinFast == 0);
  908. if (!isCurrentNull
  909. && (stepRaw < _bottomLine.current.stepMax)
  910. && (stepRaw > _bottomLine.current.stepMin)) {
  911. return;
  912. }
  913. const auto highestOneBit = [](unsigned int v) {
  914. if (!v) {
  915. return 0;
  916. }
  917. auto r = unsigned(1);
  918. while (v >>= 1) {
  919. r *= 2;
  920. }
  921. return int(r);
  922. };
  923. const auto step = highestOneBit(stepRaw) << 1;
  924. if (!isCurrentNull && (_bottomLine.current.step == step)) {
  925. return;
  926. }
  927. constexpr auto kStepRatio = 0.1;
  928. constexpr auto kFastStepOffset = 4;
  929. const auto stepMax = int(step + step * kStepRatio);
  930. const auto stepMin = int(step - step * kStepRatio);
  931. const auto stepMinFast = stepMin - kFastStepOffset;
  932. auto data = BottomCaptionLineData{
  933. .step = step,
  934. .stepMax = stepMax,
  935. .stepMin = stepMin,
  936. .stepMinFast = stepMinFast,
  937. .stepRaw = stepRaw,
  938. .alpha = 1.,
  939. };
  940. if (isCurrentNull) {
  941. _bottomLine.current = data;
  942. _bottomLine.dates.push_back(data);
  943. return;
  944. }
  945. _bottomLine.current = data;
  946. for (auto &date : _bottomLine.dates) {
  947. date.fixedAlpha = date.alpha;
  948. }
  949. _bottomLine.dates.push_back(data);
  950. if (_bottomLine.dates.size() > 2) {
  951. _bottomLine.dates.erase(begin(_bottomLine.dates));
  952. }
  953. _animationController.restartBottomLineAlpha();
  954. }
  955. void ChartWidget::updateHeader() {
  956. if (!_chartData) {
  957. return;
  958. }
  959. const auto i = _animationController.currentXIndices();
  960. _header->setSubTitle(HeaderSubTitle(_chartData, i.min, i.max));
  961. _header->update();
  962. }
  963. void ChartWidget::setupFooter() {
  964. _footer->setPaintChartCallback([=, fullXLimits = Limits{ 0., 1. }](
  965. QPainter &p,
  966. const QRect &r) {
  967. if (_chartData) {
  968. p.fillRect(r, st::boxBg);
  969. auto hp = PainterHighQualityEnabler(p);
  970. _chartView->paint(
  971. p,
  972. PaintContext{
  973. _chartData,
  974. { 0., float64(_chartData.x.size() - 1) },
  975. fullXLimits,
  976. _animationController.currentFooterHeightLimits(),
  977. r,
  978. true,
  979. });
  980. }
  981. });
  982. _animationController.addRulerRequests(
  983. ) | rpl::start_with_next([=] {
  984. _rulersView.add(
  985. _animationController.finalHeightLimits(),
  986. true);
  987. _animationController.start();
  988. }, _footer->lifetime());
  989. _footer->xPercentageLimitsChange(
  990. ) | rpl::start_with_next([=](Limits xPercentageLimits) {
  991. if (!_chartView) {
  992. return;
  993. }
  994. const auto now = crl::now();
  995. if (_details.widget
  996. && (_details.widget->xIndex() >= 0)
  997. && !_details.animation.animating()) {
  998. _details.hideOnAnimationEnd = true;
  999. _details.animation.start();
  1000. }
  1001. _animationController.setXPercentageLimits(
  1002. _chartData,
  1003. xPercentageLimits,
  1004. _chartView,
  1005. _linesFilterController,
  1006. now);
  1007. updateChartFullWidth(_chartArea->width());
  1008. updateBottomDates();
  1009. updateHeader();
  1010. if ((now - _lastHeightLimitsChanged) < kHeightLimitsUpdateTimeout) {
  1011. return;
  1012. }
  1013. _lastHeightLimitsChanged = now;
  1014. _rulersView.add(
  1015. _animationController.finalHeightLimits(),
  1016. true);
  1017. }, _footer->lifetime());
  1018. }
  1019. void ChartWidget::setupDetails() {
  1020. if (!_chartData) {
  1021. _details.widget = nullptr;
  1022. _chartArea->update();
  1023. return;
  1024. }
  1025. if (hasLocalZoom()) {
  1026. _zoomEnabled = true;
  1027. }
  1028. _details.widget = base::make_unique_q<PointDetailsWidget>(
  1029. this,
  1030. _chartData,
  1031. _zoomEnabled);
  1032. _details.widget->setClickedCallback([=] {
  1033. const auto index = _details.widget->xIndex();
  1034. if (index < 0) {
  1035. return;
  1036. }
  1037. if (hasLocalZoom()) {
  1038. processLocalZoom(index);
  1039. } else {
  1040. _zoomRequests.fire_copy(_chartData.x[index]);
  1041. }
  1042. });
  1043. _details.widget->shownValue(
  1044. ) | rpl::start_with_next([=](bool shown) {
  1045. if (shown && _details.widget->xIndex() < 0) {
  1046. _details.widget->hide();
  1047. }
  1048. }, _details.widget->lifetime());
  1049. _chartArea->mouseStateChanged(
  1050. ) | rpl::start_with_next([=](const RpMouseWidget::State &state) {
  1051. if (_animationController.animating()) {
  1052. return;
  1053. }
  1054. switch (state.mouseState) {
  1055. case QEvent::MouseButtonPress:
  1056. case QEvent::MouseMove: {
  1057. const auto wasXIndex = _details.widget->xIndex();
  1058. const auto chartRect = chartAreaRect();
  1059. const auto currentXLimits = _animationController.finalXLimits();
  1060. const auto nearestXIndex = _chartView->findXIndexByPosition(
  1061. _chartData,
  1062. currentXLimits,
  1063. chartRect,
  1064. state.point.x());
  1065. if (nearestXIndex < 0) {
  1066. _details.widget->setXIndex(nearestXIndex);
  1067. _details.widget->hide();
  1068. _chartArea->update();
  1069. return;
  1070. }
  1071. const auto currentX = 0
  1072. + chartRect.width() * InterpolationRatio(
  1073. currentXLimits.min,
  1074. currentXLimits.max,
  1075. _chartData.xPercentage[nearestXIndex]);
  1076. const auto widgetArea = _details.widget->width()
  1077. + st::statisticsDetailsPopupPadding.left();
  1078. const auto xLeft = currentX - widgetArea;
  1079. const auto x = (xLeft >= 0)
  1080. ? xLeft
  1081. : ((currentX + widgetArea - _chartArea->width()) > 0)
  1082. ? 0
  1083. : currentX;
  1084. _details.widget->moveToLeft(
  1085. std::clamp(
  1086. int(x),
  1087. _chartArea->x(),
  1088. rect::right(_chartArea) - widgetArea),
  1089. _chartArea->y());
  1090. _details.widget->setXIndex(nearestXIndex);
  1091. if (_details.widget->isHidden()) {
  1092. _details.hideOnAnimationEnd = false;
  1093. _details.animation.start();
  1094. } else if ((state.mouseState == QEvent::MouseButtonPress)
  1095. && (wasXIndex == nearestXIndex)) {
  1096. _details.hideOnAnimationEnd = true;
  1097. _details.animation.start();
  1098. }
  1099. _details.widget->show();
  1100. _chartArea->update();
  1101. } break;
  1102. case QEvent::MouseButtonRelease: {
  1103. } break;
  1104. }
  1105. }, _details.widget->lifetime());
  1106. _details.animation.init([=](crl::time now) {
  1107. const auto value = std::clamp(
  1108. (now - _details.animation.started()) / float64(200),
  1109. 0.,
  1110. 1.);
  1111. const auto alpha = _details.hideOnAnimationEnd ? (1. - value) : value;
  1112. if (_details.widget) {
  1113. _details.widget->setAlpha(alpha);
  1114. _details.widget->update();
  1115. }
  1116. if (value >= 1.) {
  1117. if (_details.hideOnAnimationEnd && _details.widget) {
  1118. _details.widget->hide();
  1119. _details.widget->setXIndex(-1);
  1120. }
  1121. _details.animation.stop();
  1122. }
  1123. _chartArea->update();
  1124. });
  1125. }
  1126. bool ChartWidget::hasLocalZoom() const {
  1127. return _chartData
  1128. && _chartView->maybeLocalZoom({
  1129. _chartData,
  1130. AbstractChartView::LocalZoomArgs::Type::CheckAvailability,
  1131. }).hasZoom;
  1132. }
  1133. void ChartWidget::processLocalZoom(int xIndex) {
  1134. using Type = AbstractChartView::LocalZoomArgs::Type;
  1135. constexpr auto kFooterZoomDuration = crl::time(400);
  1136. if (_footer->isHidden()) {
  1137. return;
  1138. }
  1139. const auto wasZoom = _footer->xPercentageLimits();
  1140. const auto header = Ui::CreateChild<Header>(this);
  1141. header->show();
  1142. _header->geometryValue(
  1143. ) | rpl::start_with_next([=](const QRect &g) {
  1144. header->setGeometry(g);
  1145. }, header->lifetime());
  1146. header->setTitle(_header->title());
  1147. header->setSubTitle(HeaderSubTitle(_chartData, xIndex, xIndex));
  1148. const auto enableMouse = [=](bool value) {
  1149. setAttribute(Qt::WA_TransparentForMouseEvents, !value);
  1150. };
  1151. const auto mouseTrackingLifetime = std::make_shared<rpl::lifetime>();
  1152. _chartView->setUpdateCallback([=] { _chartArea->update(); });
  1153. const auto createMouseTracking = [=] {
  1154. _chartArea->setMouseTracking(true);
  1155. *mouseTrackingLifetime = _chartArea->events(
  1156. ) | rpl::filter([](not_null<QEvent*> event) {
  1157. return (event->type() == QEvent::MouseMove)
  1158. || (event->type() == QEvent::Leave);
  1159. }) | rpl::start_with_next([=](not_null<QEvent*> event) {
  1160. auto pos = QPoint();
  1161. if (event->type() == QEvent::MouseMove) {
  1162. const auto e = static_cast<QMouseEvent*>(event.get());
  1163. pos = e->pos();
  1164. }
  1165. _chartView->handleMouseMove(_chartData, _chartArea->rect(), pos);
  1166. });
  1167. mouseTrackingLifetime->add(crl::guard(_chartArea.get(), [=] {
  1168. _chartArea->setMouseTracking(false);
  1169. }));
  1170. };
  1171. const auto zoomOutButton = Ui::CreateChild<Ui::RoundButton>(
  1172. header,
  1173. tr::lng_stats_zoom_out(),
  1174. st::statisticsHeaderButton);
  1175. zoomOutButton->moveToRight(
  1176. 0,
  1177. (header->height() - zoomOutButton->height()) / 2);
  1178. zoomOutButton->show();
  1179. zoomOutButton->setTextTransform(
  1180. Ui::RoundButton::TextTransform::NoTransform);
  1181. zoomOutButton->setClickedCallback([=] {
  1182. auto lifetime = std::make_shared<rpl::lifetime>();
  1183. const auto animation = lifetime->make_state<Ui::Animations::Simple>();
  1184. const auto currentXPercentage = _footer->xPercentageLimits();
  1185. animation->start([=](float64 value) {
  1186. _chartView->maybeLocalZoom({
  1187. _chartData,
  1188. Type::SkipCalculation,
  1189. value,
  1190. });
  1191. const auto p = value;
  1192. _footer->setXPercentageLimits({
  1193. anim::interpolateF(wasZoom.min, currentXPercentage.min, p),
  1194. anim::interpolateF(wasZoom.max, currentXPercentage.max, p),
  1195. });
  1196. if (value == 0.) {
  1197. if (lifetime) {
  1198. lifetime->destroy();
  1199. }
  1200. mouseTrackingLifetime->destroy();
  1201. enableMouse(true);
  1202. }
  1203. }, 1., 0., kFooterZoomDuration, anim::easeOutCirc);
  1204. enableMouse(false);
  1205. Ui::Animations::HideWidgets({ header });
  1206. });
  1207. Ui::Animations::ShowWidgets({ header });
  1208. const auto finish = [=](const Limits &zoomLimitIndices) {
  1209. createMouseTracking();
  1210. _footer->xPercentageLimitsChange(
  1211. ) | rpl::start_with_next([=](const Limits &l) {
  1212. const auto r = FindStackXIndicesFromRawXPercentages(
  1213. _chartData,
  1214. l,
  1215. zoomLimitIndices);
  1216. header->setSubTitle(HeaderSubTitle(_chartData, r.min, r.max));
  1217. header->update();
  1218. }, header->lifetime());
  1219. };
  1220. {
  1221. auto lifetime = std::make_shared<rpl::lifetime>();
  1222. const auto animation = lifetime->make_state<Ui::Animations::Simple>();
  1223. _chartView->maybeLocalZoom({ _chartData, Type::Prepare });
  1224. animation->start([=](float64 value) {
  1225. const auto zoom = _chartView->maybeLocalZoom({
  1226. _chartData,
  1227. Type::Process,
  1228. value,
  1229. xIndex,
  1230. });
  1231. const auto result = Limits{
  1232. anim::interpolateF(wasZoom.min, zoom.range.min, value),
  1233. anim::interpolateF(wasZoom.max, zoom.range.max, value),
  1234. };
  1235. _footer->setXPercentageLimits(result);
  1236. if (value == 1.) {
  1237. if (lifetime) {
  1238. lifetime->destroy();
  1239. }
  1240. finish(zoom.limitIndices);
  1241. enableMouse(true);
  1242. }
  1243. }, 0., 1., kFooterZoomDuration, anim::easeOutCirc);
  1244. enableMouse(false);
  1245. }
  1246. }
  1247. void ChartWidget::setupFilterButtons() {
  1248. if (!_chartData || (_chartData.lines.size() <= 1)) {
  1249. _filterButtons = nullptr;
  1250. _chartArea->update();
  1251. return;
  1252. }
  1253. _filterButtons = base::make_unique_q<ChartLinesFilterWidget>(this);
  1254. _filterButtons->show();
  1255. {
  1256. auto data = std::vector<ChartLinesFilterWidget::ButtonData>();
  1257. data.reserve(_chartData.lines.size());
  1258. for (const auto &line : _chartData.lines) {
  1259. data.push_back({
  1260. line.name,
  1261. line.color,
  1262. line.id,
  1263. line.isHiddenOnStart,
  1264. });
  1265. if (line.isHiddenOnStart) {
  1266. _linesFilterController->setEnabled(line.id, false, 1);
  1267. }
  1268. }
  1269. _filterButtons->fillButtons(data);
  1270. _linesFilterController->tick(1.);
  1271. }
  1272. _filterButtons->buttonEnabledChanges(
  1273. ) | rpl::start_with_next([=](const ChartLinesFilterWidget::Entry &e) {
  1274. const auto now = crl::now();
  1275. _linesFilterController->setEnabled(e.id, e.enabled, now);
  1276. _animationController.setXPercentageLimits(
  1277. _chartData,
  1278. _animationController.currentXLimits(),
  1279. _chartView,
  1280. _linesFilterController,
  1281. now);
  1282. }, _filterButtons->lifetime());
  1283. }
  1284. void ChartWidget::setChartData(
  1285. Data::StatisticalChart chartData,
  1286. ChartViewType type) {
  1287. if (width() < st::statisticsChartHeight) {
  1288. sizeValue(
  1289. ) | rpl::start_with_next([=](const QSize &s) {
  1290. if (s.width() > st::statisticsChartHeight) {
  1291. setChartData(chartData, type);
  1292. _waitingSizeLifetime.destroy();
  1293. }
  1294. }, _waitingSizeLifetime);
  1295. return;
  1296. }
  1297. if (_chartData) {
  1298. // We don't really support a replacement of chart data in runtime.
  1299. return;
  1300. }
  1301. _chartData = std::move(chartData);
  1302. FillLineColorsByKey(_chartData);
  1303. _chartView = CreateChartView(type);
  1304. _chartView->setLinesFilterController(_linesFilterController);
  1305. _rulersView.setChartData(_chartData, type, _linesFilterController);
  1306. _areRulersAbove = (type == ChartViewType::StackBar);
  1307. if (_chartData.isFooterHidden) {
  1308. _footer->hide();
  1309. }
  1310. setupDetails();
  1311. setupFilterButtons();
  1312. const auto defaultZoom = Limits{
  1313. _chartData.xPercentage[_chartData.defaultZoomXIndex.min],
  1314. _chartData.xPercentage[_chartData.defaultZoomXIndex.max],
  1315. };
  1316. _footer->setXPercentageLimits(defaultZoom);
  1317. _animationController.setXPercentageLimits(
  1318. _chartData,
  1319. defaultZoom,
  1320. _chartView,
  1321. _linesFilterController,
  1322. 0);
  1323. updateHeader();
  1324. _animationController.finish();
  1325. _rulersView.add(_animationController.finalHeightLimits(), false);
  1326. _chartArea->update();
  1327. _footer->update();
  1328. RpWidget::resizeToWidth(width());
  1329. }
  1330. void ChartWidget::setTitle(rpl::producer<QString> &&title) {
  1331. std::move(
  1332. title
  1333. ) | rpl::start_with_next([=](QString t) {
  1334. _header->setTitle(std::move(t));
  1335. _header->update();
  1336. }, _header->lifetime());
  1337. }
  1338. void ChartWidget::setZoomedChartData(
  1339. Data::StatisticalChart chartData,
  1340. float64 x,
  1341. ChartViewType type) {
  1342. _zoomedChartWidget = base::make_unique_q<ChartWidget>(
  1343. dynamic_cast<Ui::RpWidget*>(parentWidget()));
  1344. geometryValue(
  1345. ) | rpl::start_with_next([=](const QRect &geometry) {
  1346. _zoomedChartWidget->moveToLeft(geometry.x(), geometry.y());
  1347. }, _zoomedChartWidget->lifetime());
  1348. _zoomedChartWidget->show();
  1349. _zoomedChartWidget->resizeToWidth(width());
  1350. _zoomedChartWidget->setChartData(std::move(chartData), type);
  1351. const auto customHeader = Ui::CreateChild<Header>(
  1352. _zoomedChartWidget.get());
  1353. {
  1354. const auto xIndex = std::distance(
  1355. begin(_chartData.x),
  1356. ranges::find(_chartData.x, x));
  1357. customHeader->setTitle(_header->title());
  1358. if ((xIndex >= 0) && (xIndex < _chartData.x.size())) {
  1359. customHeader->setSubTitle(
  1360. HeaderSubTitle(_chartData, xIndex, xIndex));
  1361. }
  1362. const auto &headerPadding = st::statisticsChartHeaderPadding;
  1363. customHeader->moveToLeft(headerPadding.left(), headerPadding.top());
  1364. customHeader->resizeToWidth(width() - rect::m::sum::h(headerPadding));
  1365. }
  1366. const auto zoomOutButton = Ui::CreateChild<Ui::RoundButton>(
  1367. customHeader,
  1368. tr::lng_stats_zoom_out(),
  1369. st::statisticsHeaderButton);
  1370. zoomOutButton->setTextTransform(
  1371. Ui::RoundButton::TextTransform::NoTransform);
  1372. zoomOutButton->moveToRight(
  1373. 0,
  1374. (customHeader->height() - zoomOutButton->height()) / 2);
  1375. zoomOutButton->setClickedCallback([=] {
  1376. shownValue(
  1377. ) | rpl::start_with_next([=](bool shown) {
  1378. if (shown) {
  1379. _zoomedChartWidget = nullptr;
  1380. }
  1381. }, _zoomedChartWidget->lifetime());
  1382. Ui::Animations::ShowWidgets({ this });
  1383. Ui::Animations::HideWidgets({ _zoomedChartWidget.get() });
  1384. });
  1385. Ui::Animations::ShowWidgets({ _zoomedChartWidget.get(), customHeader });
  1386. Ui::Animations::HideWidgets({ this });
  1387. }
  1388. rpl::producer<float64> ChartWidget::zoomRequests() {
  1389. _zoomEnabled = true;
  1390. setupDetails();
  1391. return _zoomRequests.events();
  1392. }
  1393. } // namespace Statistic