1 | // Copyright (C) 2024 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include <QtGraphs/qsplineseries.h> |
5 | #include <private/qgraphsview_p.h> |
6 | #include <private/qsplineseries_p.h> |
7 | #include <private/qxypoint_p.h> |
8 | |
9 | QT_BEGIN_NAMESPACE |
10 | |
11 | /*! |
12 | \class QSplineSeries |
13 | \inmodule QtGraphs |
14 | \ingroup graphs_2D |
15 | \brief QSplineSeries presents data in spline graphs. |
16 | |
17 | The graph displays smooth spline segments that moves through all the points |
18 | defined in the graph. |
19 | */ |
20 | /*! |
21 | \qmltype SplineSeries |
22 | \nativetype QSplineSeries |
23 | \inqmlmodule QtGraphs |
24 | \ingroup graphs_qml_2D |
25 | \inherits QXYSeries |
26 | |
27 | \brief SplineSeries presents data in spline graphs. |
28 | |
29 | The graph displays smooth spline segments that moves through all the points |
30 | defined in the graph. |
31 | |
32 | \image graphs2d-spline.png |
33 | */ |
34 | |
35 | /*! |
36 | \qmlproperty real SplineSeries::width |
37 | The width of the line. By default, the width is 2.0. |
38 | */ |
39 | |
40 | /*! |
41 | \qmlproperty Qt::PenCapStyle SplineSeries::capStyle |
42 | Controls the cap style of the line. Set to one of \l{Qt::FlatCap}{Qt.FlatCap}, |
43 | \l{Qt::SquareCap}{Qt.SquareCap} or \l{Qt::RoundCap}{Qt.RoundCap}. By |
44 | default the cap style is Qt.SquareCap. |
45 | |
46 | \sa Qt::PenCapStyle |
47 | */ |
48 | |
49 | /*! |
50 | \qmlsignal SplineSeries::widthChanged() |
51 | This signal is emitted when the spline series width changes. |
52 | */ |
53 | |
54 | /*! |
55 | \qmlsignal SplineSeries::capStyleChanged() |
56 | This signal is emitted when the spline series cap style changes. |
57 | */ |
58 | |
59 | QSplineSeries::QSplineSeries(QObject *parent) |
60 | : QXYSeries(*(new QSplineSeriesPrivate()), parent) |
61 | {} |
62 | |
63 | QSplineSeries::~QSplineSeries() {} |
64 | |
65 | QSplineSeries::QSplineSeries(QSplineSeriesPrivate &dd, QObject *parent) |
66 | : QXYSeries(dd, parent) |
67 | {} |
68 | |
69 | void QSplineSeries::componentComplete() |
70 | { |
71 | Q_D(QSplineSeries); |
72 | |
73 | for (auto *child : children()) { |
74 | if (auto point = qobject_cast<QXYPoint *>(object: child)) |
75 | append(x: point->x(), y: point->y()); |
76 | } |
77 | |
78 | d->calculateSplinePoints(); |
79 | |
80 | if (d->m_graphTransition) |
81 | d->m_graphTransition->initialize(); |
82 | |
83 | connect(sender: this, signal: &QSplineSeries::pointAdded, context: this, slot: [d]([[maybe_unused]] int index) { |
84 | d->calculateSplinePoints(); |
85 | }); |
86 | |
87 | connect(sender: this, signal: &QSplineSeries::pointRemoved, context: this, slot: [d]([[maybe_unused]] int index) { |
88 | d->calculateSplinePoints(); |
89 | }); |
90 | |
91 | connect(sender: this, signal: &QSplineSeries::pointReplaced, context: this, slot: [d]([[maybe_unused]] int index) { |
92 | d->calculateSplinePoints(); |
93 | }); |
94 | |
95 | connect(sender: this, signal: &QSplineSeries::pointsReplaced, context: this, slot: [d]() { d->calculateSplinePoints(); }); |
96 | |
97 | QAbstractSeries::componentComplete(); |
98 | } |
99 | |
100 | QAbstractSeries::SeriesType QSplineSeries::type() const |
101 | { |
102 | return QAbstractSeries::SeriesType::Spline; |
103 | } |
104 | |
105 | QList<QPointF> &QSplineSeries::getControlPoints() |
106 | { |
107 | Q_D(QSplineSeries); |
108 | return d->m_controlPoints; |
109 | } |
110 | |
111 | qreal QSplineSeries::width() const |
112 | { |
113 | Q_D(const QSplineSeries); |
114 | return d->m_width; |
115 | } |
116 | |
117 | void QSplineSeries::setWidth(qreal newWidth) |
118 | { |
119 | Q_D(QSplineSeries); |
120 | |
121 | if (newWidth < 0) |
122 | newWidth = 0; |
123 | |
124 | if (qFuzzyCompare(p1: d->m_width, p2: newWidth)) |
125 | return; |
126 | d->m_width = newWidth; |
127 | emit widthChanged(); |
128 | emit update(); |
129 | } |
130 | |
131 | Qt::PenCapStyle QSplineSeries::capStyle() const |
132 | { |
133 | Q_D(const QSplineSeries); |
134 | return d->m_capStyle; |
135 | } |
136 | |
137 | void QSplineSeries::setCapStyle(Qt::PenCapStyle newCapStyle) |
138 | { |
139 | Q_D(QSplineSeries); |
140 | if (d->m_capStyle == newCapStyle) |
141 | return; |
142 | d->m_capStyle = newCapStyle; |
143 | emit capStyleChanged(); |
144 | emit update(); |
145 | } |
146 | |
147 | QSplineSeriesPrivate::QSplineSeriesPrivate() |
148 | : QXYSeriesPrivate() |
149 | , m_width(1.0) |
150 | , m_capStyle(Qt::PenCapStyle::SquareCap) |
151 | , m_controlPoints() |
152 | {} |
153 | |
154 | void QSplineSeriesPrivate::calculateSplinePoints() |
155 | { |
156 | if (m_points.size() == 0) { |
157 | m_controlPoints.clear(); |
158 | return; |
159 | } else if (m_points.size() == 1) { |
160 | m_controlPoints = {m_points[0], m_points[0]}; |
161 | return; |
162 | } |
163 | |
164 | QList<QPointF> controlPoints; |
165 | controlPoints.resize(size: m_points.size() * 2 - 2); |
166 | |
167 | qsizetype n = m_points.size() - 1; |
168 | |
169 | if (n == 1) { |
170 | //for n==1 |
171 | controlPoints[0].setX((2 * m_points[0].x() + m_points[1].x()) / 3); |
172 | controlPoints[0].setY((2 * m_points[0].y() + m_points[1].y()) / 3); |
173 | controlPoints[1].setX(2 * controlPoints[0].x() - m_points[0].x()); |
174 | controlPoints[1].setY(2 * controlPoints[0].y() - m_points[0].y()); |
175 | m_controlPoints = controlPoints; |
176 | } |
177 | |
178 | // Calculate first Bezier control points |
179 | // Set of equations for P0 to Pn points. |
180 | // |
181 | // | 2 1 0 0 ... 0 0 0 ... 0 0 0 | | P1_1 | | P1 + 2 * P0 | |
182 | // | 1 4 1 0 ... 0 0 0 ... 0 0 0 | | P1_2 | | 4 * P1 + 2 * P2 | |
183 | // | 0 1 4 1 ... 0 0 0 ... 0 0 0 | | P1_3 | | 4 * P2 + 2 * P3 | |
184 | // | . . . . . . . . . . . . | | ... | | ... | |
185 | // | 0 0 0 0 ... 1 4 1 ... 0 0 0 | * | P1_i | = | 4 * P(i-1) + 2 * Pi | |
186 | // | . . . . . . . . . . . . | | ... | | ... | |
187 | // | 0 0 0 0 0 0 0 0 ... 1 4 1 | | P1_(n-1)| | 4 * P(n-2) + 2 * P(n-1) | |
188 | // | 0 0 0 0 0 0 0 0 ... 0 2 7 | | P1_n | | 8 * P(n-1) + Pn | |
189 | // |
190 | QList<qreal> list; |
191 | list.resize(size: n); |
192 | |
193 | list[0] = m_points[0].x() + 2 * m_points[1].x(); |
194 | |
195 | for (int i = 1; i < n - 1; ++i) |
196 | list[i] = 4 * m_points[i].x() + 2 * m_points[i + 1].x(); |
197 | |
198 | list[n - 1] = (8 * m_points[n - 1].x() + m_points[n].x()) / 2.0; |
199 | |
200 | const QList<qreal> xControl = calculateControlPoints(list); |
201 | |
202 | list[0] = m_points[0].y() + 2 * m_points[1].y(); |
203 | |
204 | for (int i = 1; i < n - 1; ++i) |
205 | list[i] = 4 * m_points[i].y() + 2 * m_points[i + 1].y(); |
206 | |
207 | list[n - 1] = (8 * m_points[n - 1].y() + m_points[n].y()) / 2.0; |
208 | |
209 | const QList<qreal> yControl = calculateControlPoints(list); |
210 | |
211 | for (int i = 0, j = 0; i < n; ++i, ++j) { |
212 | controlPoints[j].setX(xControl[i]); |
213 | controlPoints[j].setY(yControl[i]); |
214 | |
215 | j++; |
216 | |
217 | if (i < n - 1) { |
218 | controlPoints[j].setX(2 * m_points[i + 1].x() - xControl[i + 1]); |
219 | controlPoints[j].setY(2 * m_points[i + 1].y() - yControl[i + 1]); |
220 | } else { |
221 | controlPoints[j].setX((m_points[n].x() + xControl[n - 1]) / 2); |
222 | controlPoints[j].setY((m_points[n].y() + yControl[n - 1]) / 2); |
223 | } |
224 | } |
225 | |
226 | m_controlPoints = controlPoints; |
227 | } |
228 | |
229 | QList<qreal> QSplineSeriesPrivate::calculateControlPoints(const QList<qreal> &list) |
230 | { |
231 | QList<qreal> result; |
232 | |
233 | qsizetype count = list.size(); |
234 | result.resize(size: count); |
235 | result[0] = list[0] / 2.0; |
236 | |
237 | QList<qreal> temp; |
238 | temp.resize(size: count); |
239 | temp[0] = 0; |
240 | |
241 | qreal b = 2.0; |
242 | |
243 | for (int i = 1; i < count; i++) { |
244 | temp[i] = 1 / b; |
245 | b = (i < count - 1 ? 4.0 : 3.5) - temp[i]; |
246 | result[i] = (list[i] - result[i - 1]) / b; |
247 | } |
248 | |
249 | for (int i = 1; i < count; i++) |
250 | result[count - i - 1] -= temp[count - i] * result[count - i]; |
251 | |
252 | return result; |
253 | } |
254 | |
255 | QT_END_NAMESPACE |
256 | |