KD Chart API Documentation 3.1
Loading...
Searching...
No Matches
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
13#include "KDChartCartesianAxis_p.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
26using namespace KDChart;
27
32
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) {
156 } else if (it.type() == TickIterator::MinorTick) {
158 } else {
160 }
161 p->drawLine(transLineStart, transLineEnd);
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 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
247qreal 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
264DataDimension 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
376static 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
412void 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
@ GranularitySequence_25_50
@ GranularitySequenceIrregular
@ GranularitySequence_10_20
@ GranularitySequence_10_50
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
static QPen scalePen(const QPen &pen)
const T & at(int i) const const
int count(const T &value) const const
T & first()
T & last()
void drawLine(const QLineF &line)
void setClipPath(const QPainterPath &path, Qt::ClipOperation operation)
void setPen(const QColor &color)
void addRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius, Qt::SizeMode mode)
Qt::PenStyle style() const const
qreal & rx()
qreal & ry()
qreal x() const const
qreal y() const const
Horizontal
QTextStream & dec(QTextStream &stream)

© 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 on Wed May 1 2024 00:01:10 for KD Chart API Documentation by doxygen 1.9.8