1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Charts module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 or (at your option) any later version
20** approved by the KDE Free Qt Foundation. The licenses are as published by
21** the Free Software Foundation and appearing in the file LICENSE.GPL3
22** included in the packaging of this file. Please review the following
23** information to ensure the GNU General Public License requirements will
24** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25**
26** $QT_END_LICENSE$
27**
28****************************************************************************/
29
30#include <QtCharts/qcategoryaxis.h>
31#include <QtCharts/qlogvalueaxis.h>
32#include <QtCore/qmath.h>
33#include <private/chartpresenter_p.h>
34#include <private/horizontalaxis_p.h>
35
36QT_CHARTS_BEGIN_NAMESPACE
37
38HorizontalAxis::HorizontalAxis(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis)
39 : CartesianChartAxis(axis, item, intervalAxis)
40{
41}
42
43HorizontalAxis::~HorizontalAxis()
44{
45}
46
47QSizeF HorizontalAxis::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const
48{
49 Q_UNUSED(constraint);
50 QSizeF sh(0,0);
51
52 if (axis()->titleText().isEmpty() || !titleItem()->isVisible())
53 return sh;
54
55 switch (which) {
56 case Qt::MinimumSize: {
57 QRectF titleRect = ChartPresenter::textBoundingRect(font: axis()->titleFont(),
58 QStringLiteral("..."));
59 sh = QSizeF(titleRect.width(), titleRect.height() + (titlePadding() * 2.0));
60 break;
61 }
62 case Qt::MaximumSize:
63 case Qt::PreferredSize: {
64 QRectF titleRect = ChartPresenter::textBoundingRect(font: axis()->titleFont(), text: axis()->titleText());
65 sh = QSizeF(titleRect.width(), titleRect.height() + (titlePadding() * 2.0));
66 break;
67 }
68 default:
69 break;
70 }
71
72 return sh;
73}
74
75void HorizontalAxis::updateGeometry()
76{
77 const QVector<qreal> &layout = ChartAxisElement::layout();
78 const QVector<qreal> &dynamicMinorTicklayout = ChartAxisElement::dynamicMinorTicklayout();
79
80 if (layout.isEmpty() && dynamicMinorTicklayout.isEmpty()
81 && axis()->type() != QAbstractAxis::AxisTypeLogValue) {
82 return;
83 }
84
85 QStringList labelList = labels();
86
87 QList<QGraphicsItem *> labels = labelItems();
88 QList<QGraphicsItem *> arrow = arrowItems();
89 QGraphicsTextItem *title = titleItem();
90
91 Q_ASSERT(labels.size() == labelList.size());
92 Q_ASSERT(layout.size() == labelList.size());
93
94 const QRectF &axisRect = axisGeometry();
95 const QRectF &gridRect = gridGeometry();
96
97 //arrow
98 QGraphicsLineItem *arrowItem = static_cast<QGraphicsLineItem *>(arrow.at(i: 0));
99
100 if (axis()->alignment() == Qt::AlignTop)
101 arrowItem->setLine(x1: gridRect.left(), y1: axisRect.bottom(), x2: gridRect.right(), y2: axisRect.bottom());
102 else if (axis()->alignment() == Qt::AlignBottom)
103 arrowItem->setLine(x1: gridRect.left(), y1: axisRect.top(), x2: gridRect.right(), y2: axisRect.top());
104
105 const QLatin1String ellipsis("...");
106
107 //title
108 QRectF titleBoundingRect;
109 QString titleText = axis()->titleText();
110 qreal availableSpace = axisRect.height() - labelPadding();
111 if (!titleText.isEmpty() && titleItem()->isVisible()) {
112 availableSpace -= titlePadding() * 2.0;
113 qreal minimumLabelHeight = ChartPresenter::textBoundingRect(font: axis()->labelsFont(),
114 QStringLiteral("...")).height();
115 qreal titleSpace = availableSpace - minimumLabelHeight;
116 title->setHtml(ChartPresenter::truncatedText(font: axis()->titleFont(), text: titleText, angle: qreal(0.0),
117 maxWidth: gridRect.width(), maxHeight: titleSpace,
118 boundingRect&: titleBoundingRect));
119 title->setTextWidth(titleBoundingRect.width());
120
121 titleBoundingRect = title->boundingRect();
122
123 QPointF center = gridRect.center() - titleBoundingRect.center();
124 if (axis()->alignment() == Qt::AlignTop)
125 title->setPos(ax: center.x(), ay: axisRect.top() + titlePadding());
126 else if (axis()->alignment() == Qt::AlignBottom)
127 title->setPos(ax: center.x(), ay: axisRect.bottom() - titleBoundingRect.height() - titlePadding());
128
129 availableSpace -= titleBoundingRect.height();
130 }
131
132 QList<QGraphicsItem *> lines = gridItems();
133 QList<QGraphicsItem *> shades = shadeItems();
134
135 qreal last_label_max_x = 0;
136
137 for (int i = 0; i < layout.size(); ++i) {
138 //items
139 QGraphicsLineItem *gridItem = static_cast<QGraphicsLineItem*>(lines.at(i));
140 QGraphicsLineItem *tickItem = static_cast<QGraphicsLineItem*>(arrow.at(i: i + 1));
141 QGraphicsTextItem *labelItem = static_cast<QGraphicsTextItem *>(labels.at(i));
142
143 //grid line
144 if (axis()->isReverse()) {
145 gridItem->setLine(x1: gridRect.right() - layout[i] + gridRect.left(), y1: gridRect.top(),
146 x2: gridRect.right() - layout[i] + gridRect.left(), y2: gridRect.bottom());
147 } else {
148 gridItem->setLine(x1: layout[i], y1: gridRect.top(), x2: layout[i], y2: gridRect.bottom());
149 }
150
151 //label text wrapping
152 QString text;
153 if (axis()->isReverse() && axis()->type() != QAbstractAxis::AxisTypeCategory)
154 text = labelList.at(i: labelList.count() - i - 1);
155 else
156 text = labelList.at(i);
157
158 QRectF boundingRect;
159 // don't truncate empty labels
160 if (text.isEmpty()) {
161 labelItem->setHtml(text);
162 } else {
163 qreal labelWidth = axisRect.width() / layout.count() - (2 * labelPadding());
164 // Replace digits with ellipsis "..." if number does not fit
165 QString truncatedText = ChartPresenter::truncatedText(font: axis()->labelsFont(), text,
166 angle: axis()->labelsAngle(),
167 maxWidth: labelWidth,
168 maxHeight: availableSpace, boundingRect);
169 labelItem->setTextWidth(ChartPresenter::textBoundingRect(font: axis()->labelsFont(),
170 text: truncatedText).width());
171 labelItem->setHtml(truncatedText);
172 }
173
174 //label transformation origin point
175 const QRectF& rect = labelItem->boundingRect();
176 QPointF center = rect.center();
177 labelItem->setTransformOriginPoint(ax: center.x(), ay: center.y());
178 qreal heightDiff = rect.height() - boundingRect.height();
179 qreal widthDiff = rect.width() - boundingRect.width();
180
181 //ticks and label position
182 QPointF labelPos;
183 if (axis()->alignment() == Qt::AlignTop) {
184 if (axis()->isReverse()) {
185 labelPos = QPointF(gridRect.right() - layout[layout.size() - i - 1]
186 + gridRect.left() - center.x(),
187 axisRect.bottom() - rect.height()
188 + (heightDiff / 2.0) - labelPadding());
189 tickItem->setLine(x1: gridRect.right() + gridRect.left() - layout[i],
190 y1: axisRect.bottom(),
191 x2: gridRect.right() + gridRect.left() - layout[i],
192 y2: axisRect.bottom() - labelPadding());
193 } else {
194 labelPos = QPointF(layout[i] - center.x(), axisRect.bottom() - rect.height()
195 + (heightDiff / 2.0) - labelPadding());
196 tickItem->setLine(x1: layout[i], y1: axisRect.bottom(),
197 x2: layout[i], y2: axisRect.bottom() - labelPadding());
198 }
199 } else if (axis()->alignment() == Qt::AlignBottom) {
200 if (axis()->isReverse()) {
201 labelPos = QPointF(gridRect.right() - layout[layout.size() - i - 1]
202 + gridRect.left() - center.x(),
203 axisRect.top() - (heightDiff / 2.0) + labelPadding());
204 tickItem->setLine(x1: gridRect.right() + gridRect.left() - layout[i], y1: axisRect.top(),
205 x2: gridRect.right() + gridRect.left() - layout[i],
206 y2: axisRect.top() + labelPadding());
207 } else {
208 labelPos = QPointF(layout[i] - center.x(), axisRect.top() - (heightDiff / 2.0)
209 + labelPadding());
210 tickItem->setLine(x1: layout[i], y1: axisRect.top(),
211 x2: layout[i], y2: axisRect.top() + labelPadding());
212 }
213 }
214
215 //label in between
216 bool forceHide = false;
217 if (intervalAxis() && (i + 1) != layout.size()) {
218 qreal leftBound;
219 qreal rightBound;
220 if (axis()->isReverse()) {
221 leftBound = qMax(a: gridRect.right() + gridRect.left() - layout[i + 1],
222 b: gridRect.left());
223 rightBound = qMin(a: gridRect.right() + gridRect.left() - layout[i], b: gridRect.right());
224 } else {
225 leftBound = qMax(a: layout[i], b: gridRect.left());
226 rightBound = qMin(a: layout[i + 1], b: gridRect.right());
227 }
228 const qreal delta = rightBound - leftBound;
229 if (axis()->type() != QAbstractAxis::AxisTypeCategory) {
230 // Hide label in case visible part of the category at the grid edge is too narrow
231 if (delta < boundingRect.width()
232 && (leftBound == gridRect.left() || rightBound == gridRect.right())) {
233 forceHide = true;
234 } else {
235 labelPos.setX(leftBound + (delta / 2.0) - center.x());
236 }
237 } else {
238 QCategoryAxis *categoryAxis = static_cast<QCategoryAxis *>(axis());
239 if (categoryAxis->labelsPosition() == QCategoryAxis::AxisLabelsPositionCenter) {
240 if (delta < boundingRect.width()
241 && (leftBound == gridRect.left() || rightBound == gridRect.right())) {
242 forceHide = true;
243 } else {
244 labelPos.setX(leftBound + (delta / 2.0) - center.x());
245 }
246 } else if (categoryAxis->labelsPosition()
247 == QCategoryAxis::AxisLabelsPositionOnValue) {
248 if (axis()->isReverse())
249 labelPos.setX(leftBound - center.x());
250 else
251 labelPos.setX(rightBound - center.x());
252 }
253 }
254 }
255
256 // Round to full pixel via QPoint to avoid one pixel clipping on the edge in some cases
257 labelItem->setPos(labelPos.toPoint());
258
259 //label overlap detection - compensate one pixel for rounding errors
260 if ((labelItem->pos().x() < last_label_max_x && labelItem->toPlainText() == ellipsis)
261 || forceHide
262 || (labelItem->pos().x() + (widthDiff / 2.0)) < (axisRect.left() - 1.0)
263 || (labelItem->pos().x() + (widthDiff / 2.0) - 1.0) > axisRect.right()) {
264 labelItem->setVisible(false);
265 } else {
266 labelItem->setVisible(true);
267 last_label_max_x = boundingRect.width() + labelItem->pos().x();
268 }
269
270 //shades
271 QGraphicsRectItem *shadeItem = 0;
272 if (i == 0)
273 shadeItem = static_cast<QGraphicsRectItem *>(shades.at(i: 0));
274 else if (i % 2)
275 shadeItem = static_cast<QGraphicsRectItem *>(shades.at(i: (i / 2) + 1));
276 if (shadeItem) {
277 qreal leftBound;
278 qreal rightBound;
279 if (i == 0) {
280 if (axis()->isReverse()) {
281 leftBound = gridRect.right() + gridRect.left() - layout[i];
282 rightBound = gridRect.right();
283 } else {
284 leftBound = gridRect.left();
285 rightBound = layout[0];
286 }
287 } else {
288 if (axis()->isReverse()) {
289 rightBound = gridRect.right() + gridRect.left() - layout[i];
290 if (i == layout.size() - 1) {
291 leftBound = gridRect.left();
292 } else {
293 leftBound = qMax(a: gridRect.right() + gridRect.left() - layout[i + 1],
294 b: gridRect.left());
295 }
296 } else {
297 leftBound = layout[i];
298 if (i == layout.size() - 1)
299 rightBound = gridRect.right();
300 else
301 rightBound = qMin(a: layout[i + 1], b: gridRect.right());
302 }
303 }
304 if (leftBound < gridRect.left())
305 leftBound = gridRect.left();
306 if (rightBound > gridRect.right())
307 rightBound = gridRect.right();
308 shadeItem->setRect(ax: leftBound, ay: gridRect.top(), w: rightBound - leftBound,
309 h: gridRect.height());
310 if (shadeItem->rect().width() <= 0.0)
311 shadeItem->setVisible(false);
312 else
313 shadeItem->setVisible(true);
314 }
315
316 // check if the grid line and the axis tick should be shown
317 const bool gridLineVisible = (gridItem->line().p1().x() >= gridRect.left()
318 && gridItem->line().p1().x() <= gridRect.right());
319 gridItem->setVisible(gridLineVisible);
320 tickItem->setVisible(gridLineVisible);
321 }
322
323 updateMinorTickGeometry();
324
325 // begin/end grid line in case labels between
326 if (intervalAxis()) {
327 QGraphicsLineItem *gridLine;
328 gridLine = static_cast<QGraphicsLineItem *>(lines.at(i: layout.size()));
329 gridLine->setLine(x1: gridRect.right(), y1: gridRect.top(), x2: gridRect.right(), y2: gridRect.bottom());
330 gridLine->setVisible(true);
331 gridLine = static_cast<QGraphicsLineItem*>(lines.at(i: layout.size()+1));
332 gridLine->setLine(x1: gridRect.left(), y1: gridRect.top(), x2: gridRect.left(), y2: gridRect.bottom());
333 gridLine->setVisible(true);
334 }
335}
336
337void HorizontalAxis::updateMinorTickGeometry()
338{
339 if (!axis())
340 return;
341
342 QVector<qreal> layout = ChartAxisElement::layout();
343 int minorTickCount = 0;
344 qreal tickSpacing = 0.0;
345 QVector<qreal> minorTickSpacings;
346 switch (axis()->type()) {
347 case QAbstractAxis::AxisTypeValue: {
348 const QValueAxis *valueAxis = qobject_cast<QValueAxis *>(object: axis());
349
350 minorTickCount = valueAxis->minorTickCount();
351
352 if (valueAxis->tickType() == QValueAxis::TicksFixed) {
353 if (valueAxis->tickCount() >= 2)
354 tickSpacing = layout.at(i: 0) - layout.at(i: 1);
355
356 for (int i = 0; i < minorTickCount; ++i) {
357 const qreal ratio = (1.0 / qreal(minorTickCount + 1)) * qreal(i + 1);
358 minorTickSpacings.append(t: tickSpacing * ratio);
359 }
360 }
361 break;
362 }
363 case QAbstractAxis::AxisTypeLogValue: {
364 const QLogValueAxis *logValueAxis = qobject_cast<QLogValueAxis *>(object: axis());
365 const qreal base = logValueAxis->base();
366 const qreal logBase = qLn(v: base);
367
368 minorTickCount = logValueAxis->minorTickCount();
369 if (minorTickCount < 0)
370 minorTickCount = qMax(a: int(qFloor(v: base) - 2.0), b: 0);
371
372 // Two "virtual" ticks are required to make sure that all minor ticks
373 // are displayed properly (even for the partially visible segments of
374 // the chart).
375 if (layout.size() >= 2) {
376 // Calculate tickSpacing as a difference between visible ticks
377 // whenever it is possible. Virtual ticks will not be correctly
378 // positioned when the layout is animating.
379 tickSpacing = layout.at(i: 0) - layout.at(i: 1);
380 layout.prepend(t: layout.at(i: 0) + tickSpacing);
381 layout.append(t: layout.at(i: layout.size() - 1) - tickSpacing);
382 } else {
383 const qreal logMax = qLn(v: logValueAxis->max());
384 const qreal logMin = qLn(v: logValueAxis->min());
385 const qreal logExtraMaxTick = qLn(v: qPow(x: base, y: qFloor(v: logMax / logBase) + 1.0));
386 const qreal logExtraMinTick = qLn(v: qPow(x: base, y: qCeil(v: logMin / logBase) - 1.0));
387 const qreal edge = gridGeometry().left();
388 const qreal delta = gridGeometry().width() / qAbs(t: logMax - logMin);
389 const qreal extraMaxTick = edge + (logExtraMaxTick - qMin(a: logMin, b: logMax)) * delta;
390 const qreal extraMinTick = edge + (logExtraMinTick - qMin(a: logMin, b: logMax)) * delta;
391
392 // Calculate tickSpacing using one (if layout.size() == 1) or two
393 // (if layout.size() == 0) "virtual" ticks. In both cases animation
394 // will not work as expected. This should be fixed later.
395 layout.prepend(t: extraMinTick);
396 layout.append(t: extraMaxTick);
397 tickSpacing = layout.at(i: 0) - layout.at(i: 1);
398 }
399
400 const qreal minorTickStepValue = qFabs(v: base - 1.0) / qreal(minorTickCount + 1);
401 for (int i = 0; i < minorTickCount; ++i) {
402 const qreal x = minorTickStepValue * qreal(i + 1) + 1.0;
403 const qreal minorTickSpacing = tickSpacing * (qLn(v: x) / logBase);
404 minorTickSpacings.append(t: minorTickSpacing);
405 }
406 break;
407 }
408 default:
409 // minor ticks are not supported
410 break;
411 }
412
413 const QValueAxis *valueAxis = qobject_cast<QValueAxis *>(object: axis());
414 if (valueAxis && valueAxis->tickType() == QValueAxis::TicksDynamic) {
415 const QVector<qreal> dynamicMinorTicklayout = ChartAxisElement::dynamicMinorTicklayout();
416 const QRectF &gridRect = gridGeometry();
417 const qreal deltaX = gridRect.width() / (valueAxis->max() - valueAxis->min());
418 const qreal leftPos = gridRect.left();
419 const qreal rightPos = gridRect.right();
420
421 for (int i = 0; i < dynamicMinorTicklayout.size(); i++) {
422 QGraphicsLineItem *minorGridLineItem =
423 static_cast<QGraphicsLineItem *>(minorGridItems().value(i));
424 QGraphicsLineItem *minorArrowLineItem =
425 static_cast<QGraphicsLineItem *>(minorArrowItems().value(i));
426 if (!minorGridLineItem || !minorArrowLineItem)
427 continue;
428
429 qreal minorGridLineItemX = 0.0;
430 if (axis()->isReverse())
431 minorGridLineItemX = rightPos - dynamicMinorTicklayout.at(i) * deltaX;
432 else
433 minorGridLineItemX = dynamicMinorTicklayout.at(i) * deltaX + leftPos;
434
435 qreal minorArrowLineItemY1;
436 qreal minorArrowLineItemY2;
437 switch (axis()->alignment()) {
438 case Qt::AlignTop:
439 minorArrowLineItemY1 = gridGeometry().bottom();
440 minorArrowLineItemY2 = gridGeometry().bottom() - labelPadding() / 2.0;
441 break;
442 case Qt::AlignBottom:
443 minorArrowLineItemY1 = gridGeometry().top();
444 minorArrowLineItemY2 = gridGeometry().top() + labelPadding() / 2.0;
445 break;
446 default:
447 minorArrowLineItemY1 = 0.0;
448 minorArrowLineItemY2 = 0.0;
449 break;
450 }
451
452 minorGridLineItem->setLine(x1: minorGridLineItemX, y1: gridGeometry().top(),
453 x2: minorGridLineItemX, y2: gridGeometry().bottom());
454 minorArrowLineItem->setLine(x1: minorGridLineItemX, y1: minorArrowLineItemY1,
455 x2: minorGridLineItemX, y2: minorArrowLineItemY2);
456
457 // check if the minor grid line and the minor axis arrow should be shown
458 bool minorGridLineVisible = (minorGridLineItemX >= gridGeometry().left()
459 && minorGridLineItemX <= gridGeometry().right());
460 minorGridLineItem->setVisible(minorGridLineVisible);
461 minorArrowLineItem->setVisible(minorGridLineVisible);
462 }
463 } else {
464 if (minorTickCount < 1 || tickSpacing == 0.0 || minorTickSpacings.count() != minorTickCount)
465 return;
466
467 for (int i = 0; i < layout.size() - 1; ++i) {
468 for (int j = 0; j < minorTickCount; ++j) {
469 const int minorItemIndex = i * minorTickCount + j;
470 QGraphicsLineItem *minorGridLineItem =
471 static_cast<QGraphicsLineItem *>(minorGridItems().value(i: minorItemIndex));
472 QGraphicsLineItem *minorArrowLineItem =
473 static_cast<QGraphicsLineItem *>(minorArrowItems().value(i: minorItemIndex));
474 if (!minorGridLineItem || !minorArrowLineItem)
475 continue;
476
477 const qreal minorTickSpacing = minorTickSpacings.value(i: j, defaultValue: 0.0);
478
479 qreal minorGridLineItemX = 0.0;
480 if (axis()->isReverse()) {
481 minorGridLineItemX = qFloor(v: gridGeometry().left() + gridGeometry().right()
482 - layout.at(i) + minorTickSpacing);
483 } else {
484 minorGridLineItemX = qCeil(v: layout.at(i) - minorTickSpacing);
485 }
486
487 qreal minorArrowLineItemY1;
488 qreal minorArrowLineItemY2;
489 switch (axis()->alignment()) {
490 case Qt::AlignTop:
491 minorArrowLineItemY1 = gridGeometry().top();
492 minorArrowLineItemY2 = gridGeometry().top() - labelPadding() / 2.0;
493 break;
494 case Qt::AlignBottom:
495 minorArrowLineItemY1 = gridGeometry().bottom();
496 minorArrowLineItemY2 = gridGeometry().bottom() + labelPadding() / 2.0;
497 break;
498 default:
499 minorArrowLineItemY1 = 0.0;
500 minorArrowLineItemY2 = 0.0;
501 break;
502 }
503
504 minorGridLineItem->setLine(x1: minorGridLineItemX, y1: gridGeometry().top(),
505 x2: minorGridLineItemX, y2: gridGeometry().bottom());
506 minorArrowLineItem->setLine(x1: minorGridLineItemX, y1: minorArrowLineItemY1,
507 x2: minorGridLineItemX, y2: minorArrowLineItemY2);
508
509 // check if the minor grid line and the minor axis arrow should be shown
510 bool minorGridLineVisible = (minorGridLineItemX >= gridGeometry().left()
511 && minorGridLineItemX <= gridGeometry().right());
512 minorGridLineItem->setVisible(minorGridLineVisible);
513 minorArrowLineItem->setVisible(minorGridLineVisible);
514 }
515 }
516 }
517}
518
519QT_CHARTS_END_NAMESPACE
520

source code of qtcharts/src/charts/axis/horizontalaxis.cpp