KChart

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

KDE's Doxygen guidelines are available online.