KD Chart API Documentation 3.0
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 Q_FOREACH (const AbstractDiagram *diagram, plane->diagrams()) {
137 const auto *cd = qobject_cast<const AbstractCartesianDiagram *>(diagram);
138 if (!cd) {
139 continue;
140 }
141 Q_FOREACH (const CartesianAxis *axis, cd->axes()) {
142 const CartesianAxis::Private *axisPriv = CartesianAxis::Private::get(axis);
143 if (axisPriv->isVertical() == isY) {
144 annotations.unite(axisPriv->annotations);
145 }
146 }
147 }
148 return annotations;
149}
150
151TickIterator::TickIterator(bool isY, const DataDimension &dimension, bool useAnnotationsForTicks,
152 bool hasMajorTicks, bool hasMinorTicks, CartesianCoordinatePlane *plane)
153 : m_axis(nullptr)
154 , m_dimension(dimension)
155 , m_majorThinningFactor(1)
156 , m_majorLabelCount(0)
157 , m_customTickIndex(-1)
158 , m_manualLabelIndex(-1)
159 , m_type(NoTick)
160 , m_customTick(std::numeric_limits<qreal>::infinity())
161{
162 if (useAnnotationsForTicks) {
163 m_annotations = allAxisAnnotations(plane, isY);
164 }
165 init(isY, hasMajorTicks, hasMinorTicks, plane);
166}
167
168void TickIterator::init(bool isY, bool hasMajorTicks, bool hasMinorTicks,
170{
171 Q_ASSERT(std::numeric_limits<qreal>::has_infinity);
172
173 m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
174 // sanity check against infinite loops
175 hasMajorTicks = hasMajorTicks && (m_dimension.stepWidth > 0 || m_isLogarithmic);
176 hasMinorTicks = hasMinorTicks && (m_dimension.subStepWidth > 0 || m_isLogarithmic);
177
178 XySwitch xy(isY);
179
180 GridAttributes gridAttributes = plane->gridAttributes(xy(Qt::Horizontal, Qt::Vertical));
181 m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
182 if (!m_isLogarithmic) {
183 // adjustedLowerUpperRange() is intended for use with linear scaling; specifically it would
184 // round lower bounds < 1 to 0.
185
186 const bool fixedRange = xy(plane->autoAdjustHorizontalRangeToData(),
188 >= 100;
189 const bool adjustLower = gridAttributes.adjustLowerBoundToGrid() && !fixedRange;
190 const bool adjustUpper = gridAttributes.adjustUpperBoundToGrid() && !fixedRange;
191 m_dimension = AbstractGrid::adjustedLowerUpperRange(m_dimension, adjustLower, adjustUpper);
192
193 m_decimalPlaces = numSignificantDecimalPlaces(m_dimension.stepWidth);
194 } else {
195 // the number of significant decimal places for each label naturally varies with logarithmic scaling
196 m_decimalPlaces = -1;
197 }
198
199 const qreal inf = std::numeric_limits<qreal>::infinity();
200
201 // try to place m_position just in front of the first tick to be drawn so that operator++()
202 // can be used to find the first tick
203 if (m_isLogarithmic) {
204 if (ISNAN(m_dimension.start) || ISNAN(m_dimension.end)) {
205 // this can happen in a spurious paint operation before everything is set up;
206 // just bail out to avoid an infinite loop in that case.
207 m_dimension.start = 0.0;
208 m_dimension.end = 0.0;
209 m_position = inf;
210 m_majorTick = inf;
211 m_minorTick = inf;
212 } else if (m_dimension.start >= 0) {
213 m_position = m_dimension.start ? pow(10.0, floor(log10(m_dimension.start)) - 1.0)
214 : 1e-6;
215 m_majorTick = hasMajorTicks ? m_position : inf;
216 m_minorTick = hasMinorTicks ? m_position * 20.0 : inf;
217 } else {
218 m_position = -pow(10.0, ceil(log10(-m_dimension.start)) + 1.0);
219 m_majorTick = hasMajorTicks ? m_position : inf;
220 m_minorTick = hasMinorTicks ? m_position * 0.09 : inf;
221 }
222 } else {
223 m_majorTick = hasMajorTicks ? m_dimension.start : inf;
224 m_minorTick = hasMinorTicks ? m_dimension.start : inf;
225 m_position = slightlyLessThan(m_dimension.start);
226 }
227
228 ++(*this);
229}
230
231bool TickIterator::areAlmostEqual(qreal r1, qreal r2) const
232{
233 if (!m_isLogarithmic) {
234 qreal span = m_dimension.end - m_dimension.start;
235 if (span == 0) {
236 // When start == end, we still want to show one tick if possible,
237 // which needs this function to perform a reasonable comparison.
238 span = qFuzzyIsNull(m_dimension.start) ? 1 : qAbs(m_dimension.start);
239 }
240 return qAbs(r2 - r1) < (span) * 1e-6;
241 } else {
242 return qAbs(r2 - r1) < qMax(qAbs(r1), qAbs(r2)) * 0.01;
243 }
244}
245
246bool TickIterator::isHigherPrecedence(qreal importantTick, qreal unimportantTick) const
247{
248 return importantTick != std::numeric_limits<qreal>::infinity() && (importantTick <= unimportantTick || areAlmostEqual(importantTick, unimportantTick));
249}
250
251void TickIterator::computeMajorTickLabel(int decimalPlaces)
252{
253 if (m_manualLabelIndex >= 0) {
254 m_text = m_manualLabelTexts[m_manualLabelIndex++];
255 if (m_manualLabelIndex >= m_manualLabelTexts.count()) {
256 // manual label texts repeat if there are less label texts than ticks on an axis
257 m_manualLabelIndex = 0;
258 }
259 m_type = m_majorThinningFactor > 1 ? MajorTickManualShort : MajorTickManualLong;
260 } else {
261 // if m_axis is null, we are dealing with grid lines. grid lines never need labels.
262 if (m_axis && (m_majorLabelCount++ % m_majorThinningFactor) == 0) {
264 m_dataHeaderLabels.lowerBound(slightlyLessThan(m_position));
265
266 if (it != m_dataHeaderLabels.constEnd() && areAlmostEqual(it.key(), m_position)) {
267 m_text = it.value();
268 m_type = MajorTickHeaderDataLabel;
269 } else {
270 // 'f' to avoid exponential notation for large numbers, consistent with data value text
271 if (decimalPlaces < 0) {
272 decimalPlaces = numSignificantDecimalPlaces(m_position);
273 }
274 m_text = QString::number(m_position, 'f', decimalPlaces);
275 m_type = MajorTick;
276 }
277 } else {
278 m_text.clear();
279 m_type = MajorTick;
280 }
281 }
282}
283
284void TickIterator::operator++()
285{
286 if (isAtEnd()) {
287 return;
288 }
289 const qreal inf = std::numeric_limits<qreal>::infinity();
290
291 // make sure to find the next tick at a value strictly greater than m_position
292
293 if (!m_annotations.isEmpty()) {
294 auto it = m_annotations.upperBound(m_position);
295 if (it != m_annotations.constEnd()) {
296 m_position = it.key();
297 m_text = it.value();
298 m_type = CustomTick;
299 } else {
300 m_position = inf;
301 }
302 } else if (!m_isLogarithmic && m_dimension.stepWidth * 1e6 < qMax(qAbs(m_dimension.start), qAbs(m_dimension.end))) {
303 // If the step width is too small to increase m_position at all, we get an infinite loop.
304 // This usually happens when m_dimension.start == m_dimension.end and both are very large.
305 // When start == end, the step width defaults to 1, and it doesn't scale with start or end.
306 // So currently, we bail and show no tick at all for empty ranges > 10^6, but at least we don't hang.
307 m_position = inf;
308 } else {
309 // advance the calculated ticks
310 if (m_isLogarithmic) {
311 while (m_majorTick <= m_position) {
312 m_majorTick *= m_position >= 0 ? 10 : 0.1;
313 }
314 while (m_minorTick <= m_position) {
315 // the next major tick position should be greater than this
316 m_minorTick += m_majorTick * (m_position >= 0 ? 0.1 : 1.0);
317 }
318 } else {
319 while (m_majorTick <= m_position) {
320 m_majorTick += m_dimension.stepWidth;
321 }
322 while (m_minorTick <= m_position) {
323 m_minorTick += m_dimension.subStepWidth;
324 }
325 }
326
327 while (m_customTickIndex >= 0 && m_customTick <= m_position) {
328 if (++m_customTickIndex >= m_customTicks.count()) {
329 m_customTickIndex = -1;
330 m_customTick = inf;
331 break;
332 }
333 m_customTick = m_customTicks.at(m_customTickIndex);
334 }
335
336 // now see which kind of tick we'll have
337 if (isHigherPrecedence(m_customTick, m_majorTick) && isHigherPrecedence(m_customTick, m_minorTick)) {
338 m_position = m_customTick;
339 computeMajorTickLabel(-1);
340 // override the MajorTick type here because those tick's labels are collision-tested, which we don't want
341 // for custom ticks. they may be arbitrarily close to other ticks, causing excessive label thinning.
342 if (m_type == MajorTick) {
343 m_type = CustomTick;
344 }
345 } else if (isHigherPrecedence(m_majorTick, m_minorTick)) {
346 m_position = m_majorTick;
347 if (m_minorTick != inf) {
348 // realign minor to major
349 m_minorTick = m_majorTick;
350 }
351 computeMajorTickLabel(m_decimalPlaces);
352 } else if (m_minorTick != inf) {
353 m_position = m_minorTick;
354 m_text.clear();
355 m_type = MinorTick;
356 } else {
357 m_position = inf;
358 }
359 }
360
361 if (m_position > m_dimension.end || ISNAN(m_position)) {
362 m_position = inf; // make isAtEnd() return true
363 m_text.clear();
364 m_type = NoTick;
365 }
366}
367
369 : AbstractAxis(new Private(diagram, this), diagram)
370{
371 init();
372}
373
375{
376 // when we remove the first axis it will unregister itself and
377 // propagate the next one to the primary, thus the while loop
378 while (d->mDiagram) {
379 auto *cd = qobject_cast<AbstractCartesianDiagram *>(d->mDiagram);
380 cd->takeAxis(this);
381 }
382 Q_FOREACH (AbstractDiagram *diagram, d->secondaryDiagrams) {
384 cd->takeAxis(this);
385 }
386}
387
388void CartesianAxis::init()
389{
390 d->customTickLength = 3;
391 d->position = Bottom;
393 connect(this, SIGNAL(coordinateSystemChanged()), SLOT(coordinateSystemChanged()));
394}
395
396bool CartesianAxis::compare(const CartesianAxis *other) const
397{
398 if (other == this) {
399 return true;
400 }
401 if (!other) {
402 return false;
403 }
404 return AbstractAxis::compare(other) && (position() == other->position()) && (titleText() == other->titleText()) && (titleTextAttributes() == other->titleTextAttributes());
405}
406
407void CartesianAxis::coordinateSystemChanged()
408{
409 layoutPlanes();
410}
411
413{
414 d->titleText = text;
416 layoutPlanes();
417}
418
420{
421 return d->titleText;
422}
423
425{
426 d->titleTextAttributes = a;
427 d->useDefaultTextAttributes = false;
429 layoutPlanes();
430}
431
433{
436 Measure me(ta.fontSize());
437 me.setValue(me.value() * 1.5);
438 ta.setFontSize(me);
439 return ta;
440 }
441 return d->titleTextAttributes;
442}
443
445{
446 d->useDefaultTextAttributes = true;
448 layoutPlanes();
449}
450
452{
453 return d->useDefaultTextAttributes;
454}
455
457{
458 if (d->position == p) {
459 return;
460 }
461 d->position = p;
462 // Invalidating size is not always necessary if both old and new positions are horizontal or both
463 // vertical, but in practice there could be small differences due to who-knows-what, so always adapt
464 // to the possibly new size. Changing position is expensive anyway.
466 layoutPlanes();
467}
468
469#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(Q_COMPILER_MANGLES_RETURN_TYPE)
470const
471#endif
474{
475 return d->position;
476}
477
479{
480 if (!d->diagram() || !d->diagram()->coordinatePlane()) {
481 return;
482 }
484 if (plane) {
485 plane->layoutPlanes();
486 }
487}
488
490{
491 const auto *dia =
492 qobject_cast<const AbstractCartesianDiagram *>(diagram);
493 if (dia && dia->referenceDiagram())
494 dia = dia->referenceDiagram();
495 return qobject_cast<const BarDiagram *>(dia) != 0;
496}
497
499{
500 const auto *dia =
501 qobject_cast<const AbstractCartesianDiagram *>(diagram);
502 if (dia && dia->referenceDiagram())
503 dia = dia->referenceDiagram();
504 if (qobject_cast<const BarDiagram *>(dia))
505 return true;
506 if (qobject_cast<const StockDiagram *>(dia))
507 return true;
508
509 const auto *lineDiagram = qobject_cast<const LineDiagram *>(dia);
510 return lineDiagram && lineDiagram->centerDataPoints();
511}
512
514{
515 const Qt::Orientation diagramOrientation = referenceDiagramIsBarDiagram(d->diagram()) ? (( BarDiagram * )(d->diagram()))->orientation()
516 : Qt::Vertical;
518 : position() == Left || position() == Right;
519}
520
522{
523 return !isAbscissa();
524}
525
527{
528 if (!d->diagram() || !d->diagram()->coordinatePlane()) {
529 return;
530 }
532 ctx.setPainter(painter);
534 ctx.setCoordinatePlane(plane);
535
536 ctx.setRectangle(QRectF(areaGeometry()));
537 PainterSaver painterSaver(painter);
538
539 // enable clipping only when required due to zoom, because it slows down painting
540 // (the alternative to clipping when zoomed in requires much more work to paint just the right area)
541 const qreal zoomFactor = d->isVertical() ? plane->zoomFactorY() : plane->zoomFactorX();
542 if (zoomFactor > 1.0) {
543 painter->setClipRegion(areaGeometry().adjusted(-d->amountOfLeftOverlap - 1, -d->amountOfTopOverlap - 1,
544 d->amountOfRightOverlap + 1, d->amountOfBottomOverlap + 1));
545 }
546 paintCtx(&ctx);
547}
548
549const TextAttributes CartesianAxis::Private::titleTextAttributesWithAdjustedRotation() const
550{
551 TextAttributes titleTA(titleTextAttributes);
552 int rotation = titleTA.rotation();
553 if (position == Left || position == Right) {
554 rotation += 270;
555 }
556 if (rotation >= 360) {
557 rotation -= 360;
558 }
559 // limit the allowed values to 0, 90, 180, 270
560 rotation = (rotation / 90) * 90;
561 titleTA.setRotation(rotation);
562 return titleTA;
563}
564
565QString CartesianAxis::Private::customizedLabelText(const QString &text, Qt::Orientation orientation,
566 qreal value) const
567{
568 // ### like in the old code, using int( value ) as column number...
569 QString withUnits = diagram()->unitPrefix(int(value), orientation, true) + text + diagram()->unitSuffix(int(value), orientation, true);
570 return axis()->customizedLabel(withUnits);
571}
572
573void CartesianAxis::setTitleSpace(qreal axisTitleSpace)
574{
575 d->axisTitleSpace = axisTitleSpace;
576}
577
579{
580 return d->axisTitleSpace;
581}
582
584{
585 Q_UNUSED(value)
586 // ### remove me
587}
588
590{
591 // ### remove me
592 return 1.0;
593}
594
595void CartesianAxis::Private::drawTitleText(QPainter *painter, CartesianCoordinatePlane *plane,
596 const QRect &geoRect) const
597{
598 const TextAttributes titleTA(titleTextAttributesWithAdjustedRotation());
599 if (titleTA.isVisible()) {
600 TextLayoutItem titleItem(titleText, titleTA, plane->parent(), KDChartEnums::MeasureOrientationMinimum,
602 QPointF point;
603 QSize size = titleItem.sizeHint();
604 switch (position) {
605 case Top:
606 point.setX(geoRect.left() + geoRect.width() / 2);
607 point.setY(geoRect.top() + (size.height() / 2) / axisTitleSpace);
608 size.setWidth(qMin(size.width(), axis()->geometry().width()));
609 break;
610 case Bottom:
611 point.setX(geoRect.left() + geoRect.width() / 2);
612 point.setY(geoRect.bottom() - (size.height() / 2) / axisTitleSpace);
613 size.setWidth(qMin(size.width(), axis()->geometry().width()));
614 break;
615 case Left:
616 point.setX(geoRect.left() + (size.width() / 2) / axisTitleSpace);
617 point.setY(geoRect.top() + geoRect.height() / 2);
618 size.setHeight(qMin(size.height(), axis()->geometry().height()));
619 break;
620 case Right:
621 point.setX(geoRect.right() - (size.width() / 2) / axisTitleSpace);
622 point.setY(geoRect.top() + geoRect.height() / 2);
623 size.setHeight(qMin(size.height(), axis()->geometry().height()));
624 break;
625 }
626 const PainterSaver painterSaver(painter);
627 painter->setClipping(false);
628 painter->translate(point);
629 titleItem.setGeometry(QRect(QPoint(-size.width() / 2, -size.height() / 2), size));
630 titleItem.paint(painter);
631 }
632}
633
634bool CartesianAxis::Private::isVertical() const
635{
636 return axis()->isAbscissa() == AbstractDiagram::Private::get(diagram())->isTransposed();
637}
638
640{
641 Q_ASSERT_X(d->diagram(), "CartesianAxis::paint",
642 "Function call not allowed: The axis is not assigned to any diagram.");
643
644 auto *plane = dynamic_cast<CartesianCoordinatePlane *>(context->coordinatePlane());
645 Q_ASSERT_X(plane, "CartesianAxis::paint",
646 "Bad function call: PaintContext::coordinatePlane() NOT a cartesian plane.");
647
648 // note: Not having any data model assigned is no bug
649 // but we can not draw an axis then either.
650 if (!d->diagram()->model()) {
651 return;
652 }
653
655
656 XySwitch geoXy(d->isVertical());
657
658 QPainter *const painter = context->painter();
659
660 // determine the position of the axis (also required for labels) and paint it
661
662 qreal transversePosition = signalingNaN; // in data space
663 // the next one describes an additional shift in screen space; it is unfortunately required to
664 // make axis sharing work, which uses the areaGeometry() to override the position of the axis.
666 {
667 // determine the unadulterated position in screen space
668
671 QPointF start(dimX.start, dimY.start);
672 QPointF end(dimX.end, dimY.end);
673 // consider this: you can turn a diagonal line into a horizontal or vertical line on any
674 // edge by changing just one of its four coordinates.
675 switch (position()) {
677 end.setY(dimY.start);
678 break;
680 start.setY(dimY.end);
681 break;
683 end.setX(dimX.start);
684 break;
686 start.setX(dimX.end);
687 break;
688 }
689
690 transversePosition = geoXy(start.y(), start.x());
691
692 QPointF transStart = plane->translate(start);
693 QPointF transEnd = plane->translate(end);
694
695 // an externally set areaGeometry() moves the axis position transversally; the shift is
696 // nonzero only when this is a shared axis
697
698 const QRect geo = areaGeometry();
699 switch (position()) {
702 break;
705 break;
708 break;
711 break;
712 }
713
716
717 if (rulerAttributes().showRulerLine()) {
718 bool clipSaved = context->painter()->hasClipping();
719 painter->setClipping(false);
720 painter->drawLine(transStart, transEnd);
721 painter->setClipping(clipSaved);
722 }
723 }
724
725 // paint ticks and labels
726
729
730 int labelThinningFactor = 1;
731 // TODO: label thinning also when grid line distance < 4 pixels, not only when labels collide
732 auto *tickLabel = new TextLayoutItem(QString(), labelTA, plane->parent(),
734 auto *prevTickLabel = new TextLayoutItem(QString(), labelTA, plane->parent(),
737 enum
738 {
739 Layout = 0,
740 Painting,
741 Done
742 };
743 for (int step = labelTA.isVisible() ? Layout : Painting; step < Done; step++) {
744 bool skipFirstTick = !rulerAttr.showFirstTick();
745 bool isFirstLabel = true;
746 for (TickIterator it(this, plane, labelThinningFactor, centerTicks); !it.isAtEnd(); ++it) {
747 if (skipFirstTick) {
748 skipFirstTick = false;
749 continue;
750 }
751
752 const qreal drawPos = it.position() + (centerTicks ? 0.5 : 0.);
755 geoXy.lvalue(onAxis.ry(), onAxis.rx()) += transverseScreenSpaceShift;
756 const bool isOutwardsPositive = position() == Bottom || position() == Right;
757
758 // paint the tick mark
759
761 qreal tickLen = it.type() == TickIterator::CustomTick ? d->customTickLength : tickLength(it.type() == TickIterator::MinorTick);
762 geoXy.lvalue(tickEnd.ry(), tickEnd.rx()) += isOutwardsPositive ? tickLen : -tickLen;
763
764 // those adjustments are required to paint the ticks exactly on the axis and of the right length
765 if (position() == Top) {
766 onAxis.ry() += 1;
767 tickEnd.ry() += 1;
768 } else if (position() == Left) {
769 tickEnd.rx() += 1;
770 }
771
772 if (step == Painting) {
773 painter->save();
774 if (rulerAttr.hasTickMarkPenAt(it.position())) {
775 painter->setPen(rulerAttr.tickMarkPen(it.position()));
776 } else {
777 painter->setPen(it.type() == TickIterator::MinorTick ? rulerAttr.minorTickMarkPen()
778 : rulerAttr.majorTickMarkPen());
779 }
780 painter->drawLine(onAxis, tickEnd);
781 painter->restore();
782 }
783
784 if (it.text().isEmpty() || !labelTA.isVisible()) {
785 // the following code in the loop is only label painting, so skip it
786 continue;
787 }
788
789 // paint the label
790
791 QString text = it.text();
792 if (it.type() == TickIterator::MajorTick) {
793 // add unit prefixes and suffixes, then customize
794 text = d->customizedLabelText(text, geoXy(Qt::Horizontal, Qt::Vertical), it.position());
795 } else if (it.type() == TickIterator::MajorTickHeaderDataLabel) {
796 // unit prefixes and suffixes have already been added in this case - only customize
797 text = customizedLabel(text);
798 }
799
800 tickLabel->setText(text);
801 QSizeF size = QSizeF(tickLabel->sizeHint());
802 QPolygon labelPoly = tickLabel->boundingPolygon();
803 Q_ASSERT(labelPoly.count() == 4);
804
805 // for alignment, find the label polygon edge "most parallel" and closest to the axis
806
807 int axisAngle = 0;
808 switch (position()) {
809 case Bottom:
810 axisAngle = 0;
811 break;
812 case Top:
813 axisAngle = 180;
814 break;
815 case Right:
816 axisAngle = 270;
817 break;
818 case Left:
819 axisAngle = 90;
820 break;
821 default:
822 Q_ASSERT(false);
823 }
824 // the left axis is not actually pointing down and the top axis not actually pointing
825 // left, but their corresponding closest edges of a rectangular unrotated label polygon are.
826
827 int relAngle = axisAngle - labelTA.rotation() + 45;
828 if (relAngle < 0) {
829 relAngle += 360;
830 }
831 int polyCorner1 = relAngle / 90;
833 QPoint p2 = labelPoly.at(polyCorner1 == 3 ? 0 : (polyCorner1 + 1));
834
836
837 qreal labelMargin = rulerAttr.labelMargin();
838 if (labelMargin < 0) {
839 labelMargin = QFontMetricsF(tickLabel->realFont()).height() * 0.5;
840 }
841 labelMargin -= tickLabel->marginWidth(); // make up for the margin that's already there
842
843 switch (position()) {
844 case Left:
845 labelPos += QPointF(-size.width() - labelMargin,
846 -0.45 * size.height() - 0.5 * (p1.y() + p2.y()));
847 break;
848 case Right:
849 labelPos += QPointF(labelMargin,
850 -0.45 * size.height() - 0.5 * (p1.y() + p2.y()));
851 break;
852 case Top:
853 labelPos += QPointF(-0.45 * size.width() - 0.5 * (p1.x() + p2.x()),
854 -size.height() - labelMargin);
855 break;
856 case Bottom:
857 labelPos += QPointF(-0.45 * size.width() - 0.5 * (p1.x() + p2.x()),
858 labelMargin);
859 break;
860 }
861
862 tickLabel->setGeometry(QRect(labelPos.toPoint(), size.toSize()));
863
864 if (step == Painting) {
865 tickLabel->paint(painter);
866 }
867
868 // collision check the current label against the previous one
869
870 // like in the old code, we don't shorten or decimate labels if they are already the
871 // manual short type, or if they are the manual long type and on the vertical axis
872 // ### they can still collide though, especially when they're rotated!
873 if (step == Layout) {
874 int spaceSavingRotation = geoXy(270, 0);
875 bool canRotate = labelTA.autoRotate() && labelTA.rotation() != spaceSavingRotation;
876 const bool canShortenLabels = !geoXy.isY && it.type() == TickIterator::MajorTickManualLong && it.hasShorterLabels();
877 bool collides = false;
878 if (it.type() == TickIterator::MajorTick || it.type() == TickIterator::MajorTickHeaderDataLabel
880 if (isFirstLabel) {
881 isFirstLabel = false;
882 } else {
885 }
887 }
888 if (collides) {
889 // to make room, we try in order: shorten, rotate, decimate
890 if (canRotate && !canShortenLabels) {
891 labelTA.setRotation(spaceSavingRotation);
892 // tickLabel will be reused in the next round
893 tickLabel->setTextAttributes(labelTA);
894 } else {
896 }
897 step--; // relayout
898 break;
899 }
900 }
901 }
902 }
903 delete tickLabel;
904 tickLabel = nullptr;
905 delete prevTickLabel;
906 prevTickLabel = nullptr;
907
908 if (!titleText().isEmpty()) {
909 d->drawTitleText(painter, plane, geometry());
910 }
911}
912
913/* pure virtual in QLayoutItem */
915{
916 return false; // if the axis exists, it has some (perhaps default) content
917}
918
919/* pure virtual in QLayoutItem */
921{
923 switch (position()) {
924 case Bottom:
925 case Top:
927 break;
928 case Left:
929 case Right:
931 break;
932 default:
933 Q_ASSERT(false);
934 break;
935 };
936 return ret;
937}
938
940{
941 d->cachedMaximumSize = QSize();
942}
943
944/* pure virtual in QLayoutItem */
946{
947 if (!d->cachedMaximumSize.isValid())
948 d->cachedMaximumSize = d->calculateMaximumSize();
949 return d->cachedMaximumSize;
950}
951
952QSize CartesianAxis::Private::calculateMaximumSize() const
953{
954 if (!diagram()) {
955 return QSize();
956 }
957
958 auto *plane = dynamic_cast<CartesianCoordinatePlane *>(diagram()->coordinatePlane());
959 Q_ASSERT(plane);
960 QObject *refArea = plane->parent();
961 const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks(diagram())
962 && axis()->isAbscissa();
963
964 // we ignore:
965 // - label thinning (expensive, not worst case and we want worst case)
966 // - label autorotation (expensive, obscure feature(?))
967 // - axis length (it is determined by the plane / diagram / chart anyway)
968 // - the title's influence on axis length; this one might be TODO. See KDCH-863.
969
970 XySwitch geoXy(isVertical());
971 qreal size = 0; // this is the size transverse to the axis direction
972
973 // the following variables describe how much the first and last label stick out over the axis
974 // area, so that the geometry of surrounding layout items can be adjusted to make room.
975 qreal startOverhang = 0.0;
976 qreal endOverhang = 0.0;
977
978 if (mAxis->textAttributes().isVisible()) {
979 // these four are used just to calculate startOverhang and endOverhang
980 qreal lowestLabelPosition = signalingNaN;
981 qreal highestLabelPosition = signalingNaN;
982 qreal lowestLabelLongitudinalSize = signalingNaN;
983 qreal highestLabelLongitudinalSize = signalingNaN;
984
985 TextLayoutItem tickLabel(QString(), mAxis->textAttributes(), refArea,
987 const RulerAttributes rulerAttr = mAxis->rulerAttributes();
988
989 bool showFirstTick = rulerAttr.showFirstTick();
990 for (TickIterator it(axis(), plane, 1, centerTicks); !it.isAtEnd(); ++it) {
991 const qreal drawPos = it.position() + (centerTicks ? 0.5 : 0.);
992 if (!showFirstTick) {
993 showFirstTick = true;
994 continue;
995 }
996
997 qreal labelSizeTransverse = 0.0;
998 qreal labelMargin = 0.0;
999 QString text = it.text();
1000 if (!text.isEmpty()) {
1001 QPointF labelPosition = plane->translate(QPointF(geoXy(drawPos, ( qreal )1.0),
1002 geoXy(( qreal )1.0, drawPos)));
1003 highestLabelPosition = geoXy(labelPosition.x(), labelPosition.y());
1004
1005 if (it.type() == TickIterator::MajorTick) {
1006 // add unit prefixes and suffixes, then customize
1007 text = customizedLabelText(text, geoXy(Qt::Horizontal, Qt::Vertical), it.position());
1008 } else if (it.type() == TickIterator::MajorTickHeaderDataLabel) {
1009 // unit prefixes and suffixes have already been added in this case - only customize
1010 text = axis()->customizedLabel(text);
1011 }
1012 tickLabel.setText(text);
1013
1014 QSize sz = tickLabel.sizeHint();
1015 highestLabelLongitudinalSize = geoXy(sz.width(), sz.height());
1016 if (ISNAN(lowestLabelLongitudinalSize)) {
1017 lowestLabelLongitudinalSize = highestLabelLongitudinalSize;
1018 lowestLabelPosition = highestLabelPosition;
1019 }
1020
1021 labelSizeTransverse = geoXy(sz.height(), sz.width());
1022 labelMargin = rulerAttr.labelMargin();
1023 if (labelMargin < 0) {
1024 labelMargin = QFontMetricsF(tickLabel.realFont()).height() * 0.5;
1025 }
1026 labelMargin -= tickLabel.marginWidth(); // make up for the margin that's already there
1027 }
1028 qreal tickLength = it.type() == TickIterator::CustomTick ? customTickLength : axis()->tickLength(it.type() == TickIterator::MinorTick);
1029 size = qMax(size, tickLength + labelMargin + labelSizeTransverse);
1030 }
1031
1032 const DataDimension dimX = plane->gridDimensionsList().first();
1033 const DataDimension dimY = plane->gridDimensionsList().last();
1034
1035 QPointF pt = plane->translate(QPointF(dimX.start, dimY.start));
1036 const qreal lowestPosition = geoXy(pt.x(), pt.y());
1037 pt = plane->translate(QPointF(dimX.end, dimY.end));
1038 const qreal highestPosition = geoXy(pt.x(), pt.y());
1039
1040 // the geoXy( 1.0, -1.0 ) here is necessary because Qt's y coordinate is inverted
1041 startOverhang = qMax(0.0, (lowestPosition - lowestLabelPosition) * geoXy(1.0, -1.0) + lowestLabelLongitudinalSize * 0.5);
1042 endOverhang = qMax(0.0, (highestLabelPosition - highestPosition) * geoXy(1.0, -1.0) + highestLabelLongitudinalSize * 0.5);
1043 }
1044
1045 amountOfLeftOverlap = geoXy(startOverhang, ( qreal )0.0);
1046 amountOfRightOverlap = geoXy(endOverhang, ( qreal )0.0);
1047 amountOfBottomOverlap = geoXy(( qreal )0.0, startOverhang);
1048 amountOfTopOverlap = geoXy(( qreal )0.0, endOverhang);
1049
1050 const TextAttributes titleTA = titleTextAttributesWithAdjustedRotation();
1051 if (titleTA.isVisible() && !axis()->titleText().isEmpty()) {
1052 TextLayoutItem title(axis()->titleText(), titleTA, refArea, KDChartEnums::MeasureOrientationMinimum,
1054
1055 QFontMetricsF titleFM(title.realFont(), GlobalMeasureScaling::paintDevice());
1056 size += geoXy(titleFM.height() * 0.33, titleFM.averageCharWidth() * 0.55); // spacing
1057 size += geoXy(title.sizeHint().height(), title.sizeHint().width());
1058 }
1059
1060 // the size parallel to the axis direction is not determined by us, so we just return 1
1061 return QSize(geoXy(1, int(size)), geoXy(int(size), 1));
1062}
1063
1064/* pure virtual in QLayoutItem */
1066{
1067 return maximumSize();
1068}
1069
1070/* pure virtual in QLayoutItem */
1072{
1073 return maximumSize();
1074}
1075
1076/* pure virtual in QLayoutItem */
1078{
1079 if (d->geometry != r) {
1080 d->geometry = r;
1082 }
1083}
1084
1085/* pure virtual in QLayoutItem */
1087{
1088 return d->geometry;
1089}
1090
1092{
1093 if (d->customTickLength == value) {
1094 return;
1095 }
1096 d->customTickLength = value;
1098 layoutPlanes();
1099}
1100
1102{
1103 return d->customTickLength;
1104}
1105
1106int CartesianAxis::tickLength(bool subUnitTicks) const
1107{
1109 return subUnitTicks ? rulerAttr.minorTickMarkLength() : rulerAttr.majorTickMarkLength();
1110}
1111
1113{
1114 return d->annotations;
1115}
1116
1118{
1119 if (d->annotations == annotations)
1120 return;
1121
1122 d->annotations = annotations;
1124 layoutPlanes();
1125}
1126
1128{
1129 return d->customTicksPositions;
1130}
1131
1132void CartesianAxis::setCustomTicks(const QList<qreal> &customTicksPositions)
1133{
1134 if (d->customTicksPositions == customTicksPositions)
1135 return;
1136
1137 d->customTicksPositions = customTicksPositions;
1139 layoutPlanes();
1140}
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 Feb 23 2024 00:02:57 for KD Chart API Documentation by doxygen 1.9.8