Loading CSV Data and Plotting with QCustomPlot

QCustomPlot (QCP) is a lightweight, open-source plotting library for Qt. It consists of just two files (qcustomplot.h and qcustomplot.cpp), making integration straightfroward. The code is well-structured and easy to customize, which is why it was chosen for a financial application requiring complex charting (e.g., K‑line charts and auxiliary graphs).

This article demonstrates how to build a reusable plotting widget that loads data from a CSV file and displays it as line graphs with interactive cursors. The widget supports multiple line graphs, custom colors, scatter styles, and cursor operations (single or dual cursor).

Effect Overview

The test screenshot (not included here) shows a simple line graph with cursors. The widget offers over ten display modes for line graphs, as detailed in earlier parts of this series. It loads CSV data, maps column names to graph labels, and provides getter methods for:

  • Cursor X/Y values (up to two cursors)
  • Data segments between two cursors (both X and Y for a specific graph)
  • Graph colors, scatter styles, axis titles, and cursor colors

Example of loading CSV and configuring the plot:

CSVLoader *loader = new CSVLoader(nullptr);
loader->loadFromFile(qApp->applicationDirPath() + "/temp/data.csv");
QStringList colNames = loader->columnNames();

auto dataHandler = [&](const QString &name, const QVector<double> &values) {
    int idx = colNames.indexOf(name);
    if (idx < 0) return;

    if (idx == 0) {
        // First column is the X axis (time)
        plotWidget->setKeyData(values);
    } else {
        QString label = name;
        QString unit;
        int posL = name.indexOf('(');
        int posR = name.indexOf(')');
        if (posL != -1 && posR != -1) {
            label = name.left(posL);
            unit = name.mid(posL + 1, posR - posL - 1);
        }
        plotWidget->setGraphValues(idx - 1, label, unit, values);
        plotWidget->setGraphScatterStyle(idx - 1, 4);
    }
};

// The loader emits a signal for each column
loader->processColumns(dataHandler);

Core Implementation Details

Source Structure

The main public API is exposed through the PlotWidget class (renamed from ESMPMultiPlot). Key methods:

void setGraphCount(int count);
void setKeyData(const QVector<double> &keys);
double getKeyData(double pixelX);
void setGraphValues(int index, const QString &label, const QString &unit, const QVector<double> &values);
void setGraphScatterStyle(int index, int styleId);
double getGraphValue(int graphIndex, bool isLeftCursor);
double getGraphValue(int graphIndex, double key);
void setGraphColor(int index, const QColor &color);
void setGraphColor(const QString &label, const QColor &color);
void setGraphUnit(int index, const QString &unit);
void setGraphTitle(int index, const QString &title);
void updateGraphLabel(int index, const QString &newLabel);
int graphIndexByName(const QString &label) const;
void setCursorColor(bool isLeft, const QColor &color);
void showCursors(bool visible = true);
double cursorKey(bool isLeft) const;
bool cursorVisible() const;
double cursorValue(bool isLeft) const;
void autoResizeKeyRange(bool enable);
void setKeyRange(double lower, double upper);
void setValueRange(double lower, double upper);
void applyGraphSettings();
std::shared_ptr<AxisConfig> axisCache();

Cursor Dragging

The cursor movement is handled in mouseMoveEvent. The implementation prevents the left cursor from crossing the right cursor and vice‑versa. It also updates the cursor label showing the current key (X value).

void PlotWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (!m_draggingCursor || !m_activeCursor) {
        QCustomPlot::mouseMoveEvent(event);
        return;
    }

    double newPixelX = event->pos().x();
    QCPRange bottomRange = axisRect()->axis(QCPAxis::atBottom)->range();
    double pixelMin = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(bottomRange.lower);
    double pixelMax = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(bottomRange.upper);

    if (newPixelX < pixelMin) newPixelX = pixelMin;
    if (newPixelX > pixelMax) newPixelX = pixelMax;

    if (m_draggingLeftCursor) {
        // Prevent left from crossing right
        if (m_rightCursorVisible) {
            double rightPixel = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(m_rightCursorKey);
            if (newPixelX >= rightPixel - 4)
                newPixelX = rightPixel - 4;
        }
        double newKey = axisRect()->axis(QCPAxis::atBottom)->pixelToCoord(newPixelX);
        updateCursorPosition(m_leftCursorLines, newKey);
        m_leftLabel->setText(QString::number(getKeyData(newPixelX)));
        m_leftLabel->position->setPixelPosition(QPoint(newPixelX, axisRect()->rect().bottom() + 25));
    } else {
        // Prevent right from crossing left
        if (m_leftCursorVisible) {
            double leftPixel = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(m_leftCursorKey);
            if (newPixelX <= leftPixel + 4)
                newPixelX = leftPixel + 4;
        }
        double newKey = axisRect()->axis(QCPAxis::atBottom)->pixelToCoord(newPixelX);
        updateCursorPosition(m_rightCursorLines, newKey);
        m_rightLabel->setText(QString::number(getKeyData(newPixelX)));
        m_rightLabel->position->setPixelPosition(QPoint(newPixelX, axisRect()->rect().bottom() + 25));
    }

    event->accept();
    replot();
    emit cursorMoved(m_draggingLeftCursor);
}

Setting the Number of Graphs

The widget uses a single axis rect with multiple QCPGraph objects. Each graph is placed vertically with a scaled Y‑axis. The conversion formula used internally is:

// yDisplay = (yOriginal - yZero) / yGrid + yOffset
// Inverse: yOriginal = (yDisplay - yOffset) * yGrid + yZero

When setGraphCount is called, the axis rect is configured with custom tickers and labels for each graph. Over‑ or under‑count graphs are added/removed as needed.

void PlotWidget::setGraphCount(int count)
{
    QCPAxisTickerText *leftTicker = new QCPAxisTickerText;
    axisRect()->axis(QCPAxis::atLeft)->setTicker(QSharedPointer<QCPAxisTickerText>(leftTicker));

    QCPAxisTickerText *rightTicker = new QCPAxisTickerText;
    axisRect()->axis(QCPAxis::atRight)->setTicker(QSharedPointer<QCPAxisTickerText>(rightTicker));

    int totalMajorTicks = count * 4;
    double tickSpacing = 720.0 / totalMajorTicks;
    QMap<double, QString> ticks;
    for (int i = 0; i <= totalMajorTicks; ++i)
        ticks[tickSpacing * i] = QString();

    leftTicker->setTicks(ticks);
    leftTicker->setSubTickCount(4);

    double labelDistance = 720.0 / count;
    m_yOffsets.resize(count);
    m_labels.clear();
    m_units.clear();

    for (int i = 0; i < count; ++i) {
        m_yOffsets[i] = labelDistance * i + labelDistance / 2.0;

        QCPItemText *labelItem = new QCPItemText(this);
        labelItem->position->setCoords(QPointF(0.01, 1.0 - (i + 0.5) / count));
        m_labels.prepend(labelItem);

        QCPItemText *unitItem = new QCPItemText(this);
        unitItem->position->setCoords(QPointF(0.9, 1.0 - (i + 0.5) / count));
        m_units.prepend(unitItem);
    }

    refreshItemPositions();
    m_graphConfig->resize(count);
}

Adding Graph Data

Data is added graph by graph. The method creates a QCPGraph if one doesn't exist, sets the data, and adjusts the Y‑axis range to fit the values with 20% padding.

void PlotWidget::setGraphValues(int index,
                                 const QString &xLabel,
                                 const QString &yLabel,
                                 const QVector<double> &values)
{
    if (index >= m_graphCount || values.isEmpty())
        return;

    m_graphLabels[index] = xLabel;
    m_graphUnits[index] = yLabel;
    m_dataCache[index] = values;

    QList<QCPGraph *> existingGraphs = axisRect(index)->graphs();
    QCPGraph *graph = existingGraphs.isEmpty()
        ? addGraph(axisRect(index)->axis(QCPAxis::atBottom),
                   axisRect(index)->axis(QCPAxis::atLeft))
        : existingGraphs.first();

    if (existingGraphs.isEmpty()) {
        graph->setLineStyle(QCPGraph::lsLine);
        graph->setPen(QColor(255, 0, 0, 200));
    }

    graph->setData(m_keyData, values, true);

    auto [minVal, maxVal] = std::minmax_element(values.begin(), values.end());
    double padding = (*maxVal - *minVal) * 0.2;
    double lower = *minVal - padding;
    double upper = *maxVal + padding;

    QCPAxis *leftAxis = axisRect(index)->axis(QCPAxis::atLeft);
    leftAxis->ticker()->setTickOrigin(lower);
    leftAxis->ticker()->setTickStepStrategy(QCPAxisTicker::tssReadability);
    leftAxis->ticker()->setTickCount(8);
    leftAxis->setRange(lower, upper);

    QCPAxis *rightAxis = axisRect(index)->axis(QCPAxis::atRight);
    rightAxis->ticker()->setTickOrigin(lower);
    rightAxis->ticker()->setTickStepStrategy(QCPAxisTicker::tssReadability);
    rightAxis->ticker()->setTickCount(8);
    rightAxis->setRange(lower, upper);

    int leftWidth = QFontMetrics(leftAxis->labelFont()).horizontalAdvance(xLabel);
    leftAxis->setLabel(xLabel);

    int rightWidth = QFontMetrics(axisRect(index)->axis(QCPAxis::atBottom)->labelFont()).horizontalAdvance(yLabel);
    axisRect(index)->axis(QCPAxis::atBottom)->setLabel(yLabel);
}

Setting Scatter Style

The style is set using the QCPScatterStyle::ScatterShape enum. A convenience wrapper takes the integer value of the shape.

void PlotWidget::setGraphScatterStyle(int graphIndex, int shapeIndex)
{
    QList<QCPGraph *> graphs = axisRect()->graphs();
    if (!graphs.isEmpty() && graphIndex < graphs.size()) {
        QCPGraph *graph = graphs[graphIndex];
        graph->setScatterStyle(static_cast<QCPScatterStyle::ScatterShape>(shapeIndex));
    }
}

Testing the Widget

A test application was built with a simple UI containing the widget. The CSV file format expects the first column as time (X‑axis) and subsequent columns as Y‑data. Column names (optionaly with a unit in parentheses) are used for axis labels and unit indicators on the right side.

Key test steps:

  1. Create a CSVLoader instance and load the file.
  2. Retrieve column names.
  3. Set the number of graphs equal to columns - 1.
  4. For each column, call setKeyData for the first column and setGraphValues for others.
  5. Optionally set scatter styles and key range.
void TestWidget::loadData()
{
    CSVLoader loader(nullptr);
    loader.loadFromFile(qApp->applicationDirPath() + "/temp/sample.csv");
    QStringList colNames = loader.columnNames();

    // Set number of graphs (excluding time column)
    plot->setGraphCount(colNames.size() - 1);

    // Process each column
    for (int i = 0; i < colNames.size(); ++i) {
        QString name = colNames[i];
        QVector<double> data = loader.getColumnData(name);

        if (i == 0) {
            plot->setKeyData(data);
        } else {
            QString label = name;
            QString unit;
            int l = name.indexOf('(');
            int r = name.indexOf(')');
            if (l != -1 && r != -1) {
                label = name.left(l);
                unit = name.mid(l + 1, r - l - 1);
            }
            plot->setGraphValues(i - 1, label, unit, data);
            plot->setGraphScatterStyle(i - 1, 2); // e.g., circle shape
        }
    }

    double startTime = loader.earliestTime();
    double endTime = loader.latestTime();
    plot->setKeyRange(startTime, endTime);
}

QCustomPlot is a performant and flexible libray suitable for real‑time and static charting. The provided widget encapsulates CSV loading, multi‑graph display, and cursor interaction, and can be easily extended for more advanced use cases.

Tags: qcustomplot CSV Qt line chart Cursor

Posted on Fri, 11 Sep 2026 16:47:18 +0000 by ann