Revision control

++ // Copyright (C) 2023 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "datetimeaxis/qdatetimeaxis.h"
++ #include "qabstractaxis.h"
++ #include "valueaxis/qvalueaxis.h"
++ #if QT_CONFIG(graphs_2d_bar)
++ #include <QtGraphs/QBarCategoryAxis>
++ #include <private/qbarseries_p.h>
++ #endif
++ #include <QtGraphs/QGraphsTheme>
++ #include <private/axisrenderer_p.h>
++ #include <private/qabstractaxis_p.h>
++ #include <private/qdatetimeaxis_p.h>
++ #include <private/qgraphsview_p.h>
++ #include <private/qvalueaxis_p.h>
++ #include <private/qlogvalueaxis_p.h>
++ #include <private/commonutils_p.h>
++ #include <QtQuick/private/qquickdraghandler_p.h>
++ #include <qfontmetrics.h>
++ #include <qtgraphs_tracepoints_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_TRACE_PREFIX(qtgraphs,
++ "QT_BEGIN_NAMESPACE" \
++ "#include <qnamespace.h>" \
++ "class AxisRenderer;" \
++ "QT_END_NAMESPACE"
++ )
++
++ Q_TRACE_METADATA(qtgraphs, "ENUM { } Qt::Orientation;")
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateDateTimeXAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateDateTimeXAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateAxisLabelItems_entry, int labelItemCount, int neededCount);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateAxisLabelItems_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateDateTimeYAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateDateTimeYAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateValueXAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateValueXAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateValueYAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateValueYAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateLogValueXAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateLogValueXAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateLogValueYAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateLogValueYAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateBarYAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateBarYAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateBarXAxisLabels_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateBarXAxisLabels_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateAxis_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererUpdateAxis_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererHandlePolish_entry, Qt::Orientation orientation);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAxisRendererHandlePolish_exit);
++
++ AxisRenderer::AxisRenderer(QQuickItem *parent)
++ : QQuickItem(parent)
++ {
++ m_graph = qobject_cast<QGraphsView *>(parent);
++ setFlag(QQuickItem::ItemHasContents);
++ if (m_axes1.empty())
++ m_axes1.emplace_back();
++
++ if (m_axes2.empty())
++ m_axes2.emplace_back();
++ }
++
++ AxisRenderer::~AxisRenderer() {}
++
++ QGraphsTheme *AxisRenderer::theme() const
++ {
++ return m_graph->m_theme;
++ }
++
++ void AxisRenderer::initialize()
++ {
++ if (m_initialized) {
++ return;
++ }
++ if (!window()) {
++ qCDebug(lcGraphs2D, "window doesn't exist.");
++ return;
++ }
++
++ if (m_axisGrid)
++ m_axisGrid->componentComplete();
++ if (m_axisGridShadow)
++ m_axisGridShadow->componentComplete();
++
++ for (auto&& ax : *m_horzAxes) {
++ if (ax.line)
++ ax.line->componentComplete();
++ if (ax.ticker)
++ ax.ticker->componentComplete();
++ if (ax.lineShadow)
++ ax.lineShadow->componentComplete();
++ if (ax.tickerShadow)
++ ax.tickerShadow->componentComplete();
++ }
++
++ for (auto&& ax : *m_vertAxes) {
++ if (ax.line)
++ ax.line->componentComplete();
++ if (ax.ticker)
++ ax.ticker->componentComplete();
++ if (ax.lineShadow)
++ ax.lineShadow->componentComplete();
++ if (ax.tickerShadow)
++ ax.tickerShadow->componentComplete();
++ }
++
++ m_initialized = true;
++ }
++
++ QVector2D AxisRenderer::windowToAxisCoords(QVector2D coords)
++ {
++ float x = coords.x();
++ float y = coords.y();
++ x /= width() - m_graph->m_marginLeft - m_graph->m_marginRight
++ - m_y1AxisWidth - m_y2AxisWidth;
++ y /= height() - m_graph->m_marginTop - m_graph->m_marginBottom
++ - m_x1AxisHeight - m_x2AxisHeight;
++ x *= (*m_horzAxes)[0].valueRange;
++ y *= (*m_vertAxes)[0].valueRange;
++ return QVector2D(x, y);
++ }
++
++ bool AxisRenderer::calculateZoom(QAbstractAxis *axis, qreal delta)
++ {
++ qreal zoom = 1.0;
++ std::function<void(qreal zoom)> setZoom;
++ switch (axis->type()) {
++ case QAbstractAxis::AxisType::Value: {
++ auto valueAxis = qobject_cast<QValueAxis *>(axis);
++ zoom = valueAxis->zoom();
++ setZoom = [valueAxis](qreal zoom) {valueAxis->setZoom(zoom); };
++ break;
++ }
++ case QAbstractAxis::AxisType::DateTime: {
++ auto dateTimeAxis = qobject_cast<QDateTimeAxis *>(axis);
++ zoom = dateTimeAxis->zoom();
++ setZoom = [dateTimeAxis](qreal zoom) {dateTimeAxis->setZoom(zoom); };
++ break;
++ }
++ case QAbstractAxis::AxisType::LogValue: {
++ auto logAxis = qobject_cast<QLogValueAxis *>(axis);
++ zoom = logAxis->zoom();
++ setZoom = [logAxis](qreal zoom) {logAxis->setZoom(zoom); };
++ break;
++ }
++ default: {
++ return false;
++ }
++ }
++
++ qreal change = 0.0;
++ if (delta > 0)
++ change = zoom * m_graph->m_zoomSensitivity;
++ else if (delta < 0)
++ change = -zoom * m_graph->m_zoomSensitivity;
++
++ zoom += change;
++
++ if (zoom < 0.01f)
++ zoom = 0.01;
++
++ setZoom(zoom);
++
++ return true;
++ }
++
++ bool AxisRenderer::zoom(qreal delta)
++ {
++ if (m_graph->zoomStyle() != QGraphsView::ZoomStyle::Center)
++ return false;
++
++ bool hzoomed = !zoomsHorizontalAxis() || calculateZoom((*m_horzAxes)[0].axis, delta);
++ bool vzoomed = calculateZoom((*m_vertAxes)[0].axis, delta);
++ return hzoomed && vzoomed;
++ }
++
++ const AxisRenderer::AxisProperties &AxisRenderer::getAxisX(QAbstractSeries *series) const
++ {
++ for (auto &&ax : *m_horzAxes) {
++ if (ax.axis && (ax.axis == series->axisX() || ax.axis == series->axisY()))
++ return ax;
++ }
++ return (*m_horzAxes)[0];
++ }
++
++ const AxisRenderer::AxisProperties &AxisRenderer::getAxisY(QAbstractSeries *series) const
++ {
++ for (auto &&ax : *m_vertAxes) {
++ if (ax.axis && (ax.axis == series->axisX() || ax.axis == series->axisY()))
++ return ax;
++ }
++ return (*m_vertAxes)[0];
++ }
++
++ bool AxisRenderer::handleWheel(QWheelEvent *event)
++ {
++ return zoom(-event->angleDelta().y());
++ }
++
++ void AxisRenderer::handlePinchScale(qreal delta)
++ {
++ zoom(delta - 1.0);
++ }
++
++ void AxisRenderer::handlePinchGrab(QPointingDevice::GrabTransition transition, QEventPoint point)
++ {
++ Q_UNUSED(transition)
++ Q_UNUSED(point)
++ }
++
++ void AxisRenderer::onTranslationChanged(QVector2D delta)
++ {
++ if (!m_dragState.dragging)
++ return;
++
++ m_dragState.delta += delta;
++
++ auto hax = (*m_horzAxes)[0].axis;
++ auto vax = (*m_vertAxes)[0].axis;
++
++ if (m_graph->zoomAreaEnabled() && m_graph->m_zoomAreaItem) {
++ m_graph->m_zoomAreaItem->setVisible(true);
++
++ qreal x = m_dragState.touchPositionAtPress.x();
++ if (m_dragState.delta.x() < 0)
++ x += m_dragState.delta.x();
++
++ qreal y = m_dragState.touchPositionAtPress.y();
++ if (m_dragState.delta.y() < 0)
++ y += m_dragState.delta.y();
++
++ qreal width = qAbs(m_dragState.delta.x());
++ qreal height = qAbs(m_dragState.delta.y());
++
++ m_graph->m_zoomAreaItem->setX(x);
++ m_graph->m_zoomAreaItem->setY(y);
++ m_graph->m_zoomAreaItem->setWidth(width);
++ m_graph->m_zoomAreaItem->setHeight(height);
++ }
++
++ if (m_graph->panStyle() != QGraphsView::PanStyle::Drag)
++ return;
++
++ QVector2D change(m_dragState.delta);
++ change = windowToAxisCoords(change);
++ change.setX(-change.x());
++
++ if (hax->type() == QAbstractAxis::AxisType::Value) {
++ auto axis = qobject_cast<QValueAxis *>(hax);
++ axis->setPan(m_dragState.panAtPress.x() + change.x());
++ } else if (hax->type() == QAbstractAxis::AxisType::DateTime) {
++ auto axis = qobject_cast<QDateTimeAxis *>(hax);
++ axis->setPan(m_dragState.panAtPress.x() + change.x());
++ } else if (hax->type() == QAbstractAxis::AxisType::LogValue) {
++ auto axis = qobject_cast<QLogValueAxis *>(hax);
++ axis->setPan(m_dragState.panAtPress.x() + change.x());
++ }
++
++ if (vax->type() == QAbstractAxis::AxisType::Value) {
++ auto axis = qobject_cast<QValueAxis *>(vax);
++ axis->setPan(m_dragState.panAtPress.y() + change.y());
++ } else if (vax->type() == QAbstractAxis::AxisType::DateTime) {
++ auto axis = qobject_cast<QDateTimeAxis *>(vax);
++ axis->setPan(m_dragState.panAtPress.y() + change.y());
++ } else if (vax->type() == QAbstractAxis::AxisType::LogValue) {
++ auto axis = qobject_cast<QLogValueAxis *>(vax);
++ axis->setPan(m_dragState.panAtPress.y() + change.y());
++ }
++ }
++
++ void AxisRenderer::onGrabChanged(QPointingDevice::GrabTransition transition, QEventPoint point)
++ {
++ const QPointF position = point.position();
++
++ auto &hax = (*m_horzAxes)[0];
++ auto &vax = (*m_vertAxes)[0];
++ auto htype = hax.axis->type();
++ auto vtype = vax.axis->type();
++
++ if (transition == QPointingDevice::GrabPassive
++ && point.pressPosition() == point.position()) {
++
++ m_dragState.dragging = true;
++ m_dragState.touchPositionAtPress = QVector2D(position);
++ m_dragState.delta = QVector2D(0, 0);
++
++ if (htype == QAbstractAxis::AxisType::Value) {
++ auto axis = qobject_cast<QValueAxis *>(hax.axis);
++ m_dragState.panAtPress.setX(axis->pan());
++ } else if (htype == QAbstractAxis::AxisType::DateTime) {
++ auto axis = qobject_cast<QDateTimeAxis *>(hax.axis);
++ m_dragState.panAtPress.setX(axis->pan());
++ } else if (htype == QAbstractAxis::AxisType::LogValue) {
++ auto axis = qobject_cast<QLogValueAxis *>(hax.axis);
++ m_dragState.panAtPress.setX(axis->pan());
++ }
++
++ if (vtype == QAbstractAxis::AxisType::Value) {
++ auto axis = qobject_cast<QValueAxis *>(vax.axis);
++ m_dragState.panAtPress.setY(axis->pan());
++ } else if (vtype == QAbstractAxis::AxisType::DateTime) {
++ auto axis = qobject_cast<QDateTimeAxis *>(vax.axis);
++ m_dragState.panAtPress.setY(axis->pan());
++ } else if (vtype == QAbstractAxis::AxisType::LogValue) {
++ auto axis = qobject_cast<QLogValueAxis *>(vax.axis);
++ m_dragState.panAtPress.setY(axis->pan());
++ }
++
++ } else if (m_dragState.dragging && transition == QPointingDevice::UngrabExclusive) {
++ m_dragState.dragging = false;
++
++ if (!m_graph->zoomAreaEnabled())
++ return;
++
++ if (m_graph->m_zoomAreaItem)
++ m_graph->m_zoomAreaItem->setVisible(false);
++
++ QVector2D zoomBoxEnd(position);
++ auto center = (m_dragState.touchPositionAtPress + zoomBoxEnd) / 2;
++ auto size = (m_dragState.touchPositionAtPress - zoomBoxEnd);
++ size.setX(qAbs(size.x()));
++ size.setY(qAbs(size.y()));
++
++ if (int(size.x()) == 0 || int(size.y()) == 0)
++ return;
++
++ size = windowToAxisCoords(size);
++
++ center -= QVector2D(m_graph->m_marginLeft + m_y1AxisWidth, m_graph->m_marginTop);
++ center = windowToAxisCoords(center);
++ center -= QVector2D(hax.valueRange / 2.0f, vax.valueRange / 2.0f);
++
++ if (!zoomsHorizontalAxis()) {
++ // Polar views only zoom around the radial (vertical) axis.
++ } else if (htype == QAbstractAxis::AxisType::Value) {
++ auto axis = qobject_cast<QValueAxis *>(hax.axis);
++ axis->setZoom(hax.valueRangeZoomless / size.x());
++ axis->setPan(axis->pan() + center.x());
++ } else if (htype == QAbstractAxis::AxisType::DateTime){
++ auto axis = qobject_cast<QDateTimeAxis *>(hax.axis);
++ axis->setZoom(hax.valueRangeZoomless / size.x());
++ axis->setPan(axis->pan() + center.x());
++ } else if (htype == QAbstractAxis::AxisType::LogValue){
++ auto axis = qobject_cast<QLogValueAxis *>(hax.axis);
++ axis->setZoom(hax.valueRangeZoomless / size.x());
++ axis->setPan(axis->pan() + center.x());
++ }
++
++ if (vtype == QAbstractAxis::AxisType::Value) {
++ auto axis = qobject_cast<QValueAxis *>(vax.axis);
++ axis->setZoom(vax.valueRangeZoomless / size.y());
++ axis->setPan(axis->pan() - center.y());
++ } else if (vtype == QAbstractAxis::AxisType::DateTime){
++ auto axis = qobject_cast<QDateTimeAxis *>(vax.axis);
++ axis->setZoom(vax.valueRangeZoomless / size.y());
++ axis->setPan(axis->pan() - center.y());
++ } else if (vtype == QAbstractAxis::AxisType::LogValue){
++ auto axis = qobject_cast<QLogValueAxis *>(vax.axis);
++ axis->setZoom(vax.valueRangeZoomless / size.y());
++ axis->setPan(axis->pan() - center.y());
++ }
++ }
++ }
++
++ void AxisRenderer::handlePolish()
++ {
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererHandlePolish, m_graph->orientation());
++
++ if (m_graph->panStyle() != QGraphsView::PanStyle::None
++ || m_graph->zoomStyle() != QGraphsView::ZoomStyle::None || m_graph->zoomAreaEnabled()) {
++ if (!m_dragHandler)
++ createDragHandler();
++ } else if (m_dragHandler) {
++ deleteDragHandler();
++ }
++
++ updateAxis();
++ }
++
++ void AxisRenderer::populateAxisItems()
++ {
++ // See if series is horizontal, so axis should also switch places.
++ bool vertical = true;
++ if (m_graph->orientation() == Qt::Orientation::Horizontal)
++ vertical = false;
++
++ if (vertical) {
++ m_horzAxes = &m_axes1;
++ m_vertAxes = &m_axes2;
++ } else {
++ m_horzAxes = &m_axes2;
++ m_vertAxes = &m_axes1;
++ }
++
++ if (vertical != m_wasVertical) {
++ // Orientation has changed, so clear possible custom elements
++ for (auto&& ax : m_axes1) {
++ if (ax.title)
++ ax.title->deleteLater();
++ if (ax.line)
++ ax.line->deleteLater();
++ if (ax.ticker)
++ ax.ticker->deleteLater();
++ if (ax.lineShadow)
++ ax.lineShadow->deleteLater();
++ if (ax.tickerShadow)
++ ax.tickerShadow->deleteLater();
++ for (auto&& item : ax.textItems)
++ item->deleteLater();
++ }
++
++ for (auto&& ax : m_axes2) {
++ if (ax.title)
++ ax.title->deleteLater();
++ if (ax.line)
++ ax.line->deleteLater();
++ if (ax.ticker)
++ ax.ticker->deleteLater();
++ if (ax.lineShadow)
++ ax.lineShadow->deleteLater();
++ if (ax.tickerShadow)
++ ax.tickerShadow->deleteLater();
++ for (auto&& item : ax.textItems)
++ item->deleteLater();
++ }
++
++ m_axes1.clear();
++ m_axes2.clear();
++
++ m_wasVertical = vertical;
++ }
++
++ if (m_axes1.empty())
++ m_axes1.emplace_back();
++
++ if (m_axes2.empty())
++ m_axes2.emplace_back();
++
++ m_axes1[0].axis = m_graph->axisX();
++ m_axes2[0].axis = m_graph->axisY();
++
++ for (auto&& s : m_graph->m_seriesList) {
++ if (auto series = qobject_cast<QAbstractSeries *>(s)) {
++ if (series->axisX() && series->axisX() != m_graph->axisX()) {
++ bool contains = false;
++ for (auto&& ax : m_axes1) {
++ if (ax.axis == series->axisX()) {
++ contains = true;
++ break;
++ }
++ }
++
++ if (!contains) {
++ auto &ax = m_axes1.emplace_back();
++ ax.axis = series->axisX();
++ }
++ }
++
++ if (series->axisY() && series->axisY() != m_graph->axisY()) {
++ bool contains = false;
++ for (auto&& ax : m_axes2) {
++ if (ax.axis == series->axisY()) {
++ contains = true;
++ break;
++ }
++ }
++
++ if (!contains) {
++ auto &ax = m_axes2.emplace_back();
++ ax.axis = series->axisY();
++ }
++ }
++ }
++ }
++
++ if (!m_axisGrid) {
++ m_axisGrid = new AxisGrid(this);
++ m_axisGrid->setZ(-1);
++ m_axisGrid->setupShaders();
++ m_axisGrid->setOrigo(0);
++ }
++ if (!m_axisGridShadow) {
++ m_axisGridShadow = new AxisGrid(this);
++ m_axisGridShadow->setZ(-3);
++ m_axisGridShadow->setupShaders();
++ m_axisGridShadow->setOrigo(0);
++ }
++
++ for (int i = 1; i < m_axes1.size(); i++) {
++ auto& ax = m_axes1[i];
++
++ bool used = false;
++
++ for (auto&& s : m_graph->m_seriesList) {
++ if (auto series = qobject_cast<QAbstractSeries *>(s)) {
++ if (series->axisX() && series->axisX() == ax.axis) {
++ used = true;
++ break;
++ }
++ }
++ }
++
++ if (!used) {
++ if (ax.title)
++ ax.title->deleteLater();
++ if (ax.line)
++ ax.line->deleteLater();
++ if (ax.ticker)
++ ax.ticker->deleteLater();
++ if (ax.lineShadow)
++ ax.lineShadow->deleteLater();
++ if (ax.tickerShadow)
++ ax.tickerShadow->deleteLater();
++ for (auto&& item : ax.textItems)
++ item->deleteLater();
++ m_axes1.removeAt(i);
++ i--;
++ continue;
++ }
++ }
++
++ for (int i = 1; i < m_axes2.size(); i++) {
++ auto& ax = m_axes2[i];
++
++ bool used = false;
++
++ for (auto&& s : m_graph->m_seriesList) {
++ if (auto series = qobject_cast<QAbstractSeries *>(s)) {
++ if (series->axisY() && series->axisY() == ax.axis) {
++ used = true;
++ break;
++ }
++ }
++ }
++
++ if (!used) {
++ if (ax.title)
++ ax.title->deleteLater();
++ if (ax.line)
++ ax.line->deleteLater();
++ if (ax.ticker)
++ ax.ticker->deleteLater();
++ if (ax.lineShadow)
++ ax.lineShadow->deleteLater();
++ if (ax.tickerShadow)
++ ax.tickerShadow->deleteLater();
++ for (auto&& item : ax.textItems)
++ item->deleteLater();
++ m_axes2.removeAt(i);
++ i--;
++ continue;
++ }
++ }
++
++ for (int i = 0; i < m_vertAxes->size(); i++) {
++ auto& ax = (*m_vertAxes)[i];
++
++ if (!ax.line) {
++ ax.line = new AxisLine(this);
++ ax.line->setZ(-1);
++ ax.line->setupShaders();
++ }
++ if (!ax.ticker) {
++ ax.ticker = new AxisTicker(this);
++ ax.ticker->setZ(-2);
++ ax.ticker->setOrigo(0);
++ // TODO: Configurable in theme or axis?
++ ax.ticker->setSubTickLength(0.5);
++ ax.ticker->setupShaders();
++ }
++ if (!ax.lineShadow) {
++ ax.lineShadow = new AxisLine(this);
++ ax.lineShadow->setZ(-3);
++ ax.lineShadow->setupShaders();
++ }
++ if (!ax.tickerShadow) {
++ ax.tickerShadow = new AxisTicker(this);
++ ax.tickerShadow->setZ(-3);
++ ax.tickerShadow->setOrigo(0);
++ // TODO: Configurable in theme or axis?
++ ax.tickerShadow->setSubTickLength(ax.ticker->subTickLength());
++ ax.tickerShadow->setupShaders();
++ }
++ }
++
++ for (int i = 0; i < m_horzAxes->size(); i++) {
++ auto& ax = (*m_horzAxes)[i];
++
++ if (!ax.line) {
++ ax.line = new AxisLine(this);
++ ax.line->setZ(-1);
++ ax.line->setIsHorizontal(true);
++ ax.line->setupShaders();
++ }
++ if (!ax.ticker) {
++ ax.ticker = new AxisTicker(this);
++ ax.ticker->setZ(-2);
++ ax.ticker->setIsHorizontal(true);
++ ax.ticker->setOrigo(0);
++ // TODO: Configurable in theme or axis?
++ ax.ticker->setSubTickLength(0.2);
++ ax.ticker->setupShaders();
++ }
++ if (!ax.lineShadow) {
++ ax.lineShadow = new AxisLine(this);
++ ax.lineShadow->setZ(-3);
++ ax.lineShadow->setupShaders();
++ }
++ if (!ax.tickerShadow) {
++ ax.tickerShadow = new AxisTicker(this);
++ ax.tickerShadow->setZ(-3);
++ ax.tickerShadow->setIsHorizontal(true);
++ ax.tickerShadow->setOrigo(0);
++ // TODO: Configurable in theme or axis?
++ ax.tickerShadow->setSubTickLength(ax.ticker->subTickLength());
++ ax.tickerShadow->setupShaders();
++ }
++ }
++ }
++
++ void AxisRenderer::updateAxis()
++ {
++ if (!theme())
++ return;
++
++ const qreal axisWidth = m_y1AxisWidth + m_y2AxisWidth;
++ const qreal axisHeight = m_x1AxisHeight + m_x2AxisHeight;
++
++ const qreal plotHeight = height() - m_graph->m_marginTop - m_graph->m_marginBottom - axisHeight;
++ const qreal plotWidth = width() - m_graph->m_marginLeft - m_graph->m_marginRight - axisWidth;
++
++ const bool gridVisible = theme()->isGridVisible();
++ if ((*m_vertAxes)[0].axis) {
++ m_gridVerticalLinesVisible = gridVisible && (*m_vertAxes)[0].axis->isGridVisible();
++ m_gridVerticalSubLinesVisible = gridVisible && (*m_vertAxes)[0].axis->isSubGridVisible();
++ }
++ if ((*m_horzAxes)[0].axis) {
++ m_gridHorizontalLinesVisible = gridVisible && (*m_horzAxes)[0].axis->isGridVisible();
++ m_gridHorizontalSubLinesVisible = gridVisible && (*m_horzAxes)[0].axis->isSubGridVisible();
++ }
++
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateAxis);
++
++ const qreal yMargin = m_graph->m_defaultAxisYLabelsMargin;
++ const qreal xMargin = m_graph->m_defaultAxisXLabelsMargin;
++ const qreal yTicker = m_graph->m_defaultAxisTickersWidth;
++ const qreal xTicker = m_graph->m_defaultAxisTickersHeight;
++
++ const QRectF &topSide = m_graph->m_x2AxisArea;
++ const QRectF &bottomSide = m_graph->m_x1AxisArea;
++
++ qreal topRemaining = m_x2AxisHeight;
++ qreal bottomMargin = 0;
++ qreal prevBottomGap = 0;
++ for (auto &ax : *m_horzAxes) {
++ if (!ax.axis)
++ continue;
++
++ if (isAxisBottomOrRight(ax)) {
++ bottomMargin += prevBottomGap;
++
++ const qreal sizeY = bottomSide.y() + bottomMargin;
++ const qreal labelSize = qMax(0.0, ax.size - xMargin - xTicker);
++ ax.tickersRect = QRectF(bottomSide.x(), sizeY, bottomSide.width(), xTicker);
++ ax.labelsRect = QRectF(bottomSide.x(), sizeY + xTicker + xMargin,
++ bottomSide.width(), labelSize);
++
++ bottomMargin += ax.size;
++ prevBottomGap = titleGap(ax);
++ } else {
++ const qreal sizeY = topSide.y() + topRemaining - ax.size;
++ const qreal labelSize = qMax(0.0, ax.size - xMargin - xTicker);
++ ax.labelsRect = QRectF(topSide.x(), sizeY, topSide.width(), labelSize);
++ ax.tickersRect = QRectF(topSide.x(), sizeY + labelSize + xMargin,
++ topSide.width(), xTicker);
++
++ topRemaining -= ax.size;
++ topRemaining -= titleGap(ax);
++ }
++
++ if (qobject_cast<QValueAxis *>(ax.axis)) {
++ ax.stepPx = plotWidth / (ax.valueRange / ax.valueStep);
++ double axisHorizontalValueDiff = ax.minLabel - ax.minValue;
++ ax.displacement = -(axisHorizontalValueDiff / ax.valueStep) * ax.stepPx;
++ updateValueXAxisLabels(ax, ax.labelsRect);
++ } else if (qobject_cast<QDateTimeAxis *>(ax.axis)) {
++ ax.stepPx = plotWidth
++ / (qFuzzyIsNull(ax.valueStep) ? ax.tickCount
++ : (ax.valueRange / ax.valueStep));
++ double axisHorizontalValueDiff = fmod(ax.minValue, ax.valueStep) / ax.valueStep;
++ ax.displacement = axisHorizontalValueDiff * ax.stepPx;
++ updateDateTimeXAxisLabels(ax, ax.labelsRect);
++ } else if (qobject_cast<QLogValueAxis *>(ax.axis)) {
++ ax.stepPx = plotWidth / (ax.valueRange / ax.valueStep);
++ double axisHorizontalValueDiff = ax.minLabel - ax.minValue;
++ ax.displacement = -(axisHorizontalValueDiff / ax.valueStep) * ax.stepPx;
++ updateLogValueXAxisLabels(ax, ax.labelsRect);
++ }
++ #if QT_CONFIG(graphs_2d_bar)
++ else if (qobject_cast<QBarCategoryAxis *>(ax.axis)) {
++ updateBarXAxisLabels(ax, ax.labelsRect);
++ }
++ #endif
++ }
++
++ const QRectF &leftSide = m_graph->m_y1AxisArea;
++ const QRectF &rightSide = m_graph->m_y2AxisArea;
++
++ qreal leftRemaining = m_y1AxisWidth;
++ qreal rightMargin = 0;
++ qreal prevRightGap = 0;
++
++ for (auto &ax : *m_vertAxes) {
++ if (!ax.axis)
++ continue;
++
++ if (isAxisBottomOrRight(ax)) {
++ rightMargin += prevRightGap;
++ const qreal sizeX = rightSide.x() + rightMargin;
++ const qreal labelSize = qMax<qreal>(0, ax.size - yMargin - yTicker);
++ ax.tickersRect = QRectF(sizeX, rightSide.y(), yTicker, rightSide.height());
++ ax.labelsRect = QRectF(sizeX + yTicker + yMargin, rightSide.y(),
++ labelSize, rightSide.height());
++
++ rightMargin += ax.size;
++ prevRightGap = titleGap(ax);
++ } else {
++ const qreal sizeX = leftSide.x() + leftRemaining - ax.size;
++ const qreal labelSize = qMax<qreal>(0, ax.size - yMargin - yTicker);
++ ax.labelsRect = QRectF(sizeX, leftSide.y(), labelSize, leftSide.height());
++ ax.tickersRect = QRectF(sizeX + labelSize + yMargin, leftSide.y(),
++ yTicker, leftSide.height());
++
++ leftRemaining -= ax.size;
++ leftRemaining -= titleGap(ax);
++ }
++
++ if (qobject_cast<QValueAxis *>(ax.axis)) {
++ ax.stepPx = plotHeight / (ax.valueRange / ax.valueStep);
++ double axisVerticalValueDiff = ax.minLabel - ax.minValue;
++ ax.displacement = -(axisVerticalValueDiff / ax.valueStep) * ax.stepPx;
++ updateValueYAxisLabels(ax, ax.labelsRect);
++ } else if (qobject_cast<QDateTimeAxis *>(ax.axis)) {
++ ax.stepPx = plotHeight
++ / (qFuzzyIsNull(ax.valueStep) ? ax.tickCount
++ : (ax.valueRange / ax.valueStep));
++ double axisVerticalValueDiff = fmod(ax.minValue, ax.valueStep) / ax.valueStep;
++ ax.displacement = axisVerticalValueDiff * ax.stepPx;
++ updateDateTimeYAxisLabels(ax, ax.labelsRect);
++ } else if (qobject_cast<QLogValueAxis *>(ax.axis)) {
++ ax.stepPx = plotHeight / (ax.valueRange / ax.valueStep);
++ double axisVerticalValueDiff = ax.minLabel - ax.minValue;
++ ax.displacement = -(axisVerticalValueDiff / ax.valueStep) * ax.stepPx;
++ updateLogValueYAxisLabels(ax, ax.labelsRect);
++ }
++ #if QT_CONFIG(graphs_2d_bar)
++ else if (qobject_cast<QBarCategoryAxis *>(ax.axis)) {
++ updateBarYAxisLabels(ax, ax.labelsRect);
++ }
++ #endif
++ }
++
++ updateAxisTickers();
++ updateAxisTickersShadow();
++ updateAxisGrid();
++ updateAxisGridShadow();
++ updateAxisTitles();
++ }
++
++ bool AxisRenderer::hasAxisTitle(const AxisProperties &ax) const
++ {
++ return ax.axis && ax.axis->isTitleVisible() && !ax.axis->titleText().isEmpty();
++ }
++
++ bool AxisRenderer::isAxisBottomOrRight(const AxisProperties &ax) const
++ {
++ return ax.axis->alignment() == Qt::AlignBottom || ax.axis->alignment() == Qt::AlignRight;
++ }
++
++ // Space reserved between a titled axis and whatever comes next (another axis, or the outer
++ // view margin). Grows past the default margin only when the title itself needs more room.
++ qreal AxisRenderer::titleGap(const AxisProperties &ax) const
++ {
++ if (!hasAxisTitle(ax))
++ return 0;
++ return qMax(m_graph->m_defaultAxisTitleMargin, ax.titleSize);
++ }
++
++ // How far the title extends past the outer edge of the axis' labelsRect, i.e. beyond where a
++ // default-sized title would end.
++ qreal AxisRenderer::titleOutwardExtent(const AxisProperties &ax) const
++ {
++ if (!hasAxisTitle(ax))
++ return 0;
++ const qreal margin = m_graph->m_defaultAxisTitleMargin;
++ return ax.titleSize <= margin ? ax.titleSize * 0.5 : ax.titleSize - margin * 0.5;
++ }
++
++ // Builds a printf-style spec combining decimals with the axis label format so it can be
++ // passed to AxisRenderer::formatValueLabel(), which itself no longer takes a decimals argument.
++ // A format longer than a single conversion char is a user-supplied full spec and is used as-is.
++ static QString decimalFormatSpec(const QString &format, int decimals, char defaultConversion)
++ {
++ if (format.length() <= 1) {
++ const QChar conversion = format.isEmpty() ? QLatin1Char(defaultConversion)
++ : format.front();
++ return QStringLiteral("%.%1%2").arg(decimals).arg(conversion);
++ }
++ return format;
++ }
++
++ void AxisRenderer::updateAxisMeasurements()
++ {
++ populateAxisItems();
++
++ if (!theme())
++ return;
++
++ const QFontMetricsF yFontMetrics(theme()->axisYLabelFont());
++
++ for (auto &ax : *m_vertAxes) {
++ ax.labelSize = 0;
++ ax.titleSize = hasAxisTitle(ax) ? QFontMetricsF(ax.axis->titleFont()).height() : 0;
++ }
++
++ for (auto &ax : *m_vertAxes) {
++ if (auto vaxis = qobject_cast<QValueAxis *>(ax.axis)) {
++ double step = vaxis->tickInterval();
++
++ qreal diff = vaxis->max() - vaxis->min();
++ qreal center = diff / 2.0f + vaxis->min() + vaxis->pan();
++
++ diff /= vaxis->zoom();
++
++ ax.maxValue = center + diff / 2.0f;
++ ax.minValue = center - diff / 2.0f;
++
++ ax.valueRange = ax.maxValue - ax.minValue;
++ ax.valueRangeZoomless = vaxis->max() - vaxis->min();
++
++ // If step is not manually defined (or it is invalid), calculate autostep
++ if (step <= 0)
++ step = getValueStepsFromRange(vaxis->max() - vaxis->min());
++
++ // Get smallest tick label value
++ double minLabel = vaxis->tickAnchor();
++ while (minLabel < ax.minValue)
++ minLabel += step;
++ while (minLabel >= (ax.minValue + step))
++ minLabel -= step;
++ ax.minLabel = minLabel;
++
++ ax.valueStep = step;
++ int axisVerticalSubTickCount = vaxis->subTickCount();
++ ax.subGridScale = axisVerticalSubTickCount > 0 ? 1.0 / (axisVerticalSubTickCount + 1)
++ : 1.0;
++
++ int decimals = vaxis->labelDecimals();
++ if (decimals < 0)
++ decimals = getValueDecimalsFromRange(ax.valueRange);
++
++ const QString format = vaxis->labelFormat();
++ const QString spec = decimalFormatSpec(format, decimals, 'f');
++ ax.labelSize = qMax(ax.labelSize,
++ yFontMetrics.horizontalAdvance(
++ formatValueLabel(vaxis->min(), spec, QLatin1StringView("f"))));
++ ax.labelSize = qMax(ax.labelSize,
++ yFontMetrics.horizontalAdvance(
++ formatValueLabel(vaxis->max(), spec, QLatin1StringView("f"))));
++ } else if (auto vaxis = qobject_cast<QDateTimeAxis *>(ax.axis)) {
++ // Todo: make constant for all axis, or clamp in class? (QTBUG-124736)
++ const double MAX_DIVS = 100.0;
++
++ double tickCount = std::clamp<double>(vaxis->tickCount(), 0.0, MAX_DIVS);
++ qreal diff = vaxis->max().toMSecsSinceEpoch() - vaxis->min().toMSecsSinceEpoch();
++ qreal center = diff / 2.0f + vaxis->min().toMSecsSinceEpoch() + vaxis->pan();
++
++ diff /= vaxis->zoom();
++ ax.maxValue = center + diff / 2.0f;
++ ax.minValue = center - diff / 2.0f;
++ ax.valueRange = std::abs(ax.maxValue - ax.minValue);
++ ax.valueRangeZoomless = vaxis->max().toMSecsSinceEpoch()
++ - vaxis->min().toMSecsSinceEpoch();
++
++ // in ms
++ double segment;
++ if (tickCount <= 0) {
++ segment = getValueStepsFromRange(ax.valueRange);
++ tickCount = ax.valueRange / segment;
++ } else {
++ segment = ax.valueRange / tickCount;
++ }
++
++ ax.minLabel = std::clamp(tickCount, 1.0, MAX_DIVS);
++
++ ax.valueStep = segment;
++ int axisVerticalSubTickCount = vaxis->subTickCount();
++ ax.subGridScale = axisVerticalSubTickCount > 0 ? 1.0 / (axisVerticalSubTickCount + 1)
++ : 1.0;
++ ax.tickCount = tickCount;
++
++ const QString format = vaxis->labelFormat();
++ ax.labelSize = qMax(ax.labelSize,
++ yFontMetrics.horizontalAdvance(
++ vaxis->min().toString(format)));
++ ax.labelSize = qMax(ax.labelSize,
++ yFontMetrics.horizontalAdvance(
++ vaxis->max().toString(format)));
++ } else if (auto vaxis = qobject_cast<QLogValueAxis *>(ax.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;
++ qreal center = diff / 2.0f + logMin + vaxis->pan();
++
++ diff /= vaxis->zoom();
++
++ ax.maxValue = center + diff / 2.0f;
++ ax.minValue = center - diff / 2.0f;
++
++ //use log scale to create value range
++ ax.valueRange = ax.maxValue - ax.minValue;
++ ax.valueRangeZoomless = logMax - logMin;
++
++ // Get smallest tick label value
++ ax.minLabel = qRound(ax.minValue);
++ ax.valueStep = 1;
++
++ int axisVerticalSubTickCount = vaxis->subTickCount();
++ if (axisVerticalSubTickCount == -1)
++ axisVerticalSubTickCount = int(qRound(vaxis->base()));
++ ax.subGridScale = axisVerticalSubTickCount > 0 ? 1.0 / (axisVerticalSubTickCount + 1)
++ : 1.0;
++
++ const QString format = vaxis->labelFormat();
++ int precision = vaxis->labelPrecision();
++ const QString spec = decimalFormatSpec(format, precision, 'g');
++ ax.labelSize = qMax(ax.labelSize,
++ yFontMetrics.horizontalAdvance(
++ formatValueLabel(vaxis->min(), spec, QLatin1StringView("g"))));
++ ax.labelSize = qMax(ax.labelSize,
++ yFontMetrics.horizontalAdvance(
++ formatValueLabel(vaxis->max(), spec, QLatin1StringView("g"))));
++ }
++ #if QT_CONFIG(graphs_2d_bar)
++ else if (auto vaxis = qobject_cast<QBarCategoryAxis *>(ax.axis)) {
++ ax.maxValue = vaxis->categories().size();
++ ax.minValue = 0;
++ ax.valueRange = ax.maxValue - ax.minValue;
++
++ for (const auto &category : vaxis->categories()) {
++ ax.labelSize = qMax(ax.labelSize,
++ yFontMetrics.horizontalAdvance(category));
++ }
++ }
++ #endif
++ }
++
++ for (auto &ax : *m_horzAxes)
++ ax.titleSize = hasAxisTitle(ax) ? QFontMetricsF(ax.axis->titleFont()).height() : 0;
++
++ for (auto&& ax : *m_horzAxes) {
++ if (auto haxis = qobject_cast<QValueAxis *>(ax.axis)) {
++ double step = haxis->tickInterval();
++
++ qreal diff = haxis->max() - haxis->min();
++ qreal center = diff / 2.0f + haxis->min() + haxis->pan();
++
++ diff /= haxis->zoom();
++
++ ax.maxValue = center + diff / 2.0f;
++ ax.minValue = center - diff / 2.0f;
++
++ ax.valueRange = ax.maxValue - ax.minValue;
++ ax.valueRangeZoomless = haxis->max() - haxis->min();
++
++ // If step is not manually defined (or it is invalid), calculate autostep
++ if (step <= 0)
++ step = getValueStepsFromRange(haxis->max() - haxis->min());
++
++ // Get smallest tick label value
++ double minLabel = haxis->tickAnchor();
++ while (minLabel < ax.minValue)
++ minLabel += step;
++ while (minLabel >= (ax.minValue + step))
++ minLabel -= step;
++ ax.minLabel = minLabel;
++
++ ax.valueStep = step;
++ int axisHorizontalSubTickCount = haxis->subTickCount();
++ ax.subGridScale = axisHorizontalSubTickCount > 0
++ ? 1.0 / (axisHorizontalSubTickCount + 1)
++ : 1.0;
++
++ } else if (auto haxis = qobject_cast<QDateTimeAxis *>(ax.axis)) {
++ const double MAX_DIVS = 100.0;
++
++ double tickCount = std::clamp<double>(haxis->tickCount(), 0.0, MAX_DIVS);
++ double diff = haxis->max().toMSecsSinceEpoch() - haxis->min().toMSecsSinceEpoch();
++ double center = diff / 2.0f + haxis->min().toMSecsSinceEpoch()
++ + haxis->pan();
++
++ diff /= haxis->zoom();
++ ax.maxValue = center + diff / 2.0f;
++ ax.minValue = center - diff / 2.0f;
++ ax.valueRange = std::abs(ax.maxValue - ax.minValue);
++ ax.valueRangeZoomless = haxis->max().toMSecsSinceEpoch()
++ - haxis->min().toMSecsSinceEpoch();
++
++ // in ms
++ double segment;
++ if (tickCount <= 0) {
++ segment = getValueStepsFromRange(ax.valueRange);
++ tickCount = ax.valueRange / segment;
++ } else {
++ segment = ax.valueRange / tickCount;
++ }
++
++ ax.minLabel = std::clamp(tickCount, 1.0, MAX_DIVS);
++
++ ax.valueStep = segment;
++ int axisHorizontalSubTickCount = haxis->subTickCount();
++ ax.subGridScale = axisHorizontalSubTickCount > 0
++ ? 1.0 / (axisHorizontalSubTickCount + 1)
++ : 1.0;
++ ax.tickCount = tickCount;
++
++ } else if (auto haxis = qobject_cast<QLogValueAxis *>(ax.axis)) {
++ ax.isLogarithmic = true;
++ ax.logBase = haxis->base();
++
++ qreal logBase = log(haxis->base());
++ qreal logMin = log(haxis->min()) / logBase;
++ qreal logMax = log(haxis->max()) / logBase;
++
++ qreal diff = logMax - logMin;
++ qreal center = diff / 2.0f + logMin + haxis->pan();
++
++ diff /= haxis->zoom();
++
++ ax.maxValue = center + diff / 2.0f;
++ ax.minValue = center - diff / 2.0f;
++
++ //use log scale to create value range
++ ax.valueRange = ax.maxValue - ax.minValue;
++ ax.valueRangeZoomless = logMax - logMin;
++
++ ax.minLabel = qRound(ax.minValue);
++ ax.valueStep = 1;
++
++ int axisHorizontalSubTickCount = haxis->subTickCount();
++ if (axisHorizontalSubTickCount == -1)
++ axisHorizontalSubTickCount = int(qRound(haxis->base()));
++ ax.subGridScale = axisHorizontalSubTickCount > 0 ? 1.0 / (axisHorizontalSubTickCount + 1)
++ : 1.0;
++ }
++ #if QT_CONFIG(graphs_2d_bar)
++ else if (auto haxis = qobject_cast<QBarCategoryAxis *>(ax.axis)) {
++ ax.maxValue = haxis->categories().size();
++ ax.minValue = 0;
++ ax.valueRange = ax.maxValue - ax.minValue;
++ }
++ #endif
++ }
++
++ // Add some arbitrary margin so that labels don't touch the title
++ for (auto &ax : *m_vertAxes) {
++ if (ax.labelSize > 0)
++ ax.labelSize += 8;
++ }
++
++ // Calculate space needed for each axis
++ const bool isDynamic = m_graph->dynamicLabelMargins();
++ const qreal yLabelDefault = m_graph->m_defaultAxisLabelsWidth;
++ const qreal xLabelDefault = m_graph->m_defaultAxisLabelsHeight;
++ const qreal yMargin = m_graph->m_defaultAxisYLabelsMargin;
++ const qreal xMargin = m_graph->m_defaultAxisXLabelsMargin;
++ const qreal yTicker = m_graph->m_defaultAxisTickersWidth;
++ const qreal xTicker = m_graph->m_defaultAxisTickersHeight;
++
++ m_y1AxisWidth = 0;
++ m_y2AxisWidth = 0;
++ m_x1AxisHeight = 0;
++ m_x2AxisHeight = 0;
++
++ qreal leftGap = 0;
++ qreal rightGap = 0;
++ qreal topGap = 0;
++ qreal bottomGap = 0;
++ // Outward extent of the outermost titled axis on each side, updated on every iteration so
++ // that after the loop it reflects the actual outermost axis (0 if that axis has no title).
++ qreal leftExtent = 0;
++ qreal rightExtent = 0;
++ qreal topExtent = 0;
++ qreal bottomExtent = 0;
++
++ for (auto &ax : *m_vertAxes) {
++ if (!ax.axis || !ax.axis->isVisible()) {
++ ax.size = 0;
++ continue;
++ }
++
++ ax.size = yMargin + yTicker;
++ if (isDynamic)
++ ax.size += qMax(ax.labelSize, yLabelDefault);
++ else
++ ax.size += yLabelDefault;
++
++ if (isAxisBottomOrRight(ax)) {
++ m_y2AxisWidth += rightGap;
++ m_y2AxisWidth += ax.size;
++ rightGap = titleGap(ax);
++ rightExtent = titleOutwardExtent(ax);
++ } else {
++ m_y1AxisWidth += leftGap;
++ m_y1AxisWidth += ax.size;
++ leftGap = titleGap(ax);
++ leftExtent = titleOutwardExtent(ax);
++ }
++ }
++ m_y2AxisWidth += qMax(0.0, rightExtent - m_graph->m_marginRight);
++ m_y1AxisWidth += qMax(0.0, leftExtent - m_graph->m_marginLeft);
++
++ for (auto &ax : *m_horzAxes) {
++ if (!ax.axis || !ax.axis->isVisible()) {
++ ax.size = 0;
++ continue;
++ }
++
++ ax.size = xMargin + xTicker;
++ if (isDynamic)
++ ax.size += qMax(ax.labelSize, xLabelDefault);
++ else
++ ax.size += xLabelDefault;
++
++ if (isAxisBottomOrRight(ax)) {
++ m_x1AxisHeight += bottomGap;
++ m_x1AxisHeight += ax.size;
++ bottomGap = titleGap(ax);
++ bottomExtent = titleOutwardExtent(ax);
++ } else {
++ m_x2AxisHeight += topGap;
++ m_x2AxisHeight += ax.size;
++ topGap = titleGap(ax);
++ topExtent = titleOutwardExtent(ax);
++ }
++ }
++ m_x1AxisHeight += qMax(0.0, bottomExtent - m_graph->m_marginBottom);
++ m_x2AxisHeight += qMax(0.0, topExtent - m_graph->m_marginTop);
++ }
++
++ void AxisRenderer::updateAxisTickers()
++ {
++ for (auto&& ax : *m_vertAxes) {
++ if (ax.axis) {
++ QRectF yAxisRect = ax.tickersRect;
++
++ // Note: Fix before enabling, see QTBUG-121207 and QTBUG-121211
++ //if (theme()->themeDirty()) {
++ if (ax.axis->subColor().isValid())
++ ax.ticker->setSubTickColor(ax.axis->subColor());
++ else
++ ax.ticker->setSubTickColor(theme()->axisY().subColor());
++ if (ax.axis->color().isValid())
++ ax.ticker->setTickColor(ax.axis->color());
++ else
++ ax.ticker->setTickColor(theme()->axisY().mainColor());
++ ax.ticker->setTickLineWidth(theme()->axisY().mainWidth());
++ ax.ticker->setSubTickLineWidth(theme()->axisY().subWidth());
++ ax.ticker->setSmoothing(m_graph->axisYSmoothing());
++ //}
++
++ float topPadding = m_axisGrid->gridLineWidth() * 0.5;
++ float bottomPadding = topPadding;
++ // TODO Only when changed
++ ax.ticker->setDisplacement(ax.displacement);
++ QRectF &rect = yAxisRect;
++ ax.ticker->setX(rect.x());
++ ax.ticker->setY(rect.y());
++ ax.ticker->setWidth(rect.width());
++ ax.ticker->setHeight(rect.height());
++ ax.ticker->setFlipped(ax.axis->alignment() == Qt::AlignRight || ax.axis->alignment() == Qt::AlignBottom);
++
++ ax.ticker->setSpacing((ax.ticker->height() - topPadding - bottomPadding)
++ / (ax.valueRange / ax.valueStep));
++ ax.ticker->setSubTicksVisible(!qFuzzyCompare(ax.subGridScale, 1.0));
++ ax.ticker->setSubTickScale(ax.subGridScale);
++
++ ax.ticker->setLogarithmic(ax.isLogarithmic);
++ ax.ticker->setBase(ax.logBase);
++
++ ax.ticker->setVisible(ax.axis->isVisible());
++
++ // Axis line
++ if (ax.axis->color().isValid())
++ ax.line->setColor(ax.axis->color());
++ else
++ ax.line->setColor(theme()->axisY().mainColor());
++ ax.line->setLineWidth(theme()->axisY().mainWidth());
++ ax.line->setSmoothing(m_graph->axisYSmoothing());
++
++ float xMovement = 0.5 * (ax.line->lineWidth() + ax.line->smoothing());
++ if (ax.axis->alignment() == Qt::AlignRight || ax.axis->alignment() == Qt::AlignBottom)
++ ax.line->setX(ax.ticker->x() - xMovement);
++ else
++ ax.line->setX(ax.ticker->x() + ax.ticker->width() - xMovement);
++ ax.line->setY(ax.ticker->y());
++ ax.line->setWidth(ax.line->lineWidth() + ax.line->smoothing());
++ ax.line->setHeight(ax.ticker->height());
++ ax.line->setVisible(ax.axis->isLineVisible());
++ } else {
++ // Hide all parts of vertical axis
++ ax.ticker->setVisible(false);
++ ax.line->setVisible(false);
++ for (auto &textItem : ax.textItems)
++ textItem->setVisible(false);
++ }
++ }
++
++ for (auto&& ax : *m_horzAxes) {
++ if (ax.axis) {
++ QRectF xAxisRect = ax.tickersRect;
++
++ //if (theme()->themeDirty()) {
++ if (ax.axis->subColor().isValid())
++ ax.ticker->setSubTickColor(ax.axis->subColor());
++ else
++ ax.ticker->setSubTickColor(theme()->axisX().subColor());
++ if (ax.axis->color().isValid())
++ ax.ticker->setTickColor(ax.axis->color());
++ else
++ ax.ticker->setTickColor(theme()->axisX().mainColor());
++ ax.ticker->setTickLineWidth(theme()->axisX().mainWidth());
++ ax.ticker->setSubTickLineWidth(theme()->axisX().subWidth());
++ ax.ticker->setSmoothing(m_graph->axisXSmoothing());
++ //}
++
++ float leftPadding = m_axisGrid->gridLineWidth() * 0.5;
++ float rightPadding = leftPadding;
++ // TODO Only when changed
++ ax.ticker->setDisplacement(ax.displacement);
++ QRectF &rect = xAxisRect;
++ ax.ticker->setX(rect.x());
++ ax.ticker->setY(rect.y());
++ ax.ticker->setWidth(rect.width());
++ ax.ticker->setHeight(rect.height());
++ ax.ticker->setFlipped(ax.axis->alignment() == Qt::AlignTop || ax.axis->alignment() == Qt::AlignLeft);
++
++ ax.ticker->setSpacing((ax.ticker->width() - leftPadding - rightPadding)
++ / (ax.valueRange / ax.valueStep));
++ ax.ticker->setSubTicksVisible(!qFuzzyCompare(ax.subGridScale, 1.0));
++ ax.ticker->setSubTickScale(ax.subGridScale);
++
++ ax.ticker->setLogarithmic(ax.isLogarithmic);
++ ax.ticker->setBase(ax.logBase);
++
++ ax.ticker->setVisible(ax.axis->isVisible());
++ // Axis line
++ if (ax.axis->color().isValid())
++ ax.line->setColor(ax.axis->color());
++ else
++ ax.line->setColor(theme()->axisX().mainColor());
++ ax.line->setLineWidth(theme()->axisX().mainWidth());
++ ax.line->setSmoothing(m_graph->axisXSmoothing());
++ ax.line->setX(ax.ticker->x());
++ float yMovement = 0.5 * (ax.line->lineWidth() + ax.line->smoothing());
++ if (ax.axis->alignment() == Qt::AlignTop || ax.axis->alignment() == Qt::AlignLeft)
++ ax.line->setY(ax.ticker->y() + ax.ticker->height() - yMovement);
++ else
++ ax.line->setY(ax.ticker->y() - yMovement);
++ ax.line->setWidth(ax.ticker->width());
++ ax.line->setHeight(ax.line->lineWidth() + ax.line->smoothing());
++ ax.line->setVisible(ax.axis->isLineVisible());
++ } else {
++ // Hide all parts of horizontal axis
++ ax.ticker->setVisible(false);
++ ax.line->setVisible(false);
++ for (auto &textItem : ax.textItems)
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateAxisTickersShadow()
++ {
++ for (auto&& ax : *m_vertAxes) {
++ if (ax.axis && m_graph->isShadowVisible()) {
++ ax.tickerShadow->setSubTickColor(m_graph->shadowColor());
++ ax.tickerShadow->setTickColor(m_graph->shadowColor());
++ ax.tickerShadow->setSubTickLineWidth(ax.ticker->subTickLineWidth()
++ + m_graph->shadowBarWidth());
++ ax.tickerShadow->setTickLineWidth(ax.ticker->tickLineWidth()
++ + m_graph->shadowBarWidth());
++ ax.tickerShadow->setSmoothing(ax.ticker->smoothing() + m_graph->shadowSmoothing());
++
++ // TODO Only when changed
++ ax.tickerShadow->setDisplacement(ax.ticker->displacement());
++ ax.tickerShadow->setX(ax.ticker->x() + m_graph->shadowXOffset());
++ ax.tickerShadow->setY(ax.ticker->y() + m_graph->shadowYOffset()
++ + m_graph->shadowBarWidth() * 0.5);
++ ax.tickerShadow->setWidth(ax.ticker->width());
++ ax.tickerShadow->setHeight(ax.ticker->height());
++ ax.tickerShadow->setFlipped(ax.ticker->isFlipped());
++ ax.tickerShadow->setSpacing(ax.ticker->spacing());
++ ax.tickerShadow->setSubTicksVisible(ax.ticker->subTicksVisible());
++ ax.tickerShadow->setSubTickScale(ax.ticker->subTickScale());
++
++ ax.tickerShadow->setLogarithmic(ax.ticker->isLogarithmic());
++ ax.tickerShadow->setBase(ax.ticker->base());
++
++ ax.tickerShadow->setVisible(ax.ticker->isVisible());
++ // Axis line
++ ax.lineShadow->setColor(m_graph->shadowColor());
++ ax.lineShadow->setLineWidth(ax.line->lineWidth() + m_graph->shadowBarWidth());
++ ax.lineShadow->setSmoothing(ax.line->smoothing() + m_graph->shadowSmoothing());
++ ax.lineShadow->setX(ax.line->x() + m_graph->shadowXOffset());
++ ax.lineShadow->setY(ax.line->y() + m_graph->shadowYOffset()
++ + m_graph->shadowBarWidth() * 0.5);
++ ax.lineShadow->setWidth(ax.line->width());
++ ax.lineShadow->setHeight(ax.line->height());
++ ax.lineShadow->setVisible(ax.line->isVisible());
++ } else {
++ // Hide all parts of vertical axis
++ ax.tickerShadow->setVisible(false);
++ ax.lineShadow->setVisible(false);
++ }
++ }
++
++ for (auto&& ax : *m_horzAxes) {
++ if (ax.axis && m_graph->isShadowVisible()) {
++ ax.tickerShadow->setSubTickColor(m_graph->shadowColor());
++ ax.tickerShadow->setTickColor(m_graph->shadowColor());
++ ax.tickerShadow->setSubTickLineWidth(ax.ticker->subTickLineWidth()
++ + m_graph->shadowBarWidth());
++ ax.tickerShadow->setTickLineWidth(ax.ticker->tickLineWidth()
++ + m_graph->shadowBarWidth());
++ ax.tickerShadow->setSmoothing(ax.ticker->smoothing() + m_graph->shadowSmoothing());
++
++ // TODO Only when changed
++ ax.tickerShadow->setDisplacement(ax.ticker->displacement());
++ ax.tickerShadow->setX(ax.ticker->x() + m_graph->shadowXOffset()
++ - m_graph->shadowBarWidth() * 0.5);
++ ax.tickerShadow->setY(ax.ticker->y() + m_graph->shadowYOffset());
++ ax.tickerShadow->setWidth(ax.ticker->width());
++ ax.tickerShadow->setHeight(ax.ticker->height());
++ ax.tickerShadow->setFlipped(ax.ticker->isFlipped());
++ ax.tickerShadow->setSpacing(ax.ticker->spacing());
++ ax.tickerShadow->setSubTicksVisible(ax.ticker->subTicksVisible());
++ ax.tickerShadow->setSubTickScale(ax.ticker->subTickScale());
++ ax.tickerShadow->setVisible(ax.ticker->isVisible());
++ // Axis line
++ ax.lineShadow->setColor(m_graph->shadowColor());
++ ax.lineShadow->setLineWidth(ax.line->width() + m_graph->shadowBarWidth());
++ ax.lineShadow->setSmoothing(ax.line->smoothing() + m_graph->shadowSmoothing());
++ ax.lineShadow->setX(ax.line->x() + m_graph->shadowXOffset()
++ - m_graph->shadowBarWidth() * 0.5);
++ ax.lineShadow->setY(ax.line->y() + m_graph->shadowYOffset());
++ ax.lineShadow->setWidth(ax.line->width());
++ ax.lineShadow->setHeight(ax.line->height());
++ ax.lineShadow->setVisible(ax.line->isVisible());
++ } else {
++ // Hide all parts of horizontal axis
++ ax.tickerShadow->setVisible(false);
++ ax.lineShadow->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateAxisGrid()
++ {
++ auto &hax = (*m_horzAxes)[0];
++ auto &vax = (*m_vertAxes)[0];
++
++ m_axisGrid->setGridColor(theme()->grid().mainColor());
++ m_axisGrid->setSubGridColor(theme()->grid().subColor());
++ m_axisGrid->setSubGridLineWidth(theme()->grid().subWidth());
++ m_axisGrid->setGridLineWidth(theme()->grid().mainWidth());
++ const double minimumSmoothing = 0.05;
++ m_axisGrid->setSmoothing(m_graph->gridSmoothing() + minimumSmoothing);
++ if (theme()->isPlotAreaBackgroundVisible())
++ m_axisGrid->setPlotAreaBackgroundColor(theme()->plotAreaBackgroundColor());
++ else
++ m_axisGrid->setPlotAreaBackgroundColor(QColorConstants::Transparent);
++
++ float topPadding = m_axisGrid->gridLineWidth() * 0.5;
++ float bottomPadding = topPadding;
++ float leftPadding = topPadding;
++ float rightPadding = topPadding;
++ // TODO Only when changed
++ m_axisGrid->setGridMovement(QPointF(hax.displacement, vax.displacement));
++ QRectF rect = m_graph->m_plotArea;
++ m_axisGrid->setX(rect.x());
++ m_axisGrid->setY(rect.y());
++ m_axisGrid->setWidth(rect.width());
++ m_axisGrid->setHeight(rect.height());
++
++ m_axisGrid->setGridWidth((m_axisGrid->width() - leftPadding - rightPadding)
++ / (hax.valueRange / hax.valueStep));
++ m_axisGrid->setGridHeight((m_axisGrid->height() - topPadding - bottomPadding)
++ / (vax.valueRange / vax.valueStep));
++ m_axisGrid->setGridVisibility(QVector4D(m_gridHorizontalLinesVisible,
++ m_gridVerticalLinesVisible,
++ m_gridHorizontalSubLinesVisible,
++ m_gridVerticalSubLinesVisible));
++ m_axisGrid->setVerticalSubGridScale(vax.subGridScale);
++ m_axisGrid->setHorizontalSubGridScale(hax.subGridScale);
++
++ m_axisGrid->setVerticalLogarithmic(vax.isLogarithmic);
++ m_axisGrid->setHorizontalLogarithmic(hax.isLogarithmic);
++ m_axisGrid->setVerticalBase(vax.logBase);
++ m_axisGrid->setHorizontalBase(hax.logBase);
++ }
++
++ void AxisRenderer::updateAxisGridShadow()
++ {
++ if (m_graph->isShadowVisible()) {
++ m_axisGridShadow->setGridColor(m_graph->shadowColor());
++ m_axisGridShadow->setSubGridColor(m_graph->shadowColor());
++ m_axisGridShadow->setSubGridLineWidth(m_axisGrid->subGridLineWidth() + m_graph->shadowBarWidth());
++ m_axisGridShadow->setGridLineWidth(m_axisGrid->gridLineWidth() + m_graph->shadowBarWidth());
++ m_axisGridShadow->setSmoothing(m_axisGrid->smoothing() + m_graph->shadowSmoothing());
++
++ // TODO Only when changed
++ m_axisGridShadow->setGridMovement(m_axisGrid->gridMovement());
++ m_axisGridShadow->setX(m_axisGrid->x() + m_graph->shadowXOffset() - m_graph->shadowBarWidth() * 0.5);
++ m_axisGridShadow->setY(m_axisGrid->y() + m_graph->shadowYOffset() + m_graph->shadowBarWidth() * 0.5);
++ m_axisGridShadow->setWidth(m_axisGrid->width());
++ m_axisGridShadow->setHeight(m_axisGrid->height());
++ m_axisGridShadow->setGridWidth(m_axisGrid->gridWidth());
++ m_axisGridShadow->setGridHeight(m_axisGrid->gridHeight());
++ m_axisGridShadow->setGridVisibility(m_axisGrid->gridVisibility());
++ m_axisGridShadow->setVerticalSubGridScale(m_axisGrid->verticalSubGridScale());
++ m_axisGridShadow->setHorizontalSubGridScale(m_axisGrid->horizontalSubGridScale());
++ m_axisGridShadow->setVisible(true);
++
++ m_axisGridShadow->setVerticalLogarithmic(m_axisGrid->isVerticalLogarithmic());
++ m_axisGridShadow->setHorizontalLogarithmic(m_axisGrid->isHorizontalLogarithmic());
++ m_axisGridShadow->setVerticalBase(m_axisGrid->verticalBase());
++ m_axisGridShadow->setHorizontalBase(m_axisGrid->horizontalBase());
++ } else {
++ m_axisGridShadow->setVisible(false);
++ }
++ }
++
++ void AxisRenderer::updateAxisTitles()
++ {
++ for (auto &&ax : *m_horzAxes) {
++ if (!ax.title)
++ ax.title = new QQuickText(this);
++
++ if (ax.axis && ax.axis->isTitleVisible()) {
++ ax.title->setVAlign(QQuickText::AlignVCenter);
++ ax.title->setHAlign(QQuickText::AlignHCenter);
++ ax.title->setText(ax.axis->titleText());
++ if (ax.axis->titleColor().isValid())
++ ax.title->setColor(ax.axis->titleColor());
++ else
++ ax.title->setColor(theme()->labelTextColor());
++ ax.title->setFont(ax.axis->titleFont());
++
++ // setFont()/setText() above must run before width()/height() are read below, so
++ // the position is computed from the title's up-to-date size, not last frame's.
++ const QRectF &xAxisRect = ax.labelsRect;
++ const qreal offset = qMax(0.0, ax.title->height() - m_graph->m_defaultAxisTitleMargin)
++ * 0.5;
++ if (ax.axis->alignment() == Qt::AlignTop || ax.axis->alignment() == Qt::AlignLeft) {
++ ax.title->setY(xAxisRect.y() - ax.title->height() * 0.5 - offset);
++ }
++ else {
++ ax.title->setY(xAxisRect.y() + xAxisRect.height() - ax.title->height() * 0.5
++ + offset);
++ }
++
++ ax.title->setX((2 * xAxisRect.x() - ax.title->width() + xAxisRect.width()) * 0.5);
++ ax.title->setVisible(true);
++ } else {
++ ax.title->setVisible(false);
++ }
++ }
++
++ for (auto &&ax : *m_vertAxes) {
++ if (!ax.title)
++ ax.title = new QQuickText(this);
++
++ if (ax.axis && ax.axis->isTitleVisible()) {
++ ax.title->setVAlign(QQuickText::AlignVCenter);
++ ax.title->setHAlign(QQuickText::AlignHCenter);
++ ax.title->setText(ax.axis->titleText());
++ if (ax.axis->titleColor().isValid())
++ ax.title->setColor(ax.axis->titleColor());
++ else
++ ax.title->setColor(theme()->labelTextColor());
++ ax.title->setFont(ax.axis->titleFont());
++
++ // setFont()/setText() above must run before width()/height() are read below, so
++ // the position is computed from the title's up-to-date size, not last frame's.
++ const QRectF &yAxisRect = ax.labelsRect;
++ // The title is rotated -90 degrees around its center, so its footprint along X is
++ // its (pre-rotation) height, not its width.
++ const qreal offset = qMax(0.0, ax.title->height() - m_graph->m_defaultAxisTitleMargin)
++ * 0.5;
++ if (ax.axis->alignment() == Qt::AlignRight || ax.axis->alignment() == Qt::AlignBottom)
++ ax.title->setX(yAxisRect.x() + yAxisRect.width() - ax.title->width() * 0.5 + offset);
++ else
++ ax.title->setX(yAxisRect.x() - ax.title->width() * 0.5 - offset);
++
++ ax.title->setY((2 * yAxisRect.y() - ax.title->height() + yAxisRect.height()) * 0.5);
++ ax.title->setRotation(-90);
++ ax.title->setVisible(true);
++ } else {
++ ax.title->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateAxisLabelItems(QList<QQuickItem *> &textItems,
++ qsizetype neededSize, QQmlComponent *component)
++ {
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateAxisLabelItems, textItems.count(),
++ static_cast<int>(neededSize));
++ qsizetype currentTextItemsSize = textItems.size();
++ if (currentTextItemsSize < neededSize) {
++ for (qsizetype i = currentTextItemsSize; i <= neededSize; i++) {
++ QQuickItem *item = nullptr;
++ if (component) {
++ item = qobject_cast<QQuickItem *>(
++ component->create(component->creationContext()));
++ }
++ if (!item)
++ item = new QQuickText();
++ item->setParent(this);
++ item->setParentItem(this);
++ textItems << item;
++ }
++ } else if (neededSize < currentTextItemsSize) {
++ // Hide unused text items
++ for (qsizetype i = neededSize; i < currentTextItemsSize; i++) {
++ auto textItem = textItems[i];
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::setLabelTextProperties(QQuickItem *item, const QString &text, bool xAxis,
++ QQuickText::HAlignment hAlign, QQuickText::VAlignment vAlign,
++ Qt::TextElideMode elide)
++ {
++ if (auto textItem = qobject_cast<QQuickText *>(item)) {
++ // If the component is a Text item (default), then text
++ // properties can be set directly.
++ textItem->setText(text);
++ textItem->setHeight(textItem->contentHeight()); // Default height
++ textItem->setHAlign(hAlign);
++ textItem->setVAlign(vAlign);
++ if (xAxis) {
++ textItem->setFont(theme()->axisXLabelFont());
++ textItem->setColor(theme()->axisX().labelTextColor());
++ } else {
++ textItem->setFont(theme()->axisYLabelFont());
++ textItem->setColor(theme()->axisY().labelTextColor());
++ }
++
++ QQuickText::TextElideMode e;
++ switch (elide) {
++ case Qt::ElideLeft:
++ e = QQuickText::ElideLeft;
++ break;
++ case Qt::ElideRight:
++ e = QQuickText::ElideRight;
++ break;
++ case Qt::ElideMiddle:
++ e = QQuickText::ElideMiddle;
++ break;
++ default:
++ e = QQuickText::ElideNone;
++ }
++ textItem->setElideMode(e);
++
++ } else {
++ // Check for specific dynamic properties
++ if (item->property("text").isValid())
++ item->setProperty("text", text);
++ }
++ }
++
++ #if QT_CONFIG(graphs_2d_bar)
++ void AxisRenderer::updateBarXAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QBarCategoryAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ qsizetype categoriesCount = axis->categories().size();
++ // See if we need more text items
++ updateAxisLabelItems(ax.textItems, categoriesCount, axis->labelDelegate());
++
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateBarXAxisLabels);
++ int textIndex = 0;
++ auto categories = axis->categories();
++ for (const auto &category : std::as_const(categories)) {
++ auto &textItem = ax.textItems[textIndex];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posX = rect.x() + ((float)textIndex / categoriesCount) * rect.width();
++ if (axis->labelPosition() == QBarCategoryAxis::LabelPosition::OnValue)
++ posX += (1.0 / categoriesCount * rect.width() * 0.5);
++ textItem->setX(posX);
++ float posY = rect.y();
++ textItem->setY(posY);
++ textItem->setWidth(rect.width() / categoriesCount);
++ textItem->setRotation(axis->labelsAngle());
++ if (ax.axis->alignment() == Qt::AlignTop || ax.axis->alignment() == Qt::AlignLeft) {
++ setLabelTextProperties(textItem, category, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignBottom,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, category, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignTop,
++ axis->textElideMode());
++ }
++ textItem->setHeight(rect.height());
++ textItem->setVisible(true);
++ theme()->dirtyBits()->axisXDirty = false;
++ } else {
++ textItem->setVisible(false);
++ }
++ textIndex++;
++ }
++ }
++
++ void AxisRenderer::updateBarYAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QBarCategoryAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ qsizetype categoriesCount = axis->categories().size();
++ // See if we need more text items
++ updateAxisLabelItems(ax.textItems, categoriesCount, axis->labelDelegate());
++
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateBarYAxisLabels);
++ int textIndex = 0;
++ auto categories = axis->categories();
++ for (const auto &category : std::as_const(categories)) {
++ auto &textItem = ax.textItems[textIndex];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posX = rect.x();
++ textItem->setX(posX);
++ float posY = rect.y() + ((float)textIndex / categoriesCount) * rect.height();
++ if (axis->labelPosition() == QBarCategoryAxis::LabelPosition::OnValue)
++ posY -= (1.0 / categoriesCount * rect.height() * 0.5);
++ textItem->setY(posY);
++ textItem->setWidth(rect.width());
++ textItem->setRotation(axis->labelsAngle());
++ if (ax.axis->alignment() == Qt::AlignRight || ax.axis->alignment() == Qt::AlignBottom) {
++ setLabelTextProperties(textItem, category, false,
++ QQuickText::HAlignment::AlignLeft,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, category, false,
++ QQuickText::HAlignment::AlignRight,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ }
++ textItem->setHeight(rect.height() / categoriesCount);
++ textItem->setVisible(true);
++ theme()->dirtyBits()->axisYDirty = false;
++ } else {
++ textItem->setVisible(false);
++ }
++ textIndex++;
++ }
++ }
++ #endif
++
++ void AxisRenderer::updateValueYAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QValueAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ // Create label values in the range
++ QList<double> yAxisLabelValues;
++ const int MAX_LABELS_COUNT = 100;
++ if (m_graph->orientation() == Qt::Vertical) {
++ for (double i = ax.minLabel; i <= ax.maxValue; i += ax.valueStep) {
++ yAxisLabelValues << i;
++ if (yAxisLabelValues.size() >= MAX_LABELS_COUNT)
++ break;
++ }
++ } else {
++ for (double i = ax.maxValue; i >= ax.minLabel; i -= ax.valueStep) {
++ yAxisLabelValues << i;
++ if (yAxisLabelValues.size() >= MAX_LABELS_COUNT)
++ break;
++ }
++ }
++ qsizetype categoriesCount = yAxisLabelValues.size();
++
++ // See if we need more text items
++ updateAxisLabelItems(ax.textItems, categoriesCount, axis->labelDelegate());
++
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateValueYAxisLabels);
++ for (int i = 0; i < categoriesCount; i++) {
++ auto &textItem = ax.textItems[i];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posX = rect.x();
++ textItem->setX(posX);
++ float posY = rect.y() + rect.height() - (((float)i) * ax.stepPx) + ax.displacement;
++ const double titleMargin = 0.01;
++ if ((posY - titleMargin) > (rect.height() + rect.y()) || (posY + titleMargin) < rect.y()) {
++ // Hide text item which are outside the axis area
++ textItem->setVisible(false);
++ continue;
++ }
++
++ textItem->setY(posY);
++ textItem->setWidth(rect.width());
++ textItem->setRotation(axis->labelsAngle());
++ double number = yAxisLabelValues.at(i);
++ // Format the number
++ int decimals = axis->labelDecimals();
++ if (decimals < 0)
++ decimals = getValueDecimalsFromRange(ax.valueRange);
++ const QString f = axis->labelFormat();
++ QString label = formatValueLabel(
++ number, decimalFormatSpec(f, decimals, 'f'), QLatin1StringView("f"));
++ label = axis->labelPostFormat().arg(label);
++ if (ax.axis->alignment() == Qt::AlignRight || ax.axis->alignment() == Qt::AlignBottom) {
++ setLabelTextProperties(textItem, label, false,
++ QQuickText::HAlignment::AlignLeft,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, label, false,
++ QQuickText::HAlignment::AlignRight,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ }
++ textItem->setHeight(0);
++ textItem->setVisible(true);
++ theme()->dirtyBits()->axisYDirty = false;
++ } else {
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateValueXAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QValueAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ // Create label values in the range
++ QList<double> axisLabelValues;
++ const int MAX_LABELS_COUNT = 100;
++ for (double i = ax.minLabel; i <= ax.maxValue; i += ax.valueStep) {
++ axisLabelValues << i;
++ if (axisLabelValues.size() >= MAX_LABELS_COUNT)
++ break;
++ }
++ qsizetype categoriesCount = axisLabelValues.size();
++
++ // See if we need more text items
++ updateAxisLabelItems(ax.textItems, categoriesCount, axis->labelDelegate());
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateValueXAxisLabels);
++
++ for (int i = 0; i < categoriesCount; i++) {
++ auto &textItem = ax.textItems[i];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posY = rect.y();
++ textItem->setY(posY);
++ float textItemWidth = 20;
++ float posX = rect.x() + (((float)i) * ax.stepPx) - ax.displacement;
++ const double titleMargin = 0.01;
++ if ((posX - titleMargin) > (rect.width() + rect.x()) || (posX + titleMargin) < rect.x()) {
++ // Hide text item which are outside the axis area
++ textItem->setVisible(false);
++ continue;
++ }
++ // Take text size into account only after hiding
++ posX -= 0.5 * textItemWidth;
++
++ textItem->setX(posX);
++ textItem->setWidth(textItemWidth);
++ textItem->setRotation(axis->labelsAngle());
++
++ double number = axisLabelValues.at(i);
++ // Format the number
++ int decimals = axis->labelDecimals();
++ if (decimals < 0)
++ decimals = getValueDecimalsFromRange(ax.valueRange);
++ const QString f = axis->labelFormat();
++ QString label = formatValueLabel(
++ number, decimalFormatSpec(f, decimals, 'f'), QLatin1StringView("f"));
++ label = axis->labelPostFormat().arg(label);
++
++ if (ax.axis->alignment() == Qt::AlignTop || ax.axis->alignment() == Qt::AlignLeft) {
++ setLabelTextProperties(textItem, label, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignBottom,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, label, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignTop,
++ axis->textElideMode());
++ }
++ textItem->setHeight(rect.height());
++ textItem->setVisible(true);
++ theme()->dirtyBits()->axisXDirty = false;
++ } else {
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateDateTimeYAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QDateTimeAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ qint64 maxDate = ax.maxValue;
++ qint64 minDate = ax.minValue;
++ int dateTimeSize = ax.minLabel + 1;
++ qint64 segment = (maxDate - minDate)
++ / ax.minLabel;
++
++ qint64 anchor = (minDate / segment) * segment;
++
++ // See if we need more text items
++ updateAxisLabelItems(ax.textItems, dateTimeSize, axis->labelDelegate());
++
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateDateTimeYAxisLabels);
++ for (auto i = 0; i < dateTimeSize; ++i) {
++ auto &textItem = ax.textItems[i];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posX = rect.x();
++ textItem->setX(posX);
++ float posY = rect.y() + rect.height() - (((float) i) * ax.stepPx) + ax.displacement;
++ const double titleMargin = 0.01;
++ if ((posY - titleMargin) > (rect.height() + rect.y())
++ || (posY + titleMargin) < rect.y()) {
++ // Hide text item which are outside the axis area
++ textItem->setVisible(false);
++ continue;
++ }
++
++ textItem->setY(posY);
++ textItem->setWidth(rect.width());
++ textItem->setRotation(axis->labelsAngle());
++ auto date = QDateTime::fromMSecsSinceEpoch(anchor + (segment * i));
++ QString label = date.toString(axis->labelFormat());
++ if (ax.axis->alignment() == Qt::AlignRight || ax.axis->alignment() == Qt::AlignBottom) {
++ setLabelTextProperties(textItem, label, false,
++ QQuickText::HAlignment::AlignLeft,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, label, false,
++ QQuickText::HAlignment::AlignRight,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ }
++ textItem->setHeight(0);
++ textItem->setVisible(true);
++ } else {
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateDateTimeXAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QDateTimeAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ qint64 maxDate = ax.maxValue;
++ qint64 minDate = ax.minValue;
++ int dateTimeSize = ax.minLabel + 1;
++ qint64 segment = (maxDate - minDate)
++ / ax.minLabel;
++
++ //Avoid division through zero
++ if (segment == 0)
++ segment = 1;
++ qint64 anchor = (minDate / segment) * segment;
++
++ // See if we need more text items
++ updateAxisLabelItems(ax.textItems, dateTimeSize, axis->labelDelegate());
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateDateTimeXAxisLabels);
++
++ for (auto i = 0; i < dateTimeSize; ++i) {
++ auto &textItem = ax.textItems[i];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posY = rect.y();
++ textItem->setY(posY);
++ float textItemWidth = 20;
++ float posX = rect.x() + (((float) i) * ax.stepPx) - ax.displacement;
++ const double titleMargin = 0.01;
++ if ((posX - titleMargin) > (rect.width() + rect.x())
++ || (posX + titleMargin) < rect.x()) {
++ // Hide text item which are outside the axis area
++ textItem->setVisible(false);
++ continue;
++ }
++ // Take text size into account only after hiding
++ posX += - 0.5 * textItemWidth;
++ textItem->setX(posX);
++ textItem->setWidth(textItemWidth);
++ textItem->setRotation(axis->labelsAngle());
++ auto date = QDateTime::fromMSecsSinceEpoch(anchor + (segment * i));
++ QString label = date.toString(axis->labelFormat());
++ if (ax.axis->alignment() == Qt::AlignTop || ax.axis->alignment() == Qt::AlignLeft) {
++ setLabelTextProperties(textItem, label, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignBottom,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, label, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignTop,
++ axis->textElideMode());
++ }
++ textItem->setHeight(rect.height());
++ textItem->setVisible(true);
++ } else {
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateLogValueXAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QLogValueAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ QList<double> axisLabelValues;
++ const int MAX_LABEL_COUNT = 100;
++
++ for (double i = ax.minLabel; i <= ax.maxValue; i += ax.valueStep) {
++ float value = pow(axis->base(), i);
++ axisLabelValues << value;
++ if (axisLabelValues.size() >= MAX_LABEL_COUNT)
++ break;
++ }
++ qsizetype categoriesCount = axisLabelValues.size();
++
++ updateAxisLabelItems(ax.textItems, categoriesCount, axis->labelDelegate());
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateLogValueXAxisLabels);
++
++ for (int i = 0; i < categoriesCount; i++) {
++ auto &textItem = ax.textItems[i];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posY = rect.y();
++ textItem->setY(posY);
++ float textItemWidth = 20;
++ float posX = rect.x() + (((float)i) * ax.stepPx) - ax.displacement;
++ const double titleMargin = 0.01;
++ if ((posX - titleMargin) > (rect.width() + rect.x()) || (posX + titleMargin) < rect.x()) {
++ // Hide text item which are outside the axis area
++ textItem->setVisible(false);
++ continue;
++ }
++
++ // Take text size into account only after hiding
++ posX -= 0.5 * textItemWidth;
++
++ textItem->setX(posX);
++ textItem->setWidth(textItemWidth);
++ textItem->setRotation(axis->labelsAngle());
++
++ double number = axisLabelValues.at(i);
++ // Format the number
++ QString label = formatValueLabel(
++ number,
++ decimalFormatSpec(axis->labelFormat(), axis->labelPrecision(), 'g'),
++ QLatin1StringView("g"));
++ label = axis->labelPostFormat().arg(label);
++
++ if (ax.axis->alignment() == Qt::AlignTop || ax.axis->alignment() == Qt::AlignLeft) {
++ setLabelTextProperties(textItem, label, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignBottom,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, label, true,
++ QQuickText::HAlignment::AlignHCenter,
++ QQuickText::VAlignment::AlignTop,
++ axis->textElideMode());
++ }
++ textItem->setHeight(rect.height());
++ textItem->setVisible(true);
++ theme()->dirtyBits()->axisXDirty = false;
++ } else {
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::updateLogValueYAxisLabels(AxisProperties &ax, const QRectF rect)
++ {
++ auto axis = qobject_cast<QLogValueAxis *>(ax.axis);
++ if (!axis)
++ return;
++
++ // Create label values in the range
++ QList<double> yAxisLabelValues;
++ const int MAX_LABELS_COUNT = 100;
++ if (m_graph->orientation() == Qt::Vertical) {
++ for (double i = ax.minLabel; i <= ax.maxValue; i += ax.valueStep) {
++ float value = pow(axis->base(), i);
++ yAxisLabelValues << value;
++ if (yAxisLabelValues.size() >= MAX_LABELS_COUNT)
++ break;
++ }
++ } else {
++ for (double i = ax.maxValue; i >= ax.minLabel; i -= ax.valueStep) {
++ float value = pow(axis->base(), i);
++ yAxisLabelValues << value;
++ if (yAxisLabelValues.size() >= MAX_LABELS_COUNT)
++ break;
++ }
++ }
++ qsizetype categoriesCount = yAxisLabelValues.size();
++
++ // See if we need more text items
++ updateAxisLabelItems(ax.textItems, categoriesCount, axis->labelDelegate());
++
++ Q_TRACE_SCOPE(QGraphs2DAxisRendererUpdateLogValueYAxisLabels);
++ for (int i = 0; i < categoriesCount; i++) {
++ auto &textItem = ax.textItems[i];
++ if (axis->isVisible() && axis->labelsVisible()) {
++ float posX = rect.x();
++ textItem->setX(posX);
++ float posY = rect.y() + rect.height() - (((float)i) * ax.stepPx) + ax.displacement;
++ const double titleMargin = 0.01;
++ if ((posY - titleMargin) > (rect.height() + rect.y()) || (posY + titleMargin) < rect.y()) {
++ // Hide text item which are outside the axis area
++ textItem->setVisible(false);
++ continue;
++ }
++
++ textItem->setY(posY);
++ textItem->setWidth(rect.width());
++ textItem->setRotation(axis->labelsAngle());
++ double number = yAxisLabelValues.at(i);
++ // Format the number
++ QString label = formatValueLabel(
++ number,
++ decimalFormatSpec(axis->labelFormat(), axis->labelPrecision(), 'g'),
++ QLatin1StringView("g"));
++ label = axis->labelPostFormat().arg(label);
++ if (ax.axis->alignment() == Qt::AlignRight || ax.axis->alignment() == Qt::AlignBottom) {
++ setLabelTextProperties(textItem, label, false,
++ QQuickText::HAlignment::AlignLeft,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ } else {
++ setLabelTextProperties(textItem, label, false,
++ QQuickText::HAlignment::AlignRight,
++ QQuickText::VAlignment::AlignVCenter,
++ axis->textElideMode());
++ }
++ textItem->setHeight(0);
++ textItem->setVisible(true);
++ theme()->dirtyBits()->axisYDirty = false;
++ } else {
++ textItem->setVisible(false);
++ }
++ }
++ }
++
++ void AxisRenderer::createDragHandler()
++ {
++ m_dragHandler = new QQuickDragHandler(this);
++ m_dragHandler->setDragThreshold(10);
++ m_dragHandler->setTarget(nullptr);
++ connect(m_dragHandler,
++ &QQuickDragHandler::translationChanged,
++ this,
++ &AxisRenderer::onTranslationChanged);
++ connect(m_dragHandler, &QQuickDragHandler::grabChanged, this, &AxisRenderer::onGrabChanged);
++ }
++
++ void AxisRenderer::deleteDragHandler()
++ {
++ disconnect(m_dragHandler,
++ &QQuickDragHandler::translationChanged,
++ this,
++ &AxisRenderer::onTranslationChanged);
++ disconnect(m_dragHandler, &QQuickDragHandler::grabChanged, this, &AxisRenderer::onGrabChanged);
++ m_dragHandler->deleteLater();
++ m_dragHandler = nullptr;
++ }
++
++ QString AxisRenderer::formatValueLabel(double number,
++ const QString &format,
++ QLatin1StringView defaultFormat) const
++ {
++ return CommonUtils::formatNumber(number, format.isEmpty() ? defaultFormat : format);
++ }
++
++ // Calculate suitable major step based on range
++ double AxisRenderer::getValueStepsFromRange(double range) const
++ {
++ int digits = std::ceil(std::log10(range));
++ double r = std::pow(10.0, -digits);
++ r *= 10.0;
++ double v = std::ceil(range * r) / r;
++ double step = v * 0.1;
++ // Step must always be bigger than 0
++ step = qMax(0.0001, step);
++ return step;
++ }
++
++ // Calculate suitable decimals amount based on range
++ int AxisRenderer::getValueDecimalsFromRange(double range) const
++ {
++ if (range <= 0)
++ return 0;
++ int decimals = std::ceil(std::log10(10.0 / range));
++ // Decimals must always be at least 0
++ decimals = qMax(0, decimals);
++ return decimals;
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_axisrenderer_p.cpp"