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