Qt Quick 2 Axis Formatter Example

The Qt Quick axis formatter example shows how to use predefined axis formatters and how to create a custom one.

Image non disponible

The interesting thing about this example is axis formatters, so we'll concentrate on that and skip explaining the basic functionality - for more detailed QML example documentation, see Qt Quick 2 Scatter Example.

Running the Example

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, visit Building and Running an Example.

Custom Axis Formatter

Customizing axis formatters requires subclassing the QValue3DAxisFormatter, which cannot be done in QML code alone. In this example we want an axis that interprets the float values as a timestamp and shows the date in the axis labels. To achieve this, we introduce a new class called CustomFormatter, which subclasses the QValue3DAxisFormatter:

 
Sélectionnez
class CustomFormatter : public QValue3DAxisFormatter
{
...

Since float values of a QScatter3DSeries cannot be directly cast into QDateTime values due to difference in data width, we need some sort of mapping between the two. We chose to do the mapping by specifying an origin date for the formatter and interpreting the float values from the QScatter3DSeries as date offsets to that origin value. The origin date is given as a property:

 
Sélectionnez
Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)

The mapping from value to QDateTime is done using valueToDateTime() method:

 
Sélectionnez
QDateTime CustomFormatter::valueToDateTime(qreal value) const
{
    return QDateTime(m_originDate).addMSecs(qint64(oneDayMs * value));
}

To function as an axis formatter, our CustomFormatter needs to reimplement some virtual methods:

 
Sélectionnez
virtual QValue3DAxisFormatter *createNewInstance() const;
virtual void populateCopy(QValue3DAxisFormatter &copy) const;
virtual void recalculate();
virtual QString stringForValue(qreal value, const QString &format) const;

The first two are simple, we just create a new instance of CustomFormatter and copy the necessary data over to it. These two methods are used to create and update a cache of formatter for rendering purposes. It is important to remember to call the superclass implementation of populateCopy():

 
Sélectionnez
QValue3DAxisFormatter *CustomFormatter::createNewInstance() const
{
    return new CustomFormatter();
}

void CustomFormatter::populateCopy(QValue3DAxisFormatter &copy) const
{
    QValue3DAxisFormatter::populateCopy(copy);

    CustomFormatter *customFormatter = static_cast<CustomFormatter *>(&copy);
    customFormatter->m_originDate = m_originDate;
    customFormatter->m_selectionFormat = m_selectionFormat;
}

Bulk of the work done by CustomFormatter is done in the recalculate() method, where our formatter calculates the grid, subgrid, and label positions, as well as formats the label strings. In our custom formatter we ignore the segment count of the axis and draw a grid line always at midnight. Subsegment count and label positioning is handled normally:

 
Sélectionnez
void CustomFormatter::recalculate()
{
    // We want our axis to always have gridlines at date breaks

    // Convert range into QDateTimes
    QDateTime minTime = valueToDateTime(qreal(axis()->min()));
    QDateTime maxTime = valueToDateTime(qreal(axis()->max()));

    // Find out the grid counts
    QTime midnight(0, 0);
    QDateTime minFullDate(minTime.date(), midnight);
    int gridCount = 0;
    if (minFullDate != minTime)
        minFullDate = minFullDate.addDays(1);
    QDateTime maxFullDate(maxTime.date(), midnight);

    gridCount += minFullDate.daysTo(maxFullDate) + 1;
    int subGridCount = axis()->subSegmentCount() - 1;

    // Reserve space for position arrays and label strings
    gridPositions().resize(gridCount);
    subGridPositions().resize((gridCount + 1) * subGridCount);
    labelPositions().resize(gridCount);
    labelStrings().reserve(gridCount);

    // Calculate positions and format labels
    qint64 startMs = minTime.toMSecsSinceEpoch();
    qint64 endMs = maxTime.toMSecsSinceEpoch();
    qreal dateNormalizer = endMs - startMs;
    qreal firstLineOffset = (minFullDate.toMSecsSinceEpoch() - startMs) / dateNormalizer;
    qreal segmentStep = oneDayMs / dateNormalizer;
    qreal subSegmentStep = 0;
    if (subGridCount > 0)
        subSegmentStep = segmentStep / qreal(subGridCount + 1);

    for (int i = 0; i < gridCount; i++) {
        qreal gridValue = firstLineOffset + (segmentStep * qreal(i));
        gridPositions()[i] = float(gridValue);
        labelPositions()[i] = float(gridValue);
        labelStrings() << minFullDate.addDays(i).toString(axis()->labelFormat());
    }

    for (int i = 0; i <= gridCount; i++) {
        if (subGridPositions().size()) {
            for (int j = 0; j < subGridCount; j++) {
                float position;
                if (i)
                    position =  gridPositions().at(i - 1) + subSegmentStep * (j + 1);
                else
                    position =  gridPositions().at(0) - segmentStep + subSegmentStep * (j + 1);
                if (position > 1.0f || position < 0.0f)
                    position = gridPositions().at(0);
                subGridPositions()[i * subGridCount + j] = position;
            }
        }
    }
}

The axis labels are formatted to show only the date, but for selection label we want little more resolution for the timestamp, so we specify another property for our custom formatter to allow user to customize it:

 
Sélectionnez
Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY selectionFormatChanged)

This selection format property is used in the reimplemented stringToValue method, where we ignore the submitted format and substitute the custom selection format for it:

 
Sélectionnez
QString