Implement ChartLineMarker for following a QLineSeries curve
MODIFY
src/cachesim/cacheplotview.cpp
src/cachesim/cacheplotview.cpp
#include <QtGui/QResizeEvent>
#include <QtWidgets/QGraphicsScene>
#include <QtWidgets/QGraphicsTextItem>
+
#include "callout.h"
+#include "chartlinemarker.h"
void constrainInPlotRange(QPointF& pos, QChart* chart) {
const auto plotValue = chart->mapToValue(pos);
@@ -30,10 +32,30 @@ CachePlotView::CachePlotView(QWidget* parent) : QGraphicsView(new QGraphicsScene
setMouseTracking(true);
}
+void CachePlotView::enterEvent(QEvent* event) {
+ if (m_marker && m_crosshairEnabled) {
+ m_marker->show();
+ }
+ QGraphicsView::enterEvent(event);
+}
+
+void CachePlotView::leaveEvent(QEvent* event) {
+ if (m_marker) {
+ m_marker->hide();
+ }
+ QGraphicsView::leaveEvent(event);
+}
+
void CachePlotView::setPlot(QChart* chart) {
+ if (m_chart) {
+ // Propagate the previous chart size to the new plot
+ chart->resize(m_chart->size());
+ }
+
// Clear any previous plot or plot markers
scene()->clear();
m_tooltip = nullptr;
+ m_marker = nullptr;
m_chart = chart;
scene()->addItem(chart);
@@ -44,6 +66,16 @@ void CachePlotView::setPlot(QChart* chart) {
}
}
+ if (m_chart->series().size() > 0) {
+ if (auto* lineSeries = dynamic_cast<QLineSeries*>(m_chart->series()[0])) {
+ m_marker = new ChartLineMarker(m_chart, lineSeries);
+ connect(m_marker, &ChartLineMarker::snappedMarkerChanged,
+ [=] { this->updateCoordinateValues(m_marker->getMarkerValuePos()); });
+
+ m_marker->setVisible(hasFocus());
+ }
+ }
+
m_coordX = new QGraphicsSimpleTextItem();
scene()->addItem(m_coordX);
m_coordY = new QGraphicsSimpleTextItem();
@@ -52,29 +84,48 @@ void CachePlotView::setPlot(QChart* chart) {
m_coordY->setText("Y: ");
m_coordX->setPos(m_chart->size().width() / 2 - 50, m_chart->size().height());
m_coordY->setPos(m_chart->size().width() / 2 + 50, m_chart->size().height());
+
+ resizeObjects(m_chart->size());
}
-void CachePlotView::resizeEvent(QResizeEvent* event) {
- if (scene() && m_chart) {
- scene()->setSceneRect(QRect(QPoint(0, 0), event->size()));
- m_chart->resize(event->size());
+void CachePlotView::resizeObjects(const QSizeF& size) {
+ scene()->setSceneRect(QRectF(QPoint(0, 0), size));
+ m_chart->resize(size);
m_coordX->setPos(m_chart->size().width() / 2 - 50, m_chart->size().height() - 20);
m_coordY->setPos(m_chart->size().width() / 2 + 50, m_chart->size().height() - 20);
for (auto* item : scene()->items()) {
if (auto* callout = dynamic_cast<Callout*>(item)) {
callout->updateGeometry();
}
+ if (m_crosshairEnabled && m_marker) {
+ m_marker->updateLines();
}
}
+}
+
+void CachePlotView::resizeEvent(QResizeEvent* event) {
+ if (scene() && m_chart) {
+ resizeObjects(event->size());
+ }
QGraphicsView::resizeEvent(event);
}
+void CachePlotView::updateCoordinateValues(const QPointF& pos) {
+ m_coordX->setText(QString("X: %1").arg(pos.x()));
+ m_coordY->setText(QString("Y: %1").arg(pos.y()));
+}
+
void CachePlotView::mouseMoveEvent(QMouseEvent* event) {
m_hoverPos = event->pos();
constrainInPlotRange(m_hoverPos, m_chart);
- m_coordX->setText(QString("X: %1").arg(m_chart->mapToValue(m_hoverPos).x()));
- m_coordY->setText(QString("Y: %1").arg(m_chart->mapToValue(m_hoverPos).y()));
+ if (m_crosshairEnabled && m_marker) {
+ m_marker->move(m_hoverPos);
+ } else {
+ // Only update coordinate values if the marker is not currently snapping. If it is snapping, the marker itself
+ // will emit signals with the snapped marker position.
+ updateCoordinateValues(m_chart->mapToValue(m_hoverPos));
+ }
QGraphicsView::mouseMoveEvent(event);
}

MODIFY
src/cachesim/cacheplotview.h
src/cachesim/cacheplotview.h
@@ -19,6 +19,7 @@ class Callout;
QT_CHARTS_USE_NAMESPACE
namespace Ripes {
+class ChartLineMarker;
class CachePlotView : public QGraphicsView {
Q_OBJECT
@@ -31,6 +32,8 @@ public:
protected:
void resizeEvent(QResizeEvent* event);
void mouseMoveEvent(QMouseEvent* event);
+ void enterEvent(QEvent* event) override;
+ void leaveEvent(QEvent* event) override;
public slots:
void keepCallout();
@@ -39,12 +42,16 @@ public slots:
void enableCrosshair(bool enabled);
private:
+ void updateCoordinateValues(const QPointF& pos);
+ void resizeObjects(const QSizeF& size);
+
QPointF m_hoverPos;
QGraphicsSimpleTextItem* m_coordX;
QGraphicsSimpleTextItem* m_coordY;
QChart* m_chart = nullptr;
Callout* m_tooltip = nullptr;
+ ChartLineMarker* m_marker = nullptr;
QMutex m_tooltipLock;
bool m_crosshairEnabled = true;

ADD
src/cachesim/chartlinemarker.cpp
+#include "chartlinemarker.h"
+
+#include <QtCharts/QValueAxis>
+#include <algorithm>
+
+namespace Ripes {
+
+ChartLineMarker::ChartLineMarker(QChart* parent, QLineSeries* series)
+ : QGraphicsObject(parent), m_chart(parent), m_series(series) {
+ m_hzLine = new QGraphicsLineItem(this);
+ m_vtLine = new QGraphicsLineItem(this);
+
+ setFlag(ItemHasNoContents, true);
+}
+
+QRectF ChartLineMarker::boundingRect() const {
+ return childrenBoundingRect();
+}
+
+void ChartLineMarker::snapToLine(bool enabled) {
+ if (enabled ^ m_snapToLine) {
+ m_snapToLine = enabled;
+ move(m_center);
+ }
+}
+
+void ChartLineMarker::updateLines() {
+ const QValueAxis* axisY = qobject_cast<QValueAxis*>(m_chart->axes(Qt::Vertical).first());
+ const QValueAxis* axisX = qobject_cast<QValueAxis*>(m_chart->axes(Qt::Horizontal).first());
+
+ QLineF hzLine, vtLine;
+
+ const QPointF chartBottomLeft = m_chart->mapToPosition({0, 0});
+ const QPointF chartTopRight = m_chart->mapToPosition({axisX->max(), axisY->max()});
+
+ hzLine.setP1(QPointF(chartBottomLeft.x(), m_center.y()));
+ hzLine.setP2(QPointF(chartTopRight.x(), m_center.y()));
+
+ vtLine.setP1(QPointF(m_center.x(), chartBottomLeft.y()));
+ vtLine.setP2(QPointF(m_center.x(), chartTopRight.y()));
+
+ m_hzLine->setLine(hzLine);
+ m_vtLine->setLine(vtLine);
+}
+
+QPointF ChartLineMarker::getMarkerValuePos() const {
+ return m_chart->mapToValue(m_center);
+}
+QPointF ChartLineMarker::getMarkerPos() const {
+ return m_center;
+}
+
+void ChartLineMarker::move(const QPointF& center) {
+ if (m_snapToLine) {
+ // Note: we assume that the points are in a sorted order!
+ const auto& points = m_series->pointsVector();
+ const QPointF chartPos = m_chart->mapToValue(center);
+ auto iter = std::lower_bound(points.begin(), points.end(), chartPos.x(),
+ [=](const QPointF& lhs, const qreal& closestX) { return lhs.x() < closestX; });
+ const auto& pointInSeries = *iter;
+ const QPointF pointInScene = m_chart->mapToPosition(pointInSeries);
+
+ const bool markerChanged = m_center != pointInScene;
+ m_center = pointInScene;
+ if (markerChanged) {
+ emit snappedMarkerChanged();
+ }
+
+ } else {
+ m_center = center;
+ }
+ updateLines();
+}
+
+} // namespace Ripes

ADD
src/cachesim/chartlinemarker.h
+#pragma once
+
+#include <QtCharts/QChart>
+#include <QtCharts/QLineSeries>
+
+#include <QGraphicsObject>
+
+QT_CHARTS_USE_NAMESPACE
+
+namespace Ripes {
+
+class ChartLineMarker : public QGraphicsObject {
+ Q_OBJECT
+public:
+ ChartLineMarker(QChart* parent, QLineSeries* series);
+ QRectF boundingRect() const override;
+ void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override {}
+
+ void move(const QPointF& center);
+ void snapToLine(bool enabled);
+ bool isSnapping() const { return m_snapToLine; }
+ void updateLines();
+
+ QPointF getMarkerValuePos() const;
+ QPointF getMarkerPos() const;
+
+signals:
+ void snappedMarkerChanged();
+
+private:
+ /**
+ * @brief m_snapToLine
+ * If true, the center of the marker will snap to the closest x-axis point on the passed QLineSeries object.
+ * Note: it is assumed that the points in the series are in a sorted order!
+ */
+ bool m_snapToLine = true;
+
+ QPointF m_center;
+ QChart* m_chart = nullptr;
+ QLineSeries* m_series = nullptr;
+ QGraphicsLineItem* m_hzLine = nullptr;
+ QGraphicsLineItem* m_vtLine = nullptr;
+};
+} // namespace Ripes

Implement ChartLineMarker for following a QLineSeries curve