diff options
author | Owais Akhtar <owais.akhtar@qt.io> | 2024-04-16 15:07:38 +0300 |
---|---|---|
committer | Owais Akhtar <owais.akhtar@qt.io> | 2024-05-03 15:14:11 +0300 |
commit | 688da7b80ecc7ad2a72833bee6f505c519dc70e7 (patch) | |
tree | 6b3c16ee0fae79c9f3c52e4a714f81c58a7bc114 | |
parent | 27beaeaa8acb6cc007f08fc069409f006094ad62 (diff) |
Implementation for DateTimeAxis
Task-number: QTBUG-121633
Change-Id: Ief34227fa2c94c4d9ffae2a21e35c9a994c4a360
Reviewed-by: Tomi Korpipää <tomi.korpipaa@qt.io>
-rw-r--r-- | examples/graphs/2d/testbed/CMakeLists.txt | 1 | ||||
-rw-r--r-- | examples/graphs/2d/testbed/qml/testbed/DateTimeAxis.qml | 156 | ||||
-rw-r--r-- | examples/graphs/2d/testbed/qml/testbed/StartupView.qml | 4 | ||||
-rw-r--r-- | src/graphs2d/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/graphs2d/axis/datetimeaxis/qdatetimeaxis.cpp | 291 | ||||
-rw-r--r-- | src/graphs2d/axis/datetimeaxis/qdatetimeaxis.h | 65 | ||||
-rw-r--r-- | src/graphs2d/axis/datetimeaxis/qdatetimeaxis_p.h | 51 | ||||
-rw-r--r-- | src/graphs2d/axis/qabstractaxis.cpp | 1 | ||||
-rw-r--r-- | src/graphs2d/axis/qabstractaxis.h | 6 | ||||
-rw-r--r-- | src/graphs2d/qsgrenderer/axisrenderer.cpp | 170 | ||||
-rw-r--r-- | src/graphs2d/qsgrenderer/axisrenderer_p.h | 3 | ||||
-rw-r--r-- | src/graphs2d/qsgrenderer/pointrenderer.cpp | 58 | ||||
-rw-r--r-- | tests/auto/cpp2dtest/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/cpp2dtest/qgaxis-datetime/CMakeLists.txt | 13 | ||||
-rw-r--r-- | tests/auto/cpp2dtest/qgaxis-datetime/tst_datetimeaxis.cpp | 93 | ||||
-rw-r--r-- | tests/auto/qml2dtest/axes/tst_datetimeaxis.qml | 137 |
16 files changed, 1028 insertions, 23 deletions
diff --git a/examples/graphs/2d/testbed/CMakeLists.txt b/examples/graphs/2d/testbed/CMakeLists.txt index 2dd4d5c..a8d87d4 100644 --- a/examples/graphs/2d/testbed/CMakeLists.txt +++ b/examples/graphs/2d/testbed/CMakeLists.txt @@ -68,6 +68,7 @@ qt6_add_qml_module(testbed qml/testbed/AreaSeries.qml qml/testbed/DynamicSeries.qml qml/testbed/Donut.qml + qml/testbed/DateTimeAxis.qml RESOURCES qml/testbed/images/arrow_icon.png qml/testbed/images/icon_settings.png diff --git a/examples/graphs/2d/testbed/qml/testbed/DateTimeAxis.qml b/examples/graphs/2d/testbed/qml/testbed/DateTimeAxis.qml new file mode 100644 index 0000000..c99b0ed --- /dev/null +++ b/examples/graphs/2d/testbed/qml/testbed/DateTimeAxis.qml @@ -0,0 +1,156 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtGraphs +import QtQuick.Dialogs +import QtQuick.Controls.Basic +import QtQuick.Layouts + +Rectangle { + id: mainView + width: 800 + height: 600 + color: "#202020" + + RowLayout { + id:bar + height: 100 + + Text { + Layout.leftMargin: 20 + font.pixelSize: 24 + color: "#ffffff" + text: "X:" + } + + Slider { + id: sliderX + + value: (new Date(1950,1,1)).getTime() + from: (new Date(1900,1,1)).getTime() + to: (new Date(2000,1,1)).getTime() + } + + Text { + font.pixelSize: 24 + color: "#ffffff" + text: "Y:" + } + + Slider { + id: sliderY + + value: (new Date(1950,1,1)).getTime() + from: (new Date(1900,1,1)).getTime() + to: (new Date(2000,1,1)).getTime() + } + + Text { + Layout.leftMargin: 20 + font.pixelSize: 24 + color: "#ffffff" + text: "X Ticks:" + } + + SpinBox { + onValueChanged: xAxis.tickInterval = value + } + + Text { + Layout.leftMargin: 20 + font.pixelSize: 24 + color: "#ffffff" + text: "X Format:" + } + TextField { + placeholderText: "MMMM-yyyy" + onAccepted: xAxis.labelFormat = text + } + } + + GraphsView { + id: chartView + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.top: bar.bottom + anchors.margins: 10 + backgroundColor: "#010101" + theme: gtheme + + GraphsTheme { + id: gtheme + axisYLabelFont.pixelSize: 8 + colorScheme: Qt.Dark + theme: GraphsTheme.Theme.QtGreen + } + + axisX: DateTimeAxis { + id: xAxis + minorTickCount: 2 + labelsAngle: 45 + labelFormat: "MMMM-yyyy" + tickInterval: 0 + min: new Date(1930,12,31) + max: new Date(sliderX.value) + } + + axisY: DateTimeAxis { + id: yAxis + minorTickCount: 2 + labelsAngle: 45 + labelFormat: "MMMM-yyyy" + tickInterval: 10 + min: new Date(1930,12,31) + max: new Date(sliderY.value) + } + + ToolTip { + id: tooltip + } + + onHoverEnter: { + tooltip.visible = true; + } + + onHoverExit: { + tooltip.visible = false; + } + + onHover: (seriesName, position, value) => { + tooltip.x = position.x + 1; + tooltip.y = position.y + 1; + tooltip.text = new Date(value.x).toString(); + } + + LineSeries { + id: line + theme: gtheme + width: 8 + hoverable: true + + XYPoint { x: new Date(1910, 2, 15);y: new Date(1900, 12, 31) } + XYPoint { x: new Date(1915, 2, 15);y: new Date(1910, 12, 31) } + XYPoint { x: new Date(1920, 1, 1); y: new Date(1920, 12, 31) } + XYPoint { x: new Date(1930, 12, 31); y: new Date(1960, 12, 31) } + XYPoint { x: new Date(1940, 7, 1); y: new Date(1940, 12, 31) } + XYPoint { x: new Date(1950, 8, 2); y: new Date(1950, 12, 31) } + XYPoint { x: new Date(1960, 8, 2); y: new Date(1960, 12, 31) } + } + + SplineSeries { + id: spline + theme: gtheme + width: 4 + + XYPoint { x: new Date(1910, 7, 1); y: new Date(1940, 12, 31) } + XYPoint { x: new Date(1920, 8, 2); y: new Date(1950, 12, 31) } + XYPoint { x: new Date(1930, 8, 2); y: new Date(1960, 12, 31) } + XYPoint { x: new Date(1940, 2, 15);y: new Date(1900, 12, 31) } + XYPoint { x: new Date(1955, 2, 15);y: new Date(1910, 12, 31) } + XYPoint { x: new Date(1960, 1, 1); y: new Date(1920, 12, 31) } + XYPoint { x: new Date(1970, 12, 31); y: new Date(1960, 12, 31) } + } + } +} diff --git a/examples/graphs/2d/testbed/qml/testbed/StartupView.qml b/examples/graphs/2d/testbed/qml/testbed/StartupView.qml index 60798eb..605b29b 100644 --- a/examples/graphs/2d/testbed/qml/testbed/StartupView.qml +++ b/examples/graphs/2d/testbed/qml/testbed/StartupView.qml @@ -81,6 +81,10 @@ Item { name: "BarChangingSetCount" file: "BarChangingSetCount.qml" } + ListElement { + name: "DateTime Axis" + file: "DateTimeAxis.qml" + } } Component { diff --git a/src/graphs2d/CMakeLists.txt b/src/graphs2d/CMakeLists.txt index 5377213..397eba9 100644 --- a/src/graphs2d/CMakeLists.txt +++ b/src/graphs2d/CMakeLists.txt @@ -33,6 +33,7 @@ qt_internal_extend_target(Graphs animation/qgraphpointanimation.cpp animation/qgraphpointanimation_p.h animation/qgraphtransition_p.h animation/qgraphtransition.cpp animation/qxyseriesanimation_p.h animation/qxyseriesanimation.cpp + axis/datetimeaxis/qdatetimeaxis.h axis/datetimeaxis/qdatetimeaxis.cpp axis/datetimeaxis/qdatetimeaxis_p.h ) add_subdirectory(qml/designer) diff --git a/src/graphs2d/axis/datetimeaxis/qdatetimeaxis.cpp b/src/graphs2d/axis/datetimeaxis/qdatetimeaxis.cpp new file mode 100644 index 0000000..2fdca2d --- /dev/null +++ b/src/graphs2d/axis/datetimeaxis/qdatetimeaxis.cpp @@ -0,0 +1,291 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtCore/QObject> +#include <QtGraphs/QDateTimeAxis> +#include "private/qdatetimeaxis_p.h" + +QT_BEGIN_NAMESPACE +/*! + \class QDateTimeAxis + \inmodule QtGraphs + \ingroup graphs_2D + \brief The QDateTimeAxis adds support for DateTime values to be added to a graph's axis. + + A DateTime Axis can be used to display DateTime representations with tick marks and grid lines. + The DateTime items on the axis are displayed at the position of the ticks. +*/ + +/*! + \qmltype DateTimeAxis + \instantiates QDateTimeAxis + \inqmlmodule QtGraphs + \ingroup graphs_qml_2D + \inherits AbstractAxis + \brief Adds DateTime items to a graph's axis. + + A DateTime Axis can be used to display DateTime representations with tick marks and grid lines. + The DateTime items on the axis are displayed at the position of the ticks. + + The following example code illustrates how to use the DateTimeAxis type: + \code + GraphsView { + LineSeries { + axisX: DateTimeAxis { + min: new Date(2000,1,1) + max: new Date(1970,1,1) + } + + // Add a few XYPoint data... + } + } + \endcode +*/ + +/*! + \property QDateTimeAxis::min + \brief The minimum value on the axis + + This value can be lower or higher than the maximum. + The default value is new Date(1970,1,1) +*/ +/*! + \qmlproperty real DateTimeAxis::min + The minimum value on the axis. + + This value can be lower or higher than the maximum. + The default value is new Date(1970,1,1) +*/ + +/*! + \property QDateTimeAxis::max + \brief The maximum value on the axis + + This value can be lower or higher than the minimum. + The default value is new Date(1980,1,1) +*/ +/*! + \qmlproperty real DateTimeAxis::max + The maximum value on the axis. + + This value can be lower or higher than the minimum. + The default value is new Date(1980,1,1) +*/ +/*! + \property QDateTimeAxis::minorTickCount + \brief The number of minor tick marks on the axis. This indicates how many grid lines are drawn + between major ticks on the graph. Labels are not drawn for minor ticks. The default value is 0. +*/ +/*! + \qmlproperty int DateTimeAxis::minorTickCount + The number of minor tick marks on the axis. This indicates how many grid lines are drawn + between major ticks on the graph. Labels are not drawn for minor ticks. The default value is 0. +*/ +/*! + \property QDateTimeAxis::tickInterval + \brief The interval between dynamically placed tick marks and labels. + The default value is 0, which means that intervals are automatically calculated + based on the min and max range. +*/ +/*! + \qmlproperty real DateTimeAxis::tickInterval + The interval between dynamically placed tick marks and labels. + The default value is 0, which means that intervals are automatically calculated + based on the min and max range. +*/ + +/*! + \property QDateTimeAxis::labelFormat + \brief The format of the DateTime labels on the axis. + The format property allows to signify the visual representation of the DateTime object, in days, + months, and years. The default value is dd-MMMM-yy. +*/ + +/*! + \qmlproperty string DateTimeAxis::labelFormat + The format of the DateTime labels on the axis + The format property allows to signify the visual representation of the DateTime object, in days, + months, and years. The default value is dd-MMMM-yy. + */ +/*! + \qmlsignal DateTimeAxis::minChanged(DateTime min) + This signal is emitted when the minimum value of the axis, specified by \a min, changes. +*/ +/*! + \qmlsignal DateTimeAxis::maxChanged(DateTime max) + This signal is emitted when the maximum value of the axis, specified by \a max, changes. +*/ +/*! + \qmlsignal DateTimeAxis::minorTickCountChanged(int minorTickCount) + This signal is emitted when the number of minor tick marks on the axis, specified by + \a minorTickCount, changes. +*/ +/*! + \qmlsignal DateTimeAxis::rangeChanged(real min, real max) + This signal is emitted when the minimum or maximum value of the axis, specified by \a min + and \a max, changes. +*/ +/*! + \qmlsignal DateTimeAxis::labelFormatChanged(string format) + This signal is emitted when the \a format of axis labels changes. +*/ +/*! + \qmlsignal DateTimeAxis::tickIntervalChanged(real tickInterval) + This signal is emitted when the tick interval value, specified by + \a tickInterval, changes. +*/ + +QDateTimeAxis::QDateTimeAxis(QObject *parent) + : QAbstractAxis(*(new QDateTimeAxisPrivate), parent) +{} + +QDateTimeAxis::~QDateTimeAxis() +{ + Q_D(QDateTimeAxis); + if (d->m_graph) + d->m_graph->removeAxis(this); +} + +QAbstractAxis::AxisType QDateTimeAxis::type() const +{ + return QAbstractAxis::AxisType::DateTime; +} + +void QDateTimeAxis::setMin(QDateTime min) +{ + Q_D(QDateTimeAxis); + if (min.isValid()) { + d->setRange(min.toMSecsSinceEpoch(), d->m_max); + emit minChanged(QDateTime::fromMSecsSinceEpoch(d->m_min)); + emit update(); + } +} + +QDateTime QDateTimeAxis::min() const +{ + Q_D(const QDateTimeAxis); + return QDateTime::fromMSecsSinceEpoch(d->m_min); +} + +void QDateTimeAxis::setMax(QDateTime max) +{ + Q_D(QDateTimeAxis); + if (max.isValid()) { + d->setRange(d->m_min, max.toMSecsSinceEpoch()); + emit maxChanged(QDateTime::fromMSecsSinceEpoch(d->m_max)); + emit update(); + } +} + +QDateTime QDateTimeAxis::max() const +{ + Q_D(const QDateTimeAxis); + return QDateTime::fromMSecsSinceEpoch(d->m_max); +} + +void QDateTimeAxis::setLabelFormat(QString format) +{ + Q_D(QDateTimeAxis); + if (d->m_format != format) { + d->m_format = format; + emit labelFormatChanged(format); + emit update(); + } +} + +QString QDateTimeAxis::labelFormat() const +{ + Q_D(const QDateTimeAxis); + return d->m_format; +} + +qreal QDateTimeAxis::tickInterval() const +{ + Q_D(const QDateTimeAxis); + return d->m_tickInterval; +} + +void QDateTimeAxis::setTickInterval(qreal newTickInterval) +{ + Q_D(QDateTimeAxis); + + if (newTickInterval < 0.0) + newTickInterval = 0.0; + + if (qFuzzyCompare(d->m_tickInterval, newTickInterval)) + return; + d->m_tickInterval = newTickInterval; + emit tickIntervalChanged(); + emit update(); +} + +int QDateTimeAxis::minorTickCount() const +{ + Q_D(const QDateTimeAxis); + return d->m_minorTickCount; +} + +void QDateTimeAxis::setMinorTickCount(int newMinorTickCount) +{ + Q_D(QDateTimeAxis); + + if (newMinorTickCount < 0.0) + newMinorTickCount = 0.0; + + if (d->m_minorTickCount == newMinorTickCount) + return; + d->m_minorTickCount = newMinorTickCount; + emit minorTickCountChanged(); + emit update(); +} + +/////////////////////////////////////////////////////////////////////////////// + +QDateTimeAxisPrivate::QDateTimeAxisPrivate() {} + +QDateTimeAxisPrivate::~QDateTimeAxisPrivate() {} + +void QDateTimeAxisPrivate::setMin(const QVariant &min) +{ + Q_Q(QDateTimeAxis); + if (min.canConvert<QDateTime>()) + q->setMin(min.toDateTime()); +} + +void QDateTimeAxisPrivate::setMax(const QVariant &max) +{ + Q_Q(QDateTimeAxis); + if (max.canConvert<QDateTime>()) + q->setMax(max.toDateTime()); +} + +void QDateTimeAxisPrivate::setRange(const QVariant &min, const QVariant &max) +{ + Q_Q(QDateTimeAxis); + if (min.canConvert<QDateTime>() && max.canConvert<QDateTime>()) + q->setRange(min.toDateTime(), max.toDateTime()); +} + +void QDateTimeAxisPrivate::setRange(qreal min, qreal max) +{ + Q_Q(QDateTimeAxis); + + bool changed = false; + + if (m_min != min) { + m_min = min; + changed = true; + emit q->minChanged(QDateTime::fromMSecsSinceEpoch(min)); + } + + if (m_max != max) { + m_max = max; + changed = true; + emit q->maxChanged(QDateTime::fromMSecsSinceEpoch(max)); + } + + if (changed) + emit q->rangeChanged(min, max); +} + +QT_END_NAMESPACE diff --git a/src/graphs2d/axis/datetimeaxis/qdatetimeaxis.h b/src/graphs2d/axis/datetimeaxis/qdatetimeaxis.h new file mode 100644 index 0000000..d1a1abb --- /dev/null +++ b/src/graphs2d/axis/datetimeaxis/qdatetimeaxis.h @@ -0,0 +1,65 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QDATETIMEAXIS_H +#define QDATETIMEAXIS_H + +#include <QtGraphs/QAbstractAxis> + +QT_BEGIN_NAMESPACE + +class QDateTimeAxisPrivate; + +class Q_GRAPHS_EXPORT QDateTimeAxis : public QAbstractAxis +{ + Q_OBJECT + Q_PROPERTY(QDateTime min READ min WRITE setMin NOTIFY minChanged FINAL) + Q_PROPERTY(QDateTime max READ max WRITE setMax NOTIFY maxChanged FINAL) + Q_PROPERTY( + QString labelFormat READ labelFormat WRITE setLabelFormat NOTIFY labelFormatChanged FINAL) + Q_PROPERTY( + int minorTickCount READ minorTickCount WRITE setMinorTickCount NOTIFY minorTickCountChanged) + Q_PROPERTY( + qreal tickInterval READ tickInterval WRITE setTickInterval NOTIFY tickIntervalChanged FINAL) + QML_NAMED_ELEMENT(DateTimeAxis) + +public: + explicit QDateTimeAxis(QObject *parent = nullptr); + ~QDateTimeAxis() override; + +protected: + QDateTimeAxis(QDateTimeAxisPrivate &d, QObject *parent = nullptr); + +public: + AxisType type() const override; + + //range handling + void setMin(QDateTime min); + QDateTime min() const; + void setMax(QDateTime max); + QDateTime max() const; + + void setLabelFormat(QString format); + QString labelFormat() const; + + qreal tickInterval() const; + void setTickInterval(qreal newTickInterval); + + int minorTickCount() const; + void setMinorTickCount(int newMinorTickCount); + +Q_SIGNALS: + void minChanged(QDateTime min); + void maxChanged(QDateTime max); + void labelFormatChanged(QString format); + void tickIntervalChanged(); + void minorTickCountChanged(); + +private: + Q_DECLARE_PRIVATE(QDateTimeAxis) + Q_DISABLE_COPY(QDateTimeAxis) +}; + +QT_END_NAMESPACE + +#endif // QDATETIMEAXIS_H diff --git a/src/graphs2d/axis/datetimeaxis/qdatetimeaxis_p.h b/src/graphs2d/axis/datetimeaxis/qdatetimeaxis_p.h new file mode 100644 index 0000000..0f87e69 --- /dev/null +++ b/src/graphs2d/axis/datetimeaxis/qdatetimeaxis_p.h @@ -0,0 +1,51 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +// W A R N I N G +// ------------- +// +// This file is not part of the QtGraphs API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef QDATETIMEAXIS_P_H +#define QDATETIMEAXIS_P_H + +#include <QtGraphs/QDateTimeAxis> +#include <private/qabstractaxis_p.h> + +QT_BEGIN_NAMESPACE + +class QDateTimeAxisPrivate : public QAbstractAxisPrivate +{ +public: + QDateTimeAxisPrivate(); + ~QDateTimeAxisPrivate(); + +protected: + qreal m_min = QDateTime(QDate(1970, 1, 1), QTime::fromMSecsSinceStartOfDay(0)) + .toMSecsSinceEpoch(); + qreal m_max = QDateTime(QDate(1970, 1, 1), QTime::fromMSecsSinceStartOfDay(0)) + .addYears(10) + .toMSecsSinceEpoch(); + qreal m_tickInterval = 0.0; + int m_minorTickCount = 0; + QString m_format = QStringLiteral("dd-MMMM-yy"); + +public: + void setMin(const QVariant &min) override; + void setMax(const QVariant &max) override; + void setRange(const QVariant &min, const QVariant &max) override; + void setRange(qreal min, qreal max) override; + qreal min() override { return m_min; } + qreal max() override { return m_max; } + +private: + Q_DECLARE_PUBLIC(QDateTimeAxis) +}; + +QT_END_NAMESPACE + +#endif // QDATETIMEAXIS_P_H diff --git a/src/graphs2d/axis/qabstractaxis.cpp b/src/graphs2d/axis/qabstractaxis.cpp index a27cdbe..835b71a 100644 --- a/src/graphs2d/axis/qabstractaxis.cpp +++ b/src/graphs2d/axis/qabstractaxis.cpp @@ -39,6 +39,7 @@ QT_BEGIN_NAMESPACE \value Value \value BarCategory + \value DateTime */ /*! diff --git a/src/graphs2d/axis/qabstractaxis.h b/src/graphs2d/axis/qabstractaxis.h index c9da4be..319eb65 100644 --- a/src/graphs2d/axis/qabstractaxis.h +++ b/src/graphs2d/axis/qabstractaxis.h @@ -43,11 +43,7 @@ class Q_GRAPHS_EXPORT QAbstractAxis : public QObject Q_DECLARE_PRIVATE(QAbstractAxis) public: - - enum class AxisType { - Value, - BarCategory - }; + enum class AxisType { Value, BarCategory, DateTime }; Q_ENUM(AxisType) protected: diff --git a/src/graphs2d/qsgrenderer/axisrenderer.cpp b/src/graphs2d/qsgrenderer/axisrenderer.cpp index 1326418..8a93c5c 100644 --- a/src/graphs2d/qsgrenderer/axisrenderer.cpp +++ b/src/graphs2d/qsgrenderer/axisrenderer.cpp @@ -1,11 +1,13 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include <QtGraphs/QBarCategoryAxis> +#include <QtGraphs/QGraphsTheme> #include <private/axisrenderer_p.h> #include <private/qabstractaxis_p.h> #include <private/qbarseries_p.h> +#include <private/qdatetimeaxis_p.h> #include <private/qgraphsview_p.h> -#include <QtGraphs/QBarCategoryAxis> #include <private/qvalueaxis_p.h> QT_BEGIN_NAMESPACE @@ -194,7 +196,6 @@ void AxisRenderer::updateAxis() m_axisVerticalValueStep = step; int axisVerticalMinorTickCount = vaxis->minorTickCount(); m_axisVerticalMinorTickScale = axisVerticalMinorTickCount > 0 ? 1.0 / (axisVerticalMinorTickCount + 1) : 1.0; - m_axisVerticalStepPx = (height() - m_graph->m_marginTop - m_graph->m_marginBottom - m_axisHeight) / (m_axisVerticalValueRange / m_axisVerticalValueStep); double axisVerticalValueDiff = m_axisVerticalMinLabel - m_axisVerticalMinValue; m_axisYMovement = -(axisVerticalValueDiff / m_axisVerticalValueStep) * m_axisVerticalStepPx; @@ -257,6 +258,80 @@ void AxisRenderer::updateAxis() updateBarYAxisLabels(vaxis, yAxisRect); } + if (auto vaxis = qobject_cast<QDateTimeAxis *>(m_axisVertical)) { + // Todo: make constant for all axis, or clamp in class? (QTBUG-124736) + const double MAX_DIVS = 100.0; + + double interval = std::clamp(vaxis->tickInterval(), 0.0, MAX_DIVS); + m_axisVerticalMaxValue = vaxis->max().toMSecsSinceEpoch(); + m_axisVerticalMinValue = vaxis->min().toMSecsSinceEpoch(); + m_axisVerticalValueRange = std::abs(m_axisVerticalMaxValue - m_axisVerticalMinValue); + + // in ms + double segment; + if (interval <= 0) { + segment = getValueStepsFromRange(m_axisVerticalValueRange); + interval = m_axisVerticalValueRange / segment; + } else { + segment = m_axisVerticalValueRange / interval; + } + + m_axisVerticalMinLabel = std::clamp(interval, 1.0, MAX_DIVS); + + m_axisVerticalValueStep = segment; + int axisVerticalMinorTickCount = vaxis->minorTickCount(); + m_axisVerticalMinorTickScale = axisVerticalMinorTickCount > 0 + ? 1.0 / (axisVerticalMinorTickCount + 1) + : 1.0; + m_axisVerticalStepPx = (height() - m_graph->m_marginTop - m_graph->m_marginBottom + - m_axisHeight) + / (qFuzzyCompare(segment, 0) + ? interval + : (m_axisVerticalValueRange / m_axisVerticalValueStep)); + + float rightMargin = 20; + yAxisRect = {m_graph->m_marginLeft, m_graph->m_marginTop, m_axisWidth - rightMargin, h}; + updateDateTimeYAxisLabels(vaxis, yAxisRect); + } + + if (auto haxis = qobject_cast<QDateTimeAxis *>(m_axisHorizontal)) { + const double MAX_DIVS = 100.0; + + double interval = std::clamp(haxis->tickInterval(), 0.0, MAX_DIVS); + m_axisHorizontalMaxValue = haxis->max().toMSecsSinceEpoch(); + m_axisHorizontalMinValue = haxis->min().toMSecsSinceEpoch(); + m_axisHorizontalValueRange = std::abs(m_axisHorizontalMaxValue - m_axisHorizontalMinValue); + + // in ms + double segment; + if (interval <= 0) { + segment = getValueStepsFromRange(m_axisHorizontalValueRange); + interval = m_axisHorizontalValueRange / segment; + } else { + segment = m_axisHorizontalValueRange / interval; + } + + m_axisHorizontalMinLabel = std::clamp(interval, 1.0, MAX_DIVS); + + m_axisHorizontalValueStep = segment; + int axisHorizontalMinorTickCount = haxis->minorTickCount(); + m_axisHorizontalMinorTickScale = axisHorizontalMinorTickCount > 0 + ? 1.0 / (axisHorizontalMinorTickCount + 1) + : 1.0; + m_axisHorizontalStepPx = (width() - m_graph->m_marginLeft - m_graph->m_marginRight + - m_axisWidth) + / (qFuzzyCompare(segment, 0) + ? interval + : (m_axisHorizontalValueRange / m_axisHorizontalValueStep)); + + float topMargin = 20; + xAxisRect = {m_graph->m_marginLeft + m_axisWidth, + m_graph->m_marginTop + h - m_graph->m_marginBottom + topMargin, + w, + m_axisHeight}; + updateDateTimeXAxisLabels(haxis, xAxisRect); + } + updateAxisTickers(); updateAxisTickersShadow(); updateAxisGrid(); @@ -706,6 +781,97 @@ void AxisRenderer::updateValueXAxisLabels(QValueAxis *axis, const QRectF &rect) } } +void AxisRenderer::updateDateTimeYAxisLabels(QDateTimeAxis *axis, const QRectF &rect) +{ + auto maxDate = axis->max(); + auto minDate = axis->min(); + int dateTimeSize = m_axisVerticalMinLabel + 1; + auto segment = (maxDate.toMSecsSinceEpoch() - minDate.toMSecsSinceEpoch()) + / m_axisVerticalMinLabel; + + // See if we need more text items + updateAxisLabelItems(m_yAxisTextItems, dateTimeSize); + + for (auto i = 0; i < dateTimeSize; ++i) { + auto &textItem = m_yAxisTextItems[i]; + if (axis->isVisible() && axis->labelsVisible()) { + // TODO: Not general, fix vertical align to work in all cases + float fontSize = theme()->axisYLabelFont().pixelSize() < 0 + ? theme()->axisYLabelFont().pointSize() + : theme()->axisYLabelFont().pixelSize(); + float posX = rect.x(); + textItem->setX(posX); + float posY = rect.y() + rect.height() - (((float) i) * m_axisVerticalStepPx); + 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; + } + // Take font size into account only after hiding + posY -= fontSize; + textItem->setY(posY); + textItem->setHAlign(QQuickText::HAlignment::AlignHCenter); + textItem->setVAlign(QQuickText::VAlignment::AlignVCenter); + textItem->setHAlign(QQuickText::HAlignment::AlignRight); + textItem->setVAlign(QQuickText::VAlignment::AlignBottom); + textItem->setWidth(rect.width()); + textItem->setHeight(textItem->contentHeight()); + textItem->setFont(theme()->axisYLabelFont()); + textItem->setColor(theme()->axisYLabelColor()); + textItem->setRotation(axis->labelsAngle()); + textItem->setText(minDate.addMSecs(segment * i).toString(axis->labelFormat())); + textItem->setVisible(true); + } else { + textItem->setVisible(false); + } + } +} + +void AxisRenderer::updateDateTimeXAxisLabels(QDateTimeAxis *axis, const QRectF &rect) +{ + auto maxDate = axis->max(); + auto minDate = axis->min(); + int dateTimeSize = m_axisHorizontalMinLabel + 1; + auto segment = (maxDate.toMSecsSinceEpoch() - minDate.toMSecsSinceEpoch()) + / m_axisHorizontalMinLabel; + + // See if we need more text items + updateAxisLabelItems(m_xAxisTextItems, dateTimeSize); + + for (auto i = 0; i < dateTimeSize; ++i) { + auto &textItem = m_xAxisTextItems[i]; + if (axis->isVisible() && axis->labelsVisible()) { + float posY = rect.y(); + textItem->setY(posY); + float textItemWidth = 20; + float posX = rect.x() + (((float) i) * m_axisHorizontalStepPx); + 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->setHAlign(QQuickText::HAlignment::AlignHCenter); + textItem->setVAlign(QQuickText::VAlignment::AlignBottom); + textItem->setWidth(textItemWidth); + textItem->setHeight(rect.height()); + textItem->setFont(theme()->axisYLabelFont()); + textItem->setColor(theme()->axisYLabelColor()); + textItem->setRotation(axis->labelsAngle()); + textItem->setText(minDate.addMSecs(segment * i).toString(axis->labelFormat())); + textItem->setVisible(true); + } else { + textItem->setVisible(false); + } + } +} + // Calculate suitable major step based on range double AxisRenderer::getValueStepsFromRange(double range) { diff --git a/src/graphs2d/qsgrenderer/axisrenderer_p.h b/src/graphs2d/qsgrenderer/axisrenderer_p.h index eb29848..e7b5e96 100644 --- a/src/graphs2d/qsgrenderer/axisrenderer_p.h +++ b/src/graphs2d/qsgrenderer/axisrenderer_p.h @@ -30,6 +30,7 @@ class QGraphsView; class QBarCategoryAxis; class QValueAxis; class QGraphsTheme; +class QDateTimeAxis; class AxisRenderer : public QQuickItem { @@ -49,6 +50,8 @@ public: void updateBarYAxisLabels(QBarCategoryAxis *axis, const QRectF &rect); void updateValueYAxisLabels(QValueAxis *axis, const QRectF &rect); void updateValueXAxisLabels(QValueAxis *axis, const QRectF &rect); + void updateDateTimeYAxisLabels(QDateTimeAxis *axis, const QRectF &rect); + void updateDateTimeXAxisLabels(QDateTimeAxis *axis, const QRectF &rect); void initialize(); Q_SIGNALS: diff --git a/src/graphs2d/qsgrenderer/pointrenderer.cpp b/src/graphs2d/qsgrenderer/pointrenderer.cpp index 9dd0147..42f5eb3 100644 --- a/src/graphs2d/qsgrenderer/pointrenderer.cpp +++ b/src/graphs2d/qsgrenderer/pointrenderer.cpp @@ -53,9 +53,15 @@ qreal PointRenderer::defaultSize(QXYSeries *series) void PointRenderer::calculateRenderCoordinates( AxisRenderer *axisRenderer, qreal origX, qreal origY, qreal *renderX, qreal *renderY) { + auto flipX = axisRenderer->m_axisHorizontalMaxValue < axisRenderer->m_axisHorizontalMinValue + ? -1 + : 1; + auto flipY = axisRenderer->m_axisVerticalMaxValue < axisRenderer->m_axisVerticalMinValue ? -1 + : 1; + *renderX = m_graph->m_marginLeft + axisRenderer->m_axisWidth - + m_areaWidth * origX * m_maxHorizontal - m_horizontalOffset; - *renderY = m_graph->m_marginTop + m_areaHeight - m_areaHeight * origY * m_maxVertical + + m_areaWidth * flipX * origX * m_maxHorizontal - m_horizontalOffset; + *renderY = m_graph->m_marginTop + m_areaHeight - m_areaHeight * flipY * origY * m_maxVertical + m_verticalOffset; } @@ -291,12 +297,20 @@ void PointRenderer::handlePolish(QXYSeries *series) m_maxHorizontal = m_graph->m_axisRenderer->m_axisHorizontalValueRange > 0 ? 1.0 / m_graph->m_axisRenderer->m_axisHorizontalValueRange : 100.0; - m_verticalOffset = (m_graph->m_axisRenderer->m_axisVerticalMinValue - / m_graph->m_axisRenderer->m_axisVerticalValueRange) - * m_areaHeight; - m_horizontalOffset = (m_graph->m_axisRenderer->m_axisHorizontalMinValue - / m_graph->m_axisRenderer->m_axisHorizontalValueRange) - * m_areaWidth; + + auto vmin = m_graph->m_axisRenderer->m_axisVerticalMinValue + > m_graph->m_axisRenderer->m_axisVerticalMaxValue + ? std::abs(m_graph->m_axisRenderer->m_axisVerticalMinValue) + : m_graph->m_axisRenderer->m_axisVerticalMinValue; + + m_verticalOffset = (vmin / m_graph->m_axisRenderer->m_axisVerticalValueRange) * m_areaHeight; + + auto hmin = m_graph->m_axisRenderer->m_axisHorizontalMinValue + > m_graph->m_axisRenderer->m_axisHorizontalMaxValue + ? std::abs(m_graph->m_axisRenderer->m_axisHorizontalMinValue) + : m_graph->m_axisRenderer->m_axisHorizontalMinValue; + + m_horizontalOffset = (hmin / m_graph->m_axisRenderer->m_axisHorizontalValueRange) * m_areaWidth; if (!m_groups.contains(series)) { PointGroup *group = new PointGroup(); @@ -447,6 +461,12 @@ bool PointRenderer::handleHoverMove(QHoverEvent *event) if (!group->series->hoverable()) continue; + auto axisRenderer = group->series->graph()->m_axisRenderer; + bool isHNegative = axisRenderer->m_axisHorizontalMaxValue + < axisRenderer->m_axisHorizontalMinValue; + bool isVNegative = axisRenderer->m_axisVerticalMaxValue + < axisRenderer->m_axisVerticalMinValue; + if (group->series->type() == QAbstractSeries::SeriesType::Scatter) { const QString &name = group->series->name(); @@ -479,23 +499,26 @@ bool PointRenderer::handleHoverMove(QHoverEvent *event) if (points.size() >= 2) { bool hovering = false; - for (int i = 0; i < points.size() - 1; i++) { qreal x1, y1, x2, y2; if (i == 0) { - x1 = group->shapePath->startX(); + auto curve = qobject_cast<QQuickCurve *>(group->paths[0]); + + x1 = isHNegative ? curve->x() : group->shapePath->startX(); y1 = group->shapePath->startY(); - auto curve = qobject_cast<QQuickCurve *>(group->paths[0]); - x2 = curve->x(); + x2 = isHNegative ? group->shapePath->startX() : curve->x(); y2 = curve->y(); } else { - auto curve1 = qobject_cast<QQuickCurve *>(group->paths[i - 1]); + bool n = isVNegative | isHNegative; + + auto curve1 = qobject_cast<QQuickCurve *>(group->paths[n ? i : i - 1]); + auto curve2 = qobject_cast<QQuickCurve *>(group->paths[n ? i - 1 : i]); + x1 = curve1->x(); y1 = curve1->y(); - auto curve2 = qobject_cast<QQuickCurve *>(group->paths[i]); x2 = curve2->x(); y2 = curve2->y(); } @@ -504,6 +527,7 @@ bool PointRenderer::handleHoverMove(QHoverEvent *event) if (denominator > 0) { qreal hoverDistance = qAbs((x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1)) / qSqrt(denominator); + if (hoverDistance < hoverSize) { qreal alpha = 0; qreal extrapolation = 0; @@ -520,8 +544,10 @@ bool PointRenderer::handleHoverMove(QHoverEvent *event) } if (alpha >= -extrapolation && alpha <= 1.0 + extrapolation) { - const QPointF &point1 = points[i]; - const QPointF &point2 = points[i + 1]; + bool n = isVNegative | isHNegative; + + const QPointF &point1 = points[n ? i + 1 : i]; + const QPointF &point2 = points[n ? i : i + 1]; QPointF point = (point2 * (1.0 - alpha)) + (point1 * alpha); diff --git a/tests/auto/cpp2dtest/CMakeLists.txt b/tests/auto/cpp2dtest/CMakeLists.txt index 5372b0f..1fa0bea 100644 --- a/tests/auto/cpp2dtest/CMakeLists.txt +++ b/tests/auto/cpp2dtest/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(qgaxis-abstract) add_subdirectory(qgaxis-barcategory) add_subdirectory(qgaxis-value) +add_subdirectory(qgaxis-datetime) add_subdirectory(qgbars) add_subdirectory(qgbars-set) add_subdirectory(qglines) diff --git a/tests/auto/cpp2dtest/qgaxis-datetime/CMakeLists.txt b/tests/auto/cpp2dtest/qgaxis-datetime/CMakeLists.txt new file mode 100644 index 0000000..cc40c94 --- /dev/null +++ b/tests/auto/cpp2dtest/qgaxis-datetime/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qgdatetimeaxis2d + SOURCES + tst_datetimeaxis.cpp + INCLUDE_DIRECTORIES + ../common + LIBRARIES + Qt::Gui + Qt::GuiPrivate + Qt::Graphs +) diff --git a/tests/auto/cpp2dtest/qgaxis-datetime/tst_datetimeaxis.cpp b/tests/auto/cpp2dtest/qgaxis-datetime/tst_datetimeaxis.cpp new file mode 100644 index 0000000..75dac68 --- /dev/null +++ b/tests/auto/cpp2dtest/qgaxis-datetime/tst_datetimeaxis.cpp @@ -0,0 +1,93 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtGraphs/QDateTimeAxis> +#include <QtTest/QtTest> +#include "qtestcase.h" + +class tst_datetimeaxis : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + + void construct(); + + void initialProperties(); + void initializeProperties(); + void invalidProperties(); + +private: + QDateTimeAxis *m_axis; +}; + +void tst_datetimeaxis::initTestCase() {} + +void tst_datetimeaxis::cleanupTestCase() {} + +void tst_datetimeaxis::init() +{ + m_axis = new QDateTimeAxis(); +} + +void tst_datetimeaxis::cleanup() +{ + delete m_axis; +} + +void tst_datetimeaxis::construct() +{ + QDateTimeAxis *axis = new QDateTimeAxis(); + QVERIFY(axis); + delete axis; +} + +void tst_datetimeaxis::initialProperties() +{ + QVERIFY(m_axis); + + QCOMPARE(m_axis->min(), QDateTime(QDate(1970, 1, 1), QTime::fromMSecsSinceStartOfDay(0))); + QCOMPARE(m_axis->max(), + QDateTime(QDate(1970, 1, 1), QTime::fromMSecsSinceStartOfDay(0)).addYears(10)); + QCOMPARE(m_axis->labelFormat(), "dd-MMMM-yy"); + QCOMPARE(m_axis->minorTickCount(), 0); + QCOMPARE(m_axis->tickInterval(), 0.0); +} + +void tst_datetimeaxis::initializeProperties() +{ + QVERIFY(m_axis); + + m_axis->setMin(QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0))); + m_axis->setMax(QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0)).addYears(20)); + m_axis->setLabelFormat("yyyy"); + m_axis->setMinorTickCount(2); + m_axis->setTickInterval(0.5); + + QCOMPARE(m_axis->min(), QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0))); + QCOMPARE(m_axis->max(), + QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0)).addYears(20)); + QCOMPARE(m_axis->labelFormat(), "yyyy"); + QCOMPARE(m_axis->minorTickCount(), 2); + QCOMPARE(m_axis->tickInterval(), 0.5); +} + +void tst_datetimeaxis::invalidProperties() +{ + QVERIFY(m_axis); + + m_axis->setMin(QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0)).addDays(10)); + m_axis->setMax(QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0))); + m_axis->setMinorTickCount(-1); + + QCOMPARE(m_axis->min(), QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0))); + QCOMPARE(m_axis->max(), QDateTime(QDate::currentDate(), QTime::fromMSecsSinceStartOfDay(0))); + QCOMPARE(m_axis->minorTickCount(), 0); +} + +QTEST_MAIN(tst_datetimeaxis) +#include "tst_datetimeaxis.moc" diff --git a/tests/auto/qml2dtest/axes/tst_datetimeaxis.qml b/tests/auto/qml2dtest/axes/tst_datetimeaxis.qml new file mode 100644 index 0000000..1bff858 --- /dev/null +++ b/tests/auto/qml2dtest/axes/tst_datetimeaxis.qml @@ -0,0 +1,137 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtGraphs +import QtTest + +Item { + id: top + height: 150 + width: 150 + + DateTimeAxis { + id: initial + } + + DateTimeAxis { + id: initialized + + labelFormat: "yyyy" + min: new Date(1960,1,1) + max: new Date(2000,1,1) + minorTickCount: 2 + tickInterval: 2 + + gridVisible: false + labelsAngle: 90 + labelsVisible: false + lineVisible: false + minorGridVisible: false + visible: false + } + + TestCase { + name: "DateTimeAxis Initial" + + function test_1_initial() { + compare(initial.labelFormat, "dd-MMMM-yy") + compare(initial.min, new Date(1970,0,1)) + compare(initial.max, new Date(1980,0,1)) + compare(initial.minorTickCount, 0) + compare(initial.tickInterval, 0) + } + + function test_2_initial_common() { + compare(initial.gridVisible, true) + compare(initial.labelsAngle, 0) + compare(initial.labelsVisible, true) + compare(initial.lineVisible, true) + compare(initial.minorGridVisible, true) + compare(initial.visible, true) + } + + function test_3_initial_change() { + initial.labelFormat = "yyyy" + initial.min = new Date(1960, 1, 1) + initial.max = new Date(2000, 1, 1) + initial.minorTickCount = 2 + initial.tickInterval = 2 + + initial.gridVisible = false + initial.labelsAngle = 90 + initial.labelsVisible = false + initial.lineVisible = false + initial.minorGridVisible = false + initial.visible = false + + compare(initial.labelFormat, "yyyy") + compare(initial.min, new Date(1960, 1, 1)) + compare(initial.max, new Date(2000, 1, 1)) + compare(initial.minorTickCount, 2) + compare(initial.tickInterval, 2) + + compare(initial.gridVisible, false) + compare(initial.labelsAngle, 90) + compare(initial.labelsVisible, false) + compare(initial.lineVisible, false) + compare(initial.minorGridVisible, false) + compare(initial.visible, false) + } + } + + TestCase { + name: "DateTimeAxis Initialized" + + function test_1_initialized() { + compare(initialized.labelFormat, "yyyy") + compare(initialized.min, new Date(1960, 1, 1)) + compare(initialized.max, new Date(2000, 1, 1)) + compare(initialized.minorTickCount, 2) + compare(initialized.tickInterval, 2) + + compare(initialized.gridVisible, false) + compare(initialized.labelsAngle, 90) + compare(initialized.labelsVisible, false) + compare(initialized.lineVisible, false) + compare(initialized.minorGridVisible, false) + compare(initialized.visible, false) + } + + function test_2_initialized_changed() { + initialized.labelFormat = "dddd" + initialized.min = new Date(2000, 1, 1) + initialized.max = new Date(2025, 1, 1) + initialized.minorTickCount = 8 + initialized.tickInterval = 8 + + initialized.gridVisible = true + initialized.labelsAngle = 50 + initialized.labelsVisible = true + initialized.lineVisible = true + initialized.minorGridVisible = true + initialized.visible = true + + compare(initialized.labelFormat, "dddd") + compare(initialized.min, new Date(2000, 1, 1)) + compare(initialized.max, new Date(2025, 1, 1)) + compare(initialized.minorTickCount, 8) + compare(initialized.tickInterval, 8) + + compare(initialized.gridVisible, true) + compare(initialized.labelsAngle, 50) + compare(initialized.labelsVisible, true) + compare(initialized.lineVisible, true) + compare(initialized.minorGridVisible, true) + compare(initialized.visible, true) + } + + function test_3_invalid() { + initialized.minorTickCount = -1; + initialized.tickInterval = -1; + + compare(initialized.tickInterval, 0) + compare(initialized.minorTickCount, 0) + } + } +} |