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 
21 using namespace KChart;
22 
23 CartesianGrid::CartesianGrid()
24  : AbstractGrid(), m_minsteps( 2 ), m_maxsteps( 12 )
25 {
26 }
27 
28 CartesianGrid::~CartesianGrid()
29 {
30 }
31 
32 int CartesianGrid::minimalSteps() const
33 {
34  return m_minsteps;
35 }
36 
37 void CartesianGrid::setMinimalSteps(int minsteps)
38 {
39  m_minsteps = minsteps;
40 }
41 
42 int CartesianGrid::maximalSteps() const
43 {
44  return m_maxsteps;
45 }
46 
47 void 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 
166 DataDimensionsList 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 
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 
251 qreal 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 
268 DataDimension 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 
384 static 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 
421 void 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 }
Helper class for one dimension of data, e.g.
Qt::PenStyle style() const const
void setClipPath(const QPainterPath &path, Qt::ClipOperation operation)
const T & at(int i) const const
void addRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius, Qt::SizeMode mode)
void drawLine(const QLineF &line)
const GridAttributes gridAttributes(Qt::Orientation orientation) const
int count(const T &value) const const
qreal x() const const
qreal y() const const
Stores information about painting diagrams.
void setPen(const QColor &color)
unsigned int autoAdjustVerticalRangeToData() const
Returns the maximal allowed percent of the vertical space covered by the coordinate plane that may be...
T & first()
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.
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
void drawGrid(PaintContext *context) override
Doing the actual drawing.
A set of attributes controlling the appearance of grids.
T & last()
qreal & rx()
qreal & ry()
Horizontal
Global namespace.
T qobject_cast(QObject *object)
Abstract base class for grid classes: cartesian, polar, ...
unsigned int autoAdjustHorizontalRangeToData() const
Returns the maximal allowed percent of the horizontal space covered by the coordinate plane that may ...
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Thu Jan 20 2022 22:33:24 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.