Custom Proxy Example

Using Q3DBars with a custom proxy.

The custom proxy example shows how to create a custom proxy to use with Q3DBars.

The interesting thing about custom proxy example is the custom dataset and the corresponding proxy usage, so we concentrate on that and skip explaining the basic Q3DBars functionality - for that see Bars Example.

This example defines a simple flexible data set VariantDataSet where each data item is a a variant list. Each item can have multiple different values, identified by their index in the list. In this example, the data set is storing monthly rainfall data, where the value in index zero is the year, the value in index one is the month, and the value in index two is the amount of rainfall in that month.

The custom proxy we provide here is similar to item model based proxies provided by Qt Data Visualization in that it requires a mapping to interpret the data.

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.

VariantDataSet

VariantDataSet class provides a simple API:

 typedef QVariantList VariantDataItem;
 typedef QList<VariantDataItem *> VariantDataItemList;
 ...

 void clear();

 int addItem(VariantDataItem *item);
 int addItems(VariantDataItemList *itemList);

 const VariantDataItemList &itemList() const;

 Q_SIGNALS:
 void itemsAdded(int index, int count);
 void dataCleared();

As you can see, the data items are simply QVariantList objects, and the data can be added either singly or in lists. The only additional functionality provided is clearing the data set and querying for a reference to the data contained in the set. Signals are emitted when data is added or the set is cleared.

VariantBarDataProxy

VariantBarDataProxy is a subclass of QBarDataProxy and provides a simple API of just getters and setters for the data set and the mapping:

 class VariantBarDataProxy : public QBarDataProxy
 ...

 // Doesn't gain ownership of the dataset, but does connect to it to listen for data changes.
 void setDataSet(VariantDataSet *newSet);
 VariantDataSet *dataSet();

 // Map key (row, column, value) to value index in data item (VariantItem).
 // Doesn't gain ownership of mapping, but does connect to it to listen for mapping changes.
 // Modifying mapping that is set to proxy will trigger dataset re-resolving.
 void setMapping(VariantBarDataMapping *mapping);
 VariantBarDataMapping *mapping();

On the implementation side, the proxy listens for the changes in the data set and the mapping, and resolves the data set if any changes are detected. It is not particularly efficient implementation in that any change will cause re-resolving of the entire data set, but that is not an issue for this example. The interesting part is the resolveDataSet() method:

 void VariantBarDataProxy::resolveDataSet()
 {
     // If we have no data or mapping, or the categories are not defined, simply clear the array
     if (m_dataSet.isNull() || m_mapping.isNull() || !m_mapping->rowCategories().size()
             || !m_mapping->columnCategories().size()) {
         resetArray(0);
         return;
     }
     const VariantDataItemList &itemList = m_dataSet->itemList();

     int rowIndex = m_mapping->rowIndex();
     int columnIndex = m_mapping->columnIndex();
     int valueIndex = m_mapping->valueIndex();
     const QStringList &rowList = m_mapping->rowCategories();
     const QStringList &columnList = m_mapping->columnCategories();

     // Sort values into rows and columns
     typedef QHash<QString, float> ColumnValueMap;
     QHash <QString, ColumnValueMap> itemValueMap;
     foreach (const VariantDataItem *item, itemList) {
         itemValueMap[item->at(rowIndex).toString()][item->at(columnIndex).toString()]
                 = item->at(valueIndex).toReal();
     }

     // Create a new data array in format the parent class understands
     QBarDataArray *newProxyArray = new QBarDataArray;
     foreach (QString rowKey, rowList) {
         QBarDataRow *newProxyRow = new QBarDataRow(columnList.size());
         for (int i = 0; i < columnList.size(); i++)
             (*newProxyRow)[i].setValue(itemValueMap[rowKey][columnList.at(i)]);
         newProxyArray->append(newProxyRow);
     }

     // Finally, reset the data array in the parent class
     resetArray(newProxyArray);
 }

In resolveDataSet() method we sort the variant data values into rows and columns based on the mapping. This is very similar to how QItemModelBarDataProxy handles mapping, except we use list indexes instead of item model roles here. Once the values are sorted, we generate QBarDataArray out of them, and call resetArray() method on the parent class.

VariantBarDataMapping

VariantBarDataMapping stores the mapping information between VariantDataSet data item indexes and rows, columns, and values of QBarDataArray. It also contains the lists of rows and columns to be included in the resolved data:

 Q_PROPERTY(int rowIndex READ rowIndex WRITE setRowIndex)
 Q_PROPERTY(int columnIndex READ columnIndex WRITE setColumnIndex)
 Q_PROPERTY(int valueIndex READ valueIndex WRITE setValueIndex)
 Q_PROPERTY(QStringList rowCategories READ rowCategories WRITE setRowCategories)
 Q_PROPERTY(QStringList columnCategories READ columnCategories WRITE setColumnCategories)
 ...

 VariantBarDataMapping(int rowIndex, int columnIndex, int valueIndex,
                        const QStringList &rowCategories,
                        const QStringList &columnCategories);
 ...

 void remap(int rowIndex, int columnIndex, int valueIndex,
            const QStringList &rowCategories,
            const QStringList &columnCategories);
 ...

 void mappingChanged();

The primary way to use a VariantBarDataMapping object is to give the mappings already at the constructor, though they can be set later individually or all together with the remap() method. A signal is emitted if mapping changes. It is basically a simplified version of the mapping functionality of QItemModelBarDataProxy adapted to work with variant lists instead of item models.

RainfallGraph

RainfallGraph class handles the setup of the graph. The interesting part is the addDataSet() method:

 void RainfallGraph::addDataSet()
 {
     // Create a new variant data set and data item list
     m_dataSet =  new VariantDataSet;
     VariantDataItemList *itemList = new VariantDataItemList;

     // Read data from a data file into the data item list
     QTextStream stream;
     QFile dataFile(":/data/raindata.txt");
     if (dataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
         stream.setDevice(&dataFile);
         while (!stream.atEnd()) {
             QString line = stream.readLine();
             if (line.startsWith("#")) // Ignore comments
                 continue;
             QStringList strList = line.split(",", Qt::SkipEmptyParts);
             // Each line has three data items: Year, month, and rainfall value
             if (strList.size() < 3) {
                 qWarning() << "Invalid row read from data:" << line;
                 continue;
             }
             // Store year and month as strings, and rainfall value as double
             // into a variant data item and add the item to the item list.
             VariantDataItem *newItem = new VariantDataItem;
             for (int i = 0; i < 2; i++)
                 newItem->append(strList.at(i).trimmed());
             newItem->append(strList.at(2).trimmed().toDouble());
             itemList->append(newItem);
         }
     } else {
         qWarning() << "Unable to open data file:" << dataFile.fileName();
     }

     // Add items to the data set and set it to the proxy
     m_dataSet->addItems(itemList);
     m_proxy->setDataSet(m_dataSet);

     // Create new mapping for the data and set it to the proxy
     m_mapping =  new VariantBarDataMapping(0, 1, 2, m_years, m_numericMonths);
     m_proxy->setMapping(m_mapping);
 }

The bulk of that method is used for populating the variant data set. Once the set is populated, visualizing the data is trivial with the help of our custom proxy:

 // Add items to the data set and set it to the proxy
 m_dataSet->addItems(itemList);
 m_proxy->setDataSet(m_dataSet);

 // Create new mapping for the data and set it to the proxy
 m_mapping =  new VariantBarDataMapping(0, 1, 2, m_years, m_numericMonths);
 m_proxy->setMapping(m_mapping);

Example project @ code.qt.io