1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "scatter3dcontroller_p.h"
5#include "qvalue3daxis_p.h"
6#include "qscatterdataproxy_p.h"
7#include "qscatter3dseries_p.h"
8#include <QtCore/QMutexLocker>
9
10QT_BEGIN_NAMESPACE
11
12static const int insertRemoveRecordReserveSize = 31;
13
14Scatter3DController::Scatter3DController(QRect boundRect, Q3DScene *scene)
15 : Abstract3DController(boundRect, scene),
16 m_selectedItem(invalidSelectionIndex()),
17 m_selectedItemSeries(0),
18 m_recordInsertsAndRemoves(false)
19{
20 // Setting a null axis creates a new default axis according to orientation and graph type.
21 // Note: These cannot be set in Abstract3DController constructor, as they will call virtual
22 // functions implemented by subclasses.
23 setAxisX(0);
24 setAxisY(0);
25 setAxisZ(0);
26}
27
28Scatter3DController::~Scatter3DController()
29{
30}
31
32void Scatter3DController::addSeries(QAbstract3DSeries *series)
33{
34 Q_ASSERT(series && series->type() == QAbstract3DSeries::SeriesTypeScatter);
35
36 Abstract3DController::addSeries(series);
37
38 QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(series);
39 if (scatterSeries->selectedItem() != invalidSelectionIndex())
40 setSelectedItem(index: scatterSeries->selectedItem(), series: scatterSeries);
41}
42
43void Scatter3DController::removeSeries(QAbstract3DSeries *series)
44{
45 bool wasVisible = (series && series->d_func()->m_controller == this && series->isVisible());
46
47 Abstract3DController::removeSeries(series);
48
49 if (m_selectedItemSeries == series)
50 setSelectedItem(index: invalidSelectionIndex(), series: 0);
51
52 if (wasVisible)
53 adjustAxisRanges();
54}
55
56QList<QScatter3DSeries *> Scatter3DController::scatterSeriesList()
57{
58 QList<QAbstract3DSeries *> abstractSeriesList = seriesList();
59 QList<QScatter3DSeries *> scatterSeriesList;
60 foreach (QAbstract3DSeries *abstractSeries, abstractSeriesList) {
61 QScatter3DSeries *scatterSeries = qobject_cast<QScatter3DSeries *>(object: abstractSeries);
62 if (scatterSeries)
63 scatterSeriesList.append(t: scatterSeries);
64 }
65
66 return scatterSeriesList;
67}
68
69void Scatter3DController::clearChangedItems()
70{
71 m_changedItems.clear();
72}
73
74void Scatter3DController::handleArrayReset()
75{
76 QScatter3DSeries *series;
77 if (qobject_cast<QScatterDataProxy *>(object: sender()))
78 series = static_cast<QScatterDataProxy *>(sender())->series();
79 else
80 series = static_cast<QScatter3DSeries *>(sender());
81
82 if (series->isVisible()) {
83 adjustAxisRanges();
84 m_isDataDirty = true;
85 }
86 if (!m_changedSeriesList.contains(t: series))
87 m_changedSeriesList.append(t: series);
88 setSelectedItem(index: m_selectedItem, series: m_selectedItemSeries);
89 series->d_func()->markItemLabelDirty();
90 emitNeedRender();
91}
92
93void Scatter3DController::handleItemsAdded(int startIndex, int count)
94{
95 Q_UNUSED(startIndex);
96 Q_UNUSED(count);
97 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
98 if (series->isVisible()) {
99 adjustAxisRanges();
100 m_isDataDirty = true;
101 }
102 if (!m_changedSeriesList.contains(t: series))
103 m_changedSeriesList.append(t: series);
104 emitNeedRender();
105}
106
107void Scatter3DController::handleItemsChanged(int startIndex, int count)
108{
109 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
110 int oldChangeCount = m_changedItems.size();
111 if (!oldChangeCount)
112 m_changedItems.reserve(asize: count);
113
114 for (int i = 0; i < count; i++) {
115 bool newItem = true;
116 int candidate = startIndex + i;
117 for (int j = 0; j < oldChangeCount; j++) {
118 const ChangeItem &oldChangeItem = m_changedItems.at(i: j);
119 if (oldChangeItem.index == candidate && series == oldChangeItem.series) {
120 newItem = false;
121 break;
122 }
123 }
124 if (newItem) {
125 ChangeItem newChangeItem = {.series: series, .index: candidate};
126 m_changedItems.append(t: newChangeItem);
127 if (series == m_selectedItemSeries && m_selectedItem == candidate)
128 series->d_func()->markItemLabelDirty();
129 }
130 }
131
132 if (count) {
133 m_changeTracker.itemChanged = true;
134 if (series->isVisible())
135 adjustAxisRanges();
136 emitNeedRender();
137 }
138}
139
140void Scatter3DController::handleItemsRemoved(int startIndex, int count)
141{
142 Q_UNUSED(startIndex);
143 Q_UNUSED(count);
144 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
145 if (series == m_selectedItemSeries) {
146 // If items removed from selected series before the selection, adjust the selection
147 int selectedItem = m_selectedItem;
148 if (startIndex <= selectedItem) {
149 if ((startIndex + count) > selectedItem)
150 selectedItem = -1; // Selected item removed
151 else
152 selectedItem -= count; // Move selected item down by amount of item removed
153
154 setSelectedItem(index: selectedItem, series: m_selectedItemSeries);
155 }
156 }
157
158 if (series->isVisible()) {
159 adjustAxisRanges();
160 m_isDataDirty = true;
161 }
162 if (!m_changedSeriesList.contains(t: series))
163 m_changedSeriesList.append(t: series);
164
165 if (m_recordInsertsAndRemoves) {
166 InsertRemoveRecord record(false, startIndex, count, series);
167 m_insertRemoveRecords.append(t: record);
168 }
169
170 emitNeedRender();
171}
172
173void Scatter3DController::handleItemsInserted(int startIndex, int count)
174{
175 Q_UNUSED(startIndex);
176 Q_UNUSED(count);
177 QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
178 if (series == m_selectedItemSeries) {
179 // If items inserted to selected series before the selection, adjust the selection
180 int selectedItem = m_selectedItem;
181 if (startIndex <= selectedItem) {
182 selectedItem += count;
183 setSelectedItem(index: selectedItem, series: m_selectedItemSeries);
184 }
185 }
186
187 if (series->isVisible()) {
188 adjustAxisRanges();
189 m_isDataDirty = true;
190 }
191 if (!m_changedSeriesList.contains(t: series))
192 m_changedSeriesList.append(t: series);
193
194 if (m_recordInsertsAndRemoves) {
195 InsertRemoveRecord record(true, startIndex, count, series);
196 m_insertRemoveRecords.append(t: record);
197 }
198
199 emitNeedRender();
200}
201
202void Scatter3DController::startRecordingRemovesAndInserts()
203{
204 m_recordInsertsAndRemoves = false;
205
206 if (m_scene->selectionQueryPosition() != Q3DScene::invalidSelectionPoint()) {
207 m_recordInsertsAndRemoves = true;
208 if (m_insertRemoveRecords.size()) {
209 m_insertRemoveRecords.clear();
210 // Reserve some space for remove/insert records to avoid unnecessary reallocations.
211 m_insertRemoveRecords.reserve(asize: insertRemoveRecordReserveSize);
212 }
213 }
214}
215
216void Scatter3DController::handleAxisAutoAdjustRangeChangedInOrientation(
217 QAbstract3DAxis::AxisOrientation orientation, bool autoAdjust)
218{
219 Q_UNUSED(orientation);
220 Q_UNUSED(autoAdjust);
221 adjustAxisRanges();
222}
223
224void Scatter3DController::handleAxisRangeChangedBySender(QObject *sender)
225{
226 Abstract3DController::handleAxisRangeChangedBySender(sender);
227
228 m_isDataDirty = true;
229
230 // Update selected index - may be moved offscreen
231 setSelectedItem(index: m_selectedItem, series: m_selectedItemSeries);
232}
233
234void Scatter3DController::setSelectionMode(QAbstract3DGraph::SelectionFlags mode)
235{
236 // We only support single item selection mode and no selection mode
237 if (mode != QAbstract3DGraph::SelectionItem && mode != QAbstract3DGraph::SelectionNone) {
238 qWarning(msg: "Unsupported selection mode - only none and item selection modes are supported.");
239 return;
240 }
241
242 Abstract3DController::setSelectionMode(mode);
243}
244
245void Scatter3DController::setSelectedItem(int index, QScatter3DSeries *series)
246{
247 const QScatterDataProxy *proxy = 0;
248
249 // Series may already have been removed, so check it before setting the selection.
250 if (!m_seriesList.contains(t: series))
251 series = 0;
252
253 if (series)
254 proxy = series->dataProxy();
255
256 if (!proxy || index < 0 || index >= proxy->itemCount())
257 index = invalidSelectionIndex();
258
259 if (index != m_selectedItem || series != m_selectedItemSeries) {
260 bool seriesChanged = (series != m_selectedItemSeries);
261 m_selectedItem = index;
262 m_selectedItemSeries = series;
263 m_changeTracker.selectedItemChanged = true;
264
265 // Clear selection from other series and finally set new selection to the specified series
266 foreach (QAbstract3DSeries *otherSeries, m_seriesList) {
267 QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(otherSeries);
268 if (scatterSeries != m_selectedItemSeries)
269 scatterSeries->d_func()->setSelectedItem(invalidSelectionIndex());
270 }
271 if (m_selectedItemSeries)
272 m_selectedItemSeries->d_func()->setSelectedItem(m_selectedItem);
273
274 if (seriesChanged)
275 emit selectedSeriesChanged(series: m_selectedItemSeries);
276
277 emitNeedRender();
278 }
279}
280
281void Scatter3DController::clearSelection()
282{
283 setSelectedItem(index: invalidSelectionIndex(), series: 0);
284}
285
286void Scatter3DController::adjustAxisRanges()
287{
288 QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
289 QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
290 QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
291 bool adjustX = (valueAxisX && valueAxisX->isAutoAdjustRange());
292 bool adjustY = (valueAxisY && valueAxisY->isAutoAdjustRange());
293 bool adjustZ = (valueAxisZ && valueAxisZ->isAutoAdjustRange());
294
295 if (adjustX || adjustY || adjustZ) {
296 float minValueX = 0.0f;
297 float maxValueX = 0.0f;
298 float minValueY = 0.0f;
299 float maxValueY = 0.0f;
300 float minValueZ = 0.0f;
301 float maxValueZ = 0.0f;
302 int seriesCount = m_seriesList.size();
303 for (int series = 0; series < seriesCount; series++) {
304 const QScatter3DSeries *scatterSeries =
305 static_cast<QScatter3DSeries *>(m_seriesList.at(i: series));
306 const QScatterDataProxy *proxy = scatterSeries->dataProxy();
307 if (scatterSeries->isVisible() && proxy) {
308 QVector3D minLimits;
309 QVector3D maxLimits;
310 proxy->d_func()->limitValues(minValues&: minLimits, maxValues&: maxLimits, axisX: valueAxisX, axisY: valueAxisY, axisZ: valueAxisZ);
311 if (adjustX) {
312 if (!series) {
313 // First series initializes the values
314 minValueX = minLimits.x();
315 maxValueX = maxLimits.x();
316 } else {
317 minValueX = qMin(a: minValueX, b: minLimits.x());
318 maxValueX = qMax(a: maxValueX, b: maxLimits.x());
319 }
320 }
321 if (adjustY) {
322 if (!series) {
323 // First series initializes the values
324 minValueY = minLimits.y();
325 maxValueY = maxLimits.y();
326 } else {
327 minValueY = qMin(a: minValueY, b: minLimits.y());
328 maxValueY = qMax(a: maxValueY, b: maxLimits.y());
329 }
330 }
331 if (adjustZ) {
332 if (!series) {
333 // First series initializes the values
334 minValueZ = minLimits.z();
335 maxValueZ = maxLimits.z();
336 } else {
337 minValueZ = qMin(a: minValueZ, b: minLimits.z());
338 maxValueZ = qMax(a: maxValueZ, b: maxLimits.z());
339 }
340 }
341 }
342 }
343
344 static const float adjustmentRatio = 20.0f;
345 static const float defaultAdjustment = 1.0f;
346
347 if (adjustX) {
348 // If all points at same coordinate, need to default to some valid range
349 float adjustment = 0.0f;
350 if (minValueX == maxValueX) {
351 if (adjustZ) {
352 // X and Z are linked to have similar unit size, so choose the valid range based on it
353 if (minValueZ == maxValueZ)
354 adjustment = defaultAdjustment;
355 else
356 adjustment = qAbs(t: maxValueZ - minValueZ) / adjustmentRatio;
357 } else {
358 if (valueAxisZ)
359 adjustment = qAbs(t: valueAxisZ->max() - valueAxisZ->min()) / adjustmentRatio;
360 else
361 adjustment = defaultAdjustment;
362 }
363 }
364 valueAxisX->d_func()->setRange(min: minValueX - adjustment, max: maxValueX + adjustment, suppressWarnings: true);
365 }
366 if (adjustY) {
367 // If all points at same coordinate, need to default to some valid range
368 // Y-axis unit is not dependent on other axes, so simply adjust +-1.0f
369 float adjustment = 0.0f;
370 if (minValueY == maxValueY)
371 adjustment = defaultAdjustment;
372 valueAxisY->d_func()->setRange(min: minValueY - adjustment, max: maxValueY + adjustment, suppressWarnings: true);
373 }
374 if (adjustZ) {
375 // If all points at same coordinate, need to default to some valid range
376 float adjustment = 0.0f;
377 if (minValueZ == maxValueZ) {
378 if (adjustX) {
379 // X and Z are linked to have similar unit size, so choose the valid range based on it
380 if (minValueX == maxValueX)
381 adjustment = defaultAdjustment;
382 else
383 adjustment = qAbs(t: maxValueX - minValueX) / adjustmentRatio;
384 } else {
385 if (valueAxisX)
386 adjustment = qAbs(t: valueAxisX->max() - valueAxisX->min()) / adjustmentRatio;
387 else
388 adjustment = defaultAdjustment;
389 }
390 }
391 valueAxisZ->d_func()->setRange(min: minValueZ - adjustment, max: maxValueZ + adjustment, suppressWarnings: true);
392 }
393 }
394}
395
396QT_END_NAMESPACE
397

source code of qtgraphs/src/graphs/engine/scatter3dcontroller.cpp