1 /*
2 * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved.
3 *
4 * This file is part of the KD Chart library.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation; either version 2 of
9 * the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20 #include "KChartStockDiagram_p.h"
21
22 #include "KChartPainterSaver_p.h"
23
24 using namespace KChart;
25
26
27 class Q_DECL_HIDDEN StockDiagram::Private::ThreeDPainter
28 {
29 public:
30 struct ThreeDProperties {
31 qreal depth;
32 qreal angle;
33 bool useShadowColors;
34 };
35
ThreeDPainter(QPainter * p)36 ThreeDPainter( QPainter *p )
37 : painter( p ) {};
38
39 QPolygonF drawTwoDLine( const QLineF &line, const QPen &pen,
40 const ThreeDProperties &props );
41 QPolygonF drawThreeDLine( const QLineF &line, const QBrush &brush,
42 const QPen &pen, const ThreeDProperties &props );
43 QPolygonF drawThreeDRect( const QRectF &rect, const QBrush &brush,
44 const QPen &pen, const ThreeDProperties &props );
45
46 private:
47 QPointF projectPoint( const QPointF &point, qreal depth, qreal angle ) const;
48 QColor calcShadowColor( const QColor &color, qreal angle ) const;
49
50 QPainter *painter;
51 };
52
53 /*
54 * Projects a point in 3D space
55 *
56 * @param depth The distance from the point and the projected point
57 * @param angle The angle the projected point is rotated by around the original point
58 */
projectPoint(const QPointF & point,qreal depth,qreal angle) const59 QPointF StockDiagram::Private::ThreeDPainter::projectPoint( const QPointF &point, qreal depth, qreal angle ) const
60 {
61 const qreal angleInRad = DEGTORAD( angle );
62 const qreal distX = depth * cos( angleInRad );
63 // Y coordinates are reversed on our coordinate plane
64 const qreal distY = depth * -sin( angleInRad );
65
66 return QPointF( point.x() + distX, point.y() + distY );
67 }
68
69 /*
70 * Returns the shadow color for a given color, depending on the angle of rotation
71 *
72 * @param color The color to calculate the shadow color for
73 * @param angle The angle that the colored area is rotated by
74 */
calcShadowColor(const QColor & color,qreal angle) const75 QColor StockDiagram::Private::ThreeDPainter::calcShadowColor( const QColor &color, qreal angle ) const
76 {
77 // The shadow factor determines to how many percent the brightness
78 // of the color can be reduced. That is, the darkest shadow color
79 // is color * shadowFactor.
80 const qreal shadowFactor = 0.5;
81 const qreal sinAngle = 1.0 - qAbs( sin( DEGTORAD( angle ) ) ) * shadowFactor;
82 return QColor( qRound( color.red() * sinAngle ),
83 qRound( color.green() * sinAngle ),
84 qRound( color.blue() * sinAngle ) );
85 }
86
87 /*
88 * Draws a 2D line in 3D space by painting it with a z-coordinate of props.depth / 2.0
89 *
90 * @param line The line to draw
91 * @param pen The pen to use to draw the line
92 * @param props The 3D properties to draw the line with
93 * @return The drawn line, but with a width of 2px, as a polygon
94 */
drawTwoDLine(const QLineF & line,const QPen & pen,const ThreeDProperties & props)95 QPolygonF StockDiagram::Private::ThreeDPainter::drawTwoDLine( const QLineF &line, const QPen &pen,
96 const ThreeDProperties &props )
97 {
98 // Restores the painting properties when destroyed
99 PainterSaver painterSaver( painter );
100
101 // The z coordinate to use (i.e., at what depth to draw the line)
102 const qreal z = props.depth / 2.0;
103
104 // Projec the 2D points of the line in 3D
105 const QPointF deepP1 = projectPoint( line.p1(), z, props.angle );
106 const QPointF deepP2 = projectPoint( line.p2(), z, props.angle );
107
108 // The drawn line with a width of 2px
109 QPolygonF threeDArea;
110 // The offset of the line "borders" from the center to each side
111 const QPointF offset( 0.0, 1.0 );
112 threeDArea << deepP1 - offset << deepP2 - offset
113 << deepP1 + offset << deepP2 + offset << deepP1 - offset;
114
115 painter->setPen( pen );
116 painter->drawLine( QLineF( deepP1, deepP2 ) );
117
118 return threeDArea;
119 }
120
121 /*
122 * Draws an ordinary line in 3D by expanding it in the z-axis by the given depth.
123 *
124 * @param line The line to draw
125 * @param brush The brush to fill the resulting polygon with
126 * @param pen The pen to paint the borders of the resulting polygon with
127 * @param props The 3D properties to draw the line with
128 * @return The 3D shape drawn
129 */
drawThreeDLine(const QLineF & line,const QBrush & brush,const QPen & pen,const ThreeDProperties & props)130 QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDLine( const QLineF &line, const QBrush &brush,
131 const QPen &pen, const ThreeDProperties &props )
132 {
133 // Restores the painting properties when destroyed
134 PainterSaver painterSaver( painter );
135
136 const QPointF p1 = line.p1();
137 const QPointF p2 = line.p2();
138
139 // Project the 2D points of the line in 3D
140 const QPointF deepP1 = projectPoint( p1, props.depth, props.angle );
141 const QPointF deepP2 = projectPoint( p2, props.depth, props.angle );
142
143 // The result is a 3D representation of the 2D line
144 QPolygonF threeDArea;
145 threeDArea << p1 << p2 << deepP2 << deepP1 << p1;
146
147 // Use shadow colors if ThreeDProperties::useShadowColors is set
148 // Note: Setting a new color on a brush or pen does not effect gradients or textures
149 if ( props.useShadowColors ) {
150 QBrush shadowBrush( brush );
151 QPen shadowPen( pen );
152 shadowBrush.setColor( calcShadowColor( brush.color(), props.angle ) );
153 shadowPen.setColor( calcShadowColor( pen.color(), props.angle ) );
154 painter->setBrush( shadowBrush );
155 painter->setPen( shadowPen );
156 } else {
157 painter->setBrush( brush );
158 painter->setPen( pen );
159 }
160
161 painter->drawPolygon( threeDArea );
162
163 return threeDArea;
164 }
165
166 /*
167 * Draws a 3D cuboid by extending a 2D rectangle in the z-axis
168 *
169 * @param rect The rectangle to draw
170 * @param brush The brush fill the surfaces of the cuboid with
171 * @param pen The pen to draw the edges with
172 * @param props The 3D properties to use for drawing the cuboid
173 * @return The drawn cuboid as a polygon
174 */
drawThreeDRect(const QRectF & rect,const QBrush & brush,const QPen & pen,const ThreeDProperties & props)175 QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDRect( const QRectF &rect, const QBrush &brush,
176 const QPen &pen, const ThreeDProperties &props )
177 {
178 // Restores the painting properties when destroyed
179 PainterSaver painterSaver( painter );
180
181 // Make sure that the top really is the top
182 const QRectF normalizedRect = rect.normalized();
183
184 // Calculate all the four sides of the rectangle
185 const QLineF topSide = QLineF( normalizedRect.topLeft(), normalizedRect.topRight() );
186 const QLineF bottomSide = QLineF( normalizedRect.bottomLeft(), normalizedRect.bottomRight() );
187 const QLineF leftSide = QLineF( normalizedRect.topLeft(), normalizedRect.bottomLeft() );
188 const QLineF rightSide = QLineF( normalizedRect.topRight(), normalizedRect.bottomRight() );
189
190 QPolygonF drawnPolygon;
191
192 // Shorter names are easier on the eyes
193 const qreal angle = props.angle;
194
195 // Only top and right side is visible
196 if ( angle >= 0.0 && angle < 90.0 ) {
197 drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) );
198 drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) );
199 // Only top and left side is visible
200 } else if ( angle >= 90.0 && angle < 180.0 ) {
201 drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) );
202 drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) );
203 // Only bottom and left side is visible
204 } else if ( angle >= 180.0 && angle < 270.0 ) {
205 drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) );
206 drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) );
207 // Only bottom and right side is visible
208 } else if ( angle >= 270.0 && angle <= 360.0 ) {
209 drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) );
210 drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) );
211 }
212
213 // Draw the front side
214 painter->setPen( pen );
215 painter->setBrush( brush );
216 painter->drawRect( normalizedRect );
217
218 return drawnPolygon;
219 }
220
221
Private()222 StockDiagram::Private::Private()
223 : AbstractCartesianDiagram::Private()
224 {
225 }
226
Private(const Private & r)227 StockDiagram::Private::Private( const Private& r )
228 : AbstractCartesianDiagram::Private( r )
229 {
230 }
231
~Private()232 StockDiagram::Private::~Private()
233 {
234 }
235
236 /*
237 * Projects a point onto the coordinate plane
238 *
239 * @param context The context to paint the point in
240 * @point The point to project onto the coordinate plane
241 * @return The projected point
242 */
projectPoint(PaintContext * context,const QPointF & point) const243 QPointF StockDiagram::Private::projectPoint( PaintContext *context, const QPointF &point ) const
244 {
245 return context->coordinatePlane()->translate( QPointF( point.x() + 0.5, point.y() ) );
246 }
247
248 /*
249 * Projects a candlestick onto the coordinate plane
250 *
251 * @param context The context to paint the candlestick in
252 * @param low The
253 */
projectCandlestick(PaintContext * context,const QPointF & open,const QPointF & close,qreal width) const254 QRectF StockDiagram::Private::projectCandlestick( PaintContext *context, const QPointF &open, const QPointF &close, qreal width ) const
255 {
256 const QPointF leftHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 - width / 2.0, close.y() ) );
257 const QPointF rightLowPoint = context->coordinatePlane()->translate( QPointF( open.x() + 0.5 + width / 2.0, open.y() ) );
258 const QPointF rightHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 + width / 2.0, close.y() ) );
259
260 return QRectF( leftHighPoint, QSizeF( rightHighPoint.x() - leftHighPoint.x(),
261 rightLowPoint.y() - leftHighPoint.y() ) );
262 }
263
drawOHLCBar(int dataset,const CartesianDiagramDataCompressor::DataPoint & open,const CartesianDiagramDataCompressor::DataPoint & high,const CartesianDiagramDataCompressor::DataPoint & low,const CartesianDiagramDataCompressor::DataPoint & close,PaintContext * context)264 void StockDiagram::Private::drawOHLCBar( int dataset, const CartesianDiagramDataCompressor::DataPoint &open,
265 const CartesianDiagramDataCompressor::DataPoint &high,
266 const CartesianDiagramDataCompressor::DataPoint &low,
267 const CartesianDiagramDataCompressor::DataPoint &close,
268 PaintContext *context )
269 {
270 // Note: A row in the model is a column in a StockDiagram
271 const int col = low.index.row();
272
273 StockBarAttributes attr = stockDiagram()->stockBarAttributes( col );
274 ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col );
275 const qreal tickLength = attr.tickLength();
276
277 const QPointF leftOpenPoint( open.key + 0.5 - tickLength, open.value );
278 const QPointF rightOpenPoint( open.key + 0.5, open.value );
279 const QPointF highPoint( high.key + 0.5, high.value );
280 const QPointF lowPoint( low.key + 0.5, low.value );
281 const QPointF leftClosePoint( close.key + 0.5, close.value );
282 const QPointF rightClosePoint( close.key + 0.5 + tickLength, close.value );
283
284 bool reversedOrder = false;
285 // If 3D mode is enabled, we have to make sure the z-order is right
286 if ( threeDAttr.isEnabled() ) {
287 const int angle = threeDAttr.angle();
288 // Z-order is from right to left
289 if ( ( angle >= 0 && angle < 90 ) || ( angle >= 180 && angle < 270 ) )
290 reversedOrder = true;
291 // Z-order is from left to right
292 if ( ( angle >= 90 && angle < 180 ) || ( angle >= 270 && angle <= 360 ) )
293 reversedOrder = false;
294 }
295
296 if ( reversedOrder ) {
297 if ( !open.hidden )
298 drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker
299 if ( !low.hidden && !high.hidden )
300 drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line
301 if ( !close.hidden )
302 drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker
303 } else {
304 if ( !close.hidden )
305 drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker
306 if ( !low.hidden && !high.hidden )
307 drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line
308 if ( !open.hidden )
309 drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker
310 }
311
312 LabelPaintCache lpc;
313 if ( !open.hidden ) {
314 addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr,
315 PositionPoints( leftOpenPoint ), Position::South, Position::South, open.value );
316 }
317 if ( !high.hidden ) {
318 addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr,
319 PositionPoints( highPoint ), Position::South, Position::South, high.value );
320 }
321 if ( !low.hidden ) {
322 addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr,
323 PositionPoints( lowPoint ), Position::South, Position::South, low.value );
324 }
325 if ( !close.hidden ) {
326 addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr,
327 PositionPoints( rightClosePoint ), Position::South, Position::South, close.value );
328 }
329 paintDataValueTextsAndMarkers( context, lpc, false );
330 }
331
332 /*
333 * Draws a line connecting the low and the high value of an OHLC chart
334 *
335 * @param low The low data point
336 * @param high The high data point
337 * @param context The context to draw the candlestick in
338 */
drawCandlestick(int,const CartesianDiagramDataCompressor::DataPoint & open,const CartesianDiagramDataCompressor::DataPoint & high,const CartesianDiagramDataCompressor::DataPoint & low,const CartesianDiagramDataCompressor::DataPoint & close,PaintContext * context)339 void StockDiagram::Private::drawCandlestick( int /*dataset*/, const CartesianDiagramDataCompressor::DataPoint &open,
340 const CartesianDiagramDataCompressor::DataPoint &high,
341 const CartesianDiagramDataCompressor::DataPoint &low,
342 const CartesianDiagramDataCompressor::DataPoint &close,
343 PaintContext *context )
344 {
345 PainterSaver painterSaver( context->painter() );
346
347 // Note: A row in the model is a column in a StockDiagram, and the other way around
348 const int row = low.index.row();
349 const int col = low.index.column();
350
351 QPointF bottomCandlestickPoint;
352 QPointF topCandlestickPoint;
353 QBrush brush;
354 QPen pen;
355 bool drawLowerLine;
356 bool drawCandlestick = !open.hidden && !close.hidden;
357 bool drawUpperLine;
358
359 // Find out if we need to paint a down-trend or up-trend candlestick
360 // and set brush and pen accordingly
361 // Also, determine what the top and bottom points of the candlestick are
362 if ( open.value <= close.value ) {
363 pen = stockDiagram()->upTrendCandlestickPen( row );
364 brush = stockDiagram()->upTrendCandlestickBrush( row );
365 bottomCandlestickPoint = QPointF( open.key, open.value );
366 topCandlestickPoint = QPointF( close.key, close.value );
367 drawLowerLine = !low.hidden && !open.hidden;
368 drawUpperLine = !low.hidden && !close.hidden;
369 } else {
370 pen = stockDiagram()->downTrendCandlestickPen( row );
371 brush = stockDiagram()->downTrendCandlestickBrush( row );
372 bottomCandlestickPoint = QPointF( close.key, close.value );
373 topCandlestickPoint = QPointF( open.key, open.value );
374 drawLowerLine = !low.hidden && !close.hidden;
375 drawUpperLine = !low.hidden && !open.hidden;
376 }
377
378 StockBarAttributes attr = stockDiagram()->stockBarAttributes( col );
379 ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col );
380
381 const QPointF lowPoint = projectPoint( context, QPointF( low.key, low.value ) );
382 const QPointF highPoint = projectPoint( context, QPointF( high.key, high.value ) );
383 const QLineF lowerLine = QLineF( lowPoint, projectPoint( context, bottomCandlestickPoint ) );
384 const QLineF upperLine = QLineF( projectPoint( context, topCandlestickPoint ), highPoint );
385
386 // Convert the data point into coordinates on the coordinate plane
387 QRectF candlestick = projectCandlestick( context, bottomCandlestickPoint,
388 topCandlestickPoint, attr.candlestickWidth() );
389
390 // Remember the drawn polygon to add it to the ReverseMapper later
391 QPolygonF drawnPolygon;
392
393 // Use the ThreeDPainter class to draw a 3D candlestick
394 if ( threeDAttr.isEnabled() ) {
395 ThreeDPainter threeDPainter( context->painter() );
396
397 ThreeDPainter::ThreeDProperties threeDProps;
398 threeDProps.depth = threeDAttr.depth();
399 threeDProps.angle = threeDAttr.angle();
400 threeDProps.useShadowColors = threeDAttr.useShadowColors();
401
402 // If the perspective angle is within [0,180], we paint from bottom to top,
403 // otherwise from top to bottom to ensure the correct z order
404 if ( threeDProps.angle > 0.0 && threeDProps.angle < 180.0 ) {
405 if ( drawLowerLine )
406 drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps );
407 if ( drawCandlestick )
408 drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps );
409 if ( drawUpperLine )
410 drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps );
411 } else {
412 if ( drawUpperLine )
413 drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps );
414 if ( drawCandlestick )
415 drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps );
416 if ( drawLowerLine )
417 drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps );
418 }
419 } else {
420 QPainter *const painter = context->painter();
421 painter->setBrush( brush );
422 painter->setPen( pen );
423 if ( drawLowerLine )
424 painter->drawLine( lowerLine );
425 if ( drawUpperLine )
426 painter->drawLine( upperLine );
427 if ( drawCandlestick )
428 painter->drawRect( candlestick );
429
430 // The 2D representation is the projected candlestick itself
431 drawnPolygon = candlestick;
432
433 // FIXME: Add lower and upper line to reverse mapper
434 }
435
436 LabelPaintCache lpc;
437 if ( !low.hidden )
438 addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr,
439 PositionPoints( lowPoint ), Position::South, Position::South, low.value );
440 if ( drawCandlestick ) {
441 // Both, the open as well as the close value are represented by this candlestick
442 reverseMapper.addPolygon( row, openValueColumn(), drawnPolygon );
443 reverseMapper.addPolygon( row, closeValueColumn(), drawnPolygon );
444
445 addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr,
446 PositionPoints( candlestick.bottomRight() ), Position::South, Position::South, open.value );
447 addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr,
448 PositionPoints( candlestick.topRight() ), Position::South, Position::South, close.value );
449 }
450 if ( !high.hidden )
451 addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr,
452 PositionPoints( highPoint ), Position::South, Position::South, high.value );
453
454 paintDataValueTextsAndMarkers( context, lpc, false );
455 }
456
457 /*
458 * Draws a line connecting two points
459 *
460 * @param col The column of the diagram to paint the line in
461 * @param point1 The first point
462 * @param point2 The second point
463 * @param context The context to draw the low-high line in
464 */
drawLine(int dataset,int col,const QPointF & point1,const QPointF & point2,PaintContext * context)465 void StockDiagram::Private::drawLine( int dataset, int col, const QPointF &point1, const QPointF &point2, PaintContext *context )
466 {
467 PainterSaver painterSaver( context->painter() );
468
469 // A row in the model is a column in the diagram
470 const int modelRow = col;
471 const int modelCol = 0;
472
473 const QPen pen = diagram->pen( dataset );
474 const QBrush brush = diagram->brush( dataset );
475 const ThreeDBarAttributes threeDBarAttr = stockDiagram()->threeDBarAttributes( col );
476
477 QPointF transP1 = context->coordinatePlane()->translate( point1 );
478 QPointF transP2 = context->coordinatePlane()->translate( point2 );
479 QLineF line = QLineF( transP1, transP2 );
480
481 if ( threeDBarAttr.isEnabled() ) {
482 ThreeDPainter::ThreeDProperties threeDProps;
483 threeDProps.angle = threeDBarAttr.angle();
484 threeDProps.depth = threeDBarAttr.depth();
485 threeDProps.useShadowColors = threeDBarAttr.useShadowColors();
486
487 ThreeDPainter painter( context->painter() );
488 reverseMapper.addPolygon( modelCol, modelRow, painter.drawThreeDLine( line, brush, pen, threeDProps ) );
489 } else {
490 context->painter()->setPen( pen );
491 //context->painter()->setBrush( brush );
492 reverseMapper.addLine( modelCol, modelRow, transP1, transP2 );
493 context->painter()->drawLine( line );
494 }
495 }
496
497 /*
498 * Returns the column of the open value in the model
499 *
500 * @return The column of the open value
501 */
openValueColumn() const502 int StockDiagram::Private::openValueColumn() const
503 {
504 // Return an invalid column if diagram has no open values
505 return type == HighLowClose ? -1 : 0;
506 }
507
508 /*
509 * Returns the column of the high value in the model
510 *
511 * @return The column of the high value
512 */
highValueColumn() const513 int StockDiagram::Private::highValueColumn() const
514 {
515 return type == HighLowClose ? 0 : 1;
516 }
517
518 /*
519 * Returns the column of the low value in the model
520 *
521 * @return The column of the low value
522 */
lowValueColumn() const523 int StockDiagram::Private::lowValueColumn() const
524 {
525 return type == HighLowClose ? 1 : 2;
526 }
527
528 /*
529 * Returns the column of the close value in the model
530 *
531 * @return The column of the close value
532 */
closeValueColumn() const533 int StockDiagram::Private::closeValueColumn() const
534 {
535 return type == HighLowClose ? 2 : 3;
536 }
537
538