KChart

KChartCartesianGrid.cpp
1/*
2 * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved.
3 *
4 * This file is part of the KD Chart library.
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9#include "KChartCartesianGrid.h"
10#include "KChartAbstractCartesianDiagram.h"
11#include "KChartPaintContext.h"
12#include "KChartPainterSaver_p.h"
13#include "KChartPrintingParameters.h"
14#include "KChartFrameAttributes.h"
15#include "KChartCartesianAxis_p.h"
16#include "KChartMath_p.h"
17
18#include <QPainter>
19#include <QPainterPath>
20
21using namespace KChart;
22
23CartesianGrid::CartesianGrid()
24 : AbstractGrid(), m_minsteps( 2 ), m_maxsteps( 12 )
25{
26}
27
28CartesianGrid::~CartesianGrid()
29{
30}
31
32int CartesianGrid::minimalSteps() const
33{
34 return m_minsteps;
35}
36
37void CartesianGrid::setMinimalSteps(int minsteps)
38{
39 m_minsteps = minsteps;
40}
41
42int CartesianGrid::maximalSteps() const
43{
44 return m_maxsteps;
45}
46
47void CartesianGrid::setMaximalSteps(int maxsteps)
48{
49 m_maxsteps = maxsteps;
50}
51
53{
54 CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( context->coordinatePlane() );
55 const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) );
56 const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) );
57 if ( !gridAttrsX.isGridVisible() && !gridAttrsX.isSubGridVisible() &&
58 !gridAttrsY.isGridVisible() && !gridAttrsY.isSubGridVisible() ) {
59 return;
60 }
61 // This plane is used for translating the coordinates - not for the data boundaries
62 QPainter *p = context->painter();
63 PainterSaver painterSaver( p );
64 // sharedAxisMasterPlane() changes the painter's coordinate transformation(!)
65 plane = qobject_cast< CartesianCoordinatePlane* >( plane->sharedAxisMasterPlane( context->painter() ) );
66 Q_ASSERT_X ( plane, "CartesianGrid::drawGrid",
67 "Bad function call: PaintContext::coodinatePlane() NOT a cartesian plane." );
68
69 // update the calculated mDataDimensions before using them
70 updateData( context->coordinatePlane() ); // this, in turn, calls our calculateGrid().
71 Q_ASSERT_X ( mDataDimensions.count() == 2, "CartesianGrid::drawGrid",
72 "Error: updateData did not return exactly two dimensions." );
73 if ( !isBoundariesValid( mDataDimensions ) ) {
74 return;
75 }
76
77 const DataDimension dimX = mDataDimensions.first();
78 const DataDimension dimY = mDataDimensions.last();
79 const bool isLogarithmicX = dimX.calcMode == AbstractCoordinatePlane::Logarithmic;
80 const bool isLogarithmicY = dimY.calcMode == AbstractCoordinatePlane::Logarithmic;
81
82 qreal minValueX = qMin( dimX.start, dimX.end );
83 qreal maxValueX = qMax( dimX.start, dimX.end );
84 qreal minValueY = qMin( dimY.start, dimY.end );
85 qreal maxValueY = qMax( dimY.start, dimY.end );
86 {
87 bool adjustXLower = !isLogarithmicX && gridAttrsX.adjustLowerBoundToGrid();
88 bool adjustXUpper = !isLogarithmicX && gridAttrsX.adjustUpperBoundToGrid();
89 bool adjustYLower = !isLogarithmicY && gridAttrsY.adjustLowerBoundToGrid();
90 bool adjustYUpper = !isLogarithmicY && gridAttrsY.adjustUpperBoundToGrid();
91 AbstractGrid::adjustLowerUpperRange( minValueX, maxValueX, dimX.stepWidth, adjustXLower, adjustXUpper );
92 AbstractGrid::adjustLowerUpperRange( minValueY, maxValueY, dimY.stepWidth, adjustYLower, adjustYUpper );
93 }
94
95 if ( plane->frameAttributes().isVisible() ) {
96 const qreal radius = plane->frameAttributes().cornerRadius();
97 QPainterPath path;
98 path.addRoundedRect( QRectF( plane->translate( QPointF( minValueX, minValueY ) ),
99 plane->translate( QPointF( maxValueX, maxValueY ) ) ),
100 radius, radius );
101 context->painter()->setClipPath( path );
102 }
103
104 /* TODO features from old code:
105 - MAYBE coarsen the main grid when it gets crowded (do it in calculateGrid or here?)
106 if ( ! dimX.isCalculated ) {
107 while ( screenRangeX / numberOfUnitLinesX <= MinimumPixelsBetweenLines ) {
108 dimX.stepWidth *= 10.0;
109 dimX.subStepWidth *= 10.0;
110 numberOfUnitLinesX = qAbs( dimX.distance() / dimX.stepWidth );
111 }
112 }
113 - MAYBE deactivate the sub-grid when it gets crowded
114 if ( dimX.subStepWidth && (screenRangeX / (dimX.distance() / dimX.subStepWidth)
115 <= MinimumPixelsBetweenLines) ) {
116 // de-activating grid sub steps: not enough space
117 dimX.subStepWidth = 0.0;
118 }
119 */
120
121 for ( int i = 0; i < 2; i++ ) {
122 XySwitch xy( i == 1 ); // first iteration paints the X grid lines, second paints the Y grid lines
123 const GridAttributes& gridAttrs = xy( gridAttrsX, gridAttrsY );
124 bool hasMajorLines = gridAttrs.isGridVisible();
125 bool hasMinorLines = hasMajorLines && gridAttrs.isSubGridVisible();
126 if ( !hasMajorLines && !hasMinorLines ) {
127 continue;
128 }
129
130 const DataDimension& dimension = xy( dimX, dimY );
131 const bool drawZeroLine = dimension.isCalculated && gridAttrs.zeroLinePen().style() != Qt::NoPen;
132
133 QPointF lineStart = QPointF( minValueX, minValueY ); // still need transformation to screen space
134 QPointF lineEnd = QPointF( maxValueX, maxValueY );
135
136 TickIterator it( xy.isY, dimension, gridAttrs.linesOnAnnotations(),
137 hasMajorLines, hasMinorLines, plane );
138 for ( ; !it.isAtEnd(); ++it ) {
139 if ( !gridAttrs.isOuterLinesVisible() &&
140 ( it.areAlmostEqual( it.position(), xy( minValueX, minValueY ) ) ||
141 it.areAlmostEqual( it.position(), xy( maxValueX, maxValueY ) ) ) ) {
142 continue;
143 }
144 xy.lvalue( lineStart.rx(), lineStart.ry() ) = it.position();
145 xy.lvalue( lineEnd.rx(), lineEnd.ry() ) = it.position();
146 QPointF transLineStart = plane->translate( lineStart );
147 QPointF transLineEnd = plane->translate( lineEnd );
148 if ( ISNAN( transLineStart.x() ) || ISNAN( transLineStart.y() ) ||
149 ISNAN( transLineEnd.x() ) || ISNAN( transLineEnd.y() ) ) {
150 // ### can we catch NaN problems earlier, wasting fewer cycles?
151 continue;
152 }
153 if ( it.position() == 0.0 && drawZeroLine ) {
154 p->setPen( PrintingParameters::scalePen( gridAttrsX.zeroLinePen() ) );
155 } else if ( it.type() == TickIterator::MinorTick ) {
156 p->setPen( PrintingParameters::scalePen( gridAttrs.subGridPen() ) );
157 } else {
158 p->setPen( PrintingParameters::scalePen( gridAttrs.gridPen() ) );
159 }
160 p->drawLine( transLineStart, transLineEnd );
161 }
162 }
163}
164
165
166DataDimensionsList CartesianGrid::calculateGrid( const DataDimensionsList& rawDataDimensions ) const
167{
168 Q_ASSERT_X ( rawDataDimensions.count() == 2, "CartesianGrid::calculateGrid",
169 "Error: calculateGrid() expects a list with exactly two entries." );
170
171 CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( mPlane );
172 Q_ASSERT_X ( plane, "CartesianGrid::calculateGrid",
173 "Error: PaintContext::calculatePlane() called, but no cartesian plane set." );
174
175 DataDimensionsList l( rawDataDimensions );
176#if 0
177 qDebug() << Q_FUNC_INFO << "initial grid X-range:" << l.first().start << "->" << l.first().end
178 << " substep width:" << l.first().subStepWidth;
179 qDebug() << Q_FUNC_INFO << "initial grid Y-range:" << l.last().start << "->" << l.last().end
180 << " substep width:" << l.last().subStepWidth;
181#endif
182 // rule: Returned list is either empty, or it is providing two
183 // valid dimensions, complete with two non-Zero step widths.
184 if ( isBoundariesValid( l ) ) {
185 const QPointF translatedBottomLeft( plane->translateBack( plane->geometry().bottomLeft() ) );
186 const QPointF translatedTopRight( plane->translateBack( plane->geometry().topRight() ) );
187
188 const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) );
189 const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) );
190
191 const DataDimension dimX
192 = calculateGridXY( l.first(), Qt::Horizontal,
193 gridAttrsX.adjustLowerBoundToGrid(),
194 gridAttrsX.adjustUpperBoundToGrid() );
195 if ( dimX.stepWidth ) {
196 //qDebug("CartesianGrid::calculateGrid() l.last().start: %f l.last().end: %f", l.last().start, l.last().end);
197 //qDebug(" l.first().start: %f l.first().end: %f", l.first().start, l.first().end);
198
199 // one time for the min/max value
200 const DataDimension minMaxY
201 = calculateGridXY( l.last(), Qt::Vertical,
202 gridAttrsY.adjustLowerBoundToGrid(),
203 gridAttrsY.adjustUpperBoundToGrid() );
204
205 if ( plane->autoAdjustGridToZoom()
206 && plane->axesCalcModeY() == CartesianCoordinatePlane::Linear
207 && plane->zoomFactorY() > 1.0 )
208 {
209 l.last().start = translatedBottomLeft.y();
210 l.last().end = translatedTopRight.y();
211 }
212 // and one other time for the step width
213 const DataDimension dimY
214 = calculateGridXY( l.last(), Qt::Vertical,
215 gridAttrsY.adjustLowerBoundToGrid(),
216 gridAttrsY.adjustUpperBoundToGrid() );
217 if ( dimY.stepWidth ) {
218 l.first().start = dimX.start;
219 l.first().end = dimX.end;
220 l.first().stepWidth = dimX.stepWidth;
221 l.first().subStepWidth = dimX.subStepWidth;
222 l.last().start = minMaxY.start;
223 l.last().end = minMaxY.end;
224 l.last().stepWidth = dimY.stepWidth;
225 l.last().subStepWidth = dimY.subStepWidth;
226 //qDebug() << "CartesianGrid::calculateGrid() final grid y-range:" << l.last().end - l.last().start << " step width:" << l.last().stepWidth << endl;
227 // calculate some reasonable subSteps if the
228 // user did not set the sub grid but did set
229 // the stepWidth.
230
231 // FIXME (Johannes)
232 // the last (y) dimension is not always the dimension for the ordinate!
233 // since there's no way to check for the orientation of this dimension here,
234 // we cannot automatically assume substep values
235 //if ( dimY.subStepWidth == 0 )
236 // l.last().subStepWidth = dimY.stepWidth/2;
237 //else
238 // l.last().subStepWidth = dimY.subStepWidth;
239 }
240 }
241 }
242#if 0
243 qDebug() << Q_FUNC_INFO << "final grid X-range:" << l.first().start << "->" << l.first().end
244 << " substep width:" << l.first().subStepWidth;
245 qDebug() << Q_FUNC_INFO << "final grid Y-range:" << l.last().start << "->" << l.last().end
246 << " substep width:" << l.last().subStepWidth;
247#endif
248 return l;
249}
250
251qreal fastPow10( int x )
252{
253 qreal res = 1.0;
254 if ( 0 <= x ) {
255 for ( int i = 1; i <= x; ++i )
256 res *= 10.0;
257 } else {
258 for ( int i = -1; i >= x; --i )
259 res *= 0.1;
260 }
261 return res;
262}
263
264#ifdef Q_OS_WIN
265#define trunc(x) ((int)(x))
266#endif
267
268DataDimension CartesianGrid::calculateGridXY(
269 const DataDimension& rawDataDimension,
270 Qt::Orientation orientation,
271 bool adjustLower, bool adjustUpper ) const
272{
273 CartesianCoordinatePlane* const plane = dynamic_cast<CartesianCoordinatePlane*>( mPlane );
274 if ( ( orientation == Qt::Vertical && plane->autoAdjustVerticalRangeToData() >= 100 ) ||
275 ( orientation == Qt::Horizontal && plane->autoAdjustHorizontalRangeToData() >= 100 ) ) {
276 adjustLower = false;
277 adjustUpper = false;
278 }
279
280 DataDimension dim( rawDataDimension );
281 if ( dim.isCalculated && dim.start != dim.end ) {
282 if ( dim.calcMode == AbstractCoordinatePlane::Linear ) {
283 // linear ( == not-logarithmic) calculation
284 if ( dim.stepWidth == 0.0 ) {
285 QList<qreal> granularities;
286 switch ( dim.sequence ) {
287 case KChartEnums::GranularitySequence_10_20:
288 granularities << 1.0 << 2.0;
289 break;
290 case KChartEnums::GranularitySequence_10_50:
291 granularities << 1.0 << 5.0;
292 break;
293 case KChartEnums::GranularitySequence_25_50:
294 granularities << 2.5 << 5.0;
295 break;
296 case KChartEnums::GranularitySequence_125_25:
297 granularities << 1.25 << 2.5;
298 break;
299 case KChartEnums::GranularitySequenceIrregular:
300 granularities << 1.0 << 1.25 << 2.0 << 2.5 << 5.0;
301 break;
302 }
303 //qDebug("CartesianGrid::calculateGridXY() dim.start: %f dim.end: %f", dim.start, dim.end);
304 calculateStepWidth(
305 dim.start, dim.end, granularities, orientation,
306 dim.stepWidth, dim.subStepWidth,
307 adjustLower, adjustUpper );
308 }
309 // if needed, adjust start/end to match the step width:
310 //qDebug() << "CartesianGrid::calculateGridXY() has 1st linear range: min " << dim.start << " and max" << dim.end;
311
312 AbstractGrid::adjustLowerUpperRange( dim.start, dim.end, dim.stepWidth,
313 adjustLower, adjustUpper );
314 //qDebug() << "CartesianGrid::calculateGridXY() returns linear range: min " << dim.start << " and max" << dim.end;
315 } else {
316 // logarithmic calculation with negative values
317 if ( dim.end <= 0 )
318 {
319 qreal min;
320 const qreal minRaw = qMin( dim.start, dim.end );
321 const int minLog = -static_cast<int>(trunc( log10( -minRaw ) ) );
322 if ( minLog >= 0 )
323 min = qMin( minRaw, -std::numeric_limits< qreal >::epsilon() );
324 else
325 min = -fastPow10( -(minLog-1) );
326
327 qreal max;
328 const qreal maxRaw = qMin( -std::numeric_limits< qreal >::epsilon(), qMax( dim.start, dim.end ) );
329 const int maxLog = -static_cast<int>(ceil( log10( -maxRaw ) ) );
330 if ( maxLog >= 0 )
331 max = -1;
332 else if ( fastPow10( -maxLog ) < maxRaw )
333 max = -fastPow10( -(maxLog+1) );
334 else
335 max = -fastPow10( -maxLog );
336 if ( adjustLower )
337 dim.start = min;
338 if ( adjustUpper )
339 dim.end = max;
340 dim.stepWidth = -pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) );
341 }
342 // logarithmic calculation (ignoring all negative values)
343 else
344 {
345 qreal min;
346 const qreal minRaw = qMax( qMin( dim.start, dim.end ), qreal( 0.0 ) );
347 const int minLog = static_cast<int>(trunc( log10( minRaw ) ) );
348 if ( minLog <= 0 && dim.end < 1.0 )
349 min = qMax( minRaw, std::numeric_limits< qreal >::epsilon() );
350 else if ( minLog <= 0 )
351 min = qMax( qreal(0.00001), dim.start );
352 else
353 min = fastPow10( minLog-1 );
354
355 // Uh oh. Logarithmic scaling doesn't work with a lower or upper
356 // bound being 0.
357 const bool zeroBound = dim.start == 0.0 || dim.end == 0.0;
358
359 qreal max;
360 const qreal maxRaw = qMax( qMax( dim.start, dim.end ), qreal( 0.0 ) );
361 const int maxLog = static_cast<int>(ceil( log10( maxRaw ) ) );
362 if ( maxLog <= 0 )
363 max = 1;
364 else if ( fastPow10( maxLog ) < maxRaw )
365 max = fastPow10( maxLog+1 );
366 else
367 max = fastPow10( maxLog );
368 if ( adjustLower || zeroBound )
369 dim.start = min;
370 if ( adjustUpper || zeroBound )
371 dim.end = max;
372 dim.stepWidth = pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) );
373 }
374 }
375 } else {
376 //qDebug() << "CartesianGrid::calculateGridXY() returns stepWidth 1.0 !!";
377 // Do not ignore the user configuration
378 dim.stepWidth = dim.stepWidth ? dim.stepWidth : 1.0;
379 }
380 return dim;
381}
382
383
384static void calculateSteps(
385 qreal start_, qreal end_, const QList<qreal>& list,
386 int minSteps, int maxSteps,
387 int power,
388 qreal& steps, qreal& stepWidth,
389 bool adjustLower, bool adjustUpper )
390{
391 //qDebug("-----------------------------------\nstart: %f end: %f power-of-ten: %i", start_, end_, power);
392
393 qreal distance = 0.0;
394 steps = 0.0;
395
396 const int lastIdx = list.count()-1;
397 for ( int i = 0; i <= lastIdx; ++i ) {
398 const qreal testStepWidth = list.at(lastIdx - i) * fastPow10( power );
399 //qDebug( "testing step width: %f", testStepWidth);
400 qreal start = qMin( start_, end_ );
401 qreal end = qMax( start_, end_ );
402 //qDebug("pre adjusting start: %f end: %f", start, end);
403 AbstractGrid::adjustLowerUpperRange( start, end, testStepWidth, adjustLower, adjustUpper );
404 //qDebug("post adjusting start: %f end: %f", start, end);
405
406 const qreal testDistance = qAbs(end - start);
407 const qreal testSteps = testDistance / testStepWidth;
408
409 //qDebug() << "testDistance:" << testDistance << " distance:" << distance;
410 if ( (minSteps <= testSteps) && (testSteps <= maxSteps)
411 && ( (steps == 0.0) || (testDistance <= distance) ) ) {
412 steps = testSteps;
413 stepWidth = testStepWidth;
414 distance = testDistance;
415 //qDebug( "start: %f end: %f step width: %f steps: %f distance: %f", start, end, stepWidth, steps, distance);
416 }
417 }
418}
419
420
421void CartesianGrid::calculateStepWidth(
422 qreal start_, qreal end_,
423 const QList<qreal>& granularities,
424 Qt::Orientation orientation,
425 qreal& stepWidth, qreal& subStepWidth,
426 bool adjustLower, bool adjustUpper ) const
427{
428 Q_UNUSED( orientation );
429
430 Q_ASSERT_X ( granularities.count(), "CartesianGrid::calculateStepWidth",
431 "Error: The list of GranularitySequence values is empty." );
432 QList<qreal> list( granularities );
433 std::sort(list.begin(), list.end());
434
435 const qreal start = qMin( start_, end_);
436 const qreal end = qMax( start_, end_);
437 const qreal distance = end - start;
438 //qDebug( "raw data start: %f end: %f", start, end);
439
440 qreal steps;
441 int power = 0;
442 while ( list.last() * fastPow10( power ) < distance ) {
443 ++power;
444 };
445 // We have the sequence *two* times in the calculation test list,
446 // so we will be sure to find the best match:
447 const int count = list.count();
448 QList<qreal> testList;
449
450 for ( int dec = -1; dec == -1 || fastPow10( dec + 1 ) >= distance; --dec )
451 for ( int i = 0; i < count; ++i )
452 testList << list.at(i) * fastPow10( dec );
453
454 testList << list;
455
456 do {
457 calculateSteps( start, end, testList, m_minsteps, m_maxsteps, power,
458 steps, stepWidth,
459 adjustLower, adjustUpper );
460 --power;
461 } while ( steps == 0.0 );
462 ++power;
463 //qDebug( "steps calculated: stepWidth: %f steps: %f", stepWidth, steps);
464
465 // find the matching sub-grid line width in case it is
466 // not set by the user
467
468 if ( subStepWidth == 0.0 ) {
469 if ( stepWidth == list.first() * fastPow10( power ) ) {
470 subStepWidth = list.last() * fastPow10( power-1 );
471 //qDebug("A");
472 } else if ( stepWidth == list.first() * fastPow10( power-1 ) ) {
473 subStepWidth = list.last() * fastPow10( power-2 );
474 //qDebug("B");
475 } else {
476 qreal smallerStepWidth = list.first();
477 for ( int i = 1; i < list.count(); ++i ) {
478 if ( stepWidth == list.at( i ) * fastPow10( power ) ) {
479 subStepWidth = smallerStepWidth * fastPow10( power );
480 break;
481 }
482 if ( stepWidth == list.at( i ) * fastPow10( power-1 ) ) {
483 subStepWidth = smallerStepWidth * fastPow10( power-1 );
484 break;
485 }
486 smallerStepWidth = list.at( i );
487 }
488 }
489 }
490 //qDebug("CartesianGrid::calculateStepWidth() found stepWidth %f (%f steps) and sub-stepWidth %f", stepWidth, steps, subStepWidth);
491}
QRect geometry() const override
pure virtual in QLayoutItem
Abstract base class for grid classes: cartesian, polar, ...
DataDimensionsList updateData(AbstractCoordinatePlane *plane)
Returns the cached result of data calculation.
static void adjustLowerUpperRange(qreal &start, qreal &end, qreal stepWidth, bool adjustLower, bool adjustUpper)
Adjusts start and/or end so that they are a multiple of stepWidth.
static bool isBoundariesValid(const QRectF &r)
Checks whether both coordinates of r are valid according to isValueValid.
unsigned int autoAdjustVerticalRangeToData() const
Returns the maximal allowed percent of the vertical space covered by the coordinate plane that may be...
unsigned int autoAdjustHorizontalRangeToData() const
Returns the maximal allowed percent of the horizontal space covered by the coordinate plane that may ...
const QPointF translate(const QPointF &diagramPoint) const override
Translate the given point in value space coordinates to a position in pixel space.
AbstractCoordinatePlane * sharedAxisMasterPlane(QPainter *p=nullptr) override
reimpl
const GridAttributes gridAttributes(Qt::Orientation orientation) const
bool autoAdjustGridToZoom() const
Return the status of the built-in grid adjusting feature.
void drawGrid(PaintContext *context) override
Doing the actual drawing.
Helper class for one dimension of data, e.g.
A set of attributes controlling the appearance of grids.
Stores information about painting diagrams.
Q_SCRIPTABLE Q_NOREPLY void start()
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const QList< QKeySequence > & end()
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
const_reference at(qsizetype i) const const
iterator begin()
qsizetype count() const const
iterator end()
T & first()
T & last()
void drawLine(const QLine &line)
void setClipPath(const QPainterPath &path, Qt::ClipOperation operation)
void setPen(Qt::PenStyle style)
Qt::PenStyle style() const const
qreal & rx()
qreal & ry()
qreal x() const const
qreal y() const const
QPoint bottomLeft() const const
QPoint topRight() const const
Horizontal
QTextStream & dec(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:02:46 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.