1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include <QtGraphs/QBarCategoryAxis> |
5 | #include <private/qbarcategoryaxis_p.h> |
6 | #include <QtCore/QtMath> |
7 | |
8 | QT_BEGIN_NAMESPACE |
9 | /*! |
10 | \class QBarCategoryAxis |
11 | \inmodule QtGraphs |
12 | \ingroup graphs_2D |
13 | \brief The QBarCategoryAxis class adds categories to a graph's axes. |
14 | |
15 | QBarCategoryAxis can be set up to show an axis line with tick marks, grid lines, and shades. |
16 | Categories are drawn between the ticks. |
17 | */ |
18 | |
19 | /*! |
20 | \qmltype BarCategoryAxis |
21 | \nativetype QBarCategoryAxis |
22 | \inqmlmodule QtGraphs |
23 | \ingroup graphs_qml_2D |
24 | \inherits AbstractAxis |
25 | |
26 | \brief Adds categories to a graph's axes. |
27 | |
28 | The BarCategoryAxis type can be set up to show an axis line with tick marks, grid lines, and |
29 | shades. Categories are drawn between the ticks. |
30 | |
31 | The following QML snippet illustrates how to use BarCategoryAxis: |
32 | \code |
33 | GraphsView { |
34 | anchors.fill: parent |
35 | axisX: BarCategoryAxis { |
36 | categories: ["2023", "2024", "2025"] |
37 | lineVisible: false |
38 | } |
39 | axisY: ValueAxis { } |
40 | BarSeries { |
41 | BarSet { |
42 | values: [7, 6, 9] |
43 | } |
44 | } |
45 | } |
46 | \endcode |
47 | */ |
48 | |
49 | /*! |
50 | \property QBarCategoryAxis::categories |
51 | \brief The categories of an axis. |
52 | */ |
53 | /*! |
54 | \qmlproperty list BarCategoryAxis::categories |
55 | The categories of an axis. |
56 | */ |
57 | |
58 | /*! |
59 | \property QBarCategoryAxis::min |
60 | \brief The minimum value on the axis. |
61 | |
62 | The minimum value on the axis. The given value should be contained in \c categories. |
63 | */ |
64 | /*! |
65 | \qmlproperty string BarCategoryAxis::min |
66 | The minimum value on the axis. The given value should be contained in \c categories. |
67 | */ |
68 | |
69 | /*! |
70 | \property QBarCategoryAxis::max |
71 | \brief The maximum value on the axis. |
72 | |
73 | The maximum value on the axis. The given value should be contained in \c categories. |
74 | */ |
75 | /*! |
76 | \qmlproperty string BarCategoryAxis::max |
77 | The maximum value on the axis. The given value should be contained in \c categories. |
78 | */ |
79 | |
80 | /*! |
81 | \property QBarCategoryAxis::count |
82 | \brief The number of categories of an axis. |
83 | */ |
84 | /*! |
85 | \qmlproperty int BarCategoryAxis::count |
86 | The number of categories of an axis. |
87 | */ |
88 | |
89 | /*! |
90 | \qmlsignal BarCategoryAxis::categoriesChanged() |
91 | This signal is emitted when the categories of the axis change. |
92 | */ |
93 | |
94 | /*! |
95 | \qmlsignal BarCategoryAxis::minChanged(string min) |
96 | This signal is emitted when the axis minimum value changes to \a min. |
97 | */ |
98 | |
99 | /*! |
100 | \qmlsignal BarCategoryAxis::maxChanged(string max) |
101 | This signal is emitted when the axis maximum value changes to \a max. |
102 | */ |
103 | |
104 | /*! |
105 | \qmlsignal BarCategoryAxis::countChanged() |
106 | This signal is emitted when the number of categories of an axis changes. |
107 | */ |
108 | |
109 | /*! |
110 | \qmlsignal BarCategoryAxis::categoryRangeChanged(string min, string max) |
111 | This signal is emitted when the range of categories of the axis changes. \a min and \a max are |
112 | the min and max of the new range. |
113 | */ |
114 | |
115 | /*! |
116 | Constructs an axis object that is the child of \a parent. |
117 | */ |
118 | QBarCategoryAxis::QBarCategoryAxis(QObject *parent) |
119 | : QAbstractAxis(*(new QBarCategoryAxisPrivate), parent) |
120 | { |
121 | } |
122 | |
123 | /*! |
124 | Destroys the axis object. |
125 | */ |
126 | QBarCategoryAxis::~QBarCategoryAxis() |
127 | { |
128 | } |
129 | |
130 | /*! |
131 | \internal |
132 | */ |
133 | QBarCategoryAxis::QBarCategoryAxis(QBarCategoryAxisPrivate &dd, QObject *parent) |
134 | : QAbstractAxis(dd, parent) |
135 | { |
136 | QObject::connect(sender: this, signal: &QBarCategoryAxis::categoriesChanged, context: this, slot: &QAbstractAxis::update); |
137 | } |
138 | |
139 | /*! |
140 | \qmlmethod BarCategoryAxis::append(list categories) |
141 | Appends \a categories to an axis. The maximum value on the axis will be changed |
142 | to match the last category in \a categories. If no categories were previously defined, |
143 | the minimum value on the axis will also be changed to match the first category in |
144 | \a categories. |
145 | |
146 | A category has to be a valid QString and it cannot be duplicated. Duplicated |
147 | categories will not be appended. |
148 | */ |
149 | /*! |
150 | Appends \a categories to an axis. The maximum value on the axis will be changed |
151 | to match the last category in \a categories. If no categories were previously defined, |
152 | the minimum value on the axis will also be changed to match the first category in |
153 | \a categories. |
154 | |
155 | A category has to be a valid QString and it cannot be duplicated. Duplicated |
156 | categories will not be appended. |
157 | */ |
158 | void QBarCategoryAxis::append(const QStringList &categories) |
159 | { |
160 | if (categories.isEmpty()) |
161 | return; |
162 | |
163 | Q_D(QBarCategoryAxis); |
164 | |
165 | qsizetype count = d->m_categories.size(); |
166 | |
167 | for (const auto &category : categories) { |
168 | if (!d->m_categories.contains(str: category) && !category.isNull()) { |
169 | d->m_categories.append(t: category); |
170 | } |
171 | } |
172 | |
173 | if (d->m_categories.size() == count) |
174 | return; |
175 | |
176 | if (count == 0) |
177 | setRange(minCategory: d->m_categories.first(), maxCategory: d->m_categories.last()); |
178 | else |
179 | setRange(minCategory: d->m_minCategory, maxCategory: d->m_categories.last()); |
180 | |
181 | emit categoriesChanged(); |
182 | emit countChanged(); |
183 | } |
184 | |
185 | /*! |
186 | \qmlmethod BarCategoryAxis::append(string category) |
187 | Appends \a category to an axis. The maximum value on the axis will be changed |
188 | to match the last \a category. If no categories were previously defined, the minimum |
189 | value on the axis will also be changed to match \a category. |
190 | |
191 | A category has to be a valid QString and it cannot be duplicated. Duplicated |
192 | categories will not be appended. |
193 | */ |
194 | /*! |
195 | Appends \a category to an axis. The maximum value on the axis will be changed |
196 | to match the last \a category. If no categories were previously defined, the minimum |
197 | value on the axis will also be changed to match \a category. |
198 | |
199 | A category has to be a valid QString and it cannot be duplicated. Duplicated |
200 | categories will not be appended. |
201 | */ |
202 | void QBarCategoryAxis::append(const QString &category) |
203 | { |
204 | Q_D(QBarCategoryAxis); |
205 | |
206 | qsizetype count = d->m_categories.size(); |
207 | |
208 | if (!d->m_categories.contains(str: category) && !category.isNull()) |
209 | d->m_categories.append(t: category); |
210 | |
211 | if (d->m_categories.size() == count) |
212 | return; |
213 | |
214 | if (count == 0) |
215 | setRange(minCategory: d->m_categories.last(), maxCategory: d->m_categories.last()); |
216 | else |
217 | setRange(minCategory: d->m_minCategory, maxCategory: d->m_categories.last()); |
218 | |
219 | emit categoriesChanged(); |
220 | emit countChanged(); |
221 | } |
222 | |
223 | /*! |
224 | \qmlmethod BarCategoryAxis::remove(string category) |
225 | Removes \a category from the axis. Removing a category that currently sets the |
226 | maximum or minimum value on the axis will affect the axis range. |
227 | */ |
228 | /*! |
229 | Removes \a category from the axis. Removing a category that currently sets the |
230 | maximum or minimum value on the axis will affect the axis range. |
231 | */ |
232 | void QBarCategoryAxis::remove(const QString &category) |
233 | { |
234 | Q_D(QBarCategoryAxis); |
235 | |
236 | if (d->m_categories.contains(str: category)) { |
237 | d->m_categories.removeAt(i: d->m_categories.indexOf(str: category)); |
238 | if (!d->m_categories.isEmpty()) { |
239 | if (d->m_minCategory == category) { |
240 | setRange(minCategory: d->m_categories.first(), maxCategory: d->m_maxCategory); |
241 | } else if (d->m_maxCategory == category) { |
242 | setRange(minCategory: d->m_minCategory, maxCategory: d->m_categories.last()); |
243 | } |
244 | } else { |
245 | setRange(minCategory: QString(), maxCategory: QString()); |
246 | } |
247 | emit categoriesChanged(); |
248 | emit countChanged(); |
249 | } |
250 | } |
251 | |
252 | /*! |
253 | \qmlmethod BarCategoryAxis::remove(int index) |
254 | Removes a category at \a index from the axis. Removing a category that currently sets the |
255 | maximum or minimum value on the axis will affect the axis range. |
256 | */ |
257 | /*! |
258 | Removes a category at \a index from the axis. Removing a category that currently sets the |
259 | maximum or minimum value on the axis will affect the axis range. |
260 | */ |
261 | void QBarCategoryAxis::remove(qsizetype index) |
262 | { |
263 | Q_D(QBarCategoryAxis); |
264 | if (index < 0 || index >= d->m_categories.size()) |
265 | return; |
266 | remove(category: d->m_categories.at(i: index)); |
267 | } |
268 | |
269 | /*! |
270 | \qmlmethod BarCategoryAxis::insert(int index, string category) |
271 | Inserts \a category to the axis at \a index. \a category has to be a valid QString |
272 | and it cannot be duplicated. If \a category is prepended or appended to other |
273 | categories, the minimum and maximum values on the axis are updated accordingly. |
274 | */ |
275 | /*! |
276 | Inserts \a category to the axis at \a index. \a category has to be a valid QString |
277 | and it cannot be duplicated. If \a category is prepended or appended to other |
278 | categories, the minimum and maximum values on the axis are updated accordingly. |
279 | */ |
280 | void QBarCategoryAxis::insert(qsizetype index, const QString &category) |
281 | { |
282 | Q_D(QBarCategoryAxis); |
283 | |
284 | qsizetype count = d->m_categories.size(); |
285 | |
286 | if (!d->m_categories.contains(str: category) && !category.isNull()) |
287 | d->m_categories.insert(i: index, t: category); |
288 | |
289 | if (d->m_categories.size() == count) |
290 | return; |
291 | |
292 | if (count == 0) { |
293 | setRange(minCategory: d->m_categories.first(), maxCategory: d->m_categories.first()); |
294 | } else if (index == 0) { |
295 | setRange(minCategory: d->m_categories.first(), maxCategory: d->m_maxCategory); |
296 | } else if (index == count) { |
297 | setRange(minCategory: d->m_minCategory, maxCategory: d->m_categories.last()); |
298 | } |
299 | |
300 | emit categoriesChanged(); |
301 | emit countChanged(); |
302 | } |
303 | |
304 | /*! |
305 | \qmlmethod BarCategoryAxis::replace(string oldCategory, string newCategory) |
306 | Replaces \a oldCategory with \a newCategory. If \a oldCategory does not exist on the axis, |
307 | nothing is done. \a newCategory has to be a valid QString and it cannot be duplicated. If |
308 | the minimum or maximum category is replaced, the minimum and maximum values on the axis are |
309 | updated accordingly. |
310 | */ |
311 | /*! |
312 | Replaces \a oldCategory with \a newCategory. If \a oldCategory does not exist on the axis, |
313 | nothing is done. \a newCategory has to be a valid QString and it cannot be duplicated. If |
314 | the minimum or maximum category is replaced, the minimum and maximum values on the axis are |
315 | updated accordingly. |
316 | */ |
317 | void QBarCategoryAxis::replace(const QString &oldCategory, const QString &newCategory) |
318 | { |
319 | Q_D(QBarCategoryAxis); |
320 | |
321 | qsizetype pos = d->m_categories.indexOf(str: oldCategory); |
322 | |
323 | if (pos != -1 && !d->m_categories.contains(str: newCategory) && !newCategory.isNull()) { |
324 | d->m_categories.replace(i: pos, t: newCategory); |
325 | if (d->m_minCategory == oldCategory) |
326 | setRange(minCategory: newCategory, maxCategory: d->m_maxCategory); |
327 | else if (d->m_maxCategory == oldCategory) |
328 | setRange(minCategory: d->m_minCategory, maxCategory: newCategory); |
329 | |
330 | emit categoriesChanged(); |
331 | emit countChanged(); |
332 | } |
333 | } |
334 | |
335 | /*! |
336 | \qmlmethod BarCategoryAxis::clear() |
337 | Removes all categories. Sets the maximum and minimum values of the axis range to QString::null. |
338 | */ |
339 | /*! |
340 | Removes all categories. Sets the maximum and minimum values of the axis range to QString::null. |
341 | */ |
342 | void QBarCategoryAxis::clear() |
343 | { |
344 | Q_D(QBarCategoryAxis); |
345 | d->m_categories.clear(); |
346 | setRange(minCategory: QString(), maxCategory: QString()); |
347 | emit categoriesChanged(); |
348 | emit countChanged(); |
349 | } |
350 | |
351 | /*! |
352 | Sets \a categories and discards the old ones. The axis range is adjusted to match the |
353 | first and last category in \a categories. |
354 | |
355 | A category has to be a valid QString and it cannot be duplicated. |
356 | */ |
357 | void QBarCategoryAxis::setCategories(const QStringList &categories) |
358 | { |
359 | Q_D(QBarCategoryAxis); |
360 | d->m_categories.clear(); |
361 | d->m_minCategory = QString(); |
362 | d->m_maxCategory = QString(); |
363 | d->m_min = 0; |
364 | d->m_max = 0; |
365 | d->m_count = 0; |
366 | append(categories); |
367 | } |
368 | |
369 | /*! |
370 | Returns categories. |
371 | */ |
372 | QStringList QBarCategoryAxis::categories() |
373 | { |
374 | Q_D(QBarCategoryAxis); |
375 | return d->m_categories; |
376 | } |
377 | |
378 | /*! |
379 | Returns the number of categories. |
380 | */ |
381 | qsizetype QBarCategoryAxis::count() const |
382 | { |
383 | Q_D(const QBarCategoryAxis); |
384 | return d->m_categories.size(); |
385 | } |
386 | |
387 | /*! |
388 | \qmlmethod string BarCategoryAxis::at(int index) |
389 | Returns the category at \a index. |
390 | */ |
391 | /*! |
392 | Returns the category at \a index. |
393 | */ |
394 | QString QBarCategoryAxis::at(qsizetype index) const |
395 | { |
396 | Q_D(const QBarCategoryAxis); |
397 | if (index < 0 || index >= d->m_categories.size()) |
398 | return QString(); |
399 | return d->m_categories.at(i: index); |
400 | } |
401 | |
402 | /*! |
403 | Sets the minimum category to \a min. |
404 | */ |
405 | void QBarCategoryAxis::setMin(const QString &min) |
406 | { |
407 | Q_D(QBarCategoryAxis); |
408 | d->setRange(minCategory: min, maxCategory: d->m_maxCategory); |
409 | } |
410 | |
411 | /*! |
412 | Returns the minimum category. |
413 | */ |
414 | QString QBarCategoryAxis::min() const |
415 | { |
416 | Q_D(const QBarCategoryAxis); |
417 | return d->m_minCategory; |
418 | } |
419 | |
420 | /*! |
421 | Sets the maximum category to \a max. |
422 | */ |
423 | void QBarCategoryAxis::setMax(const QString &max) |
424 | { |
425 | Q_D(QBarCategoryAxis); |
426 | d->setRange(minCategory: d->m_minCategory, maxCategory: max); |
427 | } |
428 | |
429 | /*! |
430 | Returns the maximum category. |
431 | */ |
432 | QString QBarCategoryAxis::max() const |
433 | { |
434 | Q_D(const QBarCategoryAxis); |
435 | return d->m_maxCategory; |
436 | } |
437 | |
438 | /*! |
439 | Sets the axis range from \a minCategory to \a maxCategory. |
440 | */ |
441 | void QBarCategoryAxis::setRange(const QString &minCategory, const QString &maxCategory) |
442 | { |
443 | Q_D(QBarCategoryAxis); |
444 | d->setRange(minCategory,maxCategory); |
445 | } |
446 | |
447 | /*! |
448 | Returns the type of the axis. |
449 | */ |
450 | QAbstractAxis::AxisType QBarCategoryAxis::type() const |
451 | { |
452 | return QAbstractAxis::AxisType::BarCategory; |
453 | } |
454 | |
455 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
456 | |
457 | QBarCategoryAxisPrivate::QBarCategoryAxisPrivate() |
458 | : m_min(0.0) |
459 | , m_max(0.0) |
460 | , m_count(0) |
461 | { |
462 | |
463 | } |
464 | |
465 | QBarCategoryAxisPrivate::~QBarCategoryAxisPrivate() |
466 | { |
467 | |
468 | } |
469 | |
470 | void QBarCategoryAxisPrivate::setMin(const QVariant &min) |
471 | { |
472 | setRange(min, max: m_maxCategory); |
473 | } |
474 | |
475 | void QBarCategoryAxisPrivate::setMax(const QVariant &max) |
476 | { |
477 | setRange(min: m_minCategory, max); |
478 | } |
479 | |
480 | void QBarCategoryAxisPrivate::setRange(const QVariant &min, const QVariant &max) |
481 | { |
482 | QString value1 = min.toString(); |
483 | QString value2 = max.toString(); |
484 | setRange(minCategory: value1, maxCategory: value2); |
485 | } |
486 | |
487 | void QBarCategoryAxisPrivate::setRange(qreal min, qreal max) |
488 | { |
489 | Q_Q(QBarCategoryAxis); |
490 | |
491 | bool categoryChanged = false; |
492 | bool changed = false; |
493 | |
494 | if (min > max) |
495 | return; |
496 | |
497 | if (!qFuzzyIsNull(d: m_min - min)) { |
498 | m_min = min; |
499 | changed = true; |
500 | |
501 | int imin = m_min + 0.5; |
502 | if (imin >= 0 && imin < m_categories.size()) { |
503 | QString minCategory = m_categories.at(i: imin); |
504 | if (m_minCategory != minCategory && !minCategory.isEmpty()) { |
505 | m_minCategory = minCategory; |
506 | categoryChanged = true; |
507 | emit q->minChanged(min: minCategory); |
508 | } |
509 | } |
510 | |
511 | } |
512 | |
513 | if (!qFuzzyIsNull(d: m_max - max)) { |
514 | m_max = max; |
515 | changed = true; |
516 | |
517 | int imax = m_max - 0.5; |
518 | if (imax >= 0 && imax < m_categories.size()) { |
519 | QString maxCategory = m_categories.at(i: imax); |
520 | if (m_maxCategory != maxCategory && !maxCategory.isEmpty()) { |
521 | m_maxCategory = maxCategory; |
522 | categoryChanged = true; |
523 | emit q->maxChanged(max: maxCategory); |
524 | } |
525 | } |
526 | } |
527 | |
528 | if (categoryChanged){ |
529 | emit q->categoryRangeChanged(min: m_minCategory, max: m_maxCategory); |
530 | } |
531 | |
532 | if (changed) { |
533 | emit q->rangeChanged(min: m_min, max: m_max); |
534 | } |
535 | } |
536 | |
537 | void QBarCategoryAxisPrivate::setRange(const QString &minCategory, const QString &maxCategory) |
538 | { |
539 | Q_Q(QBarCategoryAxis); |
540 | bool changed = false; |
541 | |
542 | //special case in case or clearing all categories |
543 | if (minCategory.isNull() && maxCategory.isNull()) { |
544 | m_minCategory = minCategory; |
545 | m_maxCategory = maxCategory; |
546 | m_min = 0; |
547 | m_max = 0; |
548 | m_count = 0; |
549 | emit q->minChanged(min: minCategory); |
550 | emit q->maxChanged(max: maxCategory); |
551 | emit q->categoryRangeChanged(min: m_minCategory, max: m_maxCategory); |
552 | emit q->rangeChanged(min: m_min, max: m_max); |
553 | return; |
554 | } |
555 | |
556 | if (m_categories.indexOf(str: maxCategory) < m_categories.indexOf(str: minCategory)) |
557 | return; |
558 | |
559 | if (!minCategory.isNull() && (m_minCategory != minCategory || m_minCategory.isNull()) |
560 | && m_categories.contains(str: minCategory)) { |
561 | m_minCategory = minCategory; |
562 | m_min = m_categories.indexOf(str: m_minCategory) - 0.5; |
563 | changed = true; |
564 | emit q->minChanged(min: minCategory); |
565 | } |
566 | |
567 | if (!maxCategory.isNull() && (m_maxCategory != maxCategory || m_maxCategory.isNull()) |
568 | && m_categories.contains(str: maxCategory)) { |
569 | m_maxCategory = maxCategory; |
570 | m_max = m_categories.indexOf(str: m_maxCategory) + 0.5; |
571 | changed = true; |
572 | emit q->maxChanged(max: maxCategory); |
573 | } |
574 | |
575 | if (changed) { |
576 | m_count = m_max - m_min; |
577 | emit q->categoryRangeChanged(min: m_minCategory, max: m_maxCategory); |
578 | emit q->rangeChanged(min: m_min, max: m_max); |
579 | } |
580 | } |
581 | |
582 | QT_END_NAMESPACE |
583 | |
584 | #include "moc_qbarcategoryaxis.cpp" |
585 | |