| 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 <QtGui/qtextdocument.h> |
| 34 | #include <private/chartpresenter_p.h> |
| 35 | #include <private/polarchartaxisangular_p.h> |
| 36 | |
| 37 | QT_CHARTS_BEGIN_NAMESPACE |
| 38 | |
| 39 | PolarChartAxisAngular::PolarChartAxisAngular(QAbstractAxis *axis, QGraphicsItem *item, |
| 40 | bool intervalAxis) |
| 41 | : PolarChartAxis(axis, item, intervalAxis) |
| 42 | { |
| 43 | } |
| 44 | |
| 45 | PolarChartAxisAngular::~PolarChartAxisAngular() |
| 46 | { |
| 47 | } |
| 48 | |
| 49 | void PolarChartAxisAngular::updateGeometry() |
| 50 | { |
| 51 | QGraphicsLayoutItem::updateGeometry(); |
| 52 | |
| 53 | const QVector<qreal> &layout = this->layout(); |
| 54 | if (layout.isEmpty() && axis()->type() != QAbstractAxis::AxisTypeLogValue) |
| 55 | return; |
| 56 | |
| 57 | createAxisLabels(layout); |
| 58 | QStringList labelList = labels(); |
| 59 | QPointF center = axisGeometry().center(); |
| 60 | QList<QGraphicsItem *> arrowItemList = arrowItems(); |
| 61 | QList<QGraphicsItem *> gridItemList = gridItems(); |
| 62 | QList<QGraphicsItem *> labelItemList = labelItems(); |
| 63 | QList<QGraphicsItem *> shadeItemList = shadeItems(); |
| 64 | QList<QGraphicsItem *> minorGridItemList = minorGridItems(); |
| 65 | QList<QGraphicsItem *> minorArrowItemList = minorArrowItems(); |
| 66 | QGraphicsTextItem *title = titleItem(); |
| 67 | |
| 68 | QGraphicsEllipseItem *axisLine = static_cast<QGraphicsEllipseItem *>(arrowItemList.at(i: 0)); |
| 69 | axisLine->setRect(axisGeometry()); |
| 70 | |
| 71 | qreal radius = axisGeometry().height() / 2.0; |
| 72 | |
| 73 | QRectF previousLabelRect; |
| 74 | QRectF firstLabelRect; |
| 75 | |
| 76 | qreal labelHeight = 0; |
| 77 | |
| 78 | bool firstShade = true; |
| 79 | bool nextTickVisible = false; |
| 80 | if (layout.size()) |
| 81 | nextTickVisible = !(layout.at(i: 0) < 0.0 || layout.at(i: 0) > 360.0); |
| 82 | |
| 83 | for (int i = 0; i < layout.size(); ++i) { |
| 84 | qreal angularCoordinate = layout.at(i); |
| 85 | |
| 86 | QGraphicsLineItem *gridLineItem = static_cast<QGraphicsLineItem *>(gridItemList.at(i)); |
| 87 | QGraphicsLineItem *tickItem = static_cast<QGraphicsLineItem *>(arrowItemList.at(i: i + 1)); |
| 88 | QGraphicsTextItem *labelItem = static_cast<QGraphicsTextItem *>(labelItemList.at(i)); |
| 89 | QGraphicsPathItem *shadeItem = 0; |
| 90 | if (i == 0) |
| 91 | shadeItem = static_cast<QGraphicsPathItem *>(shadeItemList.at(i: 0)); |
| 92 | else if (i % 2) |
| 93 | shadeItem = static_cast<QGraphicsPathItem *>(shadeItemList.at(i: (i / 2) + 1)); |
| 94 | |
| 95 | // Ignore ticks outside valid range |
| 96 | bool currentTickVisible = nextTickVisible; |
| 97 | if ((i == layout.size() - 1) |
| 98 | || layout.at(i: i + 1) < 0.0 |
| 99 | || layout.at(i: i + 1) > 360.0) { |
| 100 | nextTickVisible = false; |
| 101 | } else { |
| 102 | nextTickVisible = true; |
| 103 | } |
| 104 | |
| 105 | qreal labelCoordinate = angularCoordinate; |
| 106 | bool labelVisible = currentTickVisible; |
| 107 | if (intervalAxis()) { |
| 108 | qreal farEdge; |
| 109 | if (i == (layout.size() - 1)) |
| 110 | farEdge = 360.0; |
| 111 | else |
| 112 | farEdge = qMin(a: qreal(360.0), b: layout.at(i: i + 1)); |
| 113 | |
| 114 | // Adjust the labelCoordinate to show it if next tick is visible |
| 115 | if (nextTickVisible) |
| 116 | labelCoordinate = qMax(a: qreal(0.0), b: labelCoordinate); |
| 117 | |
| 118 | bool centeredLabel = true; |
| 119 | if (axis()->type() == QAbstractAxis::AxisTypeCategory) { |
| 120 | QCategoryAxis *categoryAxis = static_cast<QCategoryAxis *>(axis()); |
| 121 | if (categoryAxis->labelsPosition() == QCategoryAxis::AxisLabelsPositionOnValue) |
| 122 | centeredLabel = false; |
| 123 | } |
| 124 | if (centeredLabel) { |
| 125 | labelCoordinate = (labelCoordinate + farEdge) / 2.0; |
| 126 | // Don't display label once the category gets too small near the axis |
| 127 | if (labelCoordinate < 5.0 || labelCoordinate > 355.0) |
| 128 | labelVisible = false; |
| 129 | else |
| 130 | labelVisible = true; |
| 131 | } else { |
| 132 | labelVisible = nextTickVisible; |
| 133 | labelCoordinate = farEdge; |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | // Need this also in label calculations, so determine it first |
| 138 | QLineF tickLine(QLineF::fromPolar(length: radius - tickWidth(), angle: 90.0 - angularCoordinate).p2(), |
| 139 | QLineF::fromPolar(length: radius + tickWidth(), angle: 90.0 - angularCoordinate).p2()); |
| 140 | tickLine.translate(point: center); |
| 141 | |
| 142 | // Angular axis label |
| 143 | if (axis()->labelsVisible() && labelVisible) { |
| 144 | QRectF boundingRect = ChartPresenter::textBoundingRect(font: axis()->labelsFont(), |
| 145 | text: labelList.at(i), |
| 146 | angle: axis()->labelsAngle()); |
| 147 | labelItem->setTextWidth(boundingRect.width()); |
| 148 | labelItem->setHtml(labelList.at(i)); |
| 149 | const QRectF &rect = labelItem->boundingRect(); |
| 150 | QPointF labelCenter = rect.center(); |
| 151 | labelItem->setTransformOriginPoint(ax: labelCenter.x(), ay: labelCenter.y()); |
| 152 | boundingRect.moveCenter(p: labelCenter); |
| 153 | QPointF positionDiff(rect.topLeft() - boundingRect.topLeft()); |
| 154 | |
| 155 | QPointF labelPoint; |
| 156 | if (intervalAxis()) { |
| 157 | QLineF labelLine = QLineF::fromPolar(length: radius + tickWidth(), angle: 90.0 - labelCoordinate); |
| 158 | labelLine.translate(point: center); |
| 159 | labelPoint = labelLine.p2(); |
| 160 | } else { |
| 161 | labelPoint = tickLine.p2(); |
| 162 | } |
| 163 | |
| 164 | QRectF labelRect = moveLabelToPosition(angularCoordinate: labelCoordinate, labelPoint, labelRect: boundingRect); |
| 165 | labelItem->setPos(labelRect.topLeft() + positionDiff); |
| 166 | |
| 167 | // Store height for title calculations |
| 168 | qreal labelClearance = axisGeometry().top() - labelRect.top(); |
| 169 | labelHeight = qMax(a: labelHeight, b: labelClearance); |
| 170 | |
| 171 | // Label overlap detection |
| 172 | if (i && (previousLabelRect.intersects(r: labelRect) || firstLabelRect.intersects(r: labelRect))) { |
| 173 | labelVisible = false; |
| 174 | } else { |
| 175 | // Store labelRect for future comparison. Some area is deducted to make things look |
| 176 | // little nicer, as usually intersection happens at label corner with angular labels. |
| 177 | labelRect.adjust(xp1: -2.0, yp1: -4.0, xp2: -2.0, yp2: -4.0); |
| 178 | if (firstLabelRect.isEmpty()) |
| 179 | firstLabelRect = labelRect; |
| 180 | |
| 181 | previousLabelRect = labelRect; |
| 182 | labelVisible = true; |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | labelItem->setVisible(labelVisible); |
| 187 | if (!currentTickVisible) { |
| 188 | gridLineItem->setVisible(false); |
| 189 | tickItem->setVisible(false); |
| 190 | if (shadeItem) |
| 191 | shadeItem->setVisible(false); |
| 192 | continue; |
| 193 | } |
| 194 | |
| 195 | // Angular grid line |
| 196 | QLineF gridLine = QLineF::fromPolar(length: radius, angle: 90.0 - angularCoordinate); |
| 197 | gridLine.translate(point: center); |
| 198 | gridLineItem->setLine(gridLine); |
| 199 | gridLineItem->setVisible(true); |
| 200 | |
| 201 | // Tick |
| 202 | tickItem->setLine(tickLine); |
| 203 | tickItem->setVisible(true); |
| 204 | |
| 205 | // Shades |
| 206 | if (i % 2 || (i == 0 && !nextTickVisible)) { |
| 207 | QPainterPath path; |
| 208 | path.moveTo(p: center); |
| 209 | if (i == 0) { |
| 210 | // If first tick is also the last, we need to custom fill the first partial arc |
| 211 | // or it won't get filled. |
| 212 | path.arcTo(rect: axisGeometry(), startAngle: 90.0 - layout.at(i: 0), arcLength: layout.at(i: 0)); |
| 213 | path.closeSubpath(); |
| 214 | } else { |
| 215 | qreal nextCoordinate; |
| 216 | if (!nextTickVisible) // Last visible tick |
| 217 | nextCoordinate = 360.0; |
| 218 | else |
| 219 | nextCoordinate = layout.at(i: i + 1); |
| 220 | qreal arcSpan = angularCoordinate - nextCoordinate; |
| 221 | path.arcTo(rect: axisGeometry(), startAngle: 90.0 - angularCoordinate, arcLength: arcSpan); |
| 222 | path.closeSubpath(); |
| 223 | |
| 224 | // Add additional arc for first shade item if there is a partial arc to be filled |
| 225 | if (firstShade) { |
| 226 | QGraphicsPathItem *specialShadeItem = static_cast<QGraphicsPathItem *>(shadeItemList.at(i: 0)); |
| 227 | if (layout.at(i: i - 1) > 0.0) { |
| 228 | QPainterPath specialPath; |
| 229 | specialPath.moveTo(p: center); |
| 230 | specialPath.arcTo(rect: axisGeometry(), startAngle: 90.0 - layout.at(i: i - 1), arcLength: layout.at(i: i - 1)); |
| 231 | specialPath.closeSubpath(); |
| 232 | specialShadeItem->setPath(specialPath); |
| 233 | specialShadeItem->setVisible(true); |
| 234 | } else { |
| 235 | specialShadeItem->setVisible(false); |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | shadeItem->setPath(path); |
| 240 | shadeItem->setVisible(true); |
| 241 | firstShade = false; |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | updateMinorTickGeometry(); |
| 246 | |
| 247 | // Title, centered above the chart |
| 248 | QString titleText = axis()->titleText(); |
| 249 | if (!titleText.isEmpty() && axis()->isTitleVisible()) { |
| 250 | QRectF truncatedRect; |
| 251 | qreal availableTitleHeight = axisGeometry().height() - labelPadding() - titlePadding() * 2.0; |
| 252 | qreal minimumLabelHeight = ChartPresenter::textBoundingRect(font: axis()->labelsFont(), |
| 253 | QStringLiteral("..." )).height(); |
| 254 | availableTitleHeight -= minimumLabelHeight; |
| 255 | title->setHtml(ChartPresenter::truncatedText(font: axis()->titleFont(), text: titleText, angle: qreal(0.0), |
| 256 | maxWidth: axisGeometry().width(), maxHeight: availableTitleHeight, |
| 257 | boundingRect&: truncatedRect)); |
| 258 | title->setTextWidth(truncatedRect.width()); |
| 259 | |
| 260 | QRectF titleBoundingRect = title->boundingRect(); |
| 261 | QPointF titleCenter = center - titleBoundingRect.center(); |
| 262 | title->setPos(ax: titleCenter.x(), ay: axisGeometry().top() - titlePadding() * 2.0 - titleBoundingRect.height() - labelHeight); |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | Qt::Orientation PolarChartAxisAngular::orientation() const |
| 267 | { |
| 268 | return Qt::Horizontal; |
| 269 | } |
| 270 | |
| 271 | void PolarChartAxisAngular::createItems(int count) |
| 272 | { |
| 273 | if (arrowItems().count() == 0) { |
| 274 | // angular axis line |
| 275 | QGraphicsEllipseItem *arrow = new QGraphicsEllipseItem(presenter()->rootItem()); |
| 276 | arrow->setPen(axis()->linePen()); |
| 277 | arrowGroup()->addToGroup(item: arrow); |
| 278 | } |
| 279 | |
| 280 | QGraphicsTextItem *title = titleItem(); |
| 281 | title->setFont(axis()->titleFont()); |
| 282 | title->setDefaultTextColor(axis()->titleBrush().color()); |
| 283 | title->setHtml(axis()->titleText()); |
| 284 | |
| 285 | for (int i = 0; i < count; ++i) { |
| 286 | QGraphicsLineItem *arrow = new QGraphicsLineItem(presenter()->rootItem()); |
| 287 | QGraphicsLineItem *grid = new QGraphicsLineItem(presenter()->rootItem()); |
| 288 | QGraphicsTextItem *label = new QGraphicsTextItem(presenter()->rootItem()); |
| 289 | label->document()->setDocumentMargin(ChartPresenter::textMargin()); |
| 290 | arrow->setPen(axis()->linePen()); |
| 291 | grid->setPen(axis()->gridLinePen()); |
| 292 | label->setFont(axis()->labelsFont()); |
| 293 | label->setDefaultTextColor(axis()->labelsBrush().color()); |
| 294 | label->setRotation(axis()->labelsAngle()); |
| 295 | arrowGroup()->addToGroup(item: arrow); |
| 296 | gridGroup()->addToGroup(item: grid); |
| 297 | labelGroup()->addToGroup(item: label); |
| 298 | if (gridItems().size() == 1 || (((gridItems().size() + 1) % 2) && gridItems().size() > 0)) { |
| 299 | QGraphicsPathItem *shade = new QGraphicsPathItem(presenter()->rootItem()); |
| 300 | shade->setPen(axis()->shadesPen()); |
| 301 | shade->setBrush(axis()->shadesBrush()); |
| 302 | shadeGroup()->addToGroup(item: shade); |
| 303 | } |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | void PolarChartAxisAngular::handleArrowPenChanged(const QPen &pen) |
| 308 | { |
| 309 | bool first = true; |
| 310 | foreach (QGraphicsItem *item, arrowItems()) { |
| 311 | if (first) { |
| 312 | first = false; |
| 313 | // First arrow item is the outer circle of axis |
| 314 | static_cast<QGraphicsEllipseItem *>(item)->setPen(pen); |
| 315 | } else { |
| 316 | static_cast<QGraphicsLineItem *>(item)->setPen(pen); |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | void PolarChartAxisAngular::handleGridPenChanged(const QPen &pen) |
| 322 | { |
| 323 | foreach (QGraphicsItem *item, gridItems()) |
| 324 | static_cast<QGraphicsLineItem *>(item)->setPen(pen); |
| 325 | } |
| 326 | |
| 327 | void PolarChartAxisAngular::handleMinorArrowPenChanged(const QPen &pen) |
| 328 | { |
| 329 | foreach (QGraphicsItem *item, minorArrowItems()) { |
| 330 | static_cast<QGraphicsLineItem *>(item)->setPen(pen); |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | void PolarChartAxisAngular::handleMinorGridPenChanged(const QPen &pen) |
| 335 | { |
| 336 | foreach (QGraphicsItem *item, minorGridItems()) |
| 337 | static_cast<QGraphicsLineItem *>(item)->setPen(pen); |
| 338 | } |
| 339 | |
| 340 | void PolarChartAxisAngular::handleGridLineColorChanged(const QColor &color) |
| 341 | { |
| 342 | foreach (QGraphicsItem *item, gridItems()) { |
| 343 | QGraphicsLineItem *lineItem = static_cast<QGraphicsLineItem *>(item); |
| 344 | QPen pen = lineItem->pen(); |
| 345 | pen.setColor(color); |
| 346 | lineItem->setPen(pen); |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | void PolarChartAxisAngular::handleMinorGridLineColorChanged(const QColor &color) |
| 351 | { |
| 352 | foreach (QGraphicsItem *item, minorGridItems()) { |
| 353 | QGraphicsLineItem *lineItem = static_cast<QGraphicsLineItem *>(item); |
| 354 | QPen pen = lineItem->pen(); |
| 355 | pen.setColor(color); |
| 356 | lineItem->setPen(pen); |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | QSizeF PolarChartAxisAngular::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const |
| 361 | { |
| 362 | Q_UNUSED(which); |
| 363 | Q_UNUSED(constraint); |
| 364 | return QSizeF(-1, -1); |
| 365 | } |
| 366 | |
| 367 | qreal PolarChartAxisAngular::preferredAxisRadius(const QSizeF &maxSize) |
| 368 | { |
| 369 | qreal radius = maxSize.height() / 2.0; |
| 370 | if (maxSize.width() < maxSize.height()) |
| 371 | radius = maxSize.width() / 2.0; |
| 372 | |
| 373 | if (axis()->labelsVisible()) { |
| 374 | QVector<qreal> layout = calculateLayout(); |
| 375 | if (layout.isEmpty()) |
| 376 | return radius; |
| 377 | |
| 378 | createAxisLabels(layout); |
| 379 | QStringList labelList = labels(); |
| 380 | QFont font = axis()->labelsFont(); |
| 381 | |
| 382 | QRectF maxRect; |
| 383 | maxRect.setSize(maxSize); |
| 384 | maxRect.moveCenter(p: QPointF(0.0, 0.0)); |
| 385 | |
| 386 | // This is a horrible way to find out the maximum radius for angular axis and its labels. |
| 387 | // It just increments the radius down until everyhing fits the constraint size. |
| 388 | // Proper way would be to actually calculate it but this seems to work reasonably fast as it is. |
| 389 | bool nextTickVisible = false; |
| 390 | for (int i = 0; i < layout.size(); ) { |
| 391 | if ((i == layout.size() - 1) |
| 392 | || layout.at(i: i + 1) < 0.0 |
| 393 | || layout.at(i: i + 1) > 360.0) { |
| 394 | nextTickVisible = false; |
| 395 | } else { |
| 396 | nextTickVisible = true; |
| 397 | } |
| 398 | |
| 399 | qreal labelCoordinate = layout.at(i); |
| 400 | qreal labelVisible; |
| 401 | |
| 402 | if (intervalAxis()) { |
| 403 | qreal farEdge; |
| 404 | if (i == (layout.size() - 1)) |
| 405 | farEdge = 360.0; |
| 406 | else |
| 407 | farEdge = qMin(a: qreal(360.0), b: layout.at(i: i + 1)); |
| 408 | |
| 409 | // Adjust the labelCoordinate to show it if next tick is visible |
| 410 | if (nextTickVisible) |
| 411 | labelCoordinate = qMax(a: qreal(0.0), b: labelCoordinate); |
| 412 | |
| 413 | labelCoordinate = (labelCoordinate + farEdge) / 2.0; |
| 414 | } |
| 415 | |
| 416 | if (labelCoordinate < 0.0 || labelCoordinate > 360.0) |
| 417 | labelVisible = false; |
| 418 | else |
| 419 | labelVisible = true; |
| 420 | |
| 421 | if (!labelVisible) { |
| 422 | i++; |
| 423 | continue; |
| 424 | } |
| 425 | |
| 426 | QRectF boundingRect = ChartPresenter::textBoundingRect(font: axis()->labelsFont(), text: labelList.at(i), angle: axis()->labelsAngle()); |
| 427 | QPointF labelPoint = QLineF::fromPolar(length: radius + tickWidth(), angle: 90.0 - labelCoordinate).p2(); |
| 428 | |
| 429 | boundingRect = moveLabelToPosition(angularCoordinate: labelCoordinate, labelPoint, labelRect: boundingRect); |
| 430 | QRectF intersectRect = maxRect.intersected(r: boundingRect); |
| 431 | if (boundingRect.isEmpty() || intersectRect == boundingRect) { |
| 432 | i++; |
| 433 | } else { |
| 434 | qreal reduction(0.0); |
| 435 | // If there is no intersection, reduce by smallest dimension of label rect to be on the safe side |
| 436 | if (intersectRect.isEmpty()) { |
| 437 | reduction = qMin(a: boundingRect.height(), b: boundingRect.width()); |
| 438 | } else { |
| 439 | // Approximate needed radius reduction is the amount label rect exceeds max rect in either dimension. |
| 440 | // Could be further optimized by figuring out the proper math how to calculate exact needed reduction. |
| 441 | reduction = qMax(a: boundingRect.height() - intersectRect.height(), |
| 442 | b: boundingRect.width() - intersectRect.width()); |
| 443 | } |
| 444 | // Typically the approximated reduction is little low, so add one |
| 445 | radius -= (reduction + 1.0); |
| 446 | |
| 447 | if (radius < 1.0) // safeguard |
| 448 | return 1.0; |
| 449 | } |
| 450 | } |
| 451 | } |
| 452 | |
| 453 | if (!axis()->titleText().isEmpty() && axis()->isTitleVisible()) { |
| 454 | QRectF titleRect = ChartPresenter::textBoundingRect(font: axis()->titleFont(), text: axis()->titleText()); |
| 455 | |
| 456 | radius -= titlePadding() + (titleRect.height() / 2.0); |
| 457 | if (radius < 1.0) // safeguard |
| 458 | return 1.0; |
| 459 | } |
| 460 | |
| 461 | return radius; |
| 462 | } |
| 463 | |
| 464 | QRectF PolarChartAxisAngular::moveLabelToPosition(qreal angularCoordinate, QPointF labelPoint, QRectF labelRect) const |
| 465 | { |
| 466 | if (angularCoordinate == 0.0) |
| 467 | labelRect.moveCenter(p: labelPoint + QPointF(0, -labelRect.height() / 2.0)); |
| 468 | else if (angularCoordinate < 90.0) |
| 469 | labelRect.moveBottomLeft(p: labelPoint); |
| 470 | else if (angularCoordinate == 90.0) |
| 471 | labelRect.moveCenter(p: labelPoint + QPointF(labelRect.width() / 2.0 + 2.0, 0)); // +2 so that it does not hit the radial axis |
| 472 | else if (angularCoordinate < 180.0) |
| 473 | labelRect.moveTopLeft(p: labelPoint); |
| 474 | else if (angularCoordinate == 180.0) |
| 475 | labelRect.moveCenter(p: labelPoint + QPointF(0, labelRect.height() / 2.0)); |
| 476 | else if (angularCoordinate < 270.0) |
| 477 | labelRect.moveTopRight(p: labelPoint); |
| 478 | else if (angularCoordinate == 270.0) |
| 479 | labelRect.moveCenter(p: labelPoint + QPointF(-labelRect.width() / 2.0 - 2.0, 0)); // -2 so that it does not hit the radial axis |
| 480 | else if (angularCoordinate < 360.0) |
| 481 | labelRect.moveBottomRight(p: labelPoint); |
| 482 | else |
| 483 | labelRect.moveCenter(p: labelPoint + QPointF(0, -labelRect.height() / 2.0)); |
| 484 | return labelRect; |
| 485 | } |
| 486 | |
| 487 | void PolarChartAxisAngular::updateMinorTickGeometry() |
| 488 | { |
| 489 | if (!axis()) |
| 490 | return; |
| 491 | |
| 492 | QVector<qreal> layout = ChartAxisElement::layout(); |
| 493 | int minorTickCount = 0; |
| 494 | qreal tickAngle = 0.0; |
| 495 | QVector<qreal> minorTickAngles; |
| 496 | switch (axis()->type()) { |
| 497 | case QAbstractAxis::AxisTypeValue: { |
| 498 | const QValueAxis *valueAxis = qobject_cast<QValueAxis *>(object: axis()); |
| 499 | |
| 500 | minorTickCount = valueAxis->minorTickCount(); |
| 501 | |
| 502 | if (valueAxis->tickCount() >= 2) |
| 503 | tickAngle = layout.at(i: 1) - layout.at(i: 0); |
| 504 | |
| 505 | for (int i = 0; i < minorTickCount; ++i) { |
| 506 | const qreal ratio = (1.0 / qreal(minorTickCount + 1)) * qreal(i + 1); |
| 507 | minorTickAngles.append(t: tickAngle * ratio); |
| 508 | } |
| 509 | break; |
| 510 | } |
| 511 | case QAbstractAxis::AxisTypeLogValue: { |
| 512 | const QLogValueAxis *logValueAxis = qobject_cast<QLogValueAxis *>(object: axis()); |
| 513 | const qreal base = logValueAxis->base(); |
| 514 | const qreal logBase = qLn(v: base); |
| 515 | |
| 516 | minorTickCount = logValueAxis->minorTickCount(); |
| 517 | if (minorTickCount < 0) |
| 518 | minorTickCount = qMax(a: int(qFloor(v: base) - 2.0), b: 0); |
| 519 | |
| 520 | // Two "virtual" ticks are required to make sure that all minor ticks |
| 521 | // are displayed properly (even for the partially visible segments of |
| 522 | // the chart). |
| 523 | if (layout.size() >= 2) { |
| 524 | // Calculate tickAngle as a difference between visible ticks |
| 525 | // whenever it is possible. Virtual ticks will not be correctly |
| 526 | // positioned when the layout is animating. |
| 527 | tickAngle = layout.at(i: 1) - layout.at(i: 0); |
| 528 | layout.prepend(t: layout.at(i: 0) - tickAngle); |
| 529 | layout.append(t: layout.at(i: layout.size() - 1) + tickAngle); |
| 530 | } else { |
| 531 | const qreal logMax = qLn(v: logValueAxis->max()); |
| 532 | const qreal logMin = qLn(v: logValueAxis->min()); |
| 533 | const qreal = qLn(v: qPow(x: base, y: qFloor(v: logMax / logBase) + 1.0)); |
| 534 | const qreal = qLn(v: qPow(x: base, y: qCeil(v: logMin / logBase) - 1.0)); |
| 535 | const qreal edge = qMin(a: logMin, b: logMax); |
| 536 | const qreal delta = 360.0 / qAbs(t: logMax - logMin); |
| 537 | const qreal = edge + (logExtraMaxTick - edge) * delta; |
| 538 | const qreal = edge + (logExtraMinTick - edge) * delta; |
| 539 | |
| 540 | // Calculate tickAngle using one (if layout.size() == 1) or two |
| 541 | // (if layout.size() == 0) "virtual" ticks. In both cases animation |
| 542 | // will not work as expected. This should be fixed later. |
| 543 | layout.prepend(t: extraMinTick); |
| 544 | layout.append(t: extraMaxTick); |
| 545 | tickAngle = layout.at(i: 1) - layout.at(i: 0); |
| 546 | } |
| 547 | |
| 548 | const qreal minorTickStepValue = qFabs(v: base - 1.0) / qreal(minorTickCount + 1); |
| 549 | for (int i = 0; i < minorTickCount; ++i) { |
| 550 | const qreal x = minorTickStepValue * qreal(i + 1) + 1.0; |
| 551 | const qreal minorTickAngle = tickAngle * (qLn(v: x) / logBase); |
| 552 | minorTickAngles.append(t: minorTickAngle); |
| 553 | } |
| 554 | break; |
| 555 | } |
| 556 | default: |
| 557 | // minor ticks are not supported |
| 558 | break; |
| 559 | } |
| 560 | |
| 561 | if (minorTickCount < 1 || tickAngle == 0.0 || minorTickAngles.count() != minorTickCount) |
| 562 | return; |
| 563 | |
| 564 | const QPointF axisCenter = axisGeometry().center(); |
| 565 | const qreal axisRadius = axisGeometry().height() / 2.0; |
| 566 | |
| 567 | for (int i = 0; i < layout.size() - 1; ++i) { |
| 568 | for (int j = 0; j < minorTickCount; ++j) { |
| 569 | const int minorItemIndex = i * minorTickCount + j; |
| 570 | QGraphicsLineItem *minorGridLineItem = |
| 571 | static_cast<QGraphicsLineItem *>(minorGridItems().at(i: minorItemIndex)); |
| 572 | QGraphicsLineItem *minorArrowLineItem = |
| 573 | static_cast<QGraphicsLineItem *>(minorArrowItems().at(i: minorItemIndex)); |
| 574 | if (!minorGridLineItem || !minorArrowLineItem) |
| 575 | continue; |
| 576 | |
| 577 | const qreal minorTickAngle = 90.0 - layout.at(i) - minorTickAngles.value(i: j, defaultValue: 0.0); |
| 578 | |
| 579 | const QPointF minorArrowLinePt1 = QLineF::fromPolar(length: axisRadius - tickWidth() + 1, |
| 580 | angle: minorTickAngle).p2(); |
| 581 | const QPointF minorArrowLinePt2 = QLineF::fromPolar(length: axisRadius + tickWidth() - 1, |
| 582 | angle: minorTickAngle).p2(); |
| 583 | |
| 584 | QLineF minorGridLine = QLineF::fromPolar(length: axisRadius, angle: minorTickAngle); |
| 585 | minorGridLine.translate(point: axisCenter); |
| 586 | minorGridLineItem->setLine(minorGridLine); |
| 587 | |
| 588 | QLineF minorArrowLine(minorArrowLinePt1, minorArrowLinePt2); |
| 589 | minorArrowLine.translate(point: axisCenter); |
| 590 | minorArrowLineItem->setLine(minorArrowLine); |
| 591 | |
| 592 | // check if the minor grid line and the minor axis arrow should be shown |
| 593 | const bool minorGridLineVisible = (minorTickAngle >= -270.0 && minorTickAngle <= 90.0); |
| 594 | minorGridLineItem->setVisible(minorGridLineVisible); |
| 595 | minorArrowLineItem->setVisible(minorGridLineVisible); |
| 596 | } |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | void PolarChartAxisAngular::updateMinorTickItems() |
| 601 | { |
| 602 | int currentCount = minorArrowItems().size(); |
| 603 | int expectedCount = 0; |
| 604 | if (axis()->type() == QAbstractAxis::AxisTypeValue) { |
| 605 | QValueAxis *valueAxis = qobject_cast<QValueAxis *>(object: axis()); |
| 606 | expectedCount = valueAxis->minorTickCount() * (valueAxis->tickCount() - 1); |
| 607 | expectedCount = qMax(a: expectedCount, b: 0); |
| 608 | } else if (axis()->type() == QAbstractAxis::AxisTypeLogValue) { |
| 609 | QLogValueAxis *logValueAxis = qobject_cast<QLogValueAxis *>(object: axis()); |
| 610 | |
| 611 | int minorTickCount = logValueAxis->minorTickCount(); |
| 612 | if (minorTickCount < 0) |
| 613 | minorTickCount = qMax(a: int(qFloor(v: logValueAxis->base()) - 2.0), b: 0); |
| 614 | |
| 615 | expectedCount = minorTickCount * (logValueAxis->tickCount() + 1); |
| 616 | expectedCount = qMax(a: expectedCount, b: logValueAxis->minorTickCount()); |
| 617 | } else { |
| 618 | // minor ticks are not supported |
| 619 | return; |
| 620 | } |
| 621 | |
| 622 | int diff = expectedCount - currentCount; |
| 623 | if (diff > 0) { |
| 624 | for (int i = 0; i < diff; ++i) { |
| 625 | QGraphicsLineItem *minorGridLineItem = new QGraphicsLineItem(this); |
| 626 | minorGridLineItem->setPen(axis()->minorGridLinePen()); |
| 627 | minorGridGroup()->addToGroup(item: minorGridLineItem); |
| 628 | |
| 629 | QGraphicsLineItem *minorArrowLineItem = new QGraphicsLineItem(this); |
| 630 | minorArrowLineItem->setPen(axis()->linePen()); |
| 631 | minorArrowGroup()->addToGroup(item: minorArrowLineItem); |
| 632 | } |
| 633 | } else { |
| 634 | QList<QGraphicsItem *> minorGridItemsList = minorGridItems(); |
| 635 | QList<QGraphicsItem *> minorArrowItemsList = minorArrowItems(); |
| 636 | for (int i = 0; i > diff; --i) { |
| 637 | if (!minorGridItemsList.isEmpty()) |
| 638 | delete minorGridItemsList.takeLast(); |
| 639 | |
| 640 | if (!minorArrowItemsList.isEmpty()) |
| 641 | delete minorArrowItemsList.takeLast(); |
| 642 | } |
| 643 | } |
| 644 | } |
| 645 | |
| 646 | QT_CHARTS_END_NAMESPACE |
| 647 | |
| 648 | #include "moc_polarchartaxisangular_p.cpp" |
| 649 | |