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
8QT_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 */
82QLogValue3DAxisFormatter::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 */
94QLogValue3DAxisFormatter::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 */
104QLogValue3DAxisFormatter::~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 */
123void 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
138qreal 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 */
158void 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
168bool 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 */
189void 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
199bool QLogValue3DAxisFormatter::showEdgeLabels() const
200{
201 const Q_D(QLogValue3DAxisFormatter);
202 return d->m_showEdgeLabels;
203}
204
205/*!
206 * \internal
207 */
208QValue3DAxisFormatter *QLogValue3DAxisFormatter::createNewInstance() const
209{
210 return new QLogValue3DAxisFormatter();
211}
212
213/*!
214 * \internal
215 */
216void QLogValue3DAxisFormatter::recalculate()
217{
218 Q_D(QLogValue3DAxisFormatter);
219 d->recalculate();
220}
221
222/*!
223 * \internal
224 */
225float QLogValue3DAxisFormatter::positionAt(float value) const
226{
227 const Q_D(QLogValue3DAxisFormatter);
228 return d->positionAt(value);
229}
230
231/*!
232 * \internal
233 */
234float QLogValue3DAxisFormatter::valueAt(float position) const
235{
236 const Q_D(QLogValue3DAxisFormatter);
237 return d->valueAt(position);
238}
239
240/*!
241 * \internal
242 */
243void QLogValue3DAxisFormatter::populateCopy(QValue3DAxisFormatter &copy)
244{
245 Q_D(QLogValue3DAxisFormatter);
246 QValue3DAxisFormatter::populateCopy(copy);
247 d->populateCopy(copy);
248}
249
250// QLogValue3DAxisFormatterPrivate
251QLogValue3DAxisFormatterPrivate::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
264QLogValue3DAxisFormatterPrivate::~QLogValue3DAxisFormatterPrivate()
265{
266}
267
268void 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
392void QLogValue3DAxisFormatterPrivate::populateCopy(QValue3DAxisFormatter &copy) const
393{
394 QLogValue3DAxisFormatter *logFormatter = static_cast<QLogValue3DAxisFormatter *>(&copy);
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
403float 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
411float QLogValue3DAxisFormatterPrivate::valueAt(float position) const
412{
413 qreal logValue = (qreal(position) * m_logRangeNormalizer) + m_logMin;
414 return float(qExp(v: logValue));
415}
416
417QT_END_NAMESPACE
418

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