KD Chart API Documentation 3.1
Loading...
Searching...
No Matches
KDChartCartesianAxis.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
12#include "KDChartCartesianAxis_p.h"
13
14#include <cmath>
15
16#include <QApplication>
17#include <QBrush>
18#include <QPainter>
19#include <QPen>
20#include <QtDebug>
21
23#include "KDChartAbstractDiagram_p.h"
24#include "KDChartAbstractGrid.h"
25#include "KDChartBarDiagram.h"
26#include "KDChartChart.h"
27#include "KDChartLayoutItems.h"
28#include "KDChartLineDiagram.h"
29#include "KDChartPaintContext.h"
30#include "KDChartPainterSaver_p.h"
32#include "KDChartStockDiagram.h"
33
34#include <KDABLibFakes>
35
36using namespace KDChart;
37
38#define d (d_func())
39
40static qreal slightlyLessThan(qreal r)
41{
42 if (r == 0.0) {
43 // scale down the epsilon somewhat arbitrarily
44 return r - std::numeric_limits<qreal>::epsilon() * 1e-6;
45 }
46 // scale the epsilon so that it (hopefully) changes at least the least significant bit of r
47 qreal diff = qAbs(r) * std::numeric_limits<qreal>::epsilon() * 2.0;
48 return r - diff;
49}
50
51static int numSignificantDecimalPlaces(qreal floatNumber)
52{
53 static const int maxPlaces = 15;
54 QString sample = QString::number(floatNumber, 'f', maxPlaces).section(QLatin1Char('.'), 1, 2);
55 int ret = maxPlaces;
56 for (; ret > 0; ret--) {
57 if (sample[ret - 1] != QLatin1Char('0')) {
58 break;
59 }
60 }
61 return ret;
62}
63
64// Feature idea: In case of numeric labels, consider limiting the possible values of majorThinningFactor
65// to something like {1, 2, 5} * 10^n. Or even better, something that achieves round values in the
66// remaining labels.
67
68TickIterator::TickIterator(CartesianAxis *a, CartesianCoordinatePlane *plane, uint majorThinningFactor,
69 bool omitLastTick)
70 : m_axis(a)
71 , m_majorThinningFactor(majorThinningFactor)
72 , m_majorLabelCount(0)
73 , m_type(NoTick)
74{
75 // deal with the things that are specific to axes (like annotations), before the generic init().
76 const CartesianAxis::Private *axisPriv = CartesianAxis::Private::get(a);
77 XySwitch xy(axisPriv->isVertical());
78 m_dimension = xy(plane->gridDimensionsList().first(), plane->gridDimensionsList().last());
79 if (omitLastTick) {
80 // In bar and stock charts the last X tick is a fencepost with no associated value, which is
81 // convenient for grid painting. Here we have to manually exclude it to avoid overpainting.
82 m_dimension.end -= m_dimension.stepWidth;
83 }
84
85 m_annotations = axisPriv->annotations;
86 m_customTicks = axisPriv->customTicksPositions;
87
88 const qreal inf = std::numeric_limits<qreal>::infinity();
89
90 if (m_customTicks.count()) {
91 std::sort(m_customTicks.begin(), m_customTicks.end());
92 m_customTickIndex = 0;
93 m_customTick = m_customTicks.at(m_customTickIndex);
94 } else {
95 m_customTickIndex = -1;
96 m_customTick = inf;
97 }
98
99 if (m_majorThinningFactor > 1 && hasShorterLabels()) {
100 m_manualLabelTexts = m_axis->shortLabels();
101 } else {
102 m_manualLabelTexts = m_axis->labels();
103 }
104 m_manualLabelIndex = m_manualLabelTexts.isEmpty() ? -1 : 0;
105
106 if (!m_dimension.isCalculated) {
107 // ### depending on the data, it is difficult to impossible to choose anchors (where ticks
108 // corresponding to the header labels are) on the ordinate or even the abscissa with
109 // 2-dimensional data. this should be somewhat mitigated by isCalculated only being false
110 // when header data labels should work, at least that seems to be what the code that sets up
111 // the dimensions is trying to do.
112 QStringList dataHeaderLabels;
113 AbstractDiagram *const dia = plane->diagram();
114 dataHeaderLabels = dia->itemRowLabels();
115 if (!dataHeaderLabels.isEmpty()) {
116 AttributesModel *model = dia->attributesModel();
117 const int anchorCount = model->rowCount(QModelIndex());
118 if (anchorCount == dataHeaderLabels.count()) {
119 for (int i = 0; i < anchorCount; i++) {
120 // ### ordinal number as anchor point generally only works for 1-dimensional data
121 m_dataHeaderLabels.insert(qreal(i), dataHeaderLabels.at(i));
122 }
123 }
124 }
125 }
126
127 bool hasMajorTicks = m_axis->rulerAttributes().showMajorTickMarks();
128 bool hasMinorTicks = m_axis->rulerAttributes().showMinorTickMarks();
129
130 init(xy.isY, hasMajorTicks, hasMinorTicks, plane);
131}
132
134{
135 QMultiMap<qreal, QString> annotations;
136 const auto constDiagrams = plane->diagrams();
137 for (const AbstractDiagram *diagram : constDiagrams) {
138 const auto *cd = qobject_cast<const AbstractCartesianDiagram *>(diagram);
139 if (!cd) {
140 continue;
141 }
142 const auto axes = cd->axes();
143 for (const CartesianAxis *axis : axes) {
144 const CartesianAxis::Private *axisPriv = CartesianAxis::Private::get(axis);
145 if (axisPriv->isVertical() == isY) {
146 annotations.unite(axisPriv->annotations);
147 }
148 }
149 }
150 return annotations;
151}
152
153TickIterator::TickIterator(bool isY, const DataDimension &dimension, bool useAnnotationsForTicks,
154 bool hasMajorTicks, bool hasMinorTicks, CartesianCoordinatePlane *plane)
155 : m_axis(nullptr)
156 , m_dimension(dimension)
157 , m_majorThinningFactor(1)
158 , m_majorLabelCount(0)
159 , m_customTickIndex(-1)
160 , m_manualLabelIndex(-1)
161 , m_type(NoTick)
162 , m_customTick(std::numeric_limits<qreal>::infinity())
163{
164 if (useAnnotationsForTicks) {
165 m_annotations = allAxisAnnotations(plane, isY);
166 }
167 init(isY, hasMajorTicks, hasMinorTicks, plane);
168}
169
170void TickIterator::init(bool isY, bool hasMajorTicks, bool hasMinorTicks,
172{
173 Q_ASSERT(std::numeric_limits<qreal>::has_infinity);
174
175 m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
176 // sanity check against infinite loops
177 hasMajorTicks = hasMajorTicks && (m_dimension.stepWidth > 0 || m_isLogarithmic);
178 hasMinorTicks = hasMinorTicks && (m_dimension.subStepWidth > 0 || m_isLogarithmic);
179
180 XySwitch xy(isY);
181
182 GridAttributes gridAttributes = plane->gridAttributes(xy(Qt::Horizontal, Qt::Vertical));
183 m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
184 if (!m_isLogarithmic) {
185 // adjustedLowerUpperRange() is intended for use with linear scaling; specifically it would
186 // round lower bounds < 1 to 0.
187
188 const bool fixedRange = xy(plane->autoAdjustHorizontalRangeToData(),
190 >= 100;
191 const bool adjustLower = gridAttributes.adjustLowerBoundToGrid() && !fixedRange;
192 const bool adjustUpper = gridAttributes.adjustUpperBoundToGrid() && !fixedRange;
193 m_dimension = AbstractGrid::adjustedLowerUpperRange(m_dimension, adjustLower, adjustUpper);
194
195 m_decimalPlaces = numSignificantDecimalPlaces(m_dimension.stepWidth);
196 } else {
197 // the number of significant decimal places for each label naturally varies with logarithmic scaling
198 m_decimalPlaces = -1;
199 }
200
201 const qreal inf = std::numeric_limits<qreal>::infinity();
202
203 // try to place m_position just in front of the first tick to be drawn so that operator++()
204 // can be used to find the first tick
205 if (m_isLogarithmic) {
206 if (ISNAN(m_dimension.start) || ISNAN(m_dimension.end)) {
207 // this can happen in a spurious paint operation before everything is set up;
208 // just bail out to avoid an infinite loop in that case.
209 m_dimension.start = 0.0;
210 m_dimension.end = 0.0;
211 m_position = inf;
212 m_majorTick = inf;
213 m_minorTick = inf;
214 } else if (m_dimension.start >= 0) {
215 m_position = m_dimension.start ? pow(10.0, floor(log10(m_dimension.start)) - 1.0)
216 : 1e-6;
217 m_majorTick = hasMajorTicks ? m_position : inf;
218 m_minorTick = hasMinorTicks ? m_position * 20.0 : inf;
219 } else {
220 m_position = -pow(10.0, ceil(log10(-m_dimension.start)) + 1.0);
221 m_majorTick = hasMajorTicks ? m_position : inf;
222 m_minorTick = hasMinorTicks ? m_position * 0.09 : inf;
223 }
224 } else {
225 m_majorTick = hasMajorTicks ? m_dimension.start : inf;
226 m_minorTick = hasMinorTicks ? m_dimension.start : inf;
227 m_position = slightlyLessThan(m_dimension.start);
228 }
229
230 ++(*this);
231}
232
233bool TickIterator::areAlmostEqual(qreal r1, qreal r2) const
234{
235 if (!m_isLogarithmic) {
236 qreal span = m_dimension.end - m_dimension.start;
237 if (span == 0) {
238 // When start == end, we still want to show one tick if possible,
239 // which needs this function to perform a reasonable comparison.
240 span = qFuzzyIsNull(m_dimension.start) ? 1 : qAbs(m_dimension.start);
241 }
242 return qAbs(r2 - r1) < (span) * 1e-6;
243 } else {
244 return qAbs(r2 - r1) < qMax(qAbs(r1), qAbs(r2)) * 0.01;
245 }
246}
247
248bool TickIterator::isHigherPrecedence(qreal importantTick, qreal unimportantTick) const
249{
250 return importantTick != std::numeric_limits<qreal>::infinity() && (importantTick <= unimportantTick || areAlmostEqual(importantTick, unimportantTick));
251}
252
253void TickIterator::computeMajorTickLabel(int decimalPlaces)
254{
255 if (m_manualLabelIndex >= 0) {
256 m_text = m_manualLabelTexts[m_manualLabelIndex++];
257 if (m_manualLabelIndex >= m_manualLabelTexts.count()) {
258 // manual label texts repeat if there are less label texts than ticks on an axis
259 m_manualLabelIndex = 0;
260 }
261 m_type = m_majorThinningFactor > 1 ? MajorTickManualShort : MajorTickManualLong;
262 } else {
263 // if m_axis is null, we are dealing with grid lines. grid lines never need labels.
264 if (m_axis && (m_majorLabelCount++ % m_majorThinningFactor) == 0) {
266 m_dataHeaderLabels.lowerBound(slightlyLessThan(m_position));
267
268 if (it != m_dataHeaderLabels.constEnd() && areAlmostEqual(it.key(), m_position)) {
269 m_text = it.value();
270 m_type = MajorTickHeaderDataLabel;
271 } else {
272 // 'f' to avoid exponential notation for large numbers, consistent with data value text
273 if (decimalPlaces < 0) {
274 decimalPlaces = numSignificantDecimalPlaces(m_position);
275 }
276 m_text = QString::number(m_position, 'f', decimalPlaces);
277 m_type = MajorTick;
278 }
279 } else {
280 m_text.clear();
281 m_type = MajorTick;
282 }
283 }
284}
285
286void TickIterator::operator++()
287{
288 if (isAtEnd()) {
289 return;
290 }
291 const qreal inf = std::numeric_limits<qreal>::infinity();
292
293 // make sure to find the next tick at a value strictly greater than m_position
294
295 if (!m_annotations.isEmpty()) {
296 auto it = m_annotations.upperBound(m_position);
297 if (it != m_annotations.constEnd()) {
298 m_position = it.key();
299 m_text = it.value();
300 m_type = CustomTick;
301 } else {
302 m_position = inf;
303 }
304 } else if (!m_isLogarithmic && m_dimension.stepWidth * 1e6 < qMax(qAbs(m_dimension.start), qAbs(m_dimension.end))) {
305 // If the step width is too small to increase m_position at all, we get an infinite loop.
306 // This usually happens when m_dimension.start == m_dimension.end and both are very large.
307 // When start == end, the step width defaults to 1, and it doesn't scale with start or end.
308 // So currently, we bail and show no tick at all for empty ranges > 10^6, but at least we don't hang.
309 m_position = inf;
310 } else {
311 // advance the calculated ticks
312 if (m_isLogarithmic) {
313 while (m_majorTick <= m_position) {
314 m_majorTick *= m_position >= 0 ? 10 : 0.1;
315 }
316 while (m_minorTick <= m_position) {
317 // the next major tick position should be greater than this
318 m_minorTick += m_majorTick * (m_position >= 0 ? 0.1 : 1.0);
319 }
320 } else {
321 while (m_majorTick <= m_position) {
322 m_majorTick += m_dimension.stepWidth;
323 }
324 while (m_minorTick <= m_position) {
325 m_minorTick += m_dimension.subStepWidth;
326 }
327 }
328
329 while (m_customTickIndex >= 0 && m_customTick <= m_position) {
330 if (++m_customTickIndex >= m_customTicks.count()) {
331 m_customTickIndex = -1;
332 m_customTick = inf;
333 break;
334 }
335 m_customTick = m_customTicks.at(m_customTickIndex);
336 }
337
338 // now see which kind of tick we'll have
339 if (isHigherPrecedence(m_customTick, m_majorTick) && isHigherPrecedence(m_customTick, m_minorTick)) {
340 m_position = m_customTick;
341 computeMajorTickLabel(-1);
342 // override the MajorTick type here because those tick's labels are collision-tested, which we don't want
343 // for custom ticks. they may be arbitrarily close to other ticks, causing excessive label thinning.
344 if (m_type == MajorTick) {
345 m_type = CustomTick;
346 }
347 } else if (isHigherPrecedence(m_majorTick, m_minorTick)) {
348 m_position = m_majorTick;
349 if (m_minorTick != inf) {
350 // realign minor to major
351 m_minorTick = m_majorTick;
352 }
353 computeMajorTickLabel(m_decimalPlaces);
354 } else if (m_minorTick != inf) {
355 m_position = m_minorTick;
356 m_text.clear();
357 m_type = MinorTick;
358 } else {
359 m_position = inf;
360 }
361 }
362
363 if (m_position > m_dimension.end || ISNAN(m_position)) {
364 m_position = inf; // make isAtEnd() return true
365 m_text.clear();
366 m_type = NoTick;
367 }
368}
369
371 : AbstractAxis(new Private(diagram, this), diagram)
372{
373 init();
374}
375
377{
378 // when we remove the first axis it will unregister itself and
379 // propagate the next one to the primary, thus the while loop
380 while (d->mDiagram) {
381 auto *cd = qobject_cast<AbstractCartesianDiagram *>(d->mDiagram);
382 cd->takeAxis(this);
383 }
384 for (AbstractDiagram *diagram : qAsConst(d->secondaryDiagrams)) {
386 cd->takeAxis(this);
387 }
388}
389
390void CartesianAxis::init()
391{
392 d->customTickLength = 3;
393 d->position = Bottom;
395 connect(this, &AbstractAxis::coordinateSystemChanged, this, &CartesianAxis::coordinateSystemChanged);
396}
397
398bool CartesianAxis::compare(const CartesianAxis *other) const
399{
400 if (other == this) {
401 return true;
402 }
403 if (!other) {
404 return false;
405 }
406 return AbstractAxis::compare(other) && (position() == other->position()) && (titleText() == other->titleText()) && (titleTextAttributes() == other->titleTextAttributes());
407}
408
409void CartesianAxis::coordinateSystemChanged()
410{
411 layoutPlanes();
412}
413
415{
416 d->titleText = text;
418 layoutPlanes();
419}
420
422{
423 return d->titleText;
424}
425
427{
428 d->titleTextAttributes = a;
429 d->useDefaultTextAttributes = false;
431 layoutPlanes();
432}
433
435{
438 Measure me(ta.fontSize());
439 me.setValue(me.value() * 1.5);
440 ta.setFontSize(me);
441 return ta;
442 }
443 return d->titleTextAttributes;
444}
445
447{
448 d->useDefaultTextAttributes = true;
450 layoutPlanes();
451}
452
454{
455 return d->useDefaultTextAttributes;
456}
457
459{
460 if (d->position == p) {
461 return;
462 }
463 d->position = p;
464 // Invalidating size is not always necessary if both old and new positions are horizontal or both
465 // vertical, but in practice there could be small differences due to who-knows-what, so always adapt
466 // to the possibly new size. Changing position is expensive anyway.
468 layoutPlanes();
469}
470
471#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(Q_COMPILER_MANGLES_RETURN_TYPE)
472const
473#endif
476{
477 return d->position;
478}
479
481{
482 if (!d->diagram() || !d->diagram()->coordinatePlane()) {
483 return;
484 }
486 if (plane) {
487 plane->layoutPlanes();
488 }
489}
490
492{
493 const auto *dia =
494 qobject_cast<const AbstractCartesianDiagram *>(diagram);
495 if (dia && dia->referenceDiagram())
496 dia = dia->referenceDiagram();
497 return qobject_cast<const BarDiagram *>(dia) != 0;
498}
499
501{
502 const auto *dia =
503 qobject_cast<const AbstractCartesianDiagram *>(diagram);
504 if (dia && dia->referenceDiagram())
505 dia = dia->referenceDiagram();
506 if (qobject_cast<const BarDiagram *>(dia))
507 return true;
508 if (qobject_cast<const StockDiagram *>(dia))
509 return true;
510
511 const auto *lineDiagram = qobject_cast<const LineDiagram *>(dia);
512 return lineDiagram && lineDiagram->centerDataPoints();
513}
514
516{
517 const Qt::Orientation diagramOrientation = referenceDiagramIsBarDiagram(d->diagram()) ? (( BarDiagram * )(d->diagram()))->orientation()
518 : Qt::Vertical;
520 : position() == Left || position() == Right;
521}
522
524{
525 return !isAbscissa();
526}
527
529{
530 if (!d->diagram() || !d->diagram()->coordinatePlane()) {
531 return;
532 }
534 ctx.setPainter(painter);
536 ctx.setCoordinatePlane(plane);
537
538 ctx.setRectangle(QRectF(areaGeometry()));
539 PainterSaver painterSaver(painter);
540
541 // enable clipping only when required due to zoom, because it slows down painting
542 // (the alternative to clipping when zoomed in requires much more work to paint just the right area)
543 const qreal zoomFactor = d->isVertical() ? plane->zoomFactorY() : plane->zoomFactorX();
544 if (zoomFactor > 1.0) {
545 painter->setClipRegion(areaGeometry().adjusted(-d->amountOfLeftOverlap - 1, -d->amountOfTopOverlap - 1,
546 d->amountOfRightOverlap + 1, d->amountOfBottomOverlap + 1));
547 }
548 paintCtx(&ctx);
549}
550
551const TextAttributes CartesianAxis::Private::titleTextAttributesWithAdjustedRotation() const
552{
553 TextAttributes titleTA(titleTextAttributes);
554 int rotation = titleTA.rotation();
555 if (position == Left || position == Right) {
556 rotation += 270;
557 }
558 if (rotation >= 360) {
559 rotation -= 360;
560 }
561 // limit the allowed values to 0, 90, 180, 270
562 rotation = (rotation / 90) * 90;
563 titleTA.setRotation(rotation);
564 return titleTA;
565}
566
567QString CartesianAxis::Private::customizedLabelText(const QString &text, Qt::Orientation orientation,
568 qreal value) const
569{
570 // ### like in the old code, using int( value ) as column number...
571 QString withUnits = diagram()->unitPrefix(int(value), orientation, true) + text + diagram()->unitSuffix(int(value), orientation, true);
572 return axis()->customizedLabel(withUnits);
573}
574
575void CartesianAxis::setTitleSpace(qreal axisTitleSpace)
576{
577 d->axisTitleSpace = axisTitleSpace;
578}
579
581{
582 return d->axisTitleSpace;
583}
584
586{
587 Q_UNUSED(value)
588 // ### remove me
589}
590
592{
593 // ### remove me
594 return 1.0;
595}
596
597void CartesianAxis::Private::drawTitleText(QPainter *painter, CartesianCoordinatePlane *plane,
598 const QRect &geoRect) const
599{
600 const TextAttributes titleTA(titleTextAttributesWithAdjustedRotation());
601 if (titleTA.isVisible()) {
602 TextLayoutItem titleItem(titleText, titleTA, plane->parent(), KDChartEnums::MeasureOrientationMinimum,
604 QPointF point;
605 QSize size = titleItem.sizeHint();
606 switch (position) {
607 case Top:
608 point.setX(geoRect.left() + geoRect.width() / 2);
609 point.setY(geoRect.top() + (size.height() / 2) / axisTitleSpace);
610 size.setWidth(qMin(size.width(), axis()->geometry().width()));
611 break;
612 case Bottom:
613 point.setX(geoRect.left() + geoRect.width() / 2);
614 point.setY(geoRect.bottom() - (size.height() / 2) / axisTitleSpace);
615 size.setWidth(qMin(size.width(), axis()->geometry().width()));
616 break;
617 case Left:
618 point.setX(geoRect.left() + (size.width() / 2) / axisTitleSpace);
619 point.setY(geoRect.top() + geoRect.height() / 2);
620 size.setHeight(qMin(size.height(), axis()->geometry().height()));
621 break;
622 case Right:
623 point.setX(geoRect.right() - (size.width() / 2) / axisTitleSpace);
624 point.setY(geoRect.top() + geoRect.height() / 2);
625 size.setHeight(qMin(size.height(), axis()->geometry().height()));
626 break;
627 }
628 const PainterSaver painterSaver(painter);
629 painter->setClipping(false);
630 painter->translate(point);
631 titleItem.setGeometry(QRect(QPoint(-size.width() / 2, -size.height() / 2), size));
632 titleItem.paint(painter);
633 }
634}
635
636bool CartesianAxis::Private::isVertical() const
637{
638 return axis()->isAbscissa() == AbstractDiagram::Private::get(diagram())->isTransposed();
639}
640
642{
643 Q_ASSERT_X(d->diagram(), "CartesianAxis::paint",
644 "Function call not allowed: The axis is not assigned to any diagram.");
645
646 auto *plane = dynamic_cast<CartesianCoordinatePlane *>(context->coordinatePlane());
647 Q_ASSERT_X(plane, "CartesianAxis::paint",
648 "Bad function call: PaintContext::coordinatePlane() NOT a cartesian plane.");
649
650 // note: Not having any data model assigned is no bug
651 // but we can not draw an axis then either.
652 if (!d->diagram()->model()) {
653 return;
654 }
655
657
658 XySwitch geoXy(d->isVertical());
659
660 QPainter *const painter = context->painter();
661
662 // determine the position of the axis (also required for labels) and paint it
663
664 qreal transversePosition = signalingNaN; // in data space
665 // the next one describes an additional shift in screen space; it is unfortunately required to
666 // make axis sharing work, which uses the areaGeometry() to override the position of the axis.
668 {
669 // determine the unadulterated position in screen space
670
673 QPointF start(dimX.start, dimY.start);
674 QPointF end(dimX.end, dimY.end);
675 // consider this: you can turn a diagonal line into a horizontal or vertical line on any
676 // edge by changing just one of its four coordinates.
677 switch (position()) {
679 end.setY(dimY.start);
680 break;
682 start.setY(dimY.end);
683 break;
685 end.setX(dimX.start);
686 break;
688 start.setX(dimX.end);
689 break;
690 }
691
692 transversePosition = geoXy(start.y(), start.x());
693
694 QPointF transStart = plane->translate(start);
695 QPointF transEnd = plane->translate(end);
696
697 // an externally set areaGeometry() moves the axis position transversally; the shift is
698 // nonzero only when this is a shared axis
699
700 const QRect geo = areaGeometry();
701 switch (position()) {
704 break;
707 break;
710 break;
713 break;
714 }
715
718
719 if (rulerAttributes().showRulerLine()) {
720 bool clipSaved = context->painter()->hasClipping();
721 painter->setClipping(false);
722 painter->drawLine(transStart, transEnd);
723 painter->setClipping(clipSaved);
724 }
725 }
726
727 // paint ticks and labels
728
731
732 int labelThinningFactor = 1;
733 // TODO: label thinning also when grid line distance < 4 pixels, not only when labels collide
734 auto *tickLabel = new TextLayoutItem(QString(), labelTA, plane->parent(),
736 auto *prevTickLabel = new TextLayoutItem(QString(), labelTA, plane->parent(),
739 enum
740 {
741 Layout = 0,
742 Painting,
743 Done
744 };
745 for (int step = labelTA.isVisible() ? Layout : Painting; step < Done; step++) {
746 bool skipFirstTick = !rulerAttr.showFirstTick();
747 bool isFirstLabel = true;
748 for (TickIterator it(this, plane, labelThinningFactor, centerTicks); !it.isAtEnd(); ++it) {
749 if (skipFirstTick) {
750 skipFirstTick = false;
751 continue;
752 }
753
754 const qreal drawPos = it.position() + (centerTicks ? 0.5 : 0.);
757 geoXy.lvalue(onAxis.ry(), onAxis.rx()) += transverseScreenSpaceShift;
758 const bool isOutwardsPositive = position() == Bottom || position() == Right;
759
760 // paint the tick mark
761
763 qreal tickLen = it.type() == TickIterator::CustomTick ? d->customTickLength : tickLength(it.type() == TickIterator::MinorTick);
764 geoXy.lvalue(tickEnd.ry(), tickEnd.rx()) += isOutwardsPositive ? tickLen : -tickLen;
765
766 // those adjustments are required to paint the ticks exactly on the axis and of the right length
767 if (position() == Top) {
768 onAxis.ry() += 1;
769 tickEnd.ry() += 1;
770 } else if (position() == Left) {
771 tickEnd.rx() += 1;
772 }
773
774 if (step == Painting) {
775 painter->save();
776 if (rulerAttr.hasTickMarkPenAt(it.position())) {
777 painter->setPen(rulerAttr.tickMarkPen(it.position()));
778 } else {
779 painter->setPen(it.type() == TickIterator::MinorTick ? rulerAttr.minorTickMarkPen()
780 : rulerAttr.majorTickMarkPen());
781 }
782 painter->drawLine(onAxis, tickEnd);
783 painter->restore();
784 }
785
786 if (it.text().isEmpty() || !labelTA.isVisible()) {
787 // the following code in the loop is only label painting, so skip it
788 continue;
789 }
790
791 // paint the label
792
793 QString text = it.text();
794 if (it.type() == TickIterator::MajorTick) {
795 // add unit prefixes and suffixes, then customize
796 text = d->customizedLabelText(text, geoXy(Qt::Horizontal, Qt::Vertical), it.position());
797 } else if (it.type() == TickIterator::MajorTickHeaderDataLabel) {
798 // unit prefixes and suffixes have already been added in this case - only customize
799 text = customizedLabel(text);
800 }
801
802 tickLabel->setText(text);
803 QSizeF size = QSizeF(tickLabel->sizeHint());
804 QPolygon labelPoly = tickLabel->boundingPolygon();
805 Q_ASSERT(labelPoly.count() == 4);
806
807 // for alignment, find the label polygon edge "most parallel" and closest to the axis
808
809 int axisAngle = 0;
810 switch (position()) {
811 case Bottom:
812 axisAngle = 0;
813 break;
814 case Top:
815 axisAngle = 180;
816 break;
817 case Right:
818 axisAngle = 270;
819 break;
820 case Left:
821 axisAngle = 90;
822 break;
823 default:
824 Q_ASSERT(false);
825 }
826 // the left axis is not actually pointing down and the top axis not actually pointing
827 // left, but their corresponding closest edges of a rectangular unrotated label polygon are.
828
829 int relAngle = axisAngle - labelTA.rotation() + 45;
830 if (relAngle < 0) {
831 relAngle += 360;
832 }
833 int polyCorner1 = relAngle / 90;
835 QPoint p2 = labelPoly.at(polyCorner1 == 3 ? 0 : (polyCorner1 + 1));
836
838
839 qreal labelMargin = rulerAttr.labelMargin();
840 if (labelMargin < 0) {
841 labelMargin = QFontMetricsF(tickLabel->realFont()).height() * 0.5;
842 }
843 labelMargin -= tickLabel->marginWidth(); // make up for the margin that's already there
844
845 switch (position()) {
846 case Left:
847 labelPos += QPointF(-size.width() - labelMargin,
848 -0.45 * size.height() - 0.5 * (p1.y() + p2.y()));
849 break;
850 case Right:
851 labelPos += QPointF(labelMargin,
852 -0.45 * size.height() - 0.5 * (p1.y() + p2.y()));
853 break;
854 case Top:
855 labelPos += QPointF(-0.45 * size.width() - 0.5 * (p1.x() + p2.x()),
856 -size.height() - labelMargin);
857 break;
858 case Bottom:
859 labelPos += QPointF(-0.45 * size.width() - 0.5 * (p1.x() + p2.x()),
860 labelMargin);
861 break;
862 }
863
864 tickLabel->setGeometry(QRect(labelPos.toPoint(), size.toSize()));
865
866 if (step == Painting) {
867 tickLabel->paint(painter);
868 }
869
870 // collision check the current label against the previous one
871
872 // like in the old code, we don't shorten or decimate labels if they are already the
873 // manual short type, or if they are the manual long type and on the vertical axis
874 // ### they can still collide though, especially when they're rotated!
875 if (step == Layout) {
876 int spaceSavingRotation = geoXy(270, 0);
877 bool canRotate = labelTA.autoRotate() && labelTA.rotation() != spaceSavingRotation;
878 const bool canShortenLabels = !geoXy.isY && it.type() == TickIterator::MajorTickManualLong && it.hasShorterLabels();
879 bool collides = false;
880 if (it.type() == TickIterator::MajorTick || it.type() == TickIterator::MajorTickHeaderDataLabel
882 if (isFirstLabel) {
883 isFirstLabel = false;
884 } else {
887 }
889 }
890 if (collides) {
891 // to make room, we try in order: shorten, rotate, decimate
892 if (canRotate && !canShortenLabels) {
893 labelTA.setRotation(spaceSavingRotation);
894 // tickLabel will be reused in the next round
895 tickLabel->setTextAttributes(labelTA);
896 } else {
898 }
899 step--; // relayout
900 break;
901 }
902 }
903 }
904 }
905 delete tickLabel;
906 tickLabel = nullptr;
907 delete prevTickLabel;
908 prevTickLabel = nullptr;
909
910 if (!titleText().isEmpty()) {
911 d->drawTitleText(painter, plane, geometry());
912 }
913}
914
915/* pure virtual in QLayoutItem */
917{
918 return false; // if the axis exists, it has some (perhaps default) content
919}
920
921/* pure virtual in QLayoutItem */
923{
925 switch (position()) {
926 case Bottom:
927 case Top:
929 break;
930 case Left:
931 case Right:
933 break;
934 default:
935 Q_ASSERT(false);
936 break;
937 };
938 return ret;
939}
940
942{
943 d->cachedMaximumSize = QSize();
944}
945
946/* pure virtual in QLayoutItem */
948{
949 if (!d->cachedMaximumSize.isValid())
950 d->cachedMaximumSize = d->calculateMaximumSize();
951 return d->cachedMaximumSize;
952}
953
954QSize CartesianAxis::Private::calculateMaximumSize() const
955{
956 if (!diagram()) {
957 return QSize();
958 }
959
960 auto *plane = dynamic_cast<CartesianCoordinatePlane *>(diagram()->coordinatePlane());
961 Q_ASSERT(plane);
962 QObject *refArea = plane->parent();
963 const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks(diagram())
964 && axis()->isAbscissa();
965
966 // we ignore:
967 // - label thinning (expensive, not worst case and we want worst case)
968 // - label autorotation (expensive, obscure feature(?))
969 // - axis length (it is determined by the plane / diagram / chart anyway)
970 // - the title's influence on axis length; this one might be TODO. See KDCH-863.
971
972 XySwitch geoXy(isVertical());
973 qreal size = 0; // this is the size transverse to the axis direction
974
975 // the following variables describe how much the first and last label stick out over the axis
976 // area, so that the geometry of surrounding layout items can be adjusted to make room.
977 qreal startOverhang = 0.0;
978 qreal endOverhang = 0.0;
979
980 if (mAxis->textAttributes().isVisible()) {
981 // these four are used just to calculate startOverhang and endOverhang
982 qreal lowestLabelPosition = signalingNaN;
983 qreal highestLabelPosition = signalingNaN;
984 qreal lowestLabelLongitudinalSize = signalingNaN;
985 qreal highestLabelLongitudinalSize = signalingNaN;
986
987 TextLayoutItem tickLabel(QString(), mAxis->textAttributes(), refArea,
989 const RulerAttributes rulerAttr = mAxis->rulerAttributes();
990
991 bool showFirstTick = rulerAttr.showFirstTick();
992 for (TickIterator it(axis(), plane, 1, centerTicks); !it.isAtEnd(); ++it) {
993 const qreal drawPos = it.position() + (centerTicks ? 0.5 : 0.);
994 if (!showFirstTick) {
995 showFirstTick = true;
996 continue;
997 }
998
999 qreal labelSizeTransverse = 0.0;
1000 qreal labelMargin = 0.0;
1001 QString text = it.text();
1002 if (!text.isEmpty()) {
1003 QPointF labelPosition = plane->translate(QPointF(geoXy(drawPos, ( qreal )1.0),
1004 geoXy(( qreal )1.0, drawPos)));
1005 highestLabelPosition = geoXy(labelPosition.x(), labelPosition.y());
1006
1007 if (it.type() == TickIterator::MajorTick) {
1008 // add unit prefixes and suffixes, then customize
1009 text = customizedLabelText(text, geoXy(Qt::Horizontal, Qt::Vertical), it.position());
1010 } else if (it.type() == TickIterator::MajorTickHeaderDataLabel) {
1011 // unit prefixes and suffixes have already been added in this case - only customize
1012 text = axis()->customizedLabel(text);
1013 }
1014 tickLabel.setText(text);
1015
1016 QSize sz = tickLabel.sizeHint();
1017 highestLabelLongitudinalSize = geoXy(sz.width(), sz.height());
1018 if (ISNAN(lowestLabelLongitudinalSize)) {
1019 lowestLabelLongitudinalSize = highestLabelLongitudinalSize;
1020 lowestLabelPosition = highestLabelPosition;
1021 }
1022
1023 labelSizeTransverse = geoXy(sz.height(), sz.width());
1024 labelMargin = rulerAttr.labelMargin();
1025 if (labelMargin < 0) {
1026 labelMargin = QFontMetricsF(tickLabel.realFont()).height() * 0.5;
1027 }
1028 labelMargin -= tickLabel.marginWidth(); // make up for the margin that's already there
1029 }
1030 qreal tickLength = it.type() == TickIterator::CustomTick ? customTickLength : axis()->tickLength(it.type() == TickIterator::MinorTick);
1031 size = qMax(size, tickLength + labelMargin + labelSizeTransverse);
1032 }
1033
1034 const DataDimension dimX = plane->gridDimensionsList().first();
1035 const DataDimension dimY = plane->gridDimensionsList().last();
1036
1037 QPointF pt = plane->translate(QPointF(dimX.start, dimY.start));
1038 const qreal lowestPosition = geoXy(pt.x(), pt.y());
1039 pt = plane->translate(QPointF(dimX.end, dimY.end));
1040 const qreal highestPosition = geoXy(pt.x(), pt.y());
1041
1042 // the geoXy( 1.0, -1.0 ) here is necessary because Qt's y coordinate is inverted
1043 startOverhang = qMax(0.0, (lowestPosition - lowestLabelPosition) * geoXy(1.0, -1.0) + lowestLabelLongitudinalSize * 0.5);
1044 endOverhang = qMax(0.0, (highestLabelPosition - highestPosition) * geoXy(1.0, -1.0) + highestLabelLongitudinalSize * 0.5);
1045 }
1046
1047 amountOfLeftOverlap = geoXy(startOverhang, ( qreal )0.0);
1048 amountOfRightOverlap = geoXy(endOverhang, ( qreal )0.0);
1049 amountOfBottomOverlap = geoXy(( qreal )0.0, startOverhang);
1050 amountOfTopOverlap = geoXy(( qreal )0.0, endOverhang);
1051
1052 const TextAttributes titleTA = titleTextAttributesWithAdjustedRotation();
1053 if (titleTA.isVisible() && !axis()->titleText().isEmpty()) {
1054 TextLayoutItem title(axis()->titleText(), titleTA, refArea, KDChartEnums::MeasureOrientationMinimum,
1056
1057 QFontMetricsF titleFM(title.realFont(), GlobalMeasureScaling::paintDevice());
1058 size += geoXy(titleFM.height() * 0.33, titleFM.averageCharWidth() * 0.55); // spacing
1059 size += geoXy(title.sizeHint().height(), title.sizeHint().width());
1060 }
1061
1062 // the size parallel to the axis direction is not determined by us, so we just return 1
1063 return QSize(geoXy(1, int(size)), geoXy(int(size), 1));
1064}
1065
1066/* pure virtual in QLayoutItem */
1068{
1069 return maximumSize();
1070}
1071
1072/* pure virtual in QLayoutItem */
1074{
1075 return maximumSize();
1076}
1077
1078/* pure virtual in QLayoutItem */
1080{
1081 if (d->geometry != r) {
1082 d->geometry = r;
1084 }
1085}
1086
1087/* pure virtual in QLayoutItem */
1089{
1090 return d->geometry;
1091}
1092
1094{
1095 if (d->customTickLength == value) {
1096 return;
1097 }
1098 d->customTickLength = value;
1100 layoutPlanes();
1101}
1102
1104{
1105 return d->customTickLength;
1106}
1107
1108int CartesianAxis::tickLength(bool subUnitTicks) const
1109{
1111 return subUnitTicks ? rulerAttr.minorTickMarkLength() : rulerAttr.majorTickMarkLength();
1112}
1113
1115{
1116 return d->annotations;
1117}
1118
1120{
1121 if (d->annotations == annotations)
1122 return;
1123
1124 d->annotations = annotations;
1126 layoutPlanes();
1127}
1128
1130{
1131 return d->customTicksPositions;
1132}
1133
1134void CartesianAxis::setCustomTicks(const QList<qreal> &customTicksPositions)
1135{
1136 if (d->customTicksPositions == customTicksPositions)
1137 return;
1138
1139 d->customTicksPositions = customTicksPositions;
1141 layoutPlanes();
1142}
static int numSignificantDecimalPlaces(qreal floatNumber)
static QMultiMap< qreal, QString > allAxisAnnotations(const AbstractCoordinatePlane *plane, bool isY)
static bool referenceDiagramIsBarDiagram(const AbstractDiagram *diagram)
static qreal slightlyLessThan(qreal r)
static bool referenceDiagramNeedsCenteredAbscissaTicks(const AbstractDiagram *diagram)
@ MeasureOrientationMinimum
QRect areaGeometry() const override
RulerAttributes rulerAttributes() const
Returns the attributes to be used for painting the rulers.
virtual const QString customizedLabel(const QString &label) const
Reimplement this method if you want to adjust axis labels before they are printed.
const AbstractDiagram * diagram() const
bool compare(const AbstractAxis *other) const
TextAttributes textAttributes() const
Returns the text attributes to be used for axis labels.
Base class for diagrams based on a cartesian coordianate system.
Base class common for all coordinate planes, CartesianCoordinatePlane, PolarCoordinatePlane,...
AbstractDiagram defines the interface for diagram classes.
virtual AttributesModel * attributesModel() const
AbstractCoordinatePlane * coordinatePlane() const
static const DataDimension adjustedLowerUpperRange(const DataDimension &dim, bool adjustLower, bool adjustUpper)
A proxy model used for decorating data with attributes.
int rowCount(const QModelIndex &) const override
BarDiagram defines a common bar diagram.
Q_DECL_DEPRECATED qreal titleSpace() const
virtual void setPosition(Position p)
bool isEmpty() const override
virtual bool isOrdinate() const
void setGeometry(const QRect &r) override
QSize maximumSize() const override
Q_DECL_DEPRECATED qreal titleSize() const
QList< qreal > customTicks() const
CartesianAxis(AbstractCartesianDiagram *diagram=nullptr)
void paintCtx(PaintContext *) override
QSize sizeHint() const override
QSize minimumSize() const override
Qt::Orientations expandingDirections() const override
virtual bool isAbscissa() const
void setTitleTextAttributes(const TextAttributes &a)
bool compare(const CartesianAxis *other) const
virtual Position position() const
QRect geometry() const override
QMultiMap< qreal, QString > annotations() const
TextAttributes titleTextAttributes() const
void setAnnotations(const QMultiMap< qreal, QString > &annotations)
void paint(QPainter *) override
void setTitleText(const QString &text)
virtual int tickLength(bool subUnitTicks=false) const
void setCustomTicks(const QList< qreal > &ticksPostions)
Q_DECL_DEPRECATED void setTitleSpace(qreal value)
Q_DECL_DEPRECATED void setTitleSize(qreal value)
use setTitleTextAttributes() instead
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
const GridAttributes gridAttributes(Qt::Orientation orientation) const
Helper class for one dimension of data, e.g. for the rows in a data model, or for the labels of an ax...
static QPaintDevice * paintDevice()
A set of attributes controlling the appearance of grids.
Measure is used to specify relative and absolute sizes in KDChart, e.g. font sizes.
Stores information about painting diagrams.
void setPainter(QPainter *painter)
AbstractCoordinatePlane * coordinatePlane() const
A set of attributes controlling the appearance of axis rulers.
A set of text attributes.
qreal height() const const
const T & at(int i) const const
int count(const T &value) const const
T & first()
bool isEmpty() const const
T & last()
const Key key(const T &value, const Key &defaultKey) const const
QMap::iterator lowerBound(const Key &key)
QMap::iterator upperBound(const Key &key)
const T value(const Key &key, const T &defaultValue) const const
QMultiMap< K, V > & unite(const QMultiMap< K, V > &other)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
T qobject_cast(QObject *object)
void drawLine(const QLineF &line)
bool hasClipping() const const
void restore()
void save()
void setClipRegion(const QRegion &region, Qt::ClipOperation operation)
void setClipping(bool enable)
void setPen(const QColor &color)
void translate(const QPointF &offset)
int x() const const
int y() const const
void setX(qreal x)
void setY(qreal y)
qreal x() const const
qreal y() const const
int bottom() const const
int height() const const
int left() const const
int right() const const
int top() const const
int width() const const
int height() const const
void setHeight(int height)
void setWidth(int width)
int width() const const
qreal height() const const
QSize toSize() const const
qreal width() const const
bool isEmpty() const const
QString number(int n, int base)
QString section(QChar sep, int start, int end, QString::SectionFlags flags) const const
AlignHCenter
Horizontal

© 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 Fri Apr 26 2024 00:04:56 for KD Chart API Documentation by doxygen 1.9.8