QCustomPlot Discussion and Comments

Showing multiple sub QAxisRects in one single QCustomPlotReturn to overview

Hi, I want to show a array of graphs in a QCustomplot object but failed to figure out how to properly arrange the layout system. Please help.

Details: Say I have a array of 10 graphs, and want to show them in a 2x2 grid, each cell of the grid only show one element of the array. So for each time, only 4 of the graphs will be shown.
I will allow users to choose which 4 graphs to show, and the data of all 10 graphs are keep coming in from external part (of the same program actually).

My current 'logical' attempt look like this:

ctor(MyChartWidget): create sublayout with 4 QAxisRect + 4 QGraphs, and set the layers
init(MyChartWidget): prepare data holders for those 10 graphs, call graphSelect() for initial 4 graphs
graphSelect(MyChartWidget): assign 4 graphdata from the 10 dataholders and call replot()

Problems: The program crash whenever the second call of 'replot()'. Is that anything I missed for the layout?

MultiChartWidget::MultiChartWidget(QWidget *parent)
    : QCustomPlot(parent)
{
    max_checktable_ = 4;

    // let's start from scratch and remove the default axis rect
    plotLayout()->clear();

    // create a sub layout that we'll place in first row:
    QCPLayoutGrid *subLayout = new QCPLayoutGrid();
    plotLayout()->addElement(0, 0, subLayout);

    QList< QCPAxisRect* > axis_rect_;
    for (int i = 0; i < max_checktable_; i++)
    {
        QCPAxisRect* r = new QCPAxisRect(this);
        axis_rect_.push_back(r);
        r->setLayer("background");
        foreach (QCPAxis *axis, r->axes())
        {
            axis->setLayer("axes");
            axis->grid()->setLayer("grid");
        }

        QCPLegend* l = new QCPLegend;
        sub_legend_.push_back(l);
        l->setLayer("legend");
        l->setVisible(false);
        r->insetLayout()->addElement(l, Qt::AlignRight|Qt::AlignTop);
        r->insetLayout()->setMargins(QMargins(12, 12, 12, 12));

        QCPGraph* g = addGraph(r->axis(QCPAxis::atBottom),
                               r->axis(QCPAxis::atLeft));
        sub_graphs_.push_back(g);
    }

    subLayout->addElement(0, 0, axis_rect_[0]);
    subLayout->addElement(0, 1, axis_rect_[1]);
    subLayout->addElement(1, 0, axis_rect_[2]);
    subLayout->addElement(1, 1, axis_rect_[3]);

//    setChartTitle("Cumulative District-specific Attack Rate");
} // end_ctor(MultiChartWidget)

void MultiChartWidget::init(QStringList graphNames,
                            int defaultSelected)
{
    graph_names_.swap(graphNames);
    graph_names_.insert(0, "Overall");

    for (int i = 0; i < graph_data_.size(); i++)
    {
        graph_data_[i]->clear();
        delete graph_data_[i];
    }
    graph_data_.clear();

    for (int i = 0; i < graph_names_.size(); i++)
    {
        graph_data_.push_back(new QCPDataMap());
        selected_indices_.push_back(false);
    }

    selectGraphs(QList<int>() << defaultSelected);
} // end: MultiChartWidget::init()

void MultiChartWidget::selectGraphs(QList<int> indices)
{
    for (int i = 0; i < graph_names_.size(); i++)
        selected_indices_[i] = false;

    for (int i = 0; i < indices.size(); i++)
    {
        int selected = indices[i];

        selected_indices_[ selected ] = true;
        sub_graphs_[i]->setName( graph_names_[ selected ] );
        sub_graphs_[i]->setData( graph_data_[ selected ] );
    }
    qDebug() << "MultiChartWidget-selectGraphs-pre-replot";
    replot(); // crashes every 2nd call of this
    qDebug() << "MultiChartWidget-end-selectGraphs";
} // end: MultiChartWidget::setSelectedGraphs()

In line 12 (see above) of your selectGraphs method, you call the setData(QCPDataMap *data, copy=false) method. The copy=false causes the internal data pointer to be deleted and replaced with the new pointer data. Of course, if you call this twice with the same data pointer, it will be used after deletion (the first call).

So rather replace this by

sub_graphs_[i]->setData( graph_data_[ selected ], true );
 


Another thing I noticed: You try to set the layer of the QCPLegend before it is added to the layout and thus has access to the QCustomPlot's layer system. (This causes qDebug output that the requested layer couldn't be set.) You should move line 29 before line 27 (again, for line numbers see above) in your constructor.

1) Thanks for the advice on QCPLegend. If instead of showing legend, how to layout a QCPPlottableTitle object for each graph in the sublayout object?

2) As the data of those 10 graphs keep pumping in from other part of the program. Calling setData() with copy = true will make the graphs not responsive to the new data. So how to avoid 'deep-copy' of QCPDataMap while allow the graphs behave as real-time?

Thank you very much.

1)
I'd try a 4x2 grid and place the axis rects such that there is one empty cell above each. Then you can place a QCPPlotTitle in each of those. (see the layout system documentation for an example of how to use plot titles)

2)
Only if the data really comes in very fast, the deep copy will make a difference compared to the replot time. You should definetly benchmark this. Most people underestimate the speed with which such deep copies happen. I'd say if you're not copying 100k points per second or more, never mind the deep copy.

However, if you really want to avoid it, just call setData once with copy=false, e.g. in the init method. This way both QCustomPlot and your application share a common QCPDataMap pointer, so you can modify the data from the outside. Be warned though, that the data map is owned by the graph. So as soon as you delete the graph (or call setData a second time, as you've experienced before), the data map will also be deleted internally and thus turn the pointer that you hold externally invalid.

Thanks for advice.