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