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