KD Chart API Documentation  3.1
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 
11 #include "KDChartCartesianAxis.h"
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 
36 using namespace KDChart;
37 
38 #define d (d_func())
39 
40 static 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 
51 static 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 
68 TickIterator::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 
133 static QMultiMap<qreal, QString> allAxisAnnotations(const AbstractCoordinatePlane *plane, bool isY)
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 
153 TickIterator::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 
170 void 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 
233 bool 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 
248 bool TickIterator::isHigherPrecedence(qreal importantTick, qreal unimportantTick) const
249 {
250  return importantTick != std::numeric_limits<qreal>::infinity() && (importantTick <= unimportantTick || areAlmostEqual(importantTick, unimportantTick));
251 }
252 
253 void 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 
286 void 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 
370 CartesianAxis::CartesianAxis(AbstractCartesianDiagram *diagram)
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)) {
385  auto *cd = qobject_cast<AbstractCartesianDiagram *>(diagram);
386  cd->takeAxis(this);
387  }
388 }
389 
390 void CartesianAxis::init()
391 {
392  d->customTickLength = 3;
393  d->position = Bottom;
395  connect(this, &AbstractAxis::coordinateSystemChanged, this, &CartesianAxis::coordinateSystemChanged);
396 }
397 
398 bool 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 
409 void CartesianAxis::coordinateSystemChanged()
410 {
411  layoutPlanes();
412 }
413 
414 void CartesianAxis::setTitleText(const QString &text)
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)
472 const
473 #endif
476 {
477  return d->position;
478 }
479 
481 {
482  if (!d->diagram() || !d->diagram()->coordinatePlane()) {
483  return;
484  }
485  AbstractCoordinatePlane *plane = d->diagram()->coordinatePlane();
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;
519  return diagramOrientation == Qt::Vertical ? position() == Bottom || position() == Top
520  : position() == Left || position() == Right;
521 }
522 
524 {
525  return !isAbscissa();
526 }
527 
528 void CartesianAxis::paint(QPainter *painter)
529 {
530  if (!d->diagram() || !d->diagram()->coordinatePlane()) {
531  return;
532  }
533  PaintContext ctx;
534  ctx.setPainter(painter);
535  AbstractCoordinatePlane *const plane = d->diagram()->coordinatePlane();
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 
551 const 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 
567 QString 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 
575 void 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 
597 void 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,
603  Qt::AlignHCenter | Qt::AlignVCenter);
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 
636 bool 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 
656  const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks(d->diagram()) && isAbscissa();
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.
667  qreal transverseScreenSpaceShift = signalingNaN;
668  {
669  // determine the unadulterated position in screen space
670 
671  DataDimension dimX = plane->gridDimensionsList().first();
672  DataDimension dimY = plane->gridDimensionsList().last();
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;
681  case CartesianAxis::Top:
682  start.setY(dimY.end);
683  break;
684  case CartesianAxis::Left:
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()) {
703  transverseScreenSpaceShift = geo.top() - transStart.y();
704  break;
705  case CartesianAxis::Top:
706  transverseScreenSpaceShift = geo.bottom() - transStart.y();
707  break;
708  case CartesianAxis::Left:
709  transverseScreenSpaceShift = geo.right() - transStart.x();
710  break;
712  transverseScreenSpaceShift = geo.left() - transStart.x();
713  break;
714  }
715 
716  geoXy.lvalue(transStart.ry(), transStart.rx()) += transverseScreenSpaceShift;
717  geoXy.lvalue(transEnd.ry(), transEnd.rx()) += transverseScreenSpaceShift;
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 
729  TextAttributes labelTA = textAttributes();
730  RulerAttributes rulerAttr = rulerAttributes();
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(),
738  QPointF prevTickLabelPos;
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.);
755  QPointF onAxis = plane->translate(geoXy(QPointF(drawPos, transversePosition),
756  QPointF(transversePosition, drawPos)));
757  geoXy.lvalue(onAxis.ry(), onAxis.rx()) += transverseScreenSpaceShift;
758  const bool isOutwardsPositive = position() == Bottom || position() == Right;
759 
760  // paint the tick mark
761 
762  QPointF tickEnd = onAxis;
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;
834  QPoint p1 = labelPoly.at(polyCorner1);
835  QPoint p2 = labelPoly.at(polyCorner1 == 3 ? 0 : (polyCorner1 + 1));
836 
837  QPointF labelPos = tickEnd;
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
881  || canShortenLabels || canRotate) {
882  if (isFirstLabel) {
883  isFirstLabel = false;
884  } else {
885  collides = tickLabel->intersects(*prevTickLabel, labelPos, prevTickLabelPos);
886  qSwap(prevTickLabel, tickLabel);
887  }
888  prevTickLabelPos = labelPos;
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 {
897  labelThinningFactor++;
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 */
922 Qt::Orientations CartesianAxis::expandingDirections() const
923 {
924  Qt::Orientations ret;
925  switch (position()) {
926  case Bottom:
927  case Top:
928  ret = Qt::Horizontal;
929  break;
930  case Left:
931  case Right:
932  ret = Qt::Vertical;
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 
954 QSize 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,
1055  Qt::AlignHCenter | Qt::AlignVCenter);
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 */
1079 void CartesianAxis::setGeometry(const QRect &r)
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 
1108 int CartesianAxis::tickLength(bool subUnitTicks) const
1109 {
1110  const RulerAttributes &rulerAttr = rulerAttributes();
1111  return subUnitTicks ? rulerAttr.minorTickMarkLength() : rulerAttr.majorTickMarkLength();
1112 }
1113 
1114 QMultiMap<qreal, QString> CartesianAxis::annotations() const
1115 {
1116  return d->annotations;
1117 }
1118 
1119 void CartesianAxis::setAnnotations(const QMultiMap<qreal, QString> &annotations)
1120 {
1121  if (d->annotations == annotations)
1122  return;
1123 
1124  d->annotations = annotations;
1126  layoutPlanes();
1127 }
1128 
1129 QList<qreal> CartesianAxis::customTicks() const
1130 {
1131  return d->customTicksPositions;
1132 }
1133 
1134 void CartesianAxis::setCustomTicks(const QList<qreal> &customTicksPositions)
1135 {
1136  if (d->customTicksPositions == customTicksPositions)
1137  return;
1138 
1139  d->customTicksPositions = customTicksPositions;
1141  layoutPlanes();
1142 }
static QMultiMap< qreal, QString > allAxisAnnotations(const AbstractCoordinatePlane *plane, bool isY)
#define d
static int numSignificantDecimalPlaces(qreal floatNumber)
static bool referenceDiagramIsBarDiagram(const AbstractDiagram *diagram)
static qreal slightlyLessThan(qreal r)
static bool referenceDiagramNeedsCenteredAbscissaTicks(const AbstractDiagram *diagram)
@ MeasureOrientationMinimum
Definition: KDChartEnums.h:290
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
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
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)
bool hasDefaultTitleTextAttributes() const
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.
void setValue(qreal val)
qreal value() const
Stores information about painting diagrams.
void setPainter(QPainter *painter)
void setCoordinatePlane(AbstractCoordinatePlane *plane)
void setRectangle(const QRectF &rect)
AbstractCoordinatePlane * coordinatePlane() const
QPainter * painter() const
A set of attributes controlling the appearance of axis rulers.
bool hasTickMarkPenAt(qreal value) const
A set of text attributes.
void setFontSize(const Measure &measure)

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