From 512078eea7e96c0a2cc25db46078fc1dc66f3d8f Mon Sep 17 00:00:00 2001 From: Lukas Kosinski Date: Tue, 13 Apr 2021 14:07:33 +0200 Subject: QXYSeries: Support for selection in a plot In order to support selection of points in a chart there is a support for this in scatterchartitem and linechartitem. The interface for that is available on QXYSeries level. Selected points can have changed color and they are a bit bigger than other points. Selected points are visible regardless of other points visibility. Task-number: QTBUG-89445 Change-Id: I6d11afa84288f4f949e53b005410659b0ecf3c55 Reviewed-by: Keith Kyzivat Reviewed-by: Miikka Heikkinen --- src/charts/linechart/linechartitem.cpp | 27 +++- src/charts/scatterchart/scatterchartitem.cpp | 22 ++- src/charts/xychart/qxyseries.cpp | 233 +++++++++++++++++++++++++++ src/charts/xychart/qxyseries.h | 17 ++ src/charts/xychart/qxyseries_p.h | 5 + src/charts/xychart/xychart_p.h | 2 + 6 files changed, 297 insertions(+), 9 deletions(-) diff --git a/src/charts/linechart/linechartitem.cpp b/src/charts/linechart/linechartitem.cpp index ef35a4f2..f2b30df2 100644 --- a/src/charts/linechart/linechartitem.cpp +++ b/src/charts/linechart/linechartitem.cpp @@ -64,6 +64,8 @@ LineChartItem::LineChartItem(QLineSeries *series, QGraphicsItem *item) QObject::connect(series, SIGNAL(pointLabelsFontChanged(QFont)), this, SLOT(handleUpdated())); QObject::connect(series, SIGNAL(pointLabelsColorChanged(QColor)), this, SLOT(handleUpdated())); QObject::connect(series, SIGNAL(pointLabelsClippingChanged(bool)), this, SLOT(handleUpdated())); + connect(series, &QLineSeries::selectedColorChanged, this, &LineChartItem::handleUpdated); + connect(series, &QLineSeries::selectedPointsChanged, this, &LineChartItem::handleUpdated); handleUpdated(); } @@ -329,11 +331,12 @@ void LineChartItem::updateGeometry() void LineChartItem::handleUpdated() { - // If points visibility has changed, a geometry update is needed. - // Also, if pen changes when points are visible, geometry update is needed. bool doGeometryUpdate = (m_pointsVisible != m_series->pointsVisible()) - || (m_series->pointsVisible() && (m_linePen != m_series->pen())); + || (m_series->pointsVisible() + && (m_linePen != m_series->pen() + || m_selectedColor != m_series->selectedColor() + || m_selectedPoints != m_series->selectedPoints())); bool visibleChanged = m_series->isVisible() != isVisible(); setVisible(m_series->isVisible()); setOpacity(m_series->opacity()); @@ -343,6 +346,8 @@ void LineChartItem::handleUpdated() m_pointLabelsVisible = m_series->pointLabelsVisible(); m_pointLabelsFont = m_series->pointLabelsFont(); m_pointLabelsColor = m_series->pointLabelsColor(); + m_selectedColor = m_series->selectedColor(); + m_selectedPoints = m_series->selectedPoints(); bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping(); m_pointLabelsClipping = m_series->pointLabelsClipping(); if (doGeometryUpdate) @@ -414,14 +419,24 @@ void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt painter->restore(); - if (m_pointsVisible) { + if (m_pointsVisible || !m_selectedPoints.isEmpty()) { // draw points that lie inside clipRect only qreal ptSize = m_linePen.width() * 1.5; painter->setPen(Qt::NoPen); painter->setBrush(m_linePen.color()); for (int i = 0; i < m_linePoints.size(); ++i) { - if (clipRect.contains(m_linePoints.at(i))) - painter->drawEllipse(m_linePoints.at(i), ptSize, ptSize); + if (clipRect.contains(m_linePoints.at(i))) { + if (m_series->isPointSelected(i)) { + painter->save(); + if (m_selectedColor.isValid()) + painter->setBrush(m_selectedColor); + + painter->drawEllipse(m_linePoints.at(i), ptSize * 1.5, ptSize * 1.5); + painter->restore(); + } else if (m_pointsVisible) { + painter->drawEllipse(m_linePoints.at(i), ptSize, ptSize); + } + } } } } diff --git a/src/charts/scatterchart/scatterchartitem.cpp b/src/charts/scatterchart/scatterchartitem.cpp index 515f3678..0f81ee54 100644 --- a/src/charts/scatterchart/scatterchartitem.cpp +++ b/src/charts/scatterchart/scatterchartitem.cpp @@ -68,6 +68,8 @@ ScatterChartItem::ScatterChartItem(QScatterSeries *series, QGraphicsItem *item) QObject::connect(series, SIGNAL(pointLabelsFontChanged(QFont)), this, SLOT(handleUpdated())); QObject::connect(series, SIGNAL(pointLabelsColorChanged(QColor)), this, SLOT(handleUpdated())); QObject::connect(series, SIGNAL(pointLabelsClippingChanged(bool)), this, SLOT(handleUpdated())); + connect(series, &QXYSeries::selectedColorChanged, this, &ScatterChartItem::handleUpdated); + connect(series, &QXYSeries::selectedPointsChanged, this, &ScatterChartItem::handleUpdated); setZValue(ChartPresenter::ScatterSeriesZValue); setFlags(QGraphicsItem::ItemClipsChildrenToShape); @@ -262,8 +264,18 @@ void ScatterChartItem::setPen(const QPen &pen) void ScatterChartItem::setBrush(const QBrush &brush) { - foreach (QGraphicsItem *item , m_items.childItems()) - static_cast(item)->setBrush(brush); + const auto &items = m_items.childItems(); + for (auto item : items) { + if (m_markerMap.contains(item)) { + auto index = m_series->points().indexOf(m_markerMap[item]); + if (m_selectedPoints.contains(index)) + static_cast(item)->setBrush(m_selectedColor); + else + static_cast(item)->setBrush(brush); + } else { + static_cast(item)->setBrush(brush); + } + } } void ScatterChartItem::handleUpdated() @@ -282,7 +294,9 @@ void ScatterChartItem::handleUpdated() bool recreate = m_visible != m_series->isVisible() || m_size != m_series->markerSize() - || m_shape != m_series->markerShape(); + || m_shape != m_series->markerShape() + || m_selectedColor != m_series->selectedColor() + || m_selectedPoints != m_series->selectedPoints(); m_visible = m_series->isVisible(); m_size = m_series->markerSize(); m_shape = m_series->markerShape(); @@ -292,6 +306,8 @@ void ScatterChartItem::handleUpdated() m_pointLabelsVisible = m_series->pointLabelsVisible(); m_pointLabelsFont = m_series->pointLabelsFont(); m_pointLabelsColor = m_series->pointLabelsColor(); + m_selectedColor = m_series->selectedColor(); + m_selectedPoints = m_series->selectedPoints(); bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping(); m_pointLabelsClipping = m_series->pointLabelsClipping(); diff --git a/src/charts/xychart/qxyseries.cpp b/src/charts/xychart/qxyseries.cpp index e991755a..ca93f07a 100644 --- a/src/charts/xychart/qxyseries.cpp +++ b/src/charts/xychart/qxyseries.cpp @@ -245,6 +245,22 @@ QT_BEGIN_NAMESPACE \sa pointLabelsVisible */ +/*! + \property QXYSeries::selectedColor + \brief The color of the selected points. + + This is the fill (brush) color of points marked as selected. If not specified, + value of QXYSeries::color is used as default. + \sa color + \since 6.2 +*/ +/*! + \qmlproperty color XYSeries::selectedColor + The color of the selected points. This is the fill (brush) color of points marked + as selected. + If not specified, value of QXYSeries::color is used as default. + \sa color +*/ /*! \fn void QXYSeries::pointLabelsClippingChanged(bool clipping) This signal is emitted when the clipping of the data point labels changes to @@ -606,6 +622,159 @@ void QXYSeries::replace(const QList &points) emit pointsReplaced(); } +/*! + Returns true if point at given \a index is among selected points and false otherwise. + \note Selected points are drawn using the selected color if it was specified. + \sa selectedPoints(), setPointSelected(), setSelectedColor() + \since 6.2 + */ +bool QXYSeries::isPointSelected(int index) +{ + Q_D(QXYSeries); + return d->isPointSelected(index); +} + +/*! + Marks point at \a index as selected. + \note Emits QXYSeries::selectedPointsChanged + \sa setPointSelected() + \since 6.2 + */ +void QXYSeries::selectPoint(int index) +{ + setPointSelected(index, true); +} + +/*! + Deselects point at given \a index. + \note Emits QXYSeries::selectedPointsChanged + \sa setPointSelected() + \since 6.2 + */ +void QXYSeries::deselectPoint(int index) +{ + setPointSelected(index, false); +} + +/*! + Marks point at given \a index as either selected or deselected. + \note Selected points are drawn using the selected color if it was specified. Emits QXYSeries::selectedPointsChanged + \sa setPointSelected(), setSelectedColor() + \since 6.2 + */ +void QXYSeries::setPointSelected(int index, bool selected) +{ + Q_D(QXYSeries); + + bool callSignal = false; + d->setPointSelected(index, selected, callSignal); + + if (callSignal) + emit selectedPointsChanged(); +} + +/*! + Marks all points in the series as selected, + \note Emits QXYSeries::selectedPointsChanged + \sa setPointSelected() + \since 6.2 + */ +void QXYSeries::selectAllPoints() +{ + Q_D(QXYSeries); + + bool callSignal = false; + for (int i = 0; i < d->m_points.count(); ++i) + d->setPointSelected(i, true, callSignal); + + if (callSignal) + emit selectedPointsChanged(); +} + +/*! + Deselects all points in the series. + \note Emits QXYSeries::selectedPointsChanged + \sa setPointSelected() + \since 6.2 + */ +void QXYSeries::deselectAllPoints() +{ + Q_D(QXYSeries); + + bool callSignal = false; + for (int i = 0; i < d->m_points.count(); ++i) + d->setPointSelected(i, false, callSignal); + + if (callSignal) + emit selectedPointsChanged(); +} + +/*! + Marks multiple points passed in a \a indexes list as selected. + \note Emits QXYSeries::selectedPointsChanged + \sa setPointSelected() + \since 6.2 + */ +void QXYSeries::selectPoints(const QList &indexes) +{ + Q_D(QXYSeries); + + bool callSignal = false; + for (const int &index : indexes) + d->setPointSelected(index, true, callSignal); + + if (callSignal) + emit selectedPointsChanged(); +} + +/*! + Marks multiple points passed in a \a indexes list as deselected. + \note Emits QXYSeries::selectedPointsChanged + \sa setPointSelected() + \since 6.2 + */ +void QXYSeries::deselectPoints(const QList &indexes) +{ + Q_D(QXYSeries); + + bool callSignal = false; + for (const int &index : indexes) + d->setPointSelected(index, false, callSignal); + + if (callSignal) + emit selectedPointsChanged(); +} + +/*! + Changes selection state of points at given \a indexes to the opposite one. Makes + \note Emits QXYSeries::selectedPointsChanged + \sa setPointSelected() + \since 6.2 + */ +void QXYSeries::toggleSelection(const QList &indexes) +{ + Q_D(QXYSeries); + + bool callSignal = false; + for (const int &index : indexes) + d->setPointSelected(index, !isPointSelected(index), callSignal); + + if (callSignal) + emit selectedPointsChanged(); +} + +/*! + Returns a list of points indexes marked as selected. + Selected points are visible regardless of points visibility. + \sa setPointSelected(), pointsVisible() + \since 6.2 + */ +QList QXYSeries::selectedPoints() const +{ + Q_D(const QXYSeries); + return QList(d->m_selectedPoints.begin(), d->m_selectedPoints.end()); +} + /*! Removes the point that has the coordinates \a x and \a y from the series. \sa pointRemoved() @@ -635,6 +804,7 @@ void QXYSeries::remove(const QPointF &point) void QXYSeries::remove(int index) { Q_D(QXYSeries); + deselectPoint(index); d->m_points.remove(index); emit pointRemoved(index); } @@ -650,6 +820,13 @@ void QXYSeries::removePoints(int index, int count) // remove(qreal, qreal) overload in some implicit casting cases. Q_D(QXYSeries); if (count > 0) { + if (!d->m_selectedPoints.empty()) { + QList indexes; + for (int i = index; i < index + count; ++i) + indexes << i; + deselectPoints(indexes); + } + d->m_points.remove(index, count); emit pointsRemoved(index, count); } @@ -665,6 +842,24 @@ void QXYSeries::insert(int index, const QPointF &point) Q_D(QXYSeries); if (isValidValue(point)) { index = qMax(0, qMin(index, d->m_points.size())); + + if (!d->m_selectedPoints.isEmpty()) { + // if point was inserted we need to move already selected points by 1 + QSet selectedAfterInsert; + bool callSignal = false; + for (const auto &value : d->m_selectedPoints) { + if (value >= index) { + selectedAfterInsert << value + 1; + callSignal = true; + } else { + selectedAfterInsert << value; + } + } + d->m_selectedPoints = selectedAfterInsert; + if (callSignal) + emit selectedPointsChanged(); + } + d->m_points.insert(index, point); emit pointAdded(index); } @@ -788,6 +983,21 @@ QColor QXYSeries::color() const return pen().color(); } +void QXYSeries::setSelectedColor(const QColor &color) +{ + Q_D(QXYSeries); + if (selectedColor() != color) { + d->m_selectedColor = color; + emit selectedColorChanged(color); + } +} + +QColor QXYSeries::selectedColor() const +{ + Q_D(const QXYSeries); + return d->m_selectedColor; +} + void QXYSeries::setPointsVisible(bool visible) { Q_D(QXYSeries); @@ -1022,6 +1232,29 @@ void QXYSeriesPrivate::drawSeriesPointLabels(QPainter *painter, const QList m_points.count() - 1) + return; + + if (selected) { + if (!isPointSelected(index)) { + m_selectedPoints << index; + callSignal = true; + } + } else { + if (isPointSelected(index)) { + m_selectedPoints.remove(index); + callSignal = true; + } + } +} + +bool QXYSeriesPrivate::isPointSelected(int index) +{ + return m_selectedPoints.contains(index); +} + QT_END_NAMESPACE #include "moc_qxyseries.cpp" diff --git a/src/charts/xychart/qxyseries.h b/src/charts/xychart/qxyseries.h index 43cad845..43d96e9e 100644 --- a/src/charts/xychart/qxyseries.h +++ b/src/charts/xychart/qxyseries.h @@ -49,6 +49,7 @@ class Q_CHARTS_EXPORT QXYSeries : public QAbstractSeries Q_OBJECT Q_PROPERTY(bool pointsVisible READ pointsVisible WRITE setPointsVisible) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + Q_PROPERTY(QColor selectedColor READ color WRITE setSelectedColor NOTIFY selectedColorChanged REVISION 1) Q_PROPERTY(QString pointLabelsFormat READ pointLabelsFormat WRITE setPointLabelsFormat NOTIFY pointLabelsFormatChanged) Q_PROPERTY(bool pointLabelsVisible READ pointLabelsVisible WRITE setPointLabelsVisible NOTIFY pointLabelsVisibilityChanged) Q_PROPERTY(QFont pointLabelsFont READ pointLabelsFont WRITE setPointLabelsFont NOTIFY pointLabelsFontChanged) @@ -94,6 +95,9 @@ public: virtual void setColor(const QColor &color); virtual QColor color() const; + void setSelectedColor(const QColor &color); + QColor selectedColor() const; + void setPointsVisible(bool visible = true); bool pointsVisible() const; @@ -114,6 +118,17 @@ public: void replace(const QList &points); + bool isPointSelected(int index); + void selectPoint(int index); + void deselectPoint(int index); + void setPointSelected(int index, bool selected); + void selectAllPoints(); + void deselectAllPoints(); + void selectPoints(const QList &indexes); + void deselectPoints(const QList &indexes); + void toggleSelection(const QList &indexes); + QList selectedPoints() const; + Q_SIGNALS: void clicked(const QPointF &point); void hovered(const QPointF &point, bool state); @@ -124,6 +139,7 @@ Q_SIGNALS: void pointRemoved(int index); void pointAdded(int index); void colorChanged(QColor color); + void selectedColorChanged(const QColor &color); void pointsReplaced(); void pointLabelsFormatChanged(const QString &format); void pointLabelsVisibilityChanged(bool visible); @@ -132,6 +148,7 @@ Q_SIGNALS: void pointLabelsClippingChanged(bool clipping); void pointsRemoved(int index, int count); void penChanged(const QPen &pen); + void selectedPointsChanged(); private: Q_DECLARE_PRIVATE(QXYSeries) diff --git a/src/charts/xychart/qxyseries_p.h b/src/charts/xychart/qxyseries_p.h index 18340201..f9518c07 100644 --- a/src/charts/xychart/qxyseries_p.h +++ b/src/charts/xychart/qxyseries_p.h @@ -67,12 +67,17 @@ public: void drawSeriesPointLabels(QPainter *painter, const QList &points, const int offset = 0); + void setPointSelected(int index, bool selected, bool &callSignal); + bool isPointSelected(int index); + Q_SIGNALS: void updated(); protected: QList m_points; + QSet m_selectedPoints; QPen m_pen; + QColor m_selectedColor; QBrush m_brush; bool m_pointsVisible; QString m_pointLabelsFormat; diff --git a/src/charts/xychart/xychart_p.h b/src/charts/xychart/xychart_p.h index 63f1115f..66849218 100644 --- a/src/charts/xychart/xychart_p.h +++ b/src/charts/xychart/xychart_p.h @@ -98,6 +98,8 @@ private: protected: QXYSeries *m_series; QList m_points; + QList m_selectedPoints; + QColor m_selectedColor; XYAnimation *m_animation; bool m_dirty; -- cgit v1.2.3