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

source code of qtgraphs/src/graphs3d/axis/qlogvalue3daxisformatter.cpp