Added point details widget to chart widget.

This commit is contained in:
23rd 2023-07-07 11:28:50 +03:00 committed by John Preston
parent 70713d5f62
commit 74aae29b64
8 changed files with 151 additions and 12 deletions

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qt/qt_key_modifiers.h" #include "base/qt/qt_key_modifiers.h"
#include "statistics/linear_chart_view.h" #include "statistics/linear_chart_view.h"
#include "statistics/point_details_widget.h"
#include "ui/abstract_button.h" #include "ui/abstract_button.h"
#include "ui/effects/animation_value_f.h" #include "ui/effects/animation_value_f.h"
#include "ui/rect.h" #include "ui/rect.h"
@ -20,6 +21,10 @@ namespace {
constexpr auto kHeightLimitsUpdateTimeout = crl::time(320); constexpr auto kHeightLimitsUpdateTimeout = crl::time(320);
inline float64 InterpolationRatio(float64 from, float64 to, float64 result) {
return (result - from) / (to - from);
};
[[nodiscard]] int FindMaxValue( [[nodiscard]] int FindMaxValue(
Data::StatisticalChart &chartData, Data::StatisticalChart &chartData,
int startXIndex, int startXIndex,
@ -424,6 +429,10 @@ Limits ChartWidget::ChartAnimationController::currentXLimits() const {
return { _animationValueXMin.current(), _animationValueXMax.current() }; return { _animationValueXMin.current(), _animationValueXMax.current() };
} }
Limits ChartWidget::ChartAnimationController::finalXLimits() const {
return { _animationValueXMin.to(), _animationValueXMax.to() };
}
Limits ChartWidget::ChartAnimationController::currentHeightLimits() const { Limits ChartWidget::ChartAnimationController::currentHeightLimits() const {
return { return {
_animationValueHeightMin.current(), _animationValueHeightMin.current(),
@ -442,7 +451,7 @@ auto ChartWidget::ChartAnimationController::heightAnimationStarts() const
ChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent) ChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent)
: Ui::RpWidget(parent) : Ui::RpWidget(parent)
, _chartArea(base::make_unique_q<Ui::RpWidget>(this)) , _chartArea(base::make_unique_q<RpMouseWidget>(this))
, _footer(std::make_unique<Footer>(this)) , _footer(std::make_unique<Footer>(this))
, _animationController([=] { _chartArea->update(); }) { , _animationController([=] { _chartArea->update(); }) {
sizeValue( sizeValue(
@ -457,16 +466,31 @@ ChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent)
0, 0,
s.width(), s.width(),
s.height() - st::countryRowHeight * 2); s.height() - st::countryRowHeight * 2);
}, _footer->lifetime()); }, lifetime());
setupChartArea();
setupFooter();
resize(width(), st::confirmMaxHeight + st::countryRowHeight * 2);
}
QRect ChartWidget::chartAreaRect() const {
return _chartArea->rect()
- QMargins(
st::lineWidth,
st::boxTextFont->height,
st::lineWidth,
st::lineWidth);
}
void ChartWidget::setupChartArea() {
_chartArea->paintRequest( _chartArea->paintRequest(
) | rpl::start_with_next([=](const QRect &r) { ) | rpl::start_with_next([=](const QRect &r) {
auto p = QPainter(_chartArea.get()); auto p = QPainter(_chartArea.get());
_animationController.tick(crl::now(), _horizontalLines); _animationController.tick(crl::now(), _horizontalLines);
const auto chartRect = _chartArea->rect() const auto chartRect = chartAreaRect();
- QMargins{ 0, st::boxTextFont->height, 0, st::lineWidth };
p.fillRect(r, st::boxBg); p.fillRect(r, st::boxBg);
@ -474,20 +498,33 @@ ChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent)
PaintHorizontalLines(p, horizontalLine, chartRect); PaintHorizontalLines(p, horizontalLine, chartRect);
} }
if (_details.currentX) {
const auto lineRect = QRectF(
_details.currentX - (st::lineWidth / 2.),
0,
st::lineWidth,
_chartArea->height());
p.setOpacity(1.);
p.fillRect(lineRect, st::windowSubTextFg);
}
if (_chartData) { if (_chartData) {
Statistic::PaintLinearChartView( Statistic::PaintLinearChartView(
p, p,
_chartData, _chartData,
_animationController.currentXLimits(), _animationController.currentXLimits(),
_animationController.currentHeightLimits(), _animationController.currentHeightLimits(),
chartRect); chartRect,
{ _details.widget ? _details.widget->xIndex() : -1, 1. });
} }
for (auto &horizontalLine : _horizontalLines) { for (auto &horizontalLine : _horizontalLines) {
PaintCaptionsToHorizontalLines(p, horizontalLine, chartRect); PaintCaptionsToHorizontalLines(p, horizontalLine, chartRect);
} }
}, _footer->lifetime()); }, _footer->lifetime());
}
void ChartWidget::setupFooter() {
_footer->paintRequest( _footer->paintRequest(
) | rpl::start_with_next([=, fullXLimits = Limits{ 0., 1. }] { ) | rpl::start_with_next([=, fullXLimits = Limits{ 0., 1. }] {
auto p = QPainter(_footer.get()); auto p = QPainter(_footer.get());
@ -499,7 +536,8 @@ ChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent)
_chartData, _chartData,
fullXLimits, fullXLimits,
_footer->fullHeightLimits(), _footer->fullHeightLimits(),
_footer->rect()); _footer->rect(),
{});
} }
}, _footer->lifetime()); }, _footer->lifetime());
@ -526,12 +564,65 @@ ChartWidget::ChartWidget(not_null<Ui::RpWidget*> parent)
_animationController.resetAlpha(); _animationController.resetAlpha();
addHorizontalLine(_animationController.finalHeightLimits(), true); addHorizontalLine(_animationController.finalHeightLimits(), true);
}, _footer->lifetime()); }, _footer->lifetime());
resize(width(), st::confirmMaxHeight + st::countryRowHeight * 2); }
void ChartWidget::setupDetails() {
if (!_chartData) {
_details = {};
_chartArea->update();
return;
}
_details.widget = base::make_unique_q<PointDetailsWidget>(
this,
_chartData);
_chartArea->mouseStateChanged(
) | rpl::start_with_next([=](const RpMouseWidget::State &state) {
switch (state.mouseState) {
case QEvent::MouseButtonPress:
case QEvent::MouseMove: {
const auto chartRect = chartAreaRect();
const auto pointerRatio = std::clamp(
state.point.x() / float64(chartRect.width()),
0.,
1.);
const auto currentXLimits = _animationController.finalXLimits();
const auto rawXPercentage = anim::interpolateF(
currentXLimits.min,
currentXLimits.max,
pointerRatio);
const auto nearestXPercentageIt = ranges::lower_bound(
_chartData.xPercentage,
rawXPercentage);
const auto nearestXIndex = std::distance(
begin(_chartData.xPercentage),
nearestXPercentageIt);
_details.currentX = 0
+ chartRect.width() * InterpolationRatio(
currentXLimits.min,
currentXLimits.max,
*nearestXPercentageIt);
const auto xLeft = _details.currentX
- _details.widget->width();
const auto x = (xLeft < 0)
? (_details.currentX)
: xLeft;
_details.widget->moveToLeft(x, _chartArea->y());
_details.widget->setXIndex(nearestXIndex);
_details.widget->show();
_chartArea->update();
} break;
case QEvent::MouseButtonRelease: {
} break;
}
}, _details.widget->lifetime());
} }
void ChartWidget::setChartData(Data::StatisticalChart chartData) { void ChartWidget::setChartData(Data::StatisticalChart chartData) {
_chartData = std::move(chartData); _chartData = std::move(chartData);
setupDetails();
_footer->setFullHeightLimits(FindHeightLimitsBetweenXLimits( _footer->setFullHeightLimits(FindHeightLimitsBetweenXLimits(
_chartData, _chartData,
{ _chartData.xPercentage.front(), _chartData.xPercentage.back() })); { _chartData.xPercentage.front(), _chartData.xPercentage.back() }));
@ -543,6 +634,7 @@ void ChartWidget::setChartData(Data::StatisticalChart chartData) {
_animationController.finish(); _animationController.finish();
addHorizontalLine(_animationController.finalHeightLimits(), false); addHorizontalLine(_animationController.finalHeightLimits(), false);
_chartArea->update(); _chartArea->update();
_footer->update();
} }
void ChartWidget::addHorizontalLine(Limits newHeight, bool animated) { void ChartWidget::addHorizontalLine(Limits newHeight, bool animated) {

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Statistic { namespace Statistic {
class RpMouseWidget; class RpMouseWidget;
class PointDetailsWidget;
class ChartWidget : public Ui::RpWidget { class ChartWidget : public Ui::RpWidget {
public: public:
@ -44,6 +45,7 @@ private:
std::vector<ChartHorizontalLinesData> &horizontalLines); std::vector<ChartHorizontalLinesData> &horizontalLines);
[[nodiscard]] Limits currentXLimits() const; [[nodiscard]] Limits currentXLimits() const;
[[nodiscard]] Limits finalXLimits() const;
[[nodiscard]] Limits currentHeightLimits() const; [[nodiscard]] Limits currentHeightLimits() const;
[[nodiscard]] Limits finalHeightLimits() const; [[nodiscard]] Limits finalHeightLimits() const;
@ -72,10 +74,21 @@ private:
}; };
const base::unique_qptr<Ui::RpWidget> _chartArea; [[nodiscard]] QRect chartAreaRect() const;
std::unique_ptr<Footer> _footer;
void setupChartArea();
void setupFooter();
void setupDetails();
const base::unique_qptr<RpMouseWidget> _chartArea;
const std::unique_ptr<Footer> _footer;
Data::StatisticalChart _chartData; Data::StatisticalChart _chartData;
struct {
base::unique_qptr<PointDetailsWidget> widget;
float64 currentX = 0;
} _details;
bool _useMinHeight = false; bool _useMinHeight = false;
ChartAnimationController _animationController; ChartAnimationController _animationController;

View file

@ -10,7 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_statistics.h" #include "data/data_statistics.h"
#include "statistics/statistics_common.h" #include "statistics/statistics_common.h"
#include "ui/effects/animation_value_f.h" #include "ui/effects/animation_value_f.h"
#include "ui/painter.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_statistics.h"
namespace Statistic { namespace Statistic {
@ -19,10 +21,13 @@ void PaintLinearChartView(
const Data::StatisticalChart &chartData, const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits, const Limits &xPercentageLimits,
const Limits &heightLimits, const Limits &heightLimits,
const QRect &rect) { const QRect &rect,
const DetailsPaintContext &detailsPaintContext) {
const auto currentMinHeight = rect.y(); // const auto currentMinHeight = rect.y(); //
const auto currentMaxHeight = rect.height() + rect.y(); // const auto currentMaxHeight = rect.height() + rect.y(); //
PainterHighQualityEnabler hq(p);
for (const auto &line : chartData.lines) { for (const auto &line : chartData.lines) {
const auto additionalP = (chartData.xPercentage.size() < 2) const auto additionalP = (chartData.xPercentage.size() < 2)
? 0. ? 0.
@ -31,6 +36,7 @@ void PaintLinearChartView(
auto first = true; auto first = true;
auto chartPath = QPainterPath(); auto chartPath = QPainterPath();
auto detailsDotPoint = QPointF();
const auto startXIndex = chartData.findStartIndex( const auto startXIndex = chartData.findStartIndex(
xPercentageLimits.min); xPercentageLimits.min);
@ -53,15 +59,25 @@ void PaintLinearChartView(
const auto yPercentage = (line.y[i] - heightLimits.min) const auto yPercentage = (line.y[i] - heightLimits.min)
/ float64(heightLimits.max - heightLimits.min); / float64(heightLimits.max - heightLimits.min);
const auto yPoint = rect.y() + (1. - yPercentage) * rect.height(); const auto yPoint = rect.y() + (1. - yPercentage) * rect.height();
if ((i == detailsPaintContext.xIndex)
&& detailsPaintContext.progress > 0.) {
detailsDotPoint = QPointF(xPoint, yPoint);
}
if (first) { if (first) {
first = false; first = false;
chartPath.moveTo(xPoint, yPoint); chartPath.moveTo(xPoint, yPoint);
} }
chartPath.lineTo(xPoint, yPoint); chartPath.lineTo(xPoint, yPoint);
} }
p.setPen(line.color); p.setPen(QPen(line.color, st::statisticsChartLineWidth));
p.setBrush(Qt::NoBrush); p.setBrush(Qt::NoBrush);
p.drawPath(chartPath); p.drawPath(chartPath);
if (!detailsDotPoint.isNull()) {
p.setBrush(st::boxBg);
const auto r = st::statisticsDetailsDotRadius;
p.drawEllipse(detailsDotPoint, r, r);
}
} }
p.setPen(st::boxTextFg); p.setPen(st::boxTextFg);
} }

View file

@ -14,12 +14,14 @@ struct StatisticalChart;
namespace Statistic { namespace Statistic {
struct Limits; struct Limits;
struct DetailsPaintContext;
void PaintLinearChartView( void PaintLinearChartView(
QPainter &p, QPainter &p,
const Data::StatisticalChart &chartData, const Data::StatisticalChart &chartData,
const Limits &xPercentageLimits, const Limits &xPercentageLimits,
const Limits &heightLimits, const Limits &heightLimits,
const QRect &rect); const QRect &rect,
const DetailsPaintContext &detailsPaintContext);
} // namespace Statistic } // namespace Statistic

View file

@ -37,7 +37,12 @@ PointDetailsWidget::PointDetailsWidget(
+ st::statisticsDetailsPopupMargins.bottom()); + st::statisticsDetailsPopupMargins.bottom());
} }
int PointDetailsWidget::xIndex() const {
return _xIndex;
}
void PointDetailsWidget::setXIndex(int xIndex) { void PointDetailsWidget::setXIndex(int xIndex) {
_xIndex = xIndex;
_header.setText(_headerStyle, _chartData.getDayString(xIndex)); _header.setText(_headerStyle, _chartData.getDayString(xIndex));
_lines.clear(); _lines.clear();

View file

@ -18,6 +18,7 @@ public:
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const Data::StatisticalChart &chartData); const Data::StatisticalChart &chartData);
[[nodiscard]] int xIndex() const;
void setXIndex(int xIndex); void setXIndex(int xIndex);
protected: protected:
@ -40,6 +41,8 @@ private:
QRect _innerRect; QRect _innerRect;
QRect _textRect; QRect _textRect;
int _xIndex = -1;
std::vector<Line> _lines; std::vector<Line> _lines;
}; };

View file

@ -14,6 +14,8 @@ statisticsDetailsPopupWidth: 135px;
statisticsDetailsPopupMargins: margins(8px, 8px, 8px, 8px); statisticsDetailsPopupMargins: margins(8px, 8px, 8px, 8px);
statisticsDetailsPopupPadding: margins(6px, 6px, 6px, 6px); statisticsDetailsPopupPadding: margins(6px, 6px, 6px, 6px);
statisticsDetailsPopupMidLineSpace: 8px; statisticsDetailsPopupMidLineSpace: 8px;
statisticsDetailsDotRadius: 4px;
statisticsChartLineWidth: 2px;
statisticsDetailsPopupStyle: TextStyle(defaultTextStyle) { statisticsDetailsPopupStyle: TextStyle(defaultTextStyle) {
font: font(11px); font: font(11px);

View file

@ -14,4 +14,10 @@ struct Limits final {
float64 max = 0; float64 max = 0;
}; };
// Dot on line charts.
struct DetailsPaintContext final {
int xIndex = -1;
float64 progress = 0.;
};
} // namespace Statistic } // namespace Statistic