Search code examples
c++qt5qchartqchartview

QChartView and QScatterSeries overrdide the label of a QPointF


I have a QChartView which displays some 2D points which are representing each one a specific project I want to label each point with the project name AND NOT with it's x,y coordinates as the default behaviour enter image description here

Is there any way to achieve override the function that creates or render the labels?


Solution

  • Why this could be difficult to achieve without changing the Qt source code

    1. QXYSeries::setPointLabelsFormat wouldn't be of much help to you. It does indeed allow you to change the format of the labels, but the only variable part of it are the coordinates of the points.

    2. All the drawing is done in the private part of the Qt classes. Here is the whole story:

    The labels are drawn in the private part of QXYSeries (painter->drawText(position, pointLabel);):

    void QXYSeriesPrivate::drawSeriesPointLabels(QPainter *painter, const QVector<QPointF> &points,
                                                 const int offset)
    {
        if (points.size() == 0)
            return;
        static const QString xPointTag(QLatin1String("@xPoint"));
        static const QString yPointTag(QLatin1String("@yPoint"));
        const int labelOffset = offset + 2;
        painter->setFont(m_pointLabelsFont);
        painter->setPen(QPen(m_pointLabelsColor));
        QFontMetrics fm(painter->font());
        // m_points is used for the label here as it has the series point information
        // points variable passed is used for positioning because it has the coordinates
        const int pointCount = qMin(points.size(), m_points.size());
        for (int i(0); i < pointCount; i++) {
            QString pointLabel = m_pointLabelsFormat;
            pointLabel.replace(xPointTag, presenter()->numberToString(m_points.at(i).x()));
            pointLabel.replace(yPointTag, presenter()->numberToString(m_points.at(i).y()));
            // Position text in relation to the point
            int pointLabelWidth = fm.width(pointLabel);
            QPointF position(points.at(i));
            position.setX(position.x() - pointLabelWidth / 2);
            position.setY(position.y() - labelOffset);
            painter->drawText(position, pointLabel);
        }
    }
    

    drawSeriesPointLabels is called from the paint method of ScatterChartItem (this class is not included in the official documentation):

    void ScatterChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
    {
        Q_UNUSED(option)
        Q_UNUSED(widget)
        if (m_series->useOpenGL())
            return;
        QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());
        painter->save();
        painter->setClipRect(clipRect);
        if (m_pointLabelsVisible) {
            if (m_pointLabelsClipping)
                painter->setClipping(true);
            else
                painter->setClipping(false);
            m_series->d_func()->drawSeriesPointLabels(painter, m_points,
                                                      m_series->markerSize() / 2
                                                      + m_series->pen().width());
        }
        painter->restore();
    }
    

    The ScatterChartItem in turn is created in the private part of QScatterSeries and can't be substituted with a custom class:

    void QScatterSeriesPrivate::initializeGraphics(QGraphicsItem* parent)
    {
        Q_Q(QScatterSeries);
        ScatterChartItem *scatter = new ScatterChartItem(q,parent);
        m_item.reset(scatter);
        QAbstractSeriesPrivate::initializeGraphics(parent);
    }
    

    What you might wanna try

    1. Hide the original labels with setPointLabelsVisible(false); The labels will be drawn separately afterwards.

    2. Subclass QChartView and reimplement the paintEvent, invoking first QChartView::paintEvent and then calling a custom function (lets say drawCustomLabels), which is a modified version of QXYSeriesPrivate::drawSeriesPointLabels. By calling drawCustomLabels pass:

      • a local painter drawing on the vieport of MyChartView,
      • the points as returned by QXYSeries::points,
      • the desired offset.

    Here is an example of how the drawCustomLabels might look like:

    void MyChartView::drawCustomLabels(QPainter *painter, const QVector<QPointF> &points, const int offset)
    {
        if (points.count() == 0)
            return;
    
        QFontMetrics fm(painter->font());
        const int labelOffset = offset + 2;
    
        painter->setFont(m_pointLabelsFont); // Use QXYSeries::pointLabelsFont() to access m_pointLabelsFont
        painter->setPen(QPen(m_pointLabelsColor)); // Use QXYSeries::pointLabelsColor() to access m_pointLabelsColor
    
        for (int n(0); n < points.count(); n++) {
            QString pointLabel = "..."; // Set the desired label for the n-th point of the series
            // Position text in relation to the point
            int pointLabelWidth = fm.width(pointLabel);
            QPointF position(points.at(n));
            position.setX(position.x() - pointLabelWidth / 2);
            position.setY(position.y() - labelOffset);
            painter->drawText(position, pointLabel);
        }
    }