1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qlogvalue3daxisformatter_p.h" |
5 | #include "qvalue3daxis_p.h" |
6 | #include <QtCore/qmath.h> |
7 | |
8 | QT_BEGIN_NAMESPACE |
9 | |
10 | /*! |
11 | * \class QLogValue3DAxisFormatter |
12 | * \inmodule QtGraphs |
13 | * \ingroup graphs_3D |
14 | * \brief The QLogValue3DAxisFormatter class provides formatting rules for a |
15 | * logarithmic value axis. |
16 | * |
17 | * When a formatter is attached to a value axis, the axis range |
18 | * cannot include negative values or the zero. |
19 | * |
20 | * \sa QValue3DAxisFormatter |
21 | */ |
22 | |
23 | /*! |
24 | * \qmltype LogValue3DAxisFormatter |
25 | * \inqmlmodule QtGraphs |
26 | * \ingroup graphs_qml_3D |
27 | * \nativetype QLogValue3DAxisFormatter |
28 | * \inherits Value3DAxisFormatter |
29 | * \brief Provides formatting rules for a logarithmic value axis. |
30 | * |
31 | * When a formatter is attached to a value axis, the axis range |
32 | * cannot include negative values or the zero. |
33 | */ |
34 | |
35 | /*! |
36 | * \qmlproperty real LogValue3DAxisFormatter::base |
37 | * |
38 | * The base of the logarithm used to map axis values. If the base is non-zero, |
39 | * the parent axis segment count will be ignored when the grid line and label |
40 | * positions are calculated. If you want the range to be divided into equal |
41 | * segments like a normal value axis, set this property value to zero. |
42 | * |
43 | * The base has to be zero or a positive value and it cannot be equal to one. |
44 | * Defaults to ten. |
45 | * |
46 | * \sa Value3DAxis::segmentCount |
47 | */ |
48 | |
49 | /*! |
50 | * \qmlproperty bool LogValue3DAxisFormatter::autoSubGrid |
51 | * |
52 | * Defines whether sub-grid positions are generated automatically. |
53 | * |
54 | * If this property value is set to \c true, the parent axis sub-segment count |
55 | * is ignored when calculating sub-grid line positions. The sub-grid positions |
56 | * are generated automatically according to the \l base property value. The |
57 | * number of sub-grid lines is set to the base value minus one, rounded down. |
58 | * This property is ignored when the base value is zero. |
59 | * Defaults to \c true. |
60 | * |
61 | * \sa base, Value3DAxis::subSegmentCount |
62 | */ |
63 | |
64 | /*! |
65 | * \qmlproperty bool LogValue3DAxisFormatter::edgeLabelsVisible |
66 | * |
67 | * Defines whether the first and last label on the axis are visible. |
68 | * |
69 | * When the \l base property value is non-zero, the whole axis range is often |
70 | * not equally divided into |
71 | * segments. The first and last segments are often smaller than the other |
72 | * segments. In extreme cases, this can lead to overlapping labels on the first |
73 | * and last two grid lines. By setting this property to \c false, you can |
74 | * suppress showing the minimum and maximum labels for the axis in cases where |
75 | * the segments do not exactly fit the axis. Defaults to \c true. |
76 | * |
77 | * \sa base, Abstract3DAxis::labels |
78 | */ |
79 | |
80 | /*! |
81 | \qmlsignal LogValue3DAxisFormatter::baseChanged(real base) |
82 | |
83 | This signal is emitted when the base of the logarithm used to map axis |
84 | values changes to \a base. |
85 | */ |
86 | |
87 | /*! |
88 | \qmlsignal LogValue3DAxisFormatter::autoSubGridChanged(bool enabled) |
89 | |
90 | This signal is emitted when the value that specifies whether sub-grid |
91 | positions are generated automatically changes to \a enabled. |
92 | */ |
93 | |
94 | /*! |
95 | \qmlsignal LogValue3DAxisFormatter::edgeLabelsVisibleChanged(bool enabled) |
96 | |
97 | This signal is emitted when the value that specifies whether to show |
98 | the first and last label on the axis changes to \a enabled. |
99 | */ |
100 | |
101 | /*! |
102 | * \internal |
103 | */ |
104 | QLogValue3DAxisFormatter::QLogValue3DAxisFormatter(QLogValue3DAxisFormatterPrivate &d, |
105 | QObject *parent) |
106 | : QValue3DAxisFormatter(d, parent) |
107 | { |
108 | setAllowNegatives(false); |
109 | setAllowZero(false); |
110 | } |
111 | |
112 | /*! |
113 | * Constructs a new logarithmic value 3D axis formatter with the optional |
114 | * parent \a parent. |
115 | */ |
116 | QLogValue3DAxisFormatter::QLogValue3DAxisFormatter(QObject *parent) |
117 | : QValue3DAxisFormatter(*(new QLogValue3DAxisFormatterPrivate()), parent) |
118 | { |
119 | setAllowNegatives(false); |
120 | setAllowZero(false); |
121 | } |
122 | |
123 | /*! |
124 | * Deletes the logarithmic value 3D axis formatter. |
125 | */ |
126 | QLogValue3DAxisFormatter::~QLogValue3DAxisFormatter() {} |
127 | |
128 | /*! |
129 | * \property QLogValue3DAxisFormatter::base |
130 | * |
131 | * \brief The base of the logarithm used to map axis values. |
132 | * |
133 | * If the base is non-zero, the parent axis |
134 | * segment count will be ignored when the grid line and label positions are |
135 | * calculated. If you want the range to be divided into equal segments like a |
136 | * normal value axis, set this property value to zero. |
137 | * |
138 | * The base has to be zero or a positive value and it cannot be equal to one. |
139 | * Defaults to ten. |
140 | * |
141 | * \sa QValue3DAxis::segmentCount |
142 | */ |
143 | void QLogValue3DAxisFormatter::setBase(qreal base) |
144 | { |
145 | Q_D(QLogValue3DAxisFormatter); |
146 | if (base < 0.0f || base == 1.0f) { |
147 | qWarning( |
148 | msg: "Warning: The logarithm base must be greater than 0 and not equal to 1, attempted: %f" , |
149 | base); |
150 | return; |
151 | } |
152 | if (d->m_base != base) { |
153 | d->m_base = base; |
154 | markDirty(labelsChange: true); |
155 | emit baseChanged(base); |
156 | } |
157 | } |
158 | |
159 | qreal QLogValue3DAxisFormatter::base() const |
160 | { |
161 | Q_D(const QLogValue3DAxisFormatter); |
162 | return d->m_base; |
163 | } |
164 | |
165 | /*! |
166 | * \property QLogValue3DAxisFormatter::autoSubGrid |
167 | * |
168 | * \brief Whether sub-grid positions are generated automatically. |
169 | * |
170 | * If this property value is set to \c true, the parent axis sub-segment count |
171 | * is ignored when calculating sub-grid line positions. The sub-grid positions |
172 | * are generated automatically according to the \l base property value. The |
173 | * number of sub-grid lines is set to the base value minus one, rounded down. |
174 | * This property is ignored when the base value is zero. Defaults to \c true. |
175 | * |
176 | * \sa base, QValue3DAxis::subSegmentCount |
177 | */ |
178 | void QLogValue3DAxisFormatter::setAutoSubGrid(bool enabled) |
179 | { |
180 | Q_D(QLogValue3DAxisFormatter); |
181 | if (d->m_autoSubGrid != enabled) { |
182 | d->m_autoSubGrid = enabled; |
183 | markDirty(labelsChange: false); |
184 | emit autoSubGridChanged(enabled); |
185 | } |
186 | } |
187 | |
188 | bool QLogValue3DAxisFormatter::autoSubGrid() const |
189 | { |
190 | Q_D(const QLogValue3DAxisFormatter); |
191 | return d->m_autoSubGrid; |
192 | } |
193 | |
194 | /*! |
195 | * \property QLogValue3DAxisFormatter::edgeLabelsVisible |
196 | * |
197 | * \brief Whether the first and last label on the axis are visible. |
198 | * |
199 | * When the \l base property value is non-zero, the whole axis range is often |
200 | * not equally divided into |
201 | * segments. The first and last segments are often smaller than the other |
202 | * segments. In extreme cases, this can lead to overlapping labels on the first |
203 | * and last two grid lines. By setting this property to \c false, you can |
204 | * suppress showing the minimum and maximum labels for the axis in cases where |
205 | * the segments do not exactly fit the axis. Defaults to \c true. |
206 | * |
207 | * \sa base, QAbstract3DAxis::labels |
208 | */ |
209 | void QLogValue3DAxisFormatter::setEdgeLabelsVisible(bool enabled) |
210 | { |
211 | Q_D(QLogValue3DAxisFormatter); |
212 | if (d->m_edgeLabelsVisible != enabled) { |
213 | d->m_edgeLabelsVisible = enabled; |
214 | markDirty(labelsChange: true); |
215 | emit edgeLabelsVisibleChanged(enabled); |
216 | } |
217 | } |
218 | |
219 | bool QLogValue3DAxisFormatter::edgeLabelsVisible() const |
220 | { |
221 | Q_D(const QLogValue3DAxisFormatter); |
222 | return d->m_edgeLabelsVisible; |
223 | } |
224 | |
225 | /*! |
226 | * \internal |
227 | */ |
228 | QValue3DAxisFormatter *QLogValue3DAxisFormatter::createNewInstance() const |
229 | { |
230 | return new QLogValue3DAxisFormatter(); |
231 | } |
232 | |
233 | /*! |
234 | * \internal |
235 | */ |
236 | void QLogValue3DAxisFormatter::recalculate() |
237 | { |
238 | Q_D(QLogValue3DAxisFormatter); |
239 | d->recalculate(); |
240 | } |
241 | |
242 | /*! |
243 | * \internal |
244 | */ |
245 | float QLogValue3DAxisFormatter::positionAt(float value) const |
246 | { |
247 | Q_D(const QLogValue3DAxisFormatter); |
248 | return d->positionAt(value); |
249 | } |
250 | |
251 | /*! |
252 | * \internal |
253 | */ |
254 | float QLogValue3DAxisFormatter::valueAt(float position) const |
255 | { |
256 | Q_D(const QLogValue3DAxisFormatter); |
257 | return d->valueAt(position); |
258 | } |
259 | |
260 | /*! |
261 | * \internal |
262 | */ |
263 | void QLogValue3DAxisFormatter::populateCopy(QValue3DAxisFormatter ©) |
264 | { |
265 | Q_D(QLogValue3DAxisFormatter); |
266 | QValue3DAxisFormatter::populateCopy(copy); |
267 | d->populateCopy(copy); |
268 | } |
269 | |
270 | // QLogValue3DAxisFormatterPrivate |
271 | QLogValue3DAxisFormatterPrivate::QLogValue3DAxisFormatterPrivate() |
272 | : m_base(10.0) |
273 | , m_logMin(0.0) |
274 | , m_logMax(0.0) |
275 | , m_logRangeNormalizer(0.0) |
276 | , m_autoSubGrid(true) |
277 | , m_edgeLabelsVisible(true) |
278 | , m_evenMinSegment(true) |
279 | , m_evenMaxSegment(true) |
280 | {} |
281 | |
282 | QLogValue3DAxisFormatterPrivate::~QLogValue3DAxisFormatterPrivate() {} |
283 | |
284 | void QLogValue3DAxisFormatterPrivate::recalculate() |
285 | { |
286 | Q_Q(QLogValue3DAxisFormatter); |
287 | // When doing position/value mappings, base doesn't matter, so just use |
288 | // natural logarithm |
289 | m_logMin = qLn(v: qreal(m_min)); |
290 | m_logMax = qLn(v: qreal(m_max)); |
291 | m_logRangeNormalizer = m_logMax - m_logMin; |
292 | |
293 | int subGridCount = m_axis->subSegmentCount() - 1; |
294 | int segmentCount = m_axis->segmentCount(); |
295 | QString labelFormat = m_axis->labelFormat(); |
296 | qreal segmentStep; |
297 | if (m_base > 0.0) { |
298 | // Update parent axis segment counts |
299 | qreal logMin = qLn(v: qreal(m_min)) / qLn(v: m_base); |
300 | qreal logMax = qLn(v: qreal(m_max)) / qLn(v: m_base); |
301 | qreal logRangeNormalizer = logMax - logMin; |
302 | |
303 | qreal minDiff = qCeil(v: logMin) - logMin; |
304 | qreal maxDiff = logMax - qFloor(v: logMax); |
305 | |
306 | m_evenMinSegment = qFuzzyCompare(p1: qreal(0.0), p2: minDiff); |
307 | m_evenMaxSegment = qFuzzyCompare(p1: qreal(0.0), p2: maxDiff); |
308 | |
309 | segmentCount = qRound(d: logRangeNormalizer - minDiff - maxDiff); |
310 | |
311 | if (!m_evenMinSegment) |
312 | segmentCount++; |
313 | if (!m_evenMaxSegment) |
314 | segmentCount++; |
315 | |
316 | segmentStep = 1.0 / logRangeNormalizer; |
317 | |
318 | if (m_autoSubGrid) { |
319 | subGridCount = qCeil(v: m_base) - 2; // -2 for subgrid because subsegment count is base - 1 |
320 | if (subGridCount < 0) |
321 | subGridCount = 0; |
322 | } |
323 | |
324 | m_gridPositions.resize(size: segmentCount + 1); |
325 | m_subGridPositions.resize(size: segmentCount * subGridCount); |
326 | m_labelPositions.resize(size: segmentCount + 1); |
327 | m_labelStrings.clear(); |
328 | m_labelStrings.reserve(asize: segmentCount + 1); |
329 | |
330 | // Calculate segment positions |
331 | int index = 0; |
332 | if (!m_evenMinSegment) { |
333 | m_gridPositions[0] = 0.0f; |
334 | m_labelPositions[0] = 0.0f; |
335 | if (m_edgeLabelsVisible) |
336 | m_labelStrings << q->stringForValue(value: qreal(m_min), format: labelFormat); |
337 | else |
338 | m_labelStrings << QString(); |
339 | index++; |
340 | } |
341 | for (int i = 0; i < segmentCount; i++) { |
342 | float gridValue = float((minDiff + qreal(i)) / qreal(logRangeNormalizer)); |
343 | m_gridPositions[index] = gridValue; |
344 | m_labelPositions[index] = gridValue; |
345 | m_labelStrings << q->stringForValue(value: qPow(x: m_base, y: minDiff + qreal(i) + logMin), |
346 | format: labelFormat); |
347 | index++; |
348 | } |
349 | // Ensure max value doesn't suffer from any rounding errors |
350 | m_gridPositions[segmentCount] = 1.0f; |
351 | m_labelPositions[segmentCount] = 1.0f; |
352 | QString finalLabel; |
353 | if (m_edgeLabelsVisible || m_evenMaxSegment) |
354 | finalLabel = q->stringForValue(value: qreal(m_max), format: labelFormat); |
355 | |
356 | if (m_labelStrings.size() > segmentCount) |
357 | m_labelStrings.replace(i: segmentCount, t: finalLabel); |
358 | else |
359 | m_labelStrings << finalLabel; |
360 | } else { |
361 | // Grid lines and label positions are the same as the parent class, so call |
362 | // parent impl first to populate those |
363 | QValue3DAxisFormatterPrivate::doRecalculate(); |
364 | |
365 | // Label string list needs to be repopulated |
366 | segmentStep = 1.0 / qreal(segmentCount); |
367 | |
368 | m_labelStrings << q->stringForValue(value: qreal(m_min), format: labelFormat); |
369 | for (int i = 1; i < m_labelPositions.size() - 1; i++) |
370 | m_labelStrings[i] = q->stringForValue(value: qExp(v: segmentStep * qreal(i) * m_logRangeNormalizer |
371 | + m_logMin), |
372 | format: labelFormat); |
373 | m_labelStrings << q->stringForValue(value: qreal(m_max), format: labelFormat); |
374 | |
375 | m_evenMaxSegment = true; |
376 | m_evenMinSegment = true; |
377 | } |
378 | |
379 | // Subgrid line positions are logarithmically spaced |
380 | if (subGridCount > 0) { |
381 | float oneSegmentRange = valueAt(position: float(segmentStep)) - m_min; |
382 | float subSegmentStep = oneSegmentRange / float(subGridCount + 1); |
383 | |
384 | // Since the logarithm has the same curvature across whole axis range, we |
385 | // can just calculate subgrid positions for the first segment and replicate |
386 | // them to other segments. |
387 | QList<float> actualSubSegmentSteps(subGridCount); |
388 | |
389 | for (int i = 0; i < subGridCount; i++) { |
390 | float currentSubPosition = positionAt(value: m_min + ((i + 1) * subSegmentStep)); |
391 | actualSubSegmentSteps[i] = currentSubPosition; |
392 | } |
393 | |
394 | float firstPartialSegmentAdjustment = float(segmentStep) - m_gridPositions.at(i: 1); |
395 | for (int i = 0; i < segmentCount; i++) { |
396 | for (int j = 0; j < subGridCount; j++) { |
397 | float position = m_gridPositions.at(i) + actualSubSegmentSteps.at(i: j); |
398 | if (!m_evenMinSegment && i == 0) |
399 | position -= firstPartialSegmentAdjustment; |
400 | if (position > 1.0f) |
401 | position = 1.0f; |
402 | if (position < 0.0f) |
403 | position = 0.0f; |
404 | m_subGridPositions[i * subGridCount + j] = position; |
405 | } |
406 | } |
407 | } |
408 | } |
409 | |
410 | void QLogValue3DAxisFormatterPrivate::populateCopy(QValue3DAxisFormatter ©) const |
411 | { |
412 | QLogValue3DAxisFormatter *logFormatter = static_cast<QLogValue3DAxisFormatter *>(©); |
413 | QLogValue3DAxisFormatterPrivate *priv = logFormatter->d_func(); |
414 | |
415 | priv->m_base = m_base; |
416 | priv->m_logMin = m_logMin; |
417 | priv->m_logMax = m_logMax; |
418 | priv->m_logRangeNormalizer = m_logRangeNormalizer; |
419 | } |
420 | |
421 | float QLogValue3DAxisFormatterPrivate::positionAt(float value) const |
422 | { |
423 | qreal logValue = qLn(v: qreal(value)); |
424 | float retval = float((logValue - m_logMin) / m_logRangeNormalizer); |
425 | |
426 | return retval; |
427 | } |
428 | |
429 | float QLogValue3DAxisFormatterPrivate::valueAt(float position) const |
430 | { |
431 | qreal logValue = (qreal(position) * m_logRangeNormalizer) + m_logMin; |
432 | return float(qExp(v: logValue)); |
433 | } |
434 | |
435 | QT_END_NAMESPACE |
436 | |