Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include <QtCore/qdatetime.h>
++ #include <QtCore/qmath.h>
++ #include <QtGui/qpainterpath.h>
++ #include <qfontmetrics.h>
++ #include <QtQuick/private/qquicktext_p.h>
++ #include <QtGraphs/qabstractaxis.h>
++ #include <QtGraphs/qvalueaxis.h>
++ #include <QtGraphs/qdatetimeaxis.h>
++ #include <QtGraphs/qlogvalueaxis.h>
++ #include <QtGraphs/qgraphstheme.h>
++ #include <private/polaraxisrenderer_p.h>
++ #include <private/qgraphsview_p.h>
++ #include <private/qpolarview_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ static constexpr int kMaxTicks = 100;
++ static constexpr int kRadialLabelMargin = 16;
++
++ PolarAxisRenderer::PolarAxisRenderer(QQuickItem *parent)
++ : AxisRenderer(parent)
++ {
++ m_shape.setParentItem(this);
++ m_shape.setPreferredRendererType(QQuickShape::CurveRenderer);
++
++ m_ringsPath = new QQuickShapePath(&m_shape);
++ m_ringsPath->setAsynchronous(true);
++ auto ringsData = m_shape.data();
++ ringsData.append(&ringsData, m_ringsPath);
++
++ m_spokesPath = new QQuickShapePath(&m_shape);
++ m_spokesPath->setAsynchronous(true);
++ auto spokesData = m_shape.data();
++ spokesData.append(&spokesData, m_spokesPath);
++ }
++
++ PolarAxisRenderer::~PolarAxisRenderer() = default;
++
++ void PolarAxisRenderer::computeAxisProperties(AxisProperties &ax, QAbstractAxis *axis)
++ {
++ ax.axis = axis;
++ ax.isLogarithmic = false;
++ if (!axis) {
++ // m_axes1[0]/m_axes2[0] are reused across polishes rather than
++ // reconstructed, so the range fields have to be put back to their
++ // defaults explicitly. Consumers such as
++ // AreaRenderer::calculateRenderCoordinates() and
++ // AreaRenderer::polarRadiusAffine() read them without checking axis
++ // validity, and would otherwise keep mapping values through the range
++ // of an axis that is no longer set.
++ ax.maxValue = 20;
++ ax.minValue = 0;
++ ax.valueRange = 0;
++ ax.valueRangeZoomless = 0;
++ ax.minValueZoomless = 0;
++ ax.valueStep = 1.0;
++ ax.minLabel = 0;
++ ax.tickCount = 0.0;
++ ax.logBase = 10;
++ return;
++ }
++
++ // Unlike a Cartesian axis, the visible range is anchored at the axis minimum
++ // rather than its midpoint: for the radial axis that minimum is the polar
++ // center, which must stay fixed so zooming scales uniformly from the center
++ // instead of drifting away from it.
++ if (auto vaxis = qobject_cast<QValueAxis *>(axis)) {
++ qreal step = vaxis->tickInterval();
++
++
++ qreal diff = (vaxis->max() - vaxis->min()) / vaxis->zoom();
++ ax.minValue = vaxis->min() + vaxis->pan();
++ ax.maxValue = ax.minValue + diff;
++ ax.valueRange = ax.maxValue - ax.minValue;
++ ax.valueRangeZoomless = vaxis->max() - vaxis->min();
++ ax.minValueZoomless = vaxis->min();
++
++ if (step <= 0)
++ step = getValueStepsFromRange(vaxis->max() - vaxis->min());
++ ax.valueStep = step;
++
++ // Used by the radial axis to shift its first visible tick away from
++ // the center (see radialTickValues()); irrelevant for the angular
++ // axis, whose spokes are always evenly divided around the full circle.
++ double minLabel = vaxis->tickAnchor();
++ while (minLabel < ax.minValue)
++ minLabel += step;
++ while (minLabel >= ax.minValue + step)
++ minLabel -= step;
++ ax.minLabel = minLabel;
++ } else if (auto vaxis = qobject_cast<QDateTimeAxis *>(axis)) {
++ double tickCount = std::clamp<double>(vaxis->tickCount(), 0.0, kMaxTicks);
++
++ qreal minMs = vaxis->min().toMSecsSinceEpoch();
++ qreal diff = (vaxis->max().toMSecsSinceEpoch() - minMs) / vaxis->zoom();
++ ax.minValue = minMs + vaxis->pan();
++ ax.maxValue = ax.minValue + diff;
++ ax.valueRange = std::abs(ax.maxValue - ax.minValue);
++ ax.valueRangeZoomless = vaxis->max().toMSecsSinceEpoch() - minMs;
++ ax.minValueZoomless = minMs;
++
++ double segment;
++ if (tickCount <= 0) {
++ segment = getValueStepsFromRange(ax.valueRange);
++ tickCount = ax.valueRange / segment;
++ } else {
++ segment = ax.valueRange / tickCount;
++ }
++ ax.valueStep = segment;
++ ax.tickCount = tickCount;
++ ax.minLabel = ax.minValue;
++ } else if (auto vaxis = qobject_cast<QLogValueAxis *>(axis)) {
++ ax.isLogarithmic = true;
++ ax.logBase = vaxis->base();
++
++ qreal logBase = log(vaxis->base());
++ qreal logMin = log(vaxis->min()) / logBase;
++ qreal logMax = log(vaxis->max()) / logBase;
++
++ qreal diff = (logMax - logMin) / vaxis->zoom();
++ ax.minValue = logMin + vaxis->pan();
++ ax.maxValue = ax.minValue + diff;
++ ax.valueRange = ax.maxValue - ax.minValue;
++ ax.valueRangeZoomless = logMax - logMin;
++ ax.minValueZoomless = logMin;
++ ax.valueStep = 1;
++ ax.minLabel = ax.minValue;
++ }
++ }
++
++ void PolarAxisRenderer::updateAxisMeasurements()
++ {
++ if (m_axes1.empty())
++ m_axes1.emplace_back();
++ if (m_axes2.empty())
++ m_axes2.emplace_back();
++
++ computeAxisProperties(m_axes1[0], m_graph->axisX());
++ computeAxisProperties(m_axes2[0], m_graph->axisY());
++
++ // Reserve a band for radial axis labels, read back by
++ // QPolarView::updateComponentSizes() via radialLabelBand().
++ m_radialLabelBand = 0;
++ auto &radial = m_axes2[0];
++ if (theme() && radial.axis && radial.axis->isVisible() && radial.axis->labelsVisible()) {
++ const QFontMetricsF fm(theme()->axisYLabelFont());
++ qreal labelWidth = std::max(fm.horizontalAdvance(QString::number(radial.minValue, 'g', 4)),
++ fm.horizontalAdvance(QString::number(radial.maxValue, 'g', 4)));
++ m_radialLabelBand = labelWidth + kRadialLabelMargin;
++ }
++ }
++
++ void PolarAxisRenderer::handlePolish()
++ {
++ if (m_axes1.empty())
++ m_axes1.emplace_back();
++ if (m_axes2.empty())
++ m_axes2.emplace_back();
++
++ computeAxisProperties(m_axes1[0], m_graph->axisX());
++ computeAxisProperties(m_axes2[0], m_graph->axisY());
++
++ const QList<qreal> radialTicks = radialTickValues(m_axes2[0]);
++ updatePolarGrid(radialTicks);
++ updatePolarLabels(radialTicks);
++ }
++
++ // Rings/labels must sit at their actual value's radius rather than at an even
++ // i/ringCount fraction: valueStep is derived from the unzoomed axis range, so
++ // once zoomed it no longer divides valueRange evenly and the two diverge,
++ // most visibly for the outermost ring. tickAnchor (QValueAxis only, folded
++ // into minLabel by computeAxisProperties()) shifts where the first tick
++ // falls, e.g. min: 0, max: 10, tickInterval: 2, tickAnchor: 1 yields
++ // 1, 3, 5, 7, 9 instead of 2, 4, 6, 8, 10.
++ QList<qreal> PolarAxisRenderer::radialTickValues(const AxisProperties &radial) const
++ {
++ QList<qreal> values;
++ if (!(radial.valueRange > 0 && radial.valueStep > 0))
++ return values;
++
++ const qreal epsilon = radial.valueStep * 1e-9;
++ for (qreal value = radial.minLabel; value <= radial.maxValue + epsilon;
++ value += radial.valueStep) {
++ // A tick that lands exactly on the center (radius 0) has no ring to
++ // draw and no sensible place to anchor a label.
++ if (value > radial.minValue + epsilon)
++ values.append(value);
++ // Bail out rather than subdivide the range unboundedly; this also stops the
++ // loop from stalling when valueStep is too small to advance value at all.
++ if (values.size() >= kMaxTicks)
++ break;
++ }
++ return values;
++ }
++
++ // Number of evenly spaced spokes/angular labels around the circle. The quotient is
++ // clamped before being narrowed: qRound() returns int, so casting a quotient beyond
++ // INT_MAX would be undefined, and an unvalidated tickInterval can produce one.
++ int PolarAxisRenderer::angularTickCount(const AxisProperties &angular) const
++ {
++ if (!(angular.valueRange > 0 && angular.valueStep > 0))
++ return 0;
++
++ const qreal count = angular.valueRange / angular.valueStep;
++ return qRound(std::clamp<qreal>(count, 1.0, kMaxTicks));
++ }
++
++ // Anchors an axis label so that its edge facing (anchor) stays a constant
++ // "pad" pixels away from it, however large the label item is, instead of
++ // centering the item on (anchor) and letting it grow inward over the grid.
++ // (dx, dy) is the outward unit direction from the polar center through
++ // (anchor).
++ static void positionOutwardLabel(QQuickItem *item, const QPointF &anchor, qreal dx, qreal dy,
++ qreal pad)
++ {
++ const qreal halfWidth = item->width() / 2.0;
++ const qreal halfHeight = item->height() / 2.0;
++ item->setX(anchor.x() - halfWidth + dx * (halfWidth + pad));
++ item->setY(anchor.y() - halfHeight + dy * (halfHeight + pad));
++ }
++
++ void PolarAxisRenderer::updatePolarGrid(const QList<qreal> &radialTicks)
++ {
++ if (!theme())
++ return;
++
++ auto polarView = qobject_cast<QPolarView *>(m_graph);
++ if (!polarView)
++ return;
++
++ const QPointF center = polarView->polarCenter();
++ const qreal radius = polarView->polarRadius();
++
++ auto &radial = m_axes2[0];
++ QPainterPath rings;
++ if (radial.axis && radial.axis->isVisible() && radial.axis->isGridVisible()) {
++ for (qreal value : radialTicks) {
++ qreal r = radius * (value - radial.minValue) / radial.valueRange;
++ rings.addEllipse(center, r, r);
++ }
++ }
++ m_ringsPath->setStrokeColor(theme()->axisY().mainColor());
++ m_ringsPath->setStrokeWidth(theme()->axisY().mainWidth());
++ m_ringsPath->setFillColor(QColorConstants::Transparent);
++ m_ringsPath->setPath(rings);
++
++ auto &angular = m_axes1[0];
++ QPainterPath spokes;
++ if (angular.axis && angular.axis->isVisible() && angular.axis->isGridVisible()
++ && angular.valueRange > 0 && angular.valueStep > 0) {
++ int spokeCount = angularTickCount(angular);
++ for (int i = 0; i < spokeCount; ++i) {
++ qreal angleDeg = 360.0 * i / spokeCount;
++ qreal rad = qDegreesToRadians(angleDeg);
++ QPointF p = center + QPointF(qSin(rad) * radius, -qCos(rad) * radius);
++ spokes.moveTo(center);
++ spokes.lineTo(p);
++ }
++ }
++ m_spokesPath->setStrokeColor(theme()->axisX().mainColor());
++ m_spokesPath->setStrokeWidth(theme()->axisX().mainWidth());
++ m_spokesPath->setFillColor(QColorConstants::Transparent);
++ m_spokesPath->setPath(spokes);
++
++ m_shape.setSize(QSizeF(width(), height()));
++ }
++
++ static QString polarAxisLabelText(QAbstractAxis *axis, qreal value)
++ {
++ if (auto dtAxis = qobject_cast<QDateTimeAxis *>(axis))
++ return QDateTime::fromMSecsSinceEpoch(qint64(value)).toString(dtAxis->labelFormat());
++ if (auto logAxis = qobject_cast<QLogValueAxis *>(axis))
++ return QString::number(value, 'g', std::max(1, logAxis->labelPrecision()));
++ return QString::number(value, 'g', 4);
++ }
++
++ void PolarAxisRenderer::updatePolarLabels(const QList<qreal> &radialTicks)
++ {
++ if (!theme())
++ return;
++
++ auto polarView = qobject_cast<QPolarView *>(m_graph);
++ if (!polarView)
++ return;
++
++ const QPointF center = polarView->polarCenter();
++ const qreal radius = polarView->polarRadius();
++ const qreal pad = polarView->labelPadding();
++
++ auto &angular = m_axes1[0];
++ if (angular.axis && angular.axis->isVisible() && angular.axis->labelsVisible()
++ && angular.valueRange > 0 && angular.valueStep > 0) {
++ int tickCount = angularTickCount(angular);
++ updateAxisLabelItems(angular.textItems, tickCount, angular.axis->labelDelegate());
++ for (int i = 0; i < tickCount && i < angular.textItems.size(); ++i) {
++ qreal value = angular.minValue + i * angular.valueStep;
++ qreal angleDeg = 360.0 * (value - angular.minValue) / angular.valueRange;
++ qreal rad = qDegreesToRadians(angleDeg);
++ qreal dx = qSin(rad);
++ qreal dy = -qCos(rad);
++ QPointF p = center + QPointF(dx * radius, dy * radius);
++
++ auto item = angular.textItems[i];
++ item->setVisible(true);
++ setLabelTextProperties(item, polarAxisLabelText(angular.axis, value), true);
++ positionOutwardLabel(item, p, dx, dy, pad);
++ }
++ }
++
++ auto &radial = m_axes2[0];
++ if (radial.axis && radial.axis->isVisible() && radial.axis->labelsVisible()) {
++ updateAxisLabelItems(radial.textItems, radialTicks.size(), radial.axis->labelDelegate());
++
++ for (qsizetype i = 0; i < radialTicks.size() && i < radial.textItems.size(); ++i) {
++ qreal value = radialTicks[i];
++ qreal r = radius * (value - radial.minValue) / radial.valueRange;
++
++ auto item = radial.textItems[i];
++ item->setVisible(true);
++ setLabelTextProperties(item, polarAxisLabelText(radial.axis, value), false);
++ // The label's baseline sits right on its ring, on the vertical
++ // (angle 0) radial axis line, offset sideways by "pad" so it
++ // doesn't run into the angular axis's own label at its minimum
++ // value, which is centered right above this same line.
++ item->setX(center.x() + pad);
++ item->setY(center.y() - r - item->height());
++ }
++ }
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_polaraxisrenderer_p.cpp"