1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qvalue3daxisformatter_p.h" |
5 | #include "qvalue3daxis_p.h" |
6 | |
7 | QT_BEGIN_NAMESPACE |
8 | |
9 | /*! |
10 | * \class QValue3DAxisFormatter |
11 | * \inmodule QtGraphs |
12 | * \brief The QValue3DAxisFormatter class is a base class for value axis |
13 | * formatters. |
14 | * |
15 | * This class provides formatting rules for a linear value 3D axis. Subclass it if you |
16 | * want to implement custom value axes. |
17 | * |
18 | * The base class has no public API beyond constructors and destructors. It is meant to be only |
19 | * used internally. However, subclasses may implement public properties as needed. |
20 | * |
21 | * \sa QValue3DAxis, QLogValue3DAxisFormatter |
22 | */ |
23 | |
24 | /*! |
25 | * \qmltype ValueAxis3DFormatter |
26 | * \inqmlmodule QtGraphs |
27 | * \ingroup graphs_qml |
28 | * \instantiates QValue3DAxisFormatter |
29 | * \brief A base type for value axis formatters. |
30 | * |
31 | * This type provides formatting rules for a linear value 3D axis. |
32 | * This type is the default type for ValueAxis3D and thus never needs to be explicitly created. |
33 | * This type has no public functionality. |
34 | * |
35 | * \sa ValueAxis3D |
36 | */ |
37 | |
38 | /*! |
39 | * \internal |
40 | */ |
41 | QValue3DAxisFormatter::QValue3DAxisFormatter(QValue3DAxisFormatterPrivate *d, QObject *parent) : |
42 | QObject(parent), |
43 | d_ptr(d) |
44 | { |
45 | } |
46 | |
47 | /*! |
48 | * Constructs a new value 3D axis formatter with the optional parent \a parent. |
49 | */ |
50 | QValue3DAxisFormatter::QValue3DAxisFormatter(QObject *parent) : |
51 | QObject(parent), |
52 | d_ptr(new QValue3DAxisFormatterPrivate(this)) |
53 | { |
54 | } |
55 | |
56 | /*! |
57 | * Deletes the value 3D axis formatter. |
58 | */ |
59 | QValue3DAxisFormatter::~QValue3DAxisFormatter() |
60 | { |
61 | } |
62 | |
63 | /*! |
64 | * Allows the parent axis to have negative values if \a allow is \c true. |
65 | */ |
66 | void QValue3DAxisFormatter::setAllowNegatives(bool allow) |
67 | { |
68 | Q_D(QValue3DAxisFormatter); |
69 | d->m_allowNegatives = allow; |
70 | } |
71 | |
72 | /*! |
73 | * Returns \c true if negative values are valid values for the parent axis. |
74 | * The default implementation always returns \c true. |
75 | */ |
76 | bool QValue3DAxisFormatter::allowNegatives() const |
77 | { |
78 | const Q_D(QValue3DAxisFormatter); |
79 | return d->m_allowNegatives; |
80 | } |
81 | |
82 | /*! |
83 | * Allows the parent axis to have a zero value if \a allow is \c true. |
84 | */ |
85 | void QValue3DAxisFormatter::setAllowZero(bool allow) |
86 | { |
87 | Q_D(QValue3DAxisFormatter); |
88 | d->m_allowZero = allow; |
89 | } |
90 | |
91 | /*! |
92 | * Returns \c true if zero is a valid value for the parent axis. |
93 | * The default implementation always returns \c true. |
94 | */ |
95 | bool QValue3DAxisFormatter::allowZero() const |
96 | { |
97 | const Q_D(QValue3DAxisFormatter); |
98 | return d->m_allowZero; |
99 | } |
100 | |
101 | /*! |
102 | * \internal |
103 | */ |
104 | void QValue3DAxisFormatter::setAxis(QValue3DAxis *axis) |
105 | { |
106 | Q_ASSERT(axis); |
107 | Q_D(QValue3DAxisFormatter); |
108 | |
109 | // These signals are all connected to markDirtyNoLabelChange slot, even though most of them |
110 | // do require labels to be regenerated. This is because the label regeneration is triggered |
111 | // elsewhere in these cases. |
112 | connect(sender: axis, signal: &QValue3DAxis::segmentCountChanged, |
113 | context: this, slot: &QValue3DAxisFormatter::markDirtyNoLabelChange); |
114 | connect(sender: axis, signal: &QValue3DAxis::subSegmentCountChanged, |
115 | context: this, slot: &QValue3DAxisFormatter::markDirtyNoLabelChange); |
116 | connect(sender: axis, signal: &QValue3DAxis::labelFormatChanged, |
117 | context: this, slot: &QValue3DAxisFormatter::markDirtyNoLabelChange); |
118 | connect(sender: axis, signal: &QAbstract3DAxis::rangeChanged, |
119 | context: this, slot: &QValue3DAxisFormatter::markDirtyNoLabelChange); |
120 | |
121 | d->setAxis(axis); |
122 | } |
123 | |
124 | /*! |
125 | * \internal |
126 | */ |
127 | void QValue3DAxisFormatter::markDirtyNoLabelChange() |
128 | { |
129 | Q_D(QValue3DAxisFormatter); |
130 | d->markDirty(labelsChange: false); |
131 | } |
132 | |
133 | /*! |
134 | * Creates a new empty value 3D axis formatter. Must be reimplemented in a |
135 | * subclass. |
136 | * |
137 | * Returns the new formatter. The renderer uses this method to cache a copy of |
138 | * the formatter. The ownership of the new copy is transferred to the caller. |
139 | */ |
140 | QValue3DAxisFormatter *QValue3DAxisFormatter::createNewInstance() const |
141 | { |
142 | return new QValue3DAxisFormatter(); |
143 | } |
144 | |
145 | /*! |
146 | * Resizes and populates the label and grid line position arrays and the label |
147 | * strings array, as well as calculates any values needed to map a value to its |
148 | * position. The parent axis can be accessed from inside this function. |
149 | * |
150 | * This method must be reimplemented in a subclass if the default array contents are not suitable. |
151 | * |
152 | * See gridPositions(), subGridPositions(), labelPositions(), and labelStrings() methods for |
153 | * documentation about the arrays that need to be resized and populated. |
154 | * |
155 | * \sa gridPositions(), subGridPositions(), labelPositions(), labelStrings(), axis() |
156 | */ |
157 | void QValue3DAxisFormatter::recalculate() |
158 | { |
159 | Q_D(QValue3DAxisFormatter); |
160 | d->doRecalculate(); |
161 | } |
162 | |
163 | /*! |
164 | * Returns the formatted label string using the specified \a value and |
165 | * \a format. |
166 | * |
167 | * Reimplement this method in a subclass to resolve the formatted string for a given \a value |
168 | * if the default formatting rules specified for QValue3DAxis::labelFormat property are not |
169 | * sufficient. |
170 | * |
171 | * \sa recalculate(), labelStrings(), QValue3DAxis::labelFormat |
172 | */ |
173 | QString QValue3DAxisFormatter::stringForValue(qreal value, const QString &format) |
174 | { |
175 | Q_D(QValue3DAxisFormatter); |
176 | return d->stringForValue(value, format); |
177 | } |
178 | |
179 | /*! |
180 | * Returns the normalized position along the axis for the given \a value. |
181 | * The returned value should be between \c 0.0 (the minimum value) and |
182 | * \c 1.0 (the maximum value), inclusive, if the value is within the parent |
183 | * axis range. |
184 | * |
185 | * Reimplement this method if the position cannot be resolved by linear |
186 | * interpolation between the parent axis minimum and maximum values. |
187 | * |
188 | * \sa recalculate(), valueAt() |
189 | */ |
190 | float QValue3DAxisFormatter::positionAt(float value) const |
191 | { |
192 | const Q_D(QValue3DAxisFormatter); |
193 | return d->positionAt(value); |
194 | } |
195 | |
196 | /*! |
197 | * Returns the value at the normalized \a position along the axis. |
198 | * The \a position value should be between \c 0.0 (the minimum value) and |
199 | * \c 1.0 (the maximum value), inclusive, to obtain values within the parent |
200 | * axis range. |
201 | * |
202 | * Reimplement this method if the value cannot be resolved by linear |
203 | * interpolation between the parent axis minimum and maximum values. |
204 | * |
205 | * \sa recalculate(), positionAt() |
206 | */ |
207 | float QValue3DAxisFormatter::valueAt(float position) const |
208 | { |
209 | const Q_D(QValue3DAxisFormatter); |
210 | return d->valueAt(position); |
211 | } |
212 | |
213 | /*! |
214 | * Copies all the values necessary for resolving positions, values, and strings |
215 | * with this formatter to the \a copy of the formatter. When reimplementing |
216 | * this method in a subclass, call the superclass version at some point. |
217 | * The renderer uses this method to cache a copy of the formatter. |
218 | * |
219 | * Returns the new copy. The ownership of the new copy transfers to the caller. |
220 | */ |
221 | void QValue3DAxisFormatter::populateCopy(QValue3DAxisFormatter ©) |
222 | { |
223 | Q_D(QValue3DAxisFormatter); |
224 | d->doPopulateCopy(copy&: *(copy.d_func())); |
225 | } |
226 | |
227 | /*! |
228 | * Marks this formatter dirty, prompting the renderer to make a new copy of its cache on the next |
229 | * renderer synchronization. This method should be called by a subclass whenever the formatter |
230 | * is changed in a way that affects the resolved values. Set \a labelsChange to |
231 | * \c true if the change requires regenerating the parent axis label strings. |
232 | */ |
233 | void QValue3DAxisFormatter::markDirty(bool labelsChange) |
234 | { |
235 | Q_D(QValue3DAxisFormatter); |
236 | d->markDirty(labelsChange); |
237 | } |
238 | |
239 | /*! |
240 | * Returns the parent axis. The parent axis must only be accessed in the recalculate() |
241 | * method to maintain thread safety in environments using a threaded renderer. |
242 | * |
243 | * \sa recalculate() |
244 | */ |
245 | QValue3DAxis *QValue3DAxisFormatter::axis() const |
246 | { |
247 | const Q_D(QValue3DAxisFormatter); |
248 | return d->m_axis; |
249 | } |
250 | |
251 | /*! |
252 | * Returns a reference to the array of normalized grid line positions. |
253 | * The default array size is equal to the segment count of the parent axis plus one, but |
254 | * a subclassed implementation of the recalculate() method may resize the array differently. |
255 | * The values should be between \c 0.0 (the minimum value) and \c 1.0 (the |
256 | * maximum value), inclusive. |
257 | * |
258 | * \sa QValue3DAxis::segmentCount, recalculate() |
259 | */ |
260 | QList<float> &QValue3DAxisFormatter::gridPositions() |
261 | { |
262 | Q_D(QValue3DAxisFormatter); |
263 | return d->m_gridPositions; |
264 | } |
265 | |
266 | /*! |
267 | * Returns a reference to the array of normalized subgrid line positions. |
268 | * The default array size is equal to the segment count of the parent axis times |
269 | * the sub-segment count of the parent axis minus one, but a subclassed |
270 | * implementation of the recalculate() method may resize the array differently. |
271 | * The values should be between \c 0.0 (the minimum value) and \c 1.0 (the |
272 | * maximum value), inclusive. |
273 | * |
274 | * \sa QValue3DAxis::segmentCount, QValue3DAxis::subSegmentCount, recalculate() |
275 | */ |
276 | QList<float> &QValue3DAxisFormatter::subGridPositions() |
277 | { |
278 | Q_D(QValue3DAxisFormatter); |
279 | return d->m_subGridPositions; |
280 | } |
281 | |
282 | /*! |
283 | * Returns a reference to the array of normalized label positions. |
284 | * The default array size is equal to the segment count of the parent axis plus one, but |
285 | * a subclassed implementation of the recalculate() method may resize the array |
286 | * differently. The values should be between \c 0.0 (the minimum value) and |
287 | * \c 1.0 (the maximum value), inclusive. |
288 | * By default, the label at the index zero corresponds to the minimum value |
289 | * of the axis. |
290 | * |
291 | * \sa QValue3DAxis::segmentCount, QAbstract3DAxis::labels, recalculate() |
292 | */ |
293 | QList<float> &QValue3DAxisFormatter::labelPositions() |
294 | { |
295 | Q_D(QValue3DAxisFormatter); |
296 | return d->m_labelPositions; |
297 | } |
298 | |
299 | /*! |
300 | * Returns a reference to the string list containing formatter label strings. |
301 | * The array size must be equal to the size of the label positions array, which |
302 | * the indexes also correspond to. |
303 | * |
304 | * \sa labelPositions() |
305 | */ |
306 | QStringList &QValue3DAxisFormatter::labelStrings() |
307 | { |
308 | Q_D(QValue3DAxisFormatter); |
309 | return d->m_labelStrings; |
310 | } |
311 | |
312 | /*! |
313 | * Sets the \a locale that this formatter uses. |
314 | * The graph automatically sets the formatter's locale to a graph's locale whenever the parent axis |
315 | * is set as an active axis of the graph, the axis formatter is set to an axis attached to |
316 | * the graph, or the graph's locale changes. |
317 | * |
318 | * \sa locale(), QAbstract3DGraph::locale |
319 | */ |
320 | void QValue3DAxisFormatter::setLocale(const QLocale &locale) |
321 | { |
322 | Q_D(QValue3DAxisFormatter); |
323 | d->m_cLocaleInUse = (locale == QLocale::c()); |
324 | d->m_locale = locale; |
325 | markDirty(labelsChange: true); |
326 | } |
327 | /*! |
328 | * Returns the current locale this formatter is using. |
329 | */ |
330 | QLocale QValue3DAxisFormatter::locale() const |
331 | { |
332 | const Q_D(QValue3DAxisFormatter); |
333 | return d->m_locale; |
334 | } |
335 | |
336 | // QValue3DAxisFormatterPrivate |
337 | QValue3DAxisFormatterPrivate::QValue3DAxisFormatterPrivate(QValue3DAxisFormatter *q) |
338 | : q_ptr(q), |
339 | m_needsRecalculate(true), |
340 | m_min(0.0f), |
341 | m_max(0.0f), |
342 | m_rangeNormalizer(0.0f), |
343 | m_axis(0), |
344 | m_preparsedParamType(Utils::ParamTypeUnknown), |
345 | m_allowNegatives(true), |
346 | m_allowZero(true), |
347 | m_formatPrecision(6), // 6 and 'g' are defaults in Qt API for format precision and spec |
348 | m_formatSpec('g'), |
349 | m_cLocaleInUse(true) |
350 | { |
351 | } |
352 | |
353 | QValue3DAxisFormatterPrivate::~QValue3DAxisFormatterPrivate() |
354 | { |
355 | } |
356 | |
357 | void QValue3DAxisFormatterPrivate::recalculate() |
358 | { |
359 | Q_Q(QValue3DAxisFormatter); |
360 | // Only recalculate if we need to and have m_axis pointer. If we do not have |
361 | // m_axis, either we are not attached to an axis or this is a renderer cache. |
362 | if (m_axis && m_needsRecalculate) { |
363 | m_min = m_axis->min(); |
364 | m_max = m_axis->max(); |
365 | m_rangeNormalizer = (m_max - m_min); |
366 | |
367 | q->recalculate(); |
368 | m_needsRecalculate = false; |
369 | } |
370 | } |
371 | |
372 | void QValue3DAxisFormatterPrivate::doRecalculate() |
373 | { |
374 | Q_Q(QValue3DAxisFormatter); |
375 | int segmentCount = m_axis->segmentCount(); |
376 | int subGridCount = m_axis->subSegmentCount() - 1; |
377 | QString labelFormat = m_axis->labelFormat(); |
378 | |
379 | m_gridPositions.resize(size: segmentCount + 1); |
380 | m_subGridPositions.resize(size: segmentCount * subGridCount); |
381 | |
382 | m_labelPositions.resize(size: segmentCount + 1); |
383 | m_labelStrings.clear(); |
384 | m_labelStrings.reserve(asize: segmentCount + 1); |
385 | |
386 | // Use qreals for intermediate calculations for better accuracy on label values |
387 | qreal segmentStep = 1.0 / qreal(segmentCount); |
388 | qreal subSegmentStep = 0; |
389 | if (subGridCount > 0) |
390 | subSegmentStep = segmentStep / qreal(subGridCount + 1); |
391 | |
392 | // Calculate positions |
393 | qreal rangeNormalizer = qreal(m_max - m_min); |
394 | for (int i = 0; i < segmentCount; i++) { |
395 | qreal gridValue = segmentStep * qreal(i); |
396 | m_gridPositions[i] = float(gridValue); |
397 | m_labelPositions[i] = float(gridValue); |
398 | m_labelStrings << q->stringForValue(value: gridValue * rangeNormalizer + qreal(m_min), |
399 | format: labelFormat); |
400 | if (m_subGridPositions.size()) { |
401 | for (int j = 0; j < subGridCount; j++) |
402 | m_subGridPositions[i * subGridCount + j] = gridValue + subSegmentStep * (j + 1); |
403 | } |
404 | } |
405 | |
406 | // Ensure max value doesn't suffer from any rounding errors |
407 | m_gridPositions[segmentCount] = 1.0f; |
408 | m_labelPositions[segmentCount] = 1.0f; |
409 | m_labelStrings << q->stringForValue(value: qreal(m_max), format: labelFormat); |
410 | } |
411 | |
412 | void QValue3DAxisFormatterPrivate::populateCopy(QValue3DAxisFormatter ©) |
413 | { |
414 | Q_Q(QValue3DAxisFormatter); |
415 | recalculate(); |
416 | q->populateCopy(copy); |
417 | } |
418 | |
419 | void QValue3DAxisFormatterPrivate::doPopulateCopy(QValue3DAxisFormatterPrivate ©) |
420 | { |
421 | copy.m_min = m_min; |
422 | copy.m_max = m_max; |
423 | copy.m_rangeNormalizer = m_rangeNormalizer; |
424 | |
425 | copy.m_gridPositions = m_gridPositions; |
426 | copy.m_labelPositions = m_labelPositions; |
427 | copy.m_subGridPositions = m_subGridPositions; |
428 | } |
429 | |
430 | QString QValue3DAxisFormatterPrivate::stringForValue(qreal value, const QString &format) |
431 | { |
432 | if (m_previousLabelFormat.compare(s: format)) { |
433 | // Format string different than the previous one used, reparse it |
434 | m_labelFormatArray = format.toUtf8(); |
435 | m_previousLabelFormat = format; |
436 | m_preparsedParamType = Utils::preParseFormat(format, preStr&: m_formatPreStr, postStr&: m_formatPostStr, |
437 | precision&: m_formatPrecision, formatSpec&: m_formatSpec); |
438 | } |
439 | |
440 | if (m_cLocaleInUse) { |
441 | return Utils::formatLabelSprintf(format: m_labelFormatArray, paramType: m_preparsedParamType, value); |
442 | } else { |
443 | return Utils::formatLabelLocalized(paramType: m_preparsedParamType, value, locale: m_locale, preStr: m_formatPreStr, |
444 | postStr: m_formatPostStr, precision: m_formatPrecision, formatSpec: m_formatSpec, |
445 | format: m_labelFormatArray); |
446 | } |
447 | } |
448 | |
449 | float QValue3DAxisFormatterPrivate::positionAt(float value) const |
450 | { |
451 | return ((value - m_min) / m_rangeNormalizer); |
452 | } |
453 | |
454 | float QValue3DAxisFormatterPrivate::valueAt(float position) const |
455 | { |
456 | return ((position * m_rangeNormalizer) + m_min); |
457 | } |
458 | |
459 | void QValue3DAxisFormatterPrivate::setAxis(QValue3DAxis *axis) |
460 | { |
461 | Q_ASSERT(axis); |
462 | m_axis = axis; |
463 | } |
464 | |
465 | void QValue3DAxisFormatterPrivate::markDirty(bool labelsChange) |
466 | { |
467 | m_needsRecalculate = true; |
468 | if (m_axis) { |
469 | if (labelsChange) |
470 | m_axis->d_func()->emitLabelsChanged(); |
471 | if (m_axis && m_axis->orientation() != QAbstract3DAxis::AxisOrientationNone) |
472 | m_axis->d_func()->emitFormatterDirty(); |
473 | } |
474 | } |
475 | |
476 | QT_END_NAMESPACE |
477 | |