1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <QtCharts/qcategoryaxis.h>
5#include <QtCharts/qlogvalueaxis.h>
6#include <QtCore/qmath.h>
7#include <QtGui/qtextdocument.h>
8#include <private/chartpresenter_p.h>
9#include <private/linearrowitem_p.h>
10#include <private/polarchartaxisradial_p.h>
11
12QT_BEGIN_NAMESPACE
13
14PolarChartAxisRadial::PolarChartAxisRadial(QAbstractAxis *axis, QGraphicsItem *item,
15 bool intervalAxis)
16 : PolarChartAxis(axis, item, intervalAxis)
17{
18}
19
20PolarChartAxisRadial::~PolarChartAxisRadial()
21{
22}
23
24void PolarChartAxisRadial::updateGeometry()
25{
26 const QList<qreal> &layout = this->layout();
27 if (layout.isEmpty() && axis()->type() != QAbstractAxis::AxisTypeLogValue)
28 return;
29
30 createAxisLabels(layout);
31 QStringList labelList = labels();
32 QPointF center = axisGeometry().center();
33 QList<QGraphicsItem *> arrowItemList = arrowItems();
34 QList<QGraphicsItem *> gridItemList = gridItems();
35 QList<QGraphicsItem *> labelItemList = labelItems();
36 QList<QGraphicsItem *> shadeItemList = shadeItems();
37 QList<QGraphicsItem *> minorGridItemList = minorGridItems();
38 QList<QGraphicsItem *> minorArrowItemList = minorArrowItems();
39 QGraphicsTextItem* title = titleItem();
40 qreal radius = axisGeometry().height() / 2.0;
41
42 QLineF line(center, center + QPointF(0, -radius));
43 QGraphicsLineItem *axisLine = static_cast<QGraphicsLineItem *>(arrowItemList.at(i: 0));
44 axisLine->setLine(line);
45
46 QRectF previousLabelRect;
47 bool firstShade = true;
48 bool nextTickVisible = false;
49 if (layout.size())
50 nextTickVisible = !(layout.at(i: 0) < 0.0 || layout.at(i: 0) > radius);
51
52 for (int i = 0; i < layout.size(); ++i) {
53 qreal radialCoordinate = layout.at(i);
54
55 QGraphicsEllipseItem *gridItem = static_cast<QGraphicsEllipseItem *>(gridItemList.at(i));
56 QGraphicsLineItem *tickItem = static_cast<QGraphicsLineItem *>(arrowItemList.at(i: i + 1));
57 QGraphicsTextItem *labelItem = static_cast<QGraphicsTextItem *>(labelItemList.at(i));
58 QGraphicsPathItem *shadeItem = 0;
59 if (i == 0)
60 shadeItem = static_cast<QGraphicsPathItem *>(shadeItemList.at(i: 0));
61 else if (i % 2)
62 shadeItem = static_cast<QGraphicsPathItem *>(shadeItemList.at(i: (i / 2) + 1));
63
64 // Ignore ticks outside valid range
65 bool currentTickVisible = nextTickVisible;
66 if ((i == layout.size() - 1)
67 || layout.at(i: i + 1) < 0.0
68 || layout.at(i: i + 1) > radius) {
69 nextTickVisible = false;
70 } else {
71 nextTickVisible = true;
72 }
73
74 qreal labelCoordinate = radialCoordinate;
75 bool labelVisible = currentTickVisible;
76 qreal labelPad = labelPadding() / 2.0;
77 bool centeredLabel = true; // Only used with interval axes
78 if (intervalAxis()) {
79 qreal farEdge;
80 if (i == (layout.size() - 1))
81 farEdge = radius;
82 else
83 farEdge = qMin(a: radius, b: layout.at(i: i + 1));
84
85 // Adjust the labelCoordinate to show it if next tick is visible
86 if (nextTickVisible)
87 labelCoordinate = qMax(a: qreal(0.0), b: labelCoordinate);
88
89 if (axis()->type() == QAbstractAxis::AxisTypeCategory) {
90 QCategoryAxis *categoryAxis = static_cast<QCategoryAxis *>(axis());
91 if (categoryAxis->labelsPosition() == QCategoryAxis::AxisLabelsPositionOnValue)
92 centeredLabel = false;
93 }
94 if (centeredLabel) {
95 labelCoordinate = (labelCoordinate + farEdge) / 2.0;
96 if (labelCoordinate > 0.0 && labelCoordinate < radius)
97 labelVisible = true;
98 else
99 labelVisible = false;
100 } else {
101 labelVisible = nextTickVisible;
102 labelCoordinate = farEdge;
103 }
104 }
105
106 // Radial axis label
107 if (axis()->labelsVisible() && labelVisible) {
108 QRectF boundingRect = ChartPresenter::textBoundingRect(font: axis()->labelsFont(),
109 text: labelList.at(i),
110 angle: axis()->labelsAngle());
111 labelItem->setTextWidth(boundingRect.width());
112 labelItem->setHtml(labelList.at(i));
113 QRectF labelRect = labelItem->boundingRect();
114 QPointF labelCenter = labelRect.center();
115 labelItem->setTransformOriginPoint(ax: labelCenter.x(), ay: labelCenter.y());
116 boundingRect.moveCenter(p: labelCenter);
117 QPointF positionDiff(labelRect.topLeft() - boundingRect.topLeft());
118 QPointF labelPoint = center;
119 if (intervalAxis() && centeredLabel)
120 labelPoint += QPointF(labelPad, -labelCoordinate - (boundingRect.height() / 2.0));
121 else
122 labelPoint += QPointF(labelPad, labelPad - labelCoordinate);
123 labelRect.moveTopLeft(p: labelPoint);
124 labelItem->setPos(labelRect.topLeft() + positionDiff);
125
126 // Label overlap detection
127 labelRect.setSize(boundingRect.size());
128 if ((i && previousLabelRect.intersects(r: labelRect))
129 || !axisGeometry().contains(r: labelRect)) {
130 labelVisible = false;
131 } else {
132 previousLabelRect = labelRect;
133 labelVisible = true;
134 }
135 }
136
137 labelItem->setVisible(labelVisible);
138 if (!currentTickVisible) {
139 gridItem->setVisible(false);
140 tickItem->setVisible(false);
141 if (shadeItem)
142 shadeItem->setVisible(false);
143 continue;
144 }
145
146 // Radial grid line
147 QRectF gridRect;
148 gridRect.setWidth(radialCoordinate * 2.0);
149 gridRect.setHeight(radialCoordinate * 2.0);
150 gridRect.moveCenter(p: center);
151
152 gridItem->setRect(gridRect);
153 gridItem->setVisible(true);
154
155 // Tick
156 QLineF tickLine(-tickWidth(), 0.0, tickWidth(), 0.0);
157 tickLine.translate(adx: center.rx(), ady: gridRect.top());
158 tickItem->setLine(tickLine);
159 tickItem->setVisible(true);
160
161 // Shades
162 if (i % 2 || (i == 0 && !nextTickVisible)) {
163 QPainterPath path;
164 if (i == 0) {
165 // If first tick is also the last, we need to custom fill the inner circle
166 // or it won't get filled.
167 QRectF innerCircle(0.0, 0.0, layout.at(i: 0) * 2.0, layout.at(i: 0) * 2.0);
168 innerCircle.moveCenter(p: center);
169 path.addEllipse(rect: innerCircle);
170 } else {
171 QRectF otherGridRect;
172 if (!nextTickVisible) { // Last visible tick
173 otherGridRect = axisGeometry();
174 } else {
175 qreal otherGridRectDimension = layout.at(i: i + 1) * 2.0;
176 otherGridRect.setWidth(otherGridRectDimension);
177 otherGridRect.setHeight(otherGridRectDimension);
178 otherGridRect.moveCenter(p: center);
179 }
180 path.addEllipse(rect: gridRect);
181 path.addEllipse(rect: otherGridRect);
182
183 // Add additional shading in first visible shade item if there is a partial tick
184 // to be filled at the center (log & category axes)
185 if (firstShade) {
186 QGraphicsPathItem *specialShadeItem = static_cast<QGraphicsPathItem *>(shadeItemList.at(i: 0));
187 if (layout.at(i: i - 1) > 0.0) {
188 QRectF innerCircle(0.0, 0.0, layout.at(i: i - 1) * 2.0, layout.at(i: i - 1) * 2.0);
189 innerCircle.moveCenter(p: center);
190 QPainterPath specialPath;
191 specialPath.addEllipse(rect: innerCircle);
192 specialShadeItem->setPath(specialPath);
193 specialShadeItem->setVisible(true);
194 } else {
195 specialShadeItem->setVisible(false);
196 }
197 }
198 }
199 shadeItem->setPath(path);
200 shadeItem->setVisible(true);
201 firstShade = false;
202 }
203 }
204
205 updateMinorTickGeometry();
206
207 // Title, along the 0 axis
208 QString titleText = axis()->titleText();
209 if (!titleText.isEmpty() && axis()->isTitleVisible()) {
210 QRectF truncatedRect;
211 title->setHtml(ChartPresenter::truncatedText(font: axis()->titleFont(), text: titleText, angle: qreal(0.0),
212 maxWidth: radius, maxHeight: radius, boundingRect&: truncatedRect));
213 title->setTextWidth(truncatedRect.width());
214
215 QRectF titleBoundingRect = title->boundingRect();
216 QPointF titleCenter = titleBoundingRect.center();
217 QPointF arrowCenter = axisLine->boundingRect().center();
218 QPointF titleCenterDiff = arrowCenter - titleCenter;
219 title->setPos(ax: titleCenterDiff.x() - titlePadding() - (titleBoundingRect.height() / 2.0), ay: titleCenterDiff.y());
220 title->setTransformOriginPoint(titleCenter);
221 title->setRotation(270.0);
222 }
223
224 QGraphicsLayoutItem::updateGeometry();
225}
226
227Qt::Orientation PolarChartAxisRadial::orientation() const
228{
229 return Qt::Vertical;
230}
231
232void PolarChartAxisRadial::createItems(int count)
233{
234 if (arrowItems().size() == 0) {
235 // radial axis center line
236 QGraphicsLineItem *arrow = new LineArrowItem(this, presenter()->rootItem());
237 arrow->setPen(axis()->linePen());
238 arrowGroup()->addToGroup(item: arrow);
239 }
240
241 QGraphicsTextItem *title = titleItem();
242 title->setFont(axis()->titleFont());
243 title->setDefaultTextColor(axis()->titleBrush().color());
244 title->setHtml(axis()->titleText());
245
246 for (int i = 0; i < count; ++i) {
247 QGraphicsLineItem *arrow = new QGraphicsLineItem(presenter()->rootItem());
248 QGraphicsEllipseItem *grid = new QGraphicsEllipseItem(presenter()->rootItem());
249 QGraphicsTextItem *label = new QGraphicsTextItem(presenter()->rootItem());
250 label->document()->setDocumentMargin(ChartPresenter::textMargin());
251 arrow->setPen(axis()->linePen());
252 grid->setPen(axis()->gridLinePen());
253 label->setFont(axis()->labelsFont());
254 label->setDefaultTextColor(axis()->labelsBrush().color());
255 label->setRotation(axis()->labelsAngle());
256 arrowGroup()->addToGroup(item: arrow);
257 gridGroup()->addToGroup(item: grid);
258 labelGroup()->addToGroup(item: label);
259 if (gridItems().size() == 1 || (((gridItems().size() + 1) % 2) && gridItems().size() > 0)) {
260 QGraphicsPathItem *shade = new QGraphicsPathItem(presenter()->rootItem());
261 shade->setPen(axis()->shadesPen());
262 shade->setBrush(axis()->shadesBrush());
263 shadeGroup()->addToGroup(item: shade);
264 }
265 }
266}
267
268void PolarChartAxisRadial::handleArrowPenChanged(const QPen &pen)
269{
270 foreach (QGraphicsItem *item, arrowItems())
271 static_cast<QGraphicsLineItem *>(item)->setPen(pen);
272}
273
274void PolarChartAxisRadial::handleGridPenChanged(const QPen &pen)
275{
276 foreach (QGraphicsItem *item, gridItems())
277 static_cast<QGraphicsEllipseItem *>(item)->setPen(pen);
278}
279
280void PolarChartAxisRadial::handleMinorArrowPenChanged(const QPen &pen)
281{
282 foreach (QGraphicsItem *item, minorArrowItems())
283 static_cast<QGraphicsLineItem *>(item)->setPen(pen);
284}
285
286void PolarChartAxisRadial::handleMinorGridPenChanged(const QPen &pen)
287{
288 foreach (QGraphicsItem *item, minorGridItems())
289 static_cast<QGraphicsEllipseItem *>(item)->setPen(pen);
290}
291
292void PolarChartAxisRadial::handleGridLineColorChanged(const QColor &color)
293{
294 foreach (QGraphicsItem *item, gridItems()) {
295 QGraphicsEllipseItem *ellipseItem = static_cast<QGraphicsEllipseItem *>(item);
296 QPen pen = ellipseItem->pen();
297 pen.setColor(color);
298 ellipseItem->setPen(pen);
299 }
300}
301
302void PolarChartAxisRadial::handleMinorGridLineColorChanged(const QColor &color)
303{
304 foreach (QGraphicsItem *item, minorGridItems()) {
305 QGraphicsEllipseItem *ellipseItem = static_cast<QGraphicsEllipseItem *>(item);
306 QPen pen = ellipseItem->pen();
307 pen.setColor(color);
308 ellipseItem->setPen(pen);
309 }
310}
311
312QSizeF PolarChartAxisRadial::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const
313{
314 Q_UNUSED(which);
315 Q_UNUSED(constraint);
316 return QSizeF(-1.0, -1.0);
317}
318
319qreal PolarChartAxisRadial::preferredAxisRadius(const QSizeF &maxSize)
320{
321 qreal radius = maxSize.height() / 2.0;
322 if (maxSize.width() < maxSize.height())
323 radius = maxSize.width() / 2.0;
324 return radius;
325}
326
327void PolarChartAxisRadial::updateMinorTickGeometry()
328{
329 if (!axis())
330 return;
331
332 QList<qreal> layout = ChartAxisElement::layout();
333 int minorTickCount = 0;
334 qreal tickRadius = 0.0;
335 QList<qreal> minorTickRadiuses;
336 switch (axis()->type()) {
337 case QAbstractAxis::AxisTypeValue: {
338 const QValueAxis *valueAxis = qobject_cast<QValueAxis *>(object: axis());
339
340 minorTickCount = valueAxis->minorTickCount();
341
342 if (valueAxis->tickCount() >= 2)
343 tickRadius = layout.at(i: 1) - layout.at(i: 0);
344
345 for (int i = 0; i < minorTickCount; ++i) {
346 const qreal ratio = (1.0 / qreal(minorTickCount + 1)) * qreal(i + 1);
347 minorTickRadiuses.append(t: tickRadius * ratio);
348 }
349 break;
350 }
351 case QAbstractAxis::AxisTypeLogValue: {
352 const QLogValueAxis *logValueAxis = qobject_cast<QLogValueAxis *>(object: axis());
353 const qreal base = logValueAxis->base();
354 const qreal logBase = qLn(v: base);
355
356 minorTickCount = logValueAxis->minorTickCount();
357 if (minorTickCount < 0)
358 minorTickCount = qMax(a: qFloor(v: base) - 2, b: 0);
359
360 // Two "virtual" ticks are required to make sure that all minor ticks
361 // are displayed properly (even for the partially visible segments of
362 // the chart).
363 if (layout.size() >= 2) {
364 // Calculate tickRadius as a difference between visible ticks
365 // whenever it is possible. Virtual ticks will not be correctly
366 // positioned when the layout is animating.
367 tickRadius = layout.at(i: 1) - layout.at(i: 0);
368 layout.prepend(t: layout.at(i: 0) - tickRadius);
369 layout.append(t: layout.at(i: layout.size() - 1) + tickRadius);
370 } else {
371 const qreal logMax = qLn(v: logValueAxis->max());
372 const qreal logMin = qLn(v: logValueAxis->min());
373 const qreal logExtraMaxTick = qLn(v: qPow(x: base, y: qFloor(v: logMax / logBase) + 1.0));
374 const qreal logExtraMinTick = qLn(v: qPow(x: base, y: qCeil(v: logMin / logBase) - 1.0));
375 const qreal edge = qMin(a: logMin, b: logMax);
376 const qreal delta = (axisGeometry().width() / 2.0) / qAbs(t: logMax - logMin);
377 const qreal extraMaxTick = edge + (logExtraMaxTick - edge) * delta;
378 const qreal extraMinTick = edge + (logExtraMinTick - edge) * delta;
379
380 // Calculate tickRadius using one (if layout.size() == 1) or two
381 // (if layout.size() == 0) "virtual" ticks. In both cases animation
382 // will not work as expected. This should be fixed later.
383 layout.prepend(t: extraMinTick);
384 layout.append(t: extraMaxTick);
385 tickRadius = layout.at(i: 1) - layout.at(i: 0);
386 }
387
388 const qreal minorTickStepValue = qFabs(v: base - 1.0) / qreal(minorTickCount + 1);
389 for (int i = 0; i < minorTickCount; ++i) {
390 const qreal x = minorTickStepValue * qreal(i + 1) + 1.0;
391 const qreal minorTickSpacing = tickRadius * (qLn(v: x) / logBase);
392 minorTickRadiuses.append(t: minorTickSpacing);
393 }
394 break;
395 }
396 default:
397 // minor ticks are not supported
398 break;
399 }
400
401 if (minorTickCount < 1 || tickRadius == 0.0 || minorTickRadiuses.size() != minorTickCount)
402 return;
403
404 const QPointF axisCenter = axisGeometry().center();
405 const qreal axisRadius = axisGeometry().height() / 2.0;
406
407 for (int i = 0; i < layout.size() - 1; ++i) {
408 for (int j = 0; j < minorTickCount; ++j) {
409 const int minorItemIndex = i * minorTickCount + j;
410 QGraphicsEllipseItem *minorGridLineItem =
411 static_cast<QGraphicsEllipseItem *>(minorGridItems().at(i: minorItemIndex));
412 QGraphicsLineItem *minorArrowLineItem =
413 static_cast<QGraphicsLineItem *>(minorArrowItems().at(i: minorItemIndex));
414 if (!minorGridLineItem || !minorArrowLineItem)
415 continue;
416
417 const qreal minorTickRadius = layout.at(i) + minorTickRadiuses.value(i: j, defaultValue: 0.0);
418 const qreal minorTickDiameter = minorTickRadius * 2.0;
419
420 QRectF minorGridLine(0.0, 0.0, minorTickDiameter, minorTickDiameter);
421 minorGridLine.moveCenter(p: axisCenter);
422 minorGridLineItem->setRect(minorGridLine);
423
424 QLineF minorArrowLine(-tickWidth() + 1.0, 0.0, tickWidth() - 1.0, 0.0);
425 minorArrowLine.translate(adx: axisCenter.x(), ady: minorGridLine.top());
426 minorArrowLineItem->setLine(minorArrowLine);
427
428 // check if the minor grid line and the minor axis arrow should be shown
429 bool minorGridLineVisible = (minorTickRadius >= 0.0 && minorTickRadius <= axisRadius);
430 minorGridLineItem->setVisible(minorGridLineVisible);
431 minorArrowLineItem->setVisible(minorGridLineVisible);
432 }
433 }
434}
435
436void PolarChartAxisRadial::updateMinorTickItems()
437{
438 int currentCount = minorArrowItems().size();
439 int expectedCount = 0;
440 if (axis()->type() == QAbstractAxis::AxisTypeValue) {
441 QValueAxis *valueAxis = qobject_cast<QValueAxis *>(object: axis());
442 expectedCount = valueAxis->minorTickCount() * (valueAxis->tickCount() - 1);
443 expectedCount = qMax(a: expectedCount, b: 0);
444 } else if (axis()->type() == QAbstractAxis::AxisTypeLogValue) {
445 QLogValueAxis *logValueAxis = qobject_cast<QLogValueAxis *>(object: axis());
446
447 int minorTickCount = logValueAxis->minorTickCount();
448 if (minorTickCount < 0)
449 minorTickCount = qMax(a: qFloor(v: logValueAxis->base()) - 2, b: 0);
450
451 expectedCount = minorTickCount * (logValueAxis->tickCount() + 1);
452 expectedCount = qMax(a: expectedCount, b: logValueAxis->minorTickCount());
453 } else {
454 // minor ticks are not supported
455 return;
456 }
457
458 int diff = expectedCount - currentCount;
459 if (diff > 0) {
460 for (int i = 0; i < diff; ++i) {
461 QGraphicsEllipseItem *minorGridItem = new QGraphicsEllipseItem(presenter()->rootItem());
462 minorGridItem->setPen(axis()->minorGridLinePen());
463 minorGridGroup()->addToGroup(item: minorGridItem);
464
465 QGraphicsLineItem *minorArrowItem = new QGraphicsLineItem(presenter()->rootItem());
466 minorArrowItem->setPen(axis()->linePen());
467 minorArrowGroup()->addToGroup(item: minorArrowItem);
468 }
469 } else {
470 QList<QGraphicsItem *> minorGridItemsList = minorGridItems();
471 QList<QGraphicsItem *> minorArrowItemsList = minorArrowItems();
472 for (int i = 0; i > diff; --i) {
473 if (!minorGridItemsList.isEmpty())
474 delete minorGridItemsList.takeLast();
475
476 if (!minorArrowItemsList.isEmpty())
477 delete minorArrowItemsList.takeLast();
478 }
479 }
480}
481
482QT_END_NAMESPACE
483
484#include "moc_polarchartaxisradial_p.cpp"
485

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