Revision control

Copy as Markdown

// 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"