Qt QCustomPlot 기반 차트 시각화 입문

QCustomPlot 개요

QCustomPlot은 Qt C++ 환경에서 2D 그래프를 그리기 위한 경량 라이브러리이다. 실시간 데이터 시각화, 트렌드 차트, 산점도, 막대그래프 등 다양한 플롯을 지원하며 별도 의존 라이브러리 없이 qcustomplot.h와 qcustomplot.cpp 두 파일만 추가하면 바로 사용할 수 있다. 출력 기능을 위해 printsupport 모듈이 필요하다.

프로젝트 구성

프로젝트에 소스 파일을 포함하려면 먼저 .pro 파일에 다음과 같이 추가한다.

QT += printsupport

다음으로 qcustomplot.h와 qcustomplot.cpp를 프로젝트 디렉터리에 사하고 기존 파일로 추가한다. 사용처에서는 아래 더를 포함한다.

#include "qcustomplot.h"

위젯 배치 후 코드에서 인스턴스를 생성하면 된다.

QCustomPlot *plotView = new QCustomPlot(this);

심 클래스와 레이어

QCustomPlot은 여러 레이어로 화면을 구성한다. 대표적인 클래스는 다음과 같다.

  • QCustomPlot: 전체 차트 위젯을 관리하는 최상위 클래스
  • QCPLayer: 화면에 그려지는 모든 요소의 레이어
  • QCPAxisRect: 축 영역. 기본적으로 상하좌우 개의 축을 포함
  • QCPAbstractPlottable: QCPGraph, QCPCurve, QCPBars, QCPColorMap 등의 베이스 클래스

기본적으로 background, grid, main, axis, legend, overlay 레이어가 있으며, 일반적으로 그래프는 main 레이어에 그려진다.

기본 곡선 플롯

아래 예제는 단일 곡선을 생성하고 데이터를 연결하는 방법을 보여준다.

QVector<double> keys(101), values(101);
for (int i = 0; i < 101; ++i) {
    keys[i] = i / 5.0 - 10.0;
    values[i] = qPow(keys[i], 3);
}

QCPGraph *curve = plotView->addGraph();
curve->setData(keys, values);
curve->setName("y = x³");
curve->setPen(QPen(QColor(32, 178, 170), 2));

plotView->xAxis->setLabel("x");
plotView->yAxis->setLabel("y");
plotView->xAxis->setRange(-11, 11);
plotView->yAxis->setRange(-1100, 1100);
plotView->replot();

스타일 커스터마이징

배경 그라데이션

QLinearGradient bgGrad;
bgGrad.setStart(0, 0);
bgGrad.setFinalStop(0, 360);
bgGrad.setColorAt(0, QColor(90, 90, 90));
bgGrad.setColorAt(1, QColor(40, 40, 40));
plotView->setBackground(bgGrad);

QLinearGradient rectGrad;
rectGrad.setStart(0, 0);
rectGrad.setFinalStop(0, 360);
rectGrad.setColorAt(0, QColor(90, 90, 90));
rectGrad.setColorAt(1, QColor(35, 35, 35));
plotView->axisRect()->setBackground(rectGrad);

그리드 스타일

plotView->xAxis->grid()->setPen(QPen(QColor(150, 150, 150), 1, Qt::DotLine));
plotView->yAxis->grid()->setPen(QPen(QColor(150, 150, 150), 1, Qt::DotLine));
plotView->xAxis->grid()->setSubGridPen(QPen(QColor(70, 70, 70), 1, Qt::DotLine));
plotView->yAxis->grid()->setSubGridPen(QPen(QColor(70, 70, 70), 1, Qt::DotLine));
plotView->xAxis->grid()->setSubGridVisible(true);
plotView->yAxis->grid()->setSubGridVisible(true);
plotView->xAxis->grid()->setZeroLinePen(QPen(Qt::red));
plotView->yAxis->grid()->setZeroLinePen(QPen(Qt::red));

선 스타일 및 채우기

QPen linePen;
QStringList styleNames;
styleNames << "None" << "Line" << "StepLeft" << "StepRight" << "StepCenter" << "Impulse";

for (int s = QCPGraph::lsNone; s <= QCPGraph::lsImpulse; ++s) {
    plotView->addGraph();
    linePen.setColor(QColor(qSin(s * 1.1 + 1.0) * 70 + 90,
                            qSin(s * 0.4 + 0.5) * 70 + 90,
                            qSin(s * 0.2 + 2.0) * 70 + 90));
    plotView->graph()->setPen(linePen);
    plotView->graph()->setName(styleNames.at(s - QCPGraph::lsNone));
    plotView->graph()->setLineStyle(static_cast<QCPGraph::LineStyle>(s));
    plotView->graph()->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 6));

    QVector<double> keys(20), values(20);
    for (int k = 0; k < 20; ++k) {
        keys[k] = k / 20.0 * 6 * M_PI;
        values[k] = 5 * qCos(keys[k]) / (keys[k] + 1) + (s - QCPGraph::lsNone) * 3;
    }
    plotView->graph()->setData(keys, values);
    plotView->graph()->rescaleAxes(true);
}

두 곡선 사이 영역을 채우려면 setChannelFillGraph를 사용한다.

QCPGraph *upper = plotView->addGraph();
QCPGraph *lower = plotView->addGraph();
upper->setBrush(QBrush(QColor(0, 0, 255, 30)));
upper->setChannelFillGraph(lower);

축 제어

축 가시성

plotView->yAxis2->setVisible(true);
plotView->xAxis2->setVisible(true);
plotView->axisRect()->setupFullAxesBox();

범위 및 레이블

plotView->xAxis->setRange(-1, 1);
plotView->yAxis->setRange(0, 1);
plotView->xAxis->setLabel("시간");
plotView->yAxis->setLabel("값");
plotView->xAxis->setTickLabelRotation(30);

축 금 설정

plotView->xAxis->ticker()->setTickCount(11);
plotView->xAxis->ticker()->setTickStepStrategy(QCPAxisTicker::tssReadability);

plotView->xAxis->setTickLengthIn(3);
plotView->xAxis->setTickLengthOut(5);
plotView->xAxis->setSubTickLengthIn(15);
plotView->xAxis->setSubTickLengthOut(5);
plotView->xAxis->ticker()->setTickOrigin(1);

축 펜 및 색상

plotView->xAxis->setBasePen(QPen(Qt::white, 1));
plotView->xAxis->setTickPen(QPen(Qt::white, 1));
plotView->xAxis->setSubTickPen(QPen(Qt::white, 1));
plotView->xAxis->setTickLabelColor(Qt::white);
plotView->xAxis->setLabelColor(Qt::white);

plotView->xAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);
plotView->xAxis->setLowerEnding(QCPLineEnding::esDisc);
plotView->yAxis-&setUpperEnding(QCPLineEnding::esSquare);

축 동기화

connect(plotView->xAxis, SIGNAL(rangeChanged(QCPRange)),
        plotView->xAxis2, SLOT(setRange(QCPRange)));
connect(plotView->yAxis, SIGNAL(rangeChanged(QCPRange)),
        plotView->yAxis2, SLOT(setRange(QCPRange)));

시간 형식 축

X을 날짜/시간으로 표시하려면 QCPAxisTickerDateTime를 사용한다. 내부적으로는 epoch 기준 초 단위 double 값으로 변환해야 한다.

QSharedPointer<QCPAxisTickerDateTime> dateTicker(new QCPAxisTickerDateTime);
dateTicker->setDateTimeFormat("yyyy-MM-dd hh:mm:ss");
plotView->xAxis->setTicker(dateTicker);

QDateTime dt = QDateTime::currentDateTime();
double key = dt.toMSecsSinceEpoch() / 1000.0;

범 및 상호작용

범례를 활성화하려면 legend의 setVisible을 호출한다.

plotView->legend->setVisible(true);

사용자 상호작용은 setInteractions로 한 번에 설정할 수 있다.

plotView->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom |
                          QCP::iSelectAxes | QCP::iSelectLegend |
                          QCP::iSelectPlottables);

자동 범위 및 축소/확대

이터에 맞게 축 범위를 재조정하려면 rescaleAxes를 사용한다. 여러 곡선이 있을 때는 첫 번째는 제한 없이, 이후 곡선은 onlyEnlarge를 true로 설정해 기존 범위를 유지한다.

plotView->graph(0)->rescaleAxes();
plotView->graph(1)->rescaleAxes(true);
plotView->graph(2)->rescaleAxes(true);

확대/축소 비율은 setRangeZoomFactor로 조정한다.

plotView->axisRect()->setRangeZoomFactor(1.2, 1.0);

마우스 휠로 특정 축만 확대/축소하려면 wheelEvent를 재정의한다.

void MainWindow::wheelEvent(QWheelEvent *event)
{
    if (plotView->xAxis->selectedParts().testFlag(QCPAxis::spAxis)) {
        plotView->axisRect()->setRangeZoom(plotView->xAxis->orientation());
    } else if (plotView->yAxis->selectedParts().testFlag(QCPAxis::spAxis)) {
        plotView->axisRect()->setRangeZoom(plotView->yAxis->orientation());
    } else {
        plotView->axisRect()->setRangeZoom(Qt::Horizontal | Qt::Vertical);
    }
    QMainWindow::wheelEvent(event);
}

이미지 내보내기

그려진 차트를 PNG, PDF 등으로 저장할 수 있다.

plotView->savePng("chart.png", 400, 300);
plotView->savePdf("chart.pdf");

완성 예제

#include "qcustomplot.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QCustomPlot *plotView = new QCustomPlot(this);
    plotView->resize(400, 400);

    QVector<double> x(101), y(101);
    for (int i = 0; i < 101; ++i) {
        x[i] = i / 5.0 - 10.0;
        y[i] = qPow(x[i], 3);
    }

    QCPGraph *curve = plotView->addGraph();
    curve->setData(x, y);
    curve->setPen(QPen(QColor(32, 178, 170), 2));

    plotView->xAxis->setLabel("x");
    plotView->yAxis->setLabel("y");
    plotView->setBackground(QColor(50, 50, 50));
    plotView->xAxis->setTickLabelColor(Qt::white);
    plotView->xAxis->setLabelColor(QColor(0, 160, 230));
    plotView->xAxis->setBasePen(QPen(QColor(32, 178, 170)));
    plotView->xAxis->setTickPen(QPen(QColor(128, 0, 255)));
    plotView->xAxis->setSubTickPen(QColor(255, 165, 0));

    QFont font = plotView->xAxis->labelFont();
    font.setPixelSize(20);
    plotView->xAxis->setLabelFont(font);

    plotView->yAxis->setTickLabelColor(Qt::white);
    plotView->yAxis->setLabelColor(QColor(0, 160, 230));
    plotView->yAxis->setBasePen(QPen(QColor(32, 178, 170)));
    plotView->yAxis->setTickPen(QPen(QColor(128, 0, 255)));
    plotView->yAxis->setSubTickPen(QColor(255, 165, 0));
    QFont yFont = plotView->yAxis->labelFont();
    yFont.setPixelSize(20);
    plotView->yAxis->setLabelFont(yFont);

    plotView->xAxis->setRange(-11, 11);
    plotView->yAxis->setRange(-1100, 1100);
    plotView->replot();
}

태그: QCustomPlot Qt QCPGraph QCPAxis QCPAxisTickerDateTime

10월 11일 03:45에 게시됨