1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qcategory3daxis.h"
5#include "qquickgraphsscatter_p.h"
6#include "qscatter3dseries_p.h"
7#include "qvalue3daxis.h"
8#include "qgraphs3dlogging_p.h"
9
10QT_BEGIN_NAMESPACE
11
12/*!
13 * \class QScatter3DSeries
14 * \inmodule QtGraphs
15 * \ingroup graphs_3D
16 * \brief The QScatter3DSeries class represents a data series in a 3D scatter
17 * graph.
18 *
19 * This class manages the series-specific visual elements, as well as the series
20 * data (via a data proxy).
21 *
22 * Regarding the proxy-series relationship, it is crucial to highlight
23 * a couple of key points. In this context, data is stored in series and
24 * users can access the dataset through the series. This series is controlled
25 * or represented by a proxy object. Thus, the proxy can be used to manage various
26 * operations on the data and update the actual dataset. However, it is necessary
27 * to create a series associated with this proxy to edit the dataset.
28 *
29 * If no data proxy is set explicitly for the series, the series creates a
30 * default proxy. Setting another proxy will destroy the existing proxy and all
31 * data added to the series.
32 *
33 * QScatter3DSeries supports the following format tags for QAbstract3DSeries::setItemLabelFormat():
34 * \table
35 * \row
36 * \li @xTitle \li Title from x-axis
37 * \row
38 * \li @yTitle \li Title from y-axis
39 * \row
40 * \li @zTitle \li Title from z-axis
41 * \row
42 * \li @xLabel \li Item value formatted using the format of the x-axis.
43 * For more information, see
44 * \l{QValue3DAxis::labelFormat}.
45 * \row
46 * \li @yLabel \li Item value formatted using the format of the y-axis.
47 * For more information, see
48 * \l{QValue3DAxis::labelFormat}.
49 * \row
50 * \li @zLabel \li Item value formatted using the format of the z-axis.
51 * For more information, see
52 * \l{QValue3DAxis::labelFormat}.
53 * \row
54 * \li @seriesName \li Name of the series
55 * \endtable
56 *
57 * For example:
58 * \snippet doc_src_qtgraphs.cpp labelformat-scatter
59 *
60 * \sa {Qt Graphs Data Handling with 3D}
61 */
62
63/*!
64 * \qmltype Scatter3DSeries
65 * \inqmlmodule QtGraphs
66 * \ingroup graphs_qml_3D
67 * \nativetype QScatter3DSeries
68 * \inherits Abstract3DSeries
69 * \brief Represents a data series in a 3D scatter graph.
70 *
71 * This type manages the series specific visual elements, as well as the series
72 * data (via a data proxy).
73 *
74 * Scatter3DSeries supports the following format tags for itemLabelFormat:
75 * \table
76 * \row
77 * \li @xTitle \li Title from x-axis
78 * \row
79 * \li @yTitle \li Title from y-axis
80 * \row
81 * \li @zTitle \li Title from z-axis
82 * \row
83 * \li @xLabel \li Item value formatted using the format of the x-axis.
84 * For more information, see
85 * \l{QValue3DAxis::labelFormat}{labelFormat}.
86 * \row
87 * \li @yLabel \li Item value formatted using the format of the y-axis.
88 * For more information, see
89 * \l{QValue3DAxis::labelFormat}{labelFormat}.
90 * \row
91 * \li @zLabel \li Item value formatted using the format of the z-axis.
92 * For more information, see
93 * \l{QValue3DAxis::labelFormat}{labelFormat}.
94 * \row
95 * \li @seriesName \li Name of the series
96 * \endtable
97 *
98 * For a more complete description, see QScatter3DSeries.
99 *
100 * \sa {Qt Graphs Data Handling with 3D}
101 */
102
103/*!
104 * \qmlproperty ScatterDataProxy Scatter3DSeries::dataProxy
105 *
106 * Sets the active data proxy. The series assumes ownership of any proxy set to
107 * it and deletes any previously set proxy when a new one is added. The proxy
108 * cannot be null or set to another series.
109 */
110
111/*!
112 * \qmlproperty qsizetype Scatter3DSeries::selectedItem
113 *
114 * The item that is selected at the index in the data array of the series.
115 * Only one item can be selected at a time.
116 * To clear the selection from this series, the invalidSelectionIndex is set as the
117 * index. If this series is added to a graph, the graph can adjust the selection
118 * according to user interaction or if it becomes invalid. Selecting an item on
119 * another added series will also clear the selection. Removing items from or
120 * inserting items into the series before the selected item will adjust the
121 * selection so that the same item will stay selected.
122 *
123 * \sa GraphsItem3D::clearSelection()
124 */
125
126/*!
127 * \qmlproperty real Scatter3DSeries::itemSize
128 *
129 * Sets the item size for the series. The size must be between \c 0.0 and
130 * \c 1.0. Setting the size to \c 0.0 causes the item size to be automatically
131 * scaled based on the total number of items in all the series for the graph.
132 * The preset default is \c 0.0.
133 */
134
135/*!
136 * \qmlproperty qsizetype Scatter3DSeries::invalidSelectionIndex
137 * A constant property providing an invalid index for selection. This index is
138 * set to the selectedItem property to clear the selection from this series.
139 *
140 * \sa GraphsItem3D::clearSelection()
141 */
142
143/*!
144 * \qmlproperty ScatterDataArray Scatter3DSeries::dataArray
145 *
146 * Holds the reference to the data array.
147 *
148 * dataArrayChanged signal is emitted when data array is set, unless \a newDataArray
149 * is identical to the previous one.
150 *
151 * \note Before doing anything regarding the data array, a series must be created for
152 * the relevant proxy.
153 */
154
155/*!
156 \qmlsignal Scatter3DSeries::dataProxyChanged(ScatterDataProxy proxy)
157
158 This signal is emitted when dataProxy changes to \a proxy.
159*/
160
161/*!
162 \qmlsignal Scatter3DSeries::selectedItemChanged(qsizetype index)
163
164 This signal is emitted when selectedItem changes to \a index.
165*/
166
167/*!
168 \qmlsignal Scatter3DSeries::itemSizeChanged(float size)
169
170 This signal is emitted when itemSize changes to \a size.
171*/
172
173/*!
174 \qmlsignal Scatter3DSeries::dataArrayChanged(ScatterDataArray array)
175
176 This signal is emitted when dataArray changes to \a array.
177*/
178
179/*!
180 * Constructs a scatter 3D series with the parent \a parent.
181 */
182QScatter3DSeries::QScatter3DSeries(QObject *parent)
183 : QAbstract3DSeries(*(new QScatter3DSeriesPrivate()), parent)
184{
185 Q_D(QScatter3DSeries);
186 // Default proxy
187 d->setDataProxy(new QScatterDataProxy);
188}
189
190/*!
191 * Constructs a scatter 3D series with the data proxy \a dataProxy and the
192 * parent \a parent.
193 */
194QScatter3DSeries::QScatter3DSeries(QScatterDataProxy *dataProxy, QObject *parent)
195 : QAbstract3DSeries(*(new QScatter3DSeriesPrivate()), parent)
196{
197 Q_D(QScatter3DSeries);
198 d->setDataProxy(dataProxy);
199}
200
201/*!
202 * \internal
203 */
204QScatter3DSeries::QScatter3DSeries(QScatter3DSeriesPrivate &d, QObject *parent)
205 : QAbstract3DSeries(d, parent)
206{}
207
208/*!
209 * Deletes the scatter 3D series.
210 */
211QScatter3DSeries::~QScatter3DSeries() {}
212
213/*!
214 * \property QScatter3DSeries::dataProxy
215 *
216 * \brief The active data proxy.
217 *
218 * Sets the active data proxy for the series to \a proxy. The series assumes
219 * ownership of any proxy set to it and deletes any previously set proxy when
220 * a new one is added. The \a proxy argument cannot be null or set to another
221 * series.
222 */
223void QScatter3DSeries::setDataProxy(QScatterDataProxy *proxy)
224{
225 Q_D(QScatter3DSeries);
226 d->setDataProxy(proxy);
227}
228
229QScatterDataProxy *QScatter3DSeries::dataProxy() const
230{
231 Q_D(const QScatter3DSeries);
232 return static_cast<QScatterDataProxy *>(d->dataProxy());
233}
234
235/*!
236 * \property QScatter3DSeries::selectedItem
237 *
238 * \brief The item that is selected in the series.
239 *
240 * Selects the item at the index \a index in the data array of the series.
241 * Only one item can be selected at a time.
242 *
243 * To clear the selection from this series, invalidSelectionIndex() is set as \a
244 * index. If this series is added to a graph, the graph can adjust the selection
245 * according to user interaction or if it becomes invalid. Selecting an item on
246 * another added series will also clear the selection.
247 *
248 * Removing items from or inserting items into the series before the selected item
249 * will adjust the selection so that the same item will stay selected.
250 *
251 * \sa Q3DGraphsWidgetItem::clearSelection()
252 */
253void QScatter3DSeries::setSelectedItem(qsizetype index)
254{
255 Q_D(QScatter3DSeries);
256 // Don't do this in private to avoid loops, as that is used for callback from
257 // graph.
258 if (d->m_graph)
259 static_cast<QQuickGraphsScatter *>(d->m_graph)->setSelectedItem(index, series: this);
260 else
261 d->setSelectedItem(index);
262}
263
264qsizetype QScatter3DSeries::selectedItem() const
265{
266 Q_D(const QScatter3DSeries);
267 return d->m_selectedItem;
268}
269
270/*!
271 * \property QScatter3DSeries::itemSize
272 *
273 * \brief Item size for the series.
274 *
275 * The size must be between \c 0.0f and \c 1.0f. Setting the size to \c 0.0f
276 * causes the item size to be automatically scaled based on the total number of
277 * items in all the series for the graph.
278 *
279 * The preset default is \c 0.0f.
280 */
281void QScatter3DSeries::setItemSize(float size)
282{
283 Q_D(QScatter3DSeries);
284 if (size < 0.0f || size > 1.0f) {
285 qCWarning(lcProperties3D, "%s invalid size. Valid range for itemSize is 0.0f...1.0f",
286 qUtf8Printable(QLatin1String(__FUNCTION__)));
287 return;
288 } else if (size == d->m_itemSize) {
289 qCDebug(lcProperties3D, "%s value is already set to: %.1f",
290 qUtf8Printable(QLatin1String(__FUNCTION__)), size);
291 return;
292 }
293 d->setItemSize(size);
294 emit itemSizeChanged(size);
295}
296
297float QScatter3DSeries::itemSize() const
298{
299 Q_D(const QScatter3DSeries);
300 return d->m_itemSize;
301}
302
303/*!
304 * \property QScatter3DSeries::dataArray
305 *
306 * \brief Data array for the series.
307 *
308 * Holds the reference to the data array.
309 *
310 * dataArrayChanged signal is emitted when data array is set, unless \a newDataArray
311 * is identical to the previous one.
312 *
313 * \note Before doing anything regarding the data array, a series must be created for
314 * the relevant proxy.
315 *
316 * \sa clearArray()
317 */
318void QScatter3DSeries::setDataArray(const QScatterDataArray &newDataArray)
319{
320 Q_D(QScatter3DSeries);
321 if (d->m_dataArray.isSharedWith(other: newDataArray)) {
322 qCDebug(lcProperties3D) << __FUNCTION__
323 << "newDataArray is the same than the old one";
324 return;
325 }
326 d->setDataArray(newDataArray);
327 emit dataArrayChanged(array: newDataArray);
328}
329
330/*!
331 * Clears the data array.
332 */
333void QScatter3DSeries::clearArray()
334{
335 Q_D(QScatter3DSeries);
336 d->clearArray();
337}
338
339const QScatterDataArray &QScatter3DSeries::dataArray() const &
340{
341 Q_D(const QScatter3DSeries);
342 return d->m_dataArray;
343}
344
345QScatterDataArray QScatter3DSeries::dataArray() &&
346{
347 Q_D(QScatter3DSeries);
348 return std::move(d->m_dataArray);
349}
350
351/*!
352 * \property QScatter3DSeries::scaleArray
353 *
354 * \brief Scale array for the series.
355 *
356 * Holds the reference to the scale array.
357 *
358 * scaleArrayChanged signal is emitted when scale array is set, unless \a newScaleArray
359 * is identical to the previous one.
360 *
361 * \note Before doing anything regarding the scale array, a series must be created for
362 * the relevant proxy.
363 *
364 * \note If a scale is not specified for the data, a default value of (1.0,1.0,1.0)
365 * will be used.
366 *
367 * \sa clearScaleArray()
368 */
369void QScatter3DSeries::setScaleArray(const QList<QVector3D> &newScaleArray)
370{
371 Q_D(QScatter3DSeries);
372 if (d->m_scaleArray.isSharedWith(other: newScaleArray)) {
373 qCDebug(lcProperties3D) << __FUNCTION__
374 << "newScaleArray is the same than the old one";
375 return;
376 }
377 d->setScaleArray(newScaleArray);
378 emit scaleArrayChanged(scaleArray: newScaleArray);
379}
380
381/*!
382 * Clears the scale array.
383 */
384void QScatter3DSeries::clearScaleArray()
385{
386 Q_D(QScatter3DSeries);
387 d->clearScaleArray();
388}
389
390const QList<QVector3D> &QScatter3DSeries::scaleArray() const &
391{
392 Q_D(const QScatter3DSeries);
393 return d->m_scaleArray;
394}
395
396QList<QVector3D> QScatter3DSeries::scaleArray() &&
397{
398 Q_D(QScatter3DSeries);
399 return std::move(d->m_scaleArray);
400}
401
402/*!
403 * Returns an invalid index for selection. This index is set to the selectedItem
404 * property to clear the selection from this series.
405 *
406 * \sa Q3DGraphsWidgetItem::clearSelection()
407 */
408qsizetype QScatter3DSeries::invalidSelectionIndex()
409{
410 return QQuickGraphsScatter::invalidSelectionIndex();
411}
412
413// QScatter3DSeriesPrivate
414
415QScatter3DSeriesPrivate::QScatter3DSeriesPrivate()
416 : QAbstract3DSeriesPrivate(QAbstract3DSeries::SeriesType::Scatter)
417 , m_selectedItem(QQuickGraphsScatter::invalidSelectionIndex())
418 , m_itemSize(0.0f)
419{
420 m_itemLabelFormat = QStringLiteral("@xLabel, @yLabel, @zLabel");
421 m_mesh = QAbstract3DSeries::Mesh::Sphere;
422}
423
424QScatter3DSeriesPrivate::~QScatter3DSeriesPrivate()
425{
426 clearArray();
427}
428
429void QScatter3DSeriesPrivate::setDataProxy(QAbstractDataProxy *proxy)
430{
431 Q_Q(QScatter3DSeries);
432 Q_ASSERT(proxy->type() == QAbstractDataProxy::DataType::Scatter);
433
434 QAbstract3DSeriesPrivate::setDataProxy(proxy);
435
436 emit q->dataProxyChanged(proxy: static_cast<QScatterDataProxy *>(proxy));
437}
438
439void QScatter3DSeriesPrivate::connectGraphAndProxy(QQuickGraphsItem *newGraph)
440{
441 Q_Q(QScatter3DSeries);
442 QScatterDataProxy *scatterDataProxy = static_cast<QScatterDataProxy *>(m_dataProxy);
443
444 if (m_graph && scatterDataProxy) {
445 // Disconnect old graph/old proxy
446 QObject::disconnect(sender: scatterDataProxy, signal: 0, receiver: m_graph, member: 0);
447 QObject::disconnect(sender: q, signal: 0, receiver: m_graph, member: 0);
448 }
449
450 if (newGraph && scatterDataProxy) {
451 QQuickGraphsScatter *graph = static_cast<QQuickGraphsScatter *>(newGraph);
452 QObject::connect(sender: scatterDataProxy,
453 signal: &QScatterDataProxy::arrayReset,
454 context: graph,
455 slot: &QQuickGraphsScatter::handleArrayReset);
456 QObject::connect(sender: scatterDataProxy,
457 signal: &QScatterDataProxy::itemsAdded,
458 context: graph,
459 slot: &QQuickGraphsScatter::handleItemsAdded);
460 QObject::connect(sender: scatterDataProxy,
461 signal: &QScatterDataProxy::itemsChanged,
462 context: graph,
463 slot: &QQuickGraphsScatter::handleItemsChanged);
464 QObject::connect(sender: scatterDataProxy,
465 signal: &QScatterDataProxy::itemsRemoved,
466 context: graph,
467 slot: &QQuickGraphsScatter::handleItemsRemoved);
468 QObject::connect(sender: scatterDataProxy,
469 signal: &QScatterDataProxy::itemsInserted,
470 context: graph,
471 slot: &QQuickGraphsScatter::handleItemsInserted);
472 QObject::connect(sender: q,
473 signal: &QScatter3DSeries::dataProxyChanged,
474 context: graph,
475 slot: &QQuickGraphsScatter::handleArrayReset);
476 }
477}
478
479void QScatter3DSeriesPrivate::createItemLabel()
480{
481 Q_Q(QScatter3DSeries);
482 static const QString xTitleTag(QStringLiteral("@xTitle"));
483 static const QString yTitleTag(QStringLiteral("@yTitle"));
484 static const QString zTitleTag(QStringLiteral("@zTitle"));
485 static const QString xLabelTag(QStringLiteral("@xLabel"));
486 static const QString yLabelTag(QStringLiteral("@yLabel"));
487 static const QString zLabelTag(QStringLiteral("@zLabel"));
488 static const QString seriesNameTag(QStringLiteral("@seriesName"));
489
490 if (m_selectedItem == QScatter3DSeries::invalidSelectionIndex()) {
491 m_itemLabel = QString(hiddenLabelTag);
492 return;
493 }
494
495 QValue3DAxis *axisX = static_cast<QValue3DAxis *>(m_graph->axisX());
496 QValue3DAxis *axisY = static_cast<QValue3DAxis *>(m_graph->axisY());
497 QValue3DAxis *axisZ = static_cast<QValue3DAxis *>(m_graph->axisZ());
498 QVector3D selectedPosition = q->dataProxy()->itemAt(index: m_selectedItem).position();
499
500 m_itemLabel = m_itemLabelFormat;
501
502 m_itemLabel.replace(before: xTitleTag, after: axisX->title());
503 m_itemLabel.replace(before: yTitleTag, after: axisY->title());
504 m_itemLabel.replace(before: zTitleTag, after: axisZ->title());
505
506 if (m_itemLabel.contains(s: xLabelTag)) {
507 QString valueLabelText = axisX->formatter()->stringForValue(value: qreal(selectedPosition.x()),
508 format: axisX->labelFormat());
509 m_itemLabel.replace(before: xLabelTag, after: valueLabelText);
510 }
511 if (m_itemLabel.contains(s: yLabelTag)) {
512 QString valueLabelText = axisY->formatter()->stringForValue(value: qreal(selectedPosition.y()),
513 format: axisY->labelFormat());
514 m_itemLabel.replace(before: yLabelTag, after: valueLabelText);
515 }
516 if (m_itemLabel.contains(s: zLabelTag)) {
517 QString valueLabelText = axisZ->formatter()->stringForValue(value: qreal(selectedPosition.z()),
518 format: axisZ->labelFormat());
519 m_itemLabel.replace(before: zLabelTag, after: valueLabelText);
520 }
521 m_itemLabel.replace(before: seriesNameTag, after: m_name);
522}
523
524void QScatter3DSeriesPrivate::setSelectedItem(qsizetype index)
525{
526 Q_Q(QScatter3DSeries);
527 if (index == m_selectedItem) {
528 qCDebug(lcProperties3D, "%s Value is already set to: %" PRIdQSIZETYPE,
529 qUtf8Printable(QLatin1String(__FUNCTION__)), index);
530 return;
531 }
532 markItemLabelDirty();
533 m_selectedItem = index;
534 emit q->selectedItemChanged(index: m_selectedItem);
535}
536
537void QScatter3DSeriesPrivate::setItemSize(float size)
538{
539 m_itemSize = size;
540 if (m_graph)
541 m_graph->markSeriesVisualsDirty();
542}
543
544void QScatter3DSeriesPrivate::setDataArray(const QScatterDataArray &newDataArray)
545{
546 m_dataArray = newDataArray;
547}
548
549void QScatter3DSeriesPrivate::clearArray()
550{
551 m_dataArray.clear();
552}
553
554void QScatter3DSeriesPrivate::setScaleArray(const QList<QVector3D> &newScaleArray)
555{
556 m_scaleArray = newScaleArray;
557}
558
559void QScatter3DSeriesPrivate::clearScaleArray()
560{
561 m_scaleArray.clear();
562}
563
564QT_END_NAMESPACE
565

source code of qtgraphs/src/graphs3d/data/qscatter3dseries.cpp