File indexing completed on 2024-12-15 04:02:33
0001 /* 0002 * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. 0003 * 0004 * This file is part of the KD Chart library. 0005 * 0006 * SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "KChartStockDiagram_p.h" 0010 0011 #include "KChartPainterSaver_p.h" 0012 0013 using namespace KChart; 0014 0015 0016 class Q_DECL_HIDDEN StockDiagram::Private::ThreeDPainter 0017 { 0018 public: 0019 struct ThreeDProperties { 0020 qreal depth; 0021 qreal angle; 0022 bool useShadowColors; 0023 }; 0024 0025 ThreeDPainter( QPainter *p ) 0026 : painter( p ) {}; 0027 0028 QPolygonF drawTwoDLine( const QLineF &line, const QPen &pen, 0029 const ThreeDProperties &props ); 0030 QPolygonF drawThreeDLine( const QLineF &line, const QBrush &brush, 0031 const QPen &pen, const ThreeDProperties &props ); 0032 QPolygonF drawThreeDRect( const QRectF &rect, const QBrush &brush, 0033 const QPen &pen, const ThreeDProperties &props ); 0034 0035 private: 0036 QPointF projectPoint( const QPointF &point, qreal depth, qreal angle ) const; 0037 QColor calcShadowColor( const QColor &color, qreal angle ) const; 0038 0039 QPainter *painter; 0040 }; 0041 0042 /* 0043 * Projects a point in 3D space 0044 * 0045 * @param depth The distance from the point and the projected point 0046 * @param angle The angle the projected point is rotated by around the original point 0047 */ 0048 QPointF StockDiagram::Private::ThreeDPainter::projectPoint( const QPointF &point, qreal depth, qreal angle ) const 0049 { 0050 const qreal angleInRad = DEGTORAD( angle ); 0051 const qreal distX = depth * cos( angleInRad ); 0052 // Y coordinates are reversed on our coordinate plane 0053 const qreal distY = depth * -sin( angleInRad ); 0054 0055 return QPointF( point.x() + distX, point.y() + distY ); 0056 } 0057 0058 /* 0059 * Returns the shadow color for a given color, depending on the angle of rotation 0060 * 0061 * @param color The color to calculate the shadow color for 0062 * @param angle The angle that the colored area is rotated by 0063 */ 0064 QColor StockDiagram::Private::ThreeDPainter::calcShadowColor( const QColor &color, qreal angle ) const 0065 { 0066 // The shadow factor determines to how many percent the brightness 0067 // of the color can be reduced. That is, the darkest shadow color 0068 // is color * shadowFactor. 0069 const qreal shadowFactor = 0.5; 0070 const qreal sinAngle = 1.0 - qAbs( sin( DEGTORAD( angle ) ) ) * shadowFactor; 0071 return QColor( qRound( color.red() * sinAngle ), 0072 qRound( color.green() * sinAngle ), 0073 qRound( color.blue() * sinAngle ) ); 0074 } 0075 0076 /* 0077 * Draws a 2D line in 3D space by painting it with a z-coordinate of props.depth / 2.0 0078 * 0079 * @param line The line to draw 0080 * @param pen The pen to use to draw the line 0081 * @param props The 3D properties to draw the line with 0082 * @return The drawn line, but with a width of 2px, as a polygon 0083 */ 0084 QPolygonF StockDiagram::Private::ThreeDPainter::drawTwoDLine( const QLineF &line, const QPen &pen, 0085 const ThreeDProperties &props ) 0086 { 0087 // Restores the painting properties when destroyed 0088 PainterSaver painterSaver( painter ); 0089 0090 // The z coordinate to use (i.e., at what depth to draw the line) 0091 const qreal z = props.depth / 2.0; 0092 0093 // Projec the 2D points of the line in 3D 0094 const QPointF deepP1 = projectPoint( line.p1(), z, props.angle ); 0095 const QPointF deepP2 = projectPoint( line.p2(), z, props.angle ); 0096 0097 // The drawn line with a width of 2px 0098 QPolygonF threeDArea; 0099 // The offset of the line "borders" from the center to each side 0100 const QPointF offset( 0.0, 1.0 ); 0101 threeDArea << deepP1 - offset << deepP2 - offset 0102 << deepP1 + offset << deepP2 + offset << deepP1 - offset; 0103 0104 painter->setPen( pen ); 0105 painter->drawLine( QLineF( deepP1, deepP2 ) ); 0106 0107 return threeDArea; 0108 } 0109 0110 /* 0111 * Draws an ordinary line in 3D by expanding it in the z-axis by the given depth. 0112 * 0113 * @param line The line to draw 0114 * @param brush The brush to fill the resulting polygon with 0115 * @param pen The pen to paint the borders of the resulting polygon with 0116 * @param props The 3D properties to draw the line with 0117 * @return The 3D shape drawn 0118 */ 0119 QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDLine( const QLineF &line, const QBrush &brush, 0120 const QPen &pen, const ThreeDProperties &props ) 0121 { 0122 // Restores the painting properties when destroyed 0123 PainterSaver painterSaver( painter ); 0124 0125 const QPointF p1 = line.p1(); 0126 const QPointF p2 = line.p2(); 0127 0128 // Project the 2D points of the line in 3D 0129 const QPointF deepP1 = projectPoint( p1, props.depth, props.angle ); 0130 const QPointF deepP2 = projectPoint( p2, props.depth, props.angle ); 0131 0132 // The result is a 3D representation of the 2D line 0133 QPolygonF threeDArea; 0134 threeDArea << p1 << p2 << deepP2 << deepP1 << p1; 0135 0136 // Use shadow colors if ThreeDProperties::useShadowColors is set 0137 // Note: Setting a new color on a brush or pen does not effect gradients or textures 0138 if ( props.useShadowColors ) { 0139 QBrush shadowBrush( brush ); 0140 QPen shadowPen( pen ); 0141 shadowBrush.setColor( calcShadowColor( brush.color(), props.angle ) ); 0142 shadowPen.setColor( calcShadowColor( pen.color(), props.angle ) ); 0143 painter->setBrush( shadowBrush ); 0144 painter->setPen( shadowPen ); 0145 } else { 0146 painter->setBrush( brush ); 0147 painter->setPen( pen ); 0148 } 0149 0150 painter->drawPolygon( threeDArea ); 0151 0152 return threeDArea; 0153 } 0154 0155 /* 0156 * Draws a 3D cuboid by extending a 2D rectangle in the z-axis 0157 * 0158 * @param rect The rectangle to draw 0159 * @param brush The brush fill the surfaces of the cuboid with 0160 * @param pen The pen to draw the edges with 0161 * @param props The 3D properties to use for drawing the cuboid 0162 * @return The drawn cuboid as a polygon 0163 */ 0164 QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDRect( const QRectF &rect, const QBrush &brush, 0165 const QPen &pen, const ThreeDProperties &props ) 0166 { 0167 // Restores the painting properties when destroyed 0168 PainterSaver painterSaver( painter ); 0169 0170 // Make sure that the top really is the top 0171 const QRectF normalizedRect = rect.normalized(); 0172 0173 // Calculate all the four sides of the rectangle 0174 const QLineF topSide = QLineF( normalizedRect.topLeft(), normalizedRect.topRight() ); 0175 const QLineF bottomSide = QLineF( normalizedRect.bottomLeft(), normalizedRect.bottomRight() ); 0176 const QLineF leftSide = QLineF( normalizedRect.topLeft(), normalizedRect.bottomLeft() ); 0177 const QLineF rightSide = QLineF( normalizedRect.topRight(), normalizedRect.bottomRight() ); 0178 0179 QPolygonF drawnPolygon; 0180 0181 // Shorter names are easier on the eyes 0182 const qreal angle = props.angle; 0183 0184 // Only top and right side is visible 0185 if ( angle >= 0.0 && angle < 90.0 ) { 0186 drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) ); 0187 drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) ); 0188 // Only top and left side is visible 0189 } else if ( angle >= 90.0 && angle < 180.0 ) { 0190 drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) ); 0191 drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) ); 0192 // Only bottom and left side is visible 0193 } else if ( angle >= 180.0 && angle < 270.0 ) { 0194 drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) ); 0195 drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) ); 0196 // Only bottom and right side is visible 0197 } else if ( angle >= 270.0 && angle <= 360.0 ) { 0198 drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) ); 0199 drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) ); 0200 } 0201 0202 // Draw the front side 0203 painter->setPen( pen ); 0204 painter->setBrush( brush ); 0205 painter->drawRect( normalizedRect ); 0206 0207 return drawnPolygon; 0208 } 0209 0210 0211 StockDiagram::Private::Private() 0212 : AbstractCartesianDiagram::Private() 0213 { 0214 } 0215 0216 StockDiagram::Private::Private( const Private& r ) 0217 : AbstractCartesianDiagram::Private( r ) 0218 { 0219 } 0220 0221 StockDiagram::Private::~Private() 0222 { 0223 } 0224 0225 /* 0226 * Projects a point onto the coordinate plane 0227 * 0228 * @param context The context to paint the point in 0229 * @point The point to project onto the coordinate plane 0230 * @return The projected point 0231 */ 0232 QPointF StockDiagram::Private::projectPoint( PaintContext *context, const QPointF &point ) const 0233 { 0234 return context->coordinatePlane()->translate( QPointF( point.x() + 0.5, point.y() ) ); 0235 } 0236 0237 /* 0238 * Projects a candlestick onto the coordinate plane 0239 * 0240 * @param context The context to paint the candlestick in 0241 * @param low The 0242 */ 0243 QRectF StockDiagram::Private::projectCandlestick( PaintContext *context, const QPointF &open, const QPointF &close, qreal width ) const 0244 { 0245 const QPointF leftHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 - width / 2.0, close.y() ) ); 0246 const QPointF rightLowPoint = context->coordinatePlane()->translate( QPointF( open.x() + 0.5 + width / 2.0, open.y() ) ); 0247 const QPointF rightHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 + width / 2.0, close.y() ) ); 0248 0249 return QRectF( leftHighPoint, QSizeF( rightHighPoint.x() - leftHighPoint.x(), 0250 rightLowPoint.y() - leftHighPoint.y() ) ); 0251 } 0252 0253 void StockDiagram::Private::drawOHLCBar( int dataset, const CartesianDiagramDataCompressor::DataPoint &open, 0254 const CartesianDiagramDataCompressor::DataPoint &high, 0255 const CartesianDiagramDataCompressor::DataPoint &low, 0256 const CartesianDiagramDataCompressor::DataPoint &close, 0257 PaintContext *context ) 0258 { 0259 // Note: A row in the model is a column in a StockDiagram 0260 const int col = low.index.row(); 0261 0262 StockBarAttributes attr = stockDiagram()->stockBarAttributes( col ); 0263 ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col ); 0264 const qreal tickLength = attr.tickLength(); 0265 0266 const QPointF leftOpenPoint( open.key + 0.5 - tickLength, open.value ); 0267 const QPointF rightOpenPoint( open.key + 0.5, open.value ); 0268 const QPointF highPoint( high.key + 0.5, high.value ); 0269 const QPointF lowPoint( low.key + 0.5, low.value ); 0270 const QPointF leftClosePoint( close.key + 0.5, close.value ); 0271 const QPointF rightClosePoint( close.key + 0.5 + tickLength, close.value ); 0272 0273 bool reversedOrder = false; 0274 // If 3D mode is enabled, we have to make sure the z-order is right 0275 if ( threeDAttr.isEnabled() ) { 0276 const int angle = threeDAttr.angle(); 0277 // Z-order is from right to left 0278 if ( ( angle >= 0 && angle < 90 ) || ( angle >= 180 && angle < 270 ) ) 0279 reversedOrder = true; 0280 // Z-order is from left to right 0281 if ( ( angle >= 90 && angle < 180 ) || ( angle >= 270 && angle <= 360 ) ) 0282 reversedOrder = false; 0283 } 0284 0285 if ( reversedOrder ) { 0286 if ( !open.hidden ) 0287 drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker 0288 if ( !low.hidden && !high.hidden ) 0289 drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line 0290 if ( !close.hidden ) 0291 drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker 0292 } else { 0293 if ( !close.hidden ) 0294 drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker 0295 if ( !low.hidden && !high.hidden ) 0296 drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line 0297 if ( !open.hidden ) 0298 drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker 0299 } 0300 0301 LabelPaintCache lpc; 0302 if ( !open.hidden ) { 0303 addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr, 0304 PositionPoints( leftOpenPoint ), Position::South, Position::South, open.value ); 0305 } 0306 if ( !high.hidden ) { 0307 addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr, 0308 PositionPoints( highPoint ), Position::South, Position::South, high.value ); 0309 } 0310 if ( !low.hidden ) { 0311 addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr, 0312 PositionPoints( lowPoint ), Position::South, Position::South, low.value ); 0313 } 0314 if ( !close.hidden ) { 0315 addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr, 0316 PositionPoints( rightClosePoint ), Position::South, Position::South, close.value ); 0317 } 0318 paintDataValueTextsAndMarkers( context, lpc, false ); 0319 } 0320 0321 /* 0322 * Draws a line connecting the low and the high value of an OHLC chart 0323 * 0324 * @param low The low data point 0325 * @param high The high data point 0326 * @param context The context to draw the candlestick in 0327 */ 0328 void StockDiagram::Private::drawCandlestick( int /*dataset*/, const CartesianDiagramDataCompressor::DataPoint &open, 0329 const CartesianDiagramDataCompressor::DataPoint &high, 0330 const CartesianDiagramDataCompressor::DataPoint &low, 0331 const CartesianDiagramDataCompressor::DataPoint &close, 0332 PaintContext *context ) 0333 { 0334 PainterSaver painterSaver( context->painter() ); 0335 0336 // Note: A row in the model is a column in a StockDiagram, and the other way around 0337 const int row = low.index.row(); 0338 const int col = low.index.column(); 0339 0340 QPointF bottomCandlestickPoint; 0341 QPointF topCandlestickPoint; 0342 QBrush brush; 0343 QPen pen; 0344 bool drawLowerLine; 0345 bool drawCandlestick = !open.hidden && !close.hidden; 0346 bool drawUpperLine; 0347 0348 // Find out if we need to paint a down-trend or up-trend candlestick 0349 // and set brush and pen accordingly 0350 // Also, determine what the top and bottom points of the candlestick are 0351 if ( open.value <= close.value ) { 0352 pen = stockDiagram()->upTrendCandlestickPen( row ); 0353 brush = stockDiagram()->upTrendCandlestickBrush( row ); 0354 bottomCandlestickPoint = QPointF( open.key, open.value ); 0355 topCandlestickPoint = QPointF( close.key, close.value ); 0356 drawLowerLine = !low.hidden && !open.hidden; 0357 drawUpperLine = !low.hidden && !close.hidden; 0358 } else { 0359 pen = stockDiagram()->downTrendCandlestickPen( row ); 0360 brush = stockDiagram()->downTrendCandlestickBrush( row ); 0361 bottomCandlestickPoint = QPointF( close.key, close.value ); 0362 topCandlestickPoint = QPointF( open.key, open.value ); 0363 drawLowerLine = !low.hidden && !close.hidden; 0364 drawUpperLine = !low.hidden && !open.hidden; 0365 } 0366 0367 StockBarAttributes attr = stockDiagram()->stockBarAttributes( col ); 0368 ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col ); 0369 0370 const QPointF lowPoint = projectPoint( context, QPointF( low.key, low.value ) ); 0371 const QPointF highPoint = projectPoint( context, QPointF( high.key, high.value ) ); 0372 const QLineF lowerLine = QLineF( lowPoint, projectPoint( context, bottomCandlestickPoint ) ); 0373 const QLineF upperLine = QLineF( projectPoint( context, topCandlestickPoint ), highPoint ); 0374 0375 // Convert the data point into coordinates on the coordinate plane 0376 QRectF candlestick = projectCandlestick( context, bottomCandlestickPoint, 0377 topCandlestickPoint, attr.candlestickWidth() ); 0378 0379 // Remember the drawn polygon to add it to the ReverseMapper later 0380 QPolygonF drawnPolygon; 0381 0382 // Use the ThreeDPainter class to draw a 3D candlestick 0383 if ( threeDAttr.isEnabled() ) { 0384 ThreeDPainter threeDPainter( context->painter() ); 0385 0386 ThreeDPainter::ThreeDProperties threeDProps; 0387 threeDProps.depth = threeDAttr.depth(); 0388 threeDProps.angle = threeDAttr.angle(); 0389 threeDProps.useShadowColors = threeDAttr.useShadowColors(); 0390 0391 // If the perspective angle is within [0,180], we paint from bottom to top, 0392 // otherwise from top to bottom to ensure the correct z order 0393 if ( threeDProps.angle > 0.0 && threeDProps.angle < 180.0 ) { 0394 if ( drawLowerLine ) 0395 drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps ); 0396 if ( drawCandlestick ) 0397 drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps ); 0398 if ( drawUpperLine ) 0399 drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps ); 0400 } else { 0401 if ( drawUpperLine ) 0402 drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps ); 0403 if ( drawCandlestick ) 0404 drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps ); 0405 if ( drawLowerLine ) 0406 drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps ); 0407 } 0408 } else { 0409 QPainter *const painter = context->painter(); 0410 painter->setBrush( brush ); 0411 painter->setPen( pen ); 0412 if ( drawLowerLine ) 0413 painter->drawLine( lowerLine ); 0414 if ( drawUpperLine ) 0415 painter->drawLine( upperLine ); 0416 if ( drawCandlestick ) 0417 painter->drawRect( candlestick ); 0418 0419 // The 2D representation is the projected candlestick itself 0420 drawnPolygon = candlestick; 0421 0422 // FIXME: Add lower and upper line to reverse mapper 0423 } 0424 0425 LabelPaintCache lpc; 0426 if ( !low.hidden ) 0427 addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr, 0428 PositionPoints( lowPoint ), Position::South, Position::South, low.value ); 0429 if ( drawCandlestick ) { 0430 // Both, the open as well as the close value are represented by this candlestick 0431 reverseMapper.addPolygon( row, openValueColumn(), drawnPolygon ); 0432 reverseMapper.addPolygon( row, closeValueColumn(), drawnPolygon ); 0433 0434 addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr, 0435 PositionPoints( candlestick.bottomRight() ), Position::South, Position::South, open.value ); 0436 addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr, 0437 PositionPoints( candlestick.topRight() ), Position::South, Position::South, close.value ); 0438 } 0439 if ( !high.hidden ) 0440 addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr, 0441 PositionPoints( highPoint ), Position::South, Position::South, high.value ); 0442 0443 paintDataValueTextsAndMarkers( context, lpc, false ); 0444 } 0445 0446 /* 0447 * Draws a line connecting two points 0448 * 0449 * @param col The column of the diagram to paint the line in 0450 * @param point1 The first point 0451 * @param point2 The second point 0452 * @param context The context to draw the low-high line in 0453 */ 0454 void StockDiagram::Private::drawLine( int dataset, int col, const QPointF &point1, const QPointF &point2, PaintContext *context ) 0455 { 0456 PainterSaver painterSaver( context->painter() ); 0457 0458 // A row in the model is a column in the diagram 0459 const int modelRow = col; 0460 const int modelCol = 0; 0461 0462 const QPen pen = diagram->pen( dataset ); 0463 const QBrush brush = diagram->brush( dataset ); 0464 const ThreeDBarAttributes threeDBarAttr = stockDiagram()->threeDBarAttributes( col ); 0465 0466 QPointF transP1 = context->coordinatePlane()->translate( point1 ); 0467 QPointF transP2 = context->coordinatePlane()->translate( point2 ); 0468 QLineF line = QLineF( transP1, transP2 ); 0469 0470 if ( threeDBarAttr.isEnabled() ) { 0471 ThreeDPainter::ThreeDProperties threeDProps; 0472 threeDProps.angle = threeDBarAttr.angle(); 0473 threeDProps.depth = threeDBarAttr.depth(); 0474 threeDProps.useShadowColors = threeDBarAttr.useShadowColors(); 0475 0476 ThreeDPainter painter( context->painter() ); 0477 reverseMapper.addPolygon( modelCol, modelRow, painter.drawThreeDLine( line, brush, pen, threeDProps ) ); 0478 } else { 0479 context->painter()->setPen( pen ); 0480 //context->painter()->setBrush( brush ); 0481 reverseMapper.addLine( modelCol, modelRow, transP1, transP2 ); 0482 context->painter()->drawLine( line ); 0483 } 0484 } 0485 0486 /* 0487 * Returns the column of the open value in the model 0488 * 0489 * @return The column of the open value 0490 */ 0491 int StockDiagram::Private::openValueColumn() const 0492 { 0493 // Return an invalid column if diagram has no open values 0494 return type == HighLowClose ? -1 : 0; 0495 } 0496 0497 /* 0498 * Returns the column of the high value in the model 0499 * 0500 * @return The column of the high value 0501 */ 0502 int StockDiagram::Private::highValueColumn() const 0503 { 0504 return type == HighLowClose ? 0 : 1; 0505 } 0506 0507 /* 0508 * Returns the column of the low value in the model 0509 * 0510 * @return The column of the low value 0511 */ 0512 int StockDiagram::Private::lowValueColumn() const 0513 { 0514 return type == HighLowClose ? 1 : 2; 0515 } 0516 0517 /* 0518 * Returns the column of the close value in the model 0519 * 0520 * @return The column of the close value 0521 */ 0522 int StockDiagram::Private::closeValueColumn() const 0523 { 0524 return type == HighLowClose ? 2 : 3; 0525 } 0526