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

© 2001 Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/
https://www.kdab.com/development-resources/qt-tools/kd-chart/
Generated by doxygen 1.9.1