Source code
Revision control
Copy as Markdown
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include <QtGraphs/qsplineseries.h>
#include <private/qgraphsview_p.h>
#include <private/qsplineseries_p.h>
#include <private/qxypoint_p.h>
#include <qtgraphs_tracepoints_p.h>
QT_BEGIN_NAMESPACE
Q_TRACE_PREFIX(qtgraphs,
"QT_BEGIN_NAMESPACE" \
"class QSplineSeries;" \
"QT_END_NAMESPACE"
)
Q_TRACE_POINT(qtgraphs, QGraphs2DSplineSeriesCalculateControlPoints_entry, int pointCount);
Q_TRACE_POINT(qtgraphs, QGraphs2DSplineSeriesCalculateControlPoints_exit);
Q_TRACE_POINT(qtgraphs, QGraphs2DSplineSeriesCalculateSplinePoints_entry, int controlPointCount);
Q_TRACE_POINT(qtgraphs, QGraphs2DSplineSeriesCalculateSplinePoints_exit);
/*!
\class QSplineSeries
\inmodule QtGraphs
\ingroup graphs_2D
\brief QSplineSeries presents data in spline graphs.
The graph displays smooth spline segments that moves through all the points
defined in the graph.
*/
/*!
\qmltype SplineSeries
\nativetype QSplineSeries
\inqmlmodule QtGraphs
\ingroup graphs_qml_2D
\inherits XYSeries
\brief SplineSeries presents data in spline graphs.
The graph displays smooth spline segments that moves through all the points
defined in the graph.
\image graphs2d-spline.png {Spline graph with smooth curve through data points}
*/
/*!
\property QSplineSeries::width
The width of the spline line in pixels. The default value is 2.0.
*/
/*!
\qmlproperty real SplineSeries::width
The width of the line. By default, the width is 2.0.
*/
/*!
\property QSplineSeries::capStyle
Controls the cap style of the spline line. The value is one of Qt::PenCapStyle enum values.
The default value is Qt::SquareCap.
\sa Qt::PenCapStyle
*/
/*!
\qmlproperty Qt::PenCapStyle SplineSeries::capStyle
Controls the cap style of the line. Set to one of \l{Qt::FlatCap}{Qt.FlatCap},
\l{Qt::SquareCap}{Qt.SquareCap} or \l{Qt::RoundCap}{Qt.RoundCap}. By
default the cap style is Qt.SquareCap.
\sa Qt::PenCapStyle
*/
/*!
\qmlproperty bool SplineSeries::optimized
\since 6.11
Sets the control point fitting of the spline. When enabled a Catmull-Rom implementation is used.
By default optimized is set to false.
*/
/*!
\qmlsignal SplineSeries::widthChanged()
This signal is emitted when the spline series width changes.
*/
/*!
\qmlsignal SplineSeries::capStyleChanged()
This signal is emitted when the spline series cap style changes.
*/
/*!
\qmlsignal SplineSeries::optimizedChanged()
\since 6.11
This signal is emitted when the spline series' optimized property is set.
*/
QSplineSeries::QSplineSeries(QObject *parent)
: QXYSeries(*(new QSplineSeriesPrivate()), parent)
{}
QSplineSeries::~QSplineSeries() {}
QSplineSeries::QSplineSeries(QSplineSeriesPrivate &dd, QObject *parent)
: QXYSeries(dd, parent)
{}
void QSplineSeries::componentComplete()
{
Q_D(QSplineSeries);
for (auto *child : children()) {
if (auto point = qobject_cast<QXYPoint *>(child)) {
append(point->x(), point->y());
qCDebug(lcSeries2D, "append points x: %f y: %f to splineSeries",
point->x(),
point->y());
}
}
d->calculateSplinePoints();
if (d->m_graphTransition)
d->m_graphTransition->initialize();
connect(this, &QSplineSeries::pointAdded, this, [d]([[maybe_unused]] int index) {
d->calculateSplinePoints();
});
connect(this, &QSplineSeries::pointsAdded, this,
[d]([[maybe_unused]] qsizetype start, [[maybe_unused]] qsizetype end) {
d->calculateSplinePoints();
});
connect(this, &QSplineSeries::pointRemoved, this, [d]([[maybe_unused]] int index) {
d->calculateSplinePoints();
});
connect(this, &QSplineSeries::pointsRemoved, this
, [d]([[maybe_unused]] int index, [[maybe_unused]] int count) {
d->calculateSplinePoints();
});
connect(this, &QSplineSeries::pointReplaced, this, [d]([[maybe_unused]] int index) {
d->calculateSplinePoints();
});
connect(this, &QSplineSeries::pointsReplaced, this, [d]() { d->calculateSplinePoints(); });
qCDebug(lcEvents2D, "QSplineSeries::componentComplete.");
QAbstractSeries::componentComplete();
}
QAbstractSeries::SeriesType QSplineSeries::type() const
{
return QAbstractSeries::SeriesType::Spline;
}
QList<QPointF> &QSplineSeries::getControlPoints()
{
Q_D(QSplineSeries);
return d->m_controlPoints;
}
qreal QSplineSeries::width() const
{
Q_D(const QSplineSeries);
return d->m_width;
}
void QSplineSeries::setWidth(qreal newWidth)
{
Q_D(QSplineSeries);
if (newWidth < 0) {
qCWarning(lcProperties2D, "QSplineSeries::setWidth. Tried to use invalid width,"
"width has been automatically set to 0");
newWidth = 0;
}
if (qFuzzyCompare(d->m_width + 1, newWidth + 1)) {
qCDebug(lcProperties2D, "QSplineSeries::setWidth. Width is already set to: %f",
newWidth);
return;
}
d->m_width = newWidth;
emit widthChanged();
emit update();
}
Qt::PenCapStyle QSplineSeries::capStyle() const
{
Q_D(const QSplineSeries);
return d->m_capStyle;
}
void QSplineSeries::setCapStyle(Qt::PenCapStyle newCapStyle)
{
Q_D(QSplineSeries);
if (d->m_capStyle == newCapStyle) {
qCDebug(lcProperties2D) << "QSplineSeries::setCapStyle. CapStyle is already set to:"
<< newCapStyle;
return;
}
d->m_capStyle = newCapStyle;
emit capStyleChanged();
emit update();
}
bool QSplineSeries::isOptimized() const
{
Q_D(const QSplineSeries);
return d->m_optimized;
}
void QSplineSeries::setOptimized(bool optimized)
{
Q_D(QSplineSeries);
if (d->m_optimized == optimized) {
qCDebug(lcProperties2D) << "QSplineSeries::setOptimized. Optimize is already set to:"
<< optimized;
return;
}
d->m_optimized = optimized;
d->calculateSplinePoints();
emit optimizedChanged(optimized);
emit update();
}
QSplineSeriesPrivate::QSplineSeriesPrivate()
: QXYSeriesPrivate(QAbstractSeries::SeriesType::Spline)
, m_width(1.0)
, m_capStyle(Qt::PenCapStyle::SquareCap)
, m_controlPoints()
, m_optimized(false)
{}
void QSplineSeriesPrivate::calculateSplinePoints()
{
if (m_points.size() == 0) {
m_controlPoints.clear();
qCWarning(lcSeries2D, "points list size is 0, can't calculate spline points.");
return;
} else if (m_points.size() == 1) {
qCWarning(lcSeries2D, "points list size is 1, can't calculate spline points.");
m_controlPoints = {m_points[0], m_points[0]};
return;
}
if (m_optimized) {
m_controlPoints = calculateCatmullRomPoints(m_points);
return;
}
QList<QPointF> controlPoints;
controlPoints.resize(m_points.size() * 2 - 2);
qsizetype n = m_points.size() - 1;
Q_TRACE_SCOPE(QGraphs2DSplineSeriesCalculateSplinePoints, n);
if (n == 1) {
//for n==1
controlPoints[0].setX((2 * m_points[0].x() + m_points[1].x()) / 3);
controlPoints[0].setY((2 * m_points[0].y() + m_points[1].y()) / 3);
controlPoints[1].setX(2 * controlPoints[0].x() - m_points[0].x());
controlPoints[1].setY(2 * controlPoints[0].y() - m_points[0].y());
m_controlPoints = controlPoints;
}
// Calculate first Bezier control points
// Set of equations for P0 to Pn points.
//
// | 2 1 0 0 ... 0 0 0 ... 0 0 0 | | P1_1 | | P1 + 2 * P0 |
// | 1 4 1 0 ... 0 0 0 ... 0 0 0 | | P1_2 | | 4 * P1 + 2 * P2 |
// | 0 1 4 1 ... 0 0 0 ... 0 0 0 | | P1_3 | | 4 * P2 + 2 * P3 |
// | . . . . . . . . . . . . | | ... | | ... |
// | 0 0 0 0 ... 1 4 1 ... 0 0 0 | * | P1_i | = | 4 * P(i-1) + 2 * Pi |
// | . . . . . . . . . . . . | | ... | | ... |
// | 0 0 0 0 0 0 0 0 ... 1 4 1 | | P1_(n-1)| | 4 * P(n-2) + 2 * P(n-1) |
// | 0 0 0 0 0 0 0 0 ... 0 2 7 | | P1_n | | 8 * P(n-1) + Pn |
//
QList<QPointF> list;
list.resize(n);
list[0].setX(m_points[0].x() + 2 * m_points[1].x());
list[0].setY(m_points[0].y() + 2 * m_points[1].y());
for (int i = 1; i < n - 1; ++i) {
list[i].setX(4 * m_points[i].x() + 2 * m_points[i + 1].x());
list[i].setY(4 * m_points[i].y() + 2 * m_points[i + 1].y());
}
list[n - 1].setX((8 * m_points[n - 1].x() + m_points[n].x()) / 2.0);
list[n - 1].setY((8 * m_points[n - 1].y() + m_points[n].y()) / 2.0);
const QList<QPointF> control = calculateControlPoints(list);
for (int i = 0, j = 0; i < n; ++i, ++j) {
controlPoints[j].setX(control[i].x());
controlPoints[j].setY(control[i].y());
j++;
if (i < n - 1) {
controlPoints[j].setX(2 * m_points[i + 1].x() - control[i + 1].x());
controlPoints[j].setY(2 * m_points[i + 1].y() - control[i + 1].y());
} else {
controlPoints[j].setX((m_points[n].x() + control[n - 1].x()) / 2);
controlPoints[j].setY((m_points[n].y() + control[n - 1].y()) / 2);
}
}
m_controlPoints = controlPoints;
}
QList<QPointF> QSplineSeriesPrivate::calculateControlPoints(const QList<QPointF> &list)
{
QList<QPointF> result;
qsizetype count = list.size();
Q_TRACE_SCOPE(QGraphs2DSplineSeriesCalculateControlPoints, count);
result.resize(count);
result[0].setX(list[0].x() / 2.0);
result[0].setY(list[0].y() / 2.0);
QList<QPointF> temp;
temp.resize(count);
temp[0] = {0, 0};
qreal bx = 2.0;
qreal by = 2.0;
for (int i = 1; i < count; i++) {
temp[i].setX(1 / bx);
bx = (i < count - 1 ? 4.0 : 3.5) - temp[i].x();
result[i].setX((list[i].x() - result[i - 1].x()) / bx);
temp[i].setY(1 / by);
by = (i < count - 1 ? 4.0 : 3.5) - temp[i].y();
result[i].setY((list[i].y() - result[i - 1].y()) / by);
}
for (int i = 1; i < count; i++) {
result[count - i - 1].setX(
result[count - i - 1].x() - temp[count - i].x() * result[count - i].x());
result[count - i - 1].setY(
result[count - i - 1].y() - temp[count - i].y() * result[count - i].y());
}
return result;
}
// Converting Catmull-Rom to Bezier
QList<QPointF> QSplineSeriesPrivate::calculateCatmullRomPoints(const QList<QPointF> &list)
{
const qreal tension = 0.5f;
QList<QPointF> result;
result.resize(list.size() * 2 - 2);
auto mirror1x = list[0].x() - (list[1].x() - list[0].x());
auto mirror1y = list[0].y() - (list[1].y() - list[0].y());
auto mirror2x = list[list.size() - 1].x()
- (list[list.size() - 2].x() - list[list.size() - 1].x());
auto mirror2y = list[list.size() - 1].y()
- (list[list.size() - 2].y() - list[list.size() - 1].y());
for (auto i = 0, j = 0; i < list.size() - 1; i++, j += 2) {
auto p0 = i == 0 ? QPointF{mirror1x, mirror1y} : list[i - 1];
auto p1 = list[i];
auto p2 = list[i + 1];
auto p3 = i < list.size() - 2 ? list[i + 2] : QPointF{mirror2x, mirror2y};
auto c1 = p1 + (p2 - p0) / (6 * tension);
auto c2 = p2 - (p3 - p1) / (6 * tension);
result[j] = c1;
result[j + 1] = c2;
}
return result;
}
QT_END_NAMESPACE
#include "moc_qsplineseries.cpp"