From 5addb87d55c02d085945e5ece5a211a2883182ed Mon Sep 17 00:00:00 2001 From: "Jesper K. Pedersen" Date: Mon, 1 Dec 2025 17:45:44 +0100 Subject: [PATCH 1/3] Support showing median values in CandleStick plots. --- examples/Stock/Advanced/OHLC.csv | 14 +++++----- examples/Stock/Advanced/mainwindow.cpp | 2 ++ src/KDChart/Cartesian/KDChartStockDiagram.cpp | 28 +++++++++++++++++-- src/KDChart/Cartesian/KDChartStockDiagram.h | 2 ++ .../Cartesian/KDChartStockDiagram_p.cpp | 27 +++++++++++++++++- src/KDChart/Cartesian/KDChartStockDiagram_p.h | 3 +- 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/examples/Stock/Advanced/OHLC.csv b/examples/Stock/Advanced/OHLC.csv index 3f741c7bd..014716ba0 100644 --- a/examples/Stock/Advanced/OHLC.csv +++ b/examples/Stock/Advanced/OHLC.csv @@ -1,8 +1,8 @@ ,"Foobar Inc." -January, 20.3, 42.4, 10.3, 32.0 -February, 32.0, 37.3, 20.0, 25.9 -March, 25.9, 33.7, 20.3, 29.0 -April, 29.9, 53.3, 28.1, 39.0 -May, 39.0, 45.0, 36.4, 41.9 -June, 41.9, 63.3, 41.0, 58.7 -July, 58.7, 68.5, 50.7, 55.3 +January, 20.3, 42.4, 10.3, 32.0, 23 +February, 32.0, 37.3, 20.0, 25.9, 28.4 +March, 25.9, 33.7, 20.3, 29.0, 26.5 +April, 29.9, 53.3, 28.1, 39.0, 35 +May, 39.0, 45.0, 36.4, 41.9, 40 +June, 41.9, 63.3, 41.0, 58.7, 55 +July, 58.7, 68.5, 50.7, 55.3, 56 diff --git a/examples/Stock/Advanced/mainwindow.cpp b/examples/Stock/Advanced/mainwindow.cpp index e7548b786..7845601c7 100644 --- a/examples/Stock/Advanced/mainwindow.cpp +++ b/examples/Stock/Advanced/mainwindow.cpp @@ -72,6 +72,7 @@ void MainWindow::applyColor(const QColor &color) QColor inverse(255 - color.red(), 255 - color.green(), 255 - color.blue()); m_diagram.setPen(1, QPen(inverse.darker(130))); m_diagram.setBrush(1, QBrush(inverse)); + QPalette pal = colorChooser->palette(); pal.setBrush(QPalette::Button, QBrush(color)); colorChooser->setPalette(pal); @@ -133,6 +134,7 @@ void MainWindow::on_stockTypeCB_currentIndexChanged(int index) } else if (text == "Candlestick") { m_diagram.setType(StockDiagram::Candlestick); m_diagram.setModel(&m_OHLCModel); + m_diagram.setDataContainsMedianValues(true); } m_chart->coordinatePlane()->replaceDiagram(&m_diagram); diff --git a/src/KDChart/Cartesian/KDChartStockDiagram.cpp b/src/KDChart/Cartesian/KDChartStockDiagram.cpp index f9b0e8a0d..62ca05cf1 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram.cpp +++ b/src/KDChart/Cartesian/KDChartStockDiagram.cpp @@ -286,7 +286,19 @@ void StockDiagram::paint(PaintContext *context) PainterSaver painterSaver(context->painter()); const int rowCount = attributesModel()->rowCount(attributesModelRootIndex()); - const int divisor = (d->type == OpenHighLowClose || d->type == Candlestick) ? 4 : 3; + const int divisor = [&] { + switch (d->type) { + case HighLowClose: + return 3; + case OpenHighLowClose: + return 4; + case Candlestick: + return d->dataContainsMedian ? 5 : 4; + break; + } + Q_UNREACHABLE(); + }(); + const int colCount = attributesModel()->columnCount(attributesModelRootIndex()) / divisor; for (int col = 0; col < colCount; ++col) { for (int row = 0; row < rowCount; row++) { @@ -295,6 +307,8 @@ void StockDiagram::paint(PaintContext *context) CartesianDiagramDataCompressor::DataPoint open; CartesianDiagramDataCompressor::DataPoint close; CartesianDiagramDataCompressor::DataPoint volume; + CartesianDiagramDataCompressor::DataPoint median; + if (d->type == HighLowClose) { const CartesianDiagramDataCompressor::CachePosition highPos(row, col * divisor); @@ -312,6 +326,10 @@ void StockDiagram::paint(PaintContext *context) low = d->compressor.data(lowPos); high = d->compressor.data(highPos); close = d->compressor.data(closePos); + if (d->type == Candlestick) { + const CartesianDiagramDataCompressor::CachePosition medianPos(row, col * divisor + 4); + median = d->compressor.data(medianPos); + } } switch (d->type) { @@ -323,7 +341,8 @@ void StockDiagram::paint(PaintContext *context) d->drawOHLCBar(col, open, high, low, close, context); break; case Candlestick: - d->drawCandlestick(col, open, high, low, close, context); + median.hidden = !d->dataContainsMedian; + d->drawCandlestick(col, open, high, low, close, median, context); break; } } @@ -352,6 +371,11 @@ qreal StockDiagram::threeDItemDepth(const QModelIndex &index) const return 1.0; } +void StockDiagram::setDataContainsMedianValues(bool b) +{ + d->dataContainsMedian = b; +} + const QPair StockDiagram::calculateDataBoundaries() const { const int rowCount = attributesModel()->rowCount(attributesModelRootIndex()); diff --git a/src/KDChart/Cartesian/KDChartStockDiagram.h b/src/KDChart/Cartesian/KDChartStockDiagram.h index 1deb2c558..e71602490 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram.h +++ b/src/KDChart/Cartesian/KDChartStockDiagram.h @@ -98,6 +98,8 @@ class KDCHART_EXPORT StockDiagram : public AbstractCartesianDiagram qreal threeDItemDepth(int column) const override; qreal threeDItemDepth(const QModelIndex &index) const override; + void setDataContainsMedianValues(bool b = true); + protected: const QPair calculateDataBoundaries() const override; }; diff --git a/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp b/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp index 02820be04..eb961c23c 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp +++ b/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp @@ -25,7 +25,7 @@ class StockDiagram::Private::ThreeDPainter }; ThreeDPainter(QPainter *p) - : painter(p) { }; + : painter(p) {}; QPolygonF drawTwoDLine(const QLineF &line, const QPen &pen, const ThreeDProperties &props); @@ -328,6 +328,7 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag const CartesianDiagramDataCompressor::DataPoint &high, const CartesianDiagramDataCompressor::DataPoint &low, const CartesianDiagramDataCompressor::DataPoint &close, + const CartesianDiagramDataCompressor::DataPoint &median, PaintContext *context) { PainterSaver painterSaver(context->painter()); @@ -342,6 +343,7 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag QPen pen; bool drawLowerLine; bool drawCandlestick = !open.hidden && !close.hidden; + bool drawMedian = !median.hidden; bool drawUpperLine; // Find out if we need to paint a down-trend or up-trend candlestick @@ -363,6 +365,19 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag drawUpperLine = !low.hidden && !open.hidden; } + auto medianColor = [&] { + auto brushColor = brush.color(); + if (brushColor.isValid()) { + return (0.21 * brushColor.red() + 0.72 * brushColor.green() + 0.07 * brushColor.blue()) < 127 + ? QColor(Qt::white) + : QColor(Qt::black); + } else { + // Fallback to pen color, we could do better by letting the caller provide the pen, but I'm out of attention span for that. + return pen.color(); + } + }(); + + StockBarAttributes attr = stockDiagram()->stockBarAttributes(col); ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes(col); @@ -375,6 +390,9 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag QRectF candlestick = projectCandlestick(context, bottomCandlestickPoint, topCandlestickPoint, attr.candlestickWidth()); + const QPointF medianPoint = projectPoint(context, QPointF(median.key, median.value)); + const QLineF medianLine = QLineF(candlestick.left(), medianPoint.y(), candlestick.right(), medianPoint.y()); + // Remember the drawn polygon to add it to the ReverseMapper later QPolygonF drawnPolygon; @@ -396,6 +414,7 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag drawnPolygon = threeDPainter.drawThreeDRect(candlestick, brush, pen, threeDProps); if (drawUpperLine) drawnPolygon = threeDPainter.drawTwoDLine(upperLine, pen, threeDProps); + // TODO: Draw median in 3D } else { if (drawUpperLine) drawnPolygon = threeDPainter.drawTwoDLine(upperLine, pen, threeDProps); @@ -403,6 +422,7 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag drawnPolygon = threeDPainter.drawThreeDRect(candlestick, brush, pen, threeDProps); if (drawLowerLine) drawnPolygon = threeDPainter.drawTwoDLine(lowerLine, pen, threeDProps); + // TODO: Draw median in 3D } } else { QPainter *const painter = context->painter(); @@ -414,6 +434,11 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag painter->drawLine(upperLine); if (drawCandlestick) painter->drawRect(candlestick); + if (drawMedian) { + painter->setPen(medianColor); + painter->drawLine(medianLine); + painter->setPen(pen); + } // The 2D representation is the projected candlestick itself drawnPolygon = candlestick; diff --git a/src/KDChart/Cartesian/KDChartStockDiagram_p.h b/src/KDChart/Cartesian/KDChartStockDiagram_p.h index c9b8613a8..6ce773346 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram_p.h +++ b/src/KDChart/Cartesian/KDChartStockDiagram_p.h @@ -45,6 +45,7 @@ class StockDiagram::Private : public AbstractCartesianDiagram::Private QPen lowHighLinePen; QMap lowHighLinePens; + bool dataContainsMedian = false; void drawOHLCBar(int dataset, const CartesianDiagramDataCompressor::DataPoint &open, const CartesianDiagramDataCompressor::DataPoint &high, @@ -58,7 +59,7 @@ class StockDiagram::Private : public AbstractCartesianDiagram::Private void drawCandlestick(int dataset, const CartesianDiagramDataCompressor::DataPoint &open, const CartesianDiagramDataCompressor::DataPoint &high, const CartesianDiagramDataCompressor::DataPoint &low, - const CartesianDiagramDataCompressor::DataPoint &close, + const CartesianDiagramDataCompressor::DataPoint &close, const CartesianDiagramDataCompressor::DataPoint &median, PaintContext *context); private: From fb859e04c9c69dd6fd77a0dbcdd19753a41ca33d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:52:22 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/KDChart/Cartesian/KDChartStockDiagram_p.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp b/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp index eb961c23c..491fc957f 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp +++ b/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp @@ -25,7 +25,7 @@ class StockDiagram::Private::ThreeDPainter }; ThreeDPainter(QPainter *p) - : painter(p) {}; + : painter(p) { }; QPolygonF drawTwoDLine(const QLineF &line, const QPen &pen, const ThreeDProperties &props); From ea10d94780742bc50803a5bc5e5ebde6843fe70b Mon Sep 17 00:00:00 2001 From: "Jesper K. Pedersen" Date: Mon, 1 Dec 2025 20:35:56 +0100 Subject: [PATCH 3/3] Fixed from review --- src/KDChart/Cartesian/KDChartStockDiagram.cpp | 1 - src/KDChart/Cartesian/KDChartStockDiagram_p.cpp | 12 +++++++----- src/KDChart/Cartesian/KDChartStockDiagram_p.h | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/KDChart/Cartesian/KDChartStockDiagram.cpp b/src/KDChart/Cartesian/KDChartStockDiagram.cpp index 62ca05cf1..32b3340bb 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram.cpp +++ b/src/KDChart/Cartesian/KDChartStockDiagram.cpp @@ -294,7 +294,6 @@ void StockDiagram::paint(PaintContext *context) return 4; case Candlestick: return d->dataContainsMedian ? 5 : 4; - break; } Q_UNREACHABLE(); }(); diff --git a/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp b/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp index 491fc957f..25302f358 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp +++ b/src/KDChart/Cartesian/KDChartStockDiagram_p.cpp @@ -375,7 +375,7 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag // Fallback to pen color, we could do better by letting the caller provide the pen, but I'm out of attention span for that. return pen.color(); } - }(); + }; StockBarAttributes attr = stockDiagram()->stockBarAttributes(col); @@ -390,8 +390,10 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag QRectF candlestick = projectCandlestick(context, bottomCandlestickPoint, topCandlestickPoint, attr.candlestickWidth()); - const QPointF medianPoint = projectPoint(context, QPointF(median.key, median.value)); - const QLineF medianLine = QLineF(candlestick.left(), medianPoint.y(), candlestick.right(), medianPoint.y()); + const auto medianLine = [&] { + const QPointF medianPoint = projectPoint(context, QPointF(median.key, median.value)); + return QLineF(candlestick.left(), medianPoint.y(), candlestick.right(), medianPoint.y()); + }; // Remember the drawn polygon to add it to the ReverseMapper later QPolygonF drawnPolygon; @@ -435,8 +437,8 @@ void StockDiagram::Private::drawCandlestick(int /*dataset*/, const CartesianDiag if (drawCandlestick) painter->drawRect(candlestick); if (drawMedian) { - painter->setPen(medianColor); - painter->drawLine(medianLine); + painter->setPen(medianColor()); + painter->drawLine(medianLine()); painter->setPen(pen); } diff --git a/src/KDChart/Cartesian/KDChartStockDiagram_p.h b/src/KDChart/Cartesian/KDChartStockDiagram_p.h index 6ce773346..aa795c4d6 100644 --- a/src/KDChart/Cartesian/KDChartStockDiagram_p.h +++ b/src/KDChart/Cartesian/KDChartStockDiagram_p.h @@ -59,7 +59,8 @@ class StockDiagram::Private : public AbstractCartesianDiagram::Private void drawCandlestick(int dataset, const CartesianDiagramDataCompressor::DataPoint &open, const CartesianDiagramDataCompressor::DataPoint &high, const CartesianDiagramDataCompressor::DataPoint &low, - const CartesianDiagramDataCompressor::DataPoint &close, const CartesianDiagramDataCompressor::DataPoint &median, + const CartesianDiagramDataCompressor::DataPoint &close, + const CartesianDiagramDataCompressor::DataPoint &median, PaintContext *context); private: