Creating dynamic axis tags using items

This example demonstrates a more advanced usage of QCustomPlot's item system. A new small class AxisTag is created which manages a group of items that together form the tag which points to the axis and highlights a certain coordinate.

To showcase it in the main application, two axes are created on the right side of the axis rect, and two corresponding tags are made to indicate the rightmost data point values of two graphs which are continuously updated.

The example project accompanying this tutorial is called axis-tags-example and will be part of the full package download for the upcoming QCP 2.0.1

A new class to handle the involved Items

The tags you see in the screenshot above consist of two visible items: a QCPItemText which gives us the current coordinate as text surrounded by a rectangular border, and a QCPItemLine with the line-ending of its head set to an arrow shape provides the arrow pointing to the left.

However, there is another invisible item involved to help with positioning of the tag. A QCPItemTracer is sitting at the right axis rect border (the horizontal position of the inner axis) at the height of the respective value coordinate. It provides the main parent anchor for the other items, so moving that tracer up and down will move the entire tag up and down.

Managing all three items separately in the MainWindow code would be prone to errors and not good style. Therefore a new class AxisTag is created, which takes care of setting up and handling the three items. Here is the header code for the AxisTag class:

#include <QObject>
#include "qcustomplot.h"

class AxisTag : public QObject
{
  Q_OBJECT
public:
  explicit AxisTag(QCPAxis *parentAxis);
  virtual ~AxisTag();
  
  // setters:
  void setPen(const QPen &pen);
  void setBrush(const QBrush &brush);
  void setText(const QString &text);
  
  // getters:
  QPen pen() const { return mLabel->pen(); }
  QBrush brush() const { return mLabel->brush(); }
  QString text() const { return mLabel->text(); }
  
  // other methods:
  void updatePosition(double value);
  
protected:
  QCPAxis *mAxis;
  QPointer<QCPItemTracer> mDummyTracer;
  QPointer<QCPItemLine> mArrow;
  QPointer<QCPItemText> mLabel;
};

The interface is kept to a minimum for clarity of this example. In a real-world case one might want more customization and extra functionality of this class, e.g. a generalization to also support other axis orientations.

The next code segment is the implementation of the AxisTag class. In its constructor, the following anchor parent-child relationships between the three items are established. The cyan blue circles indicate the QCPItemTracer position, QCPItemLine end/start position, and the QCPItemText position. 

You can find explanations of the relevant code segments inline:

#include "axistag.h"

AxisTag::AxisTag(QCPAxis *parentAxis) :
  QObject(parentAxis),
  mAxis(parentAxis)
{
  // The dummy tracer serves here as an invisible anchor which always sticks to the right side of
  // the axis rect
  mDummyTracer = new QCPItemTracer(mAxis->parentPlot());
  mDummyTracer->setVisible(false);
  mDummyTracer->position->setTypeX(QCPItemPosition::ptAxisRectRatio);
  mDummyTracer->position->setTypeY(QCPItemPosition::ptPlotCoords);
  mDummyTracer->position->setAxisRect(mAxis->axisRect());
  mDummyTracer->position->setAxes(0, mAxis);
  mDummyTracer->position->setCoords(1, 0);
  
  // the arrow end (head) is set to move along with the dummy tracer by setting it as its parent
  // anchor. Its coordinate system (setCoords) is thus pixels, and this is how the needed horizontal
  // offset for the tag of the second y axis is achieved. This horizontal offset gets dynamically
  // updated in AxisTag::updatePosition. the arrow "start" is simply set to have the "end" as parent
  // anchor. It is given a horizontal offset to the right, which results in a 15 pixel long arrow.
  mArrow = new QCPItemLine(mAxis->parentPlot());
  mArrow->setLayer("overlay");
  mArrow->setClipToAxisRect(false);
  mArrow->setHead(QCPLineEnding::esSpikeArrow);
  mArrow->end->setParentAnchor(mDummyTracer->position);
  mArrow->start->setParentAnchor(mArrow->end);
  mArrow->start->setCoords(15, 0);
  
  // The text label is anchored at the arrow start (tail) and has its "position" aligned at the
  // left, and vertically centered to the text label box.
  mLabel = new QCPItemText(mAxis->parentPlot());
  mLabel->setLayer("overlay");
  mLabel->setClipToAxisRect(false);
  mLabel->setPadding(QMargins(3, 0, 3, 0));
  mLabel->setBrush(QBrush(Qt::white));
  mLabel->setPen(QPen(Qt::blue));
  mLabel->setPositionAlignment(Qt::AlignLeft|Qt::AlignVCenter);
  mLabel->position->setParentAnchor(mArrow->start);
}

AxisTag::~AxisTag()
{
  if (mDummyTracer)
    mDummyTracer->parentPlot()->removeItem(mDummyTracer);
  if (mArrow)
    mArrow->parentPlot()->removeItem(mArrow);
  if (mLabel)
    mLabel->parentPlot()->removeItem(mLabel);
}

void AxisTag::setPen(const QPen &pen)
{
  mArrow->setPen(pen);
  mLabel->setPen(pen);
}

void AxisTag::setBrush(const QBrush &brush)
{
  mLabel->setBrush(brush);
}

void AxisTag::setText(const QString &text)
{
  mLabel->setText(text);
}

void AxisTag::updatePosition(double value)
{
  // since both the arrow and the text label are chained to the dummy tracer (via anchor
  // parent-child relationships) it is sufficient to update the dummy tracer coordinates. The
  // Horizontal coordinate type was set to ptAxisRectRatio so to keep it aligned at the right side
  // of the axis rect, it is always kept at 1. The vertical coordinate type was set to
  // ptPlotCoordinates of the passed parent axis, so the vertical coordinate is set to the new
  // value.
  mDummyTracer->position->setCoords(1, value);
  
  // We want the arrow head to be at the same horizontal position as the axis backbone, even if
  // the axis has a certain offset from the axis rect border (like the added second y axis). Thus we
  // set the horizontal pixel position of the arrow end (head) to the axis offset (the pixel
  // distance to the axis rect border). This works because the parent anchor of the arrow end is
  // the dummy tracer, which, as described earlier, is tied to the right axis rect border.
  mArrow->end->setCoords(mAxis->offset(), 0);
}

The main application

The main application now makes use of this new AxisTag class and thereby avoids getting its hands dirty with direct item manipulation – All this is handled inside the AxisTag instances.

Here is the header of the MainWindow class. As before, explanations can be found in inline comments:

#include <QMainWindow>
#include "qcustomplot.h"
#include "axistag.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
  Q_OBJECT
  
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();
  
private slots:
  void timerSlot();
  
private:
  Ui::MainWindow *ui;
  QCustomPlot *mPlot;
  QPointer<QCPGraph> mGraph1;
  QPointer<QCPGraph> mGraph2;
  AxisTag *mTag1;
  AxisTag *mTag2;
  QTimer mDataTimer;
};

Finally the implementation of the MainWindow class. It basically sets up two y Axes on the right, two graphs, and two axis tags. The timerSlot() is repeatedly called by a QTimer. In the slot, new data points are added to the graphs and the tags are updated via their AxisTag::updatePosition method.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow),
  mPlot(0),
  mTag1(0),
  mTag2(0)
{
  ui->setupUi(this);
  
  mPlot = new QCustomPlot(this);
  setCentralWidget(mPlot);
  
  // configure plot to have two right axes:
  mPlot->yAxis->setTickLabels(false);
  connect(mPlot->yAxis2, SIGNAL(rangeChanged(QCPRange)), mPlot->yAxis, SLOT(setRange(QCPRange))); // left axis only mirrors inner right axis
  mPlot->yAxis2->setVisible(true);
  mPlot->axisRect()->addAxis(QCPAxis::atRight);
  mPlot->axisRect()->axis(QCPAxis::atRight, 0)->setPadding(30); // add some padding to have space for tags
  mPlot->axisRect()->axis(QCPAxis::atRight, 1)->setPadding(30); // add some padding to have space for tags
  
  // create graphs:
  mGraph1 = mPlot->addGraph(mPlot->xAxis, mPlot->axisRect()->axis(QCPAxis::atRight, 0));
  mGraph2 = mPlot->addGraph(mPlot->xAxis, mPlot->axisRect()->axis(QCPAxis::atRight, 1));
  mGraph1->setPen(QPen(QColor(250, 120, 0)));
  mGraph2->setPen(QPen(QColor(0, 180, 60)));
  
  // create tags with newly introduced AxisTag class (see axistag.h/.cpp):
  mTag1 = new AxisTag(mGraph1->valueAxis());
  mTag1->setPen(mGraph1->pen());
  mTag2 = new AxisTag(mGraph2->valueAxis());
  mTag2->setPen(mGraph2->pen());
  
  connect(&mDataTimer, SIGNAL(timeout()), this, SLOT(timerSlot()));
  mDataTimer.start(40);
}

MainWindow::~MainWindow()
{
  delete ui;
}

void MainWindow::timerSlot()
{
  // calculate and add a new data point to each graph:
  mGraph1->addData(mGraph1->dataCount(), qSin(mGraph1->dataCount()/50.0)+qSin(mGraph1->dataCount()/50.0/0.3843)*0.25);
  mGraph2->addData(mGraph2->dataCount(), qCos(mGraph2->dataCount()/50.0)+qSin(mGraph2->dataCount()/50.0/0.4364)*0.15);

  // make key axis range scroll with the data:
  mPlot->xAxis->rescale();
  mGraph1->rescaleValueAxis(false, true);
  mGraph2->rescaleValueAxis(false, true);
  mPlot->xAxis->setRange(mPlot->xAxis->range().upper, 100, Qt::AlignRight);
  
  // update the vertical axis tag positions and texts to match the rightmost data point of the graphs:
  double graph1Value = mGraph1->dataMainValue(mGraph1->dataCount()-1);
  double graph2Value = mGraph2->dataMainValue(mGraph2->dataCount()-1);
  mTag1->updatePosition(graph1Value);
  mTag2->updatePosition(graph2Value);
  mTag1->setText(QString::number(graph1Value, 'f', 2));
  mTag2->setText(QString::number(graph2Value, 'f', 2));
  
  mPlot->replot();
}