1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Charts module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 or (at your option) any later version |
20 | ** approved by the KDE Free Qt Foundation. The licenses are as published by |
21 | ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
22 | ** included in the packaging of this file. Please review the following |
23 | ** information to ensure the GNU General Public License requirements will |
24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
25 | ** |
26 | ** $QT_END_LICENSE$ |
27 | ** |
28 | ****************************************************************************/ |
29 | |
30 | #include "chart-widget.h" |
31 | |
32 | #include <QtCharts/QLineSeries> |
33 | #include <QtCharts/QLegend> |
34 | #include <QtCharts/QBarCategoryAxis> |
35 | #include <QtCharts/QValueAxis> |
36 | #include <QtCharts/QLogValueAxis> |
37 | #include <QtCharts/QBarSet> |
38 | #include <QtCharts/QHorizontalBarSeries> |
39 | #include <QtCharts/QHorizontalPercentBarSeries> |
40 | #include <QtCharts/QHorizontalStackedBarSeries> |
41 | #include <QtCharts/QPercentBarSeries> |
42 | #include <QtCharts/QStackedBarSeries> |
43 | #include <QElapsedTimer> |
44 | #include <QDebug> |
45 | |
46 | //#define VALUE_LOGGING 1 |
47 | |
48 | const int initialCount = 100000; |
49 | const int visibleCount = 20; |
50 | const int storeCount = 100000000; |
51 | const int interval = 500; |
52 | |
53 | const int initialSeriesCount = 2; |
54 | const int maxSeriesCount = 2; |
55 | const bool removeSeries = false; |
56 | const int = 7; |
57 | |
58 | const int initialSetCount = 3; |
59 | const int maxSetCount = 3; |
60 | const bool removeSets = false; |
61 | const int = 3; |
62 | |
63 | const bool initialLabels = true; |
64 | const int labelsTrigger = -1; |
65 | const int visibleTrigger = -1; |
66 | const int appendFrequency = 1; |
67 | const int animationTrigger = -1; |
68 | const bool = false; |
69 | |
70 | const bool animation = true; |
71 | const int animationDuration = 300; |
72 | |
73 | const bool horizontal = false; |
74 | const bool percent = false; |
75 | const bool stacked = true; |
76 | |
77 | const bool negativeValues = false; |
78 | const bool mixedValues = false; |
79 | |
80 | // Negative indexes are counted from end of the set. |
81 | const bool doReplace = false; |
82 | const bool doRemove = false; |
83 | const bool doInsert = false; |
84 | const bool singleReplace = false; |
85 | const bool singleRemove = false; |
86 | const bool singleInsert = false; |
87 | const int removeIndex = -7; |
88 | const int replaceIndex = -3; |
89 | const int insertIndex = -5; |
90 | |
91 | const bool logarithmic = false; |
92 | const bool barCategories = false; |
93 | |
94 | const qreal barWidth = 0.9; |
95 | |
96 | // Note: reverse axes are not fully supported for bars (animation and label positioning break a bit) |
97 | const bool reverseBar = false; |
98 | const bool reverseValue = false; |
99 | |
100 | static int counter = 1; |
101 | static const QString nameTemplate = QStringLiteral("Set %1/%2" ); |
102 | |
103 | ChartWidget::ChartWidget(QWidget *parent) : |
104 | QWidget(parent), |
105 | m_chart(new QChart()), |
106 | m_chartView(new QChartView(this)), |
107 | m_setCount(initialSetCount), |
108 | m_seriesCount(initialSeriesCount), |
109 | m_extraScroll(0.0) |
110 | { |
111 | m_elapsedTimer.start(); |
112 | |
113 | if (logarithmic) { |
114 | QLogValueAxis *logAxis = new QLogValueAxis; |
115 | logAxis->setBase(2); |
116 | m_valueAxis = logAxis; |
117 | } else { |
118 | m_valueAxis = new QValueAxis; |
119 | } |
120 | |
121 | if (barCategories) |
122 | m_barAxis = new QBarCategoryAxis; |
123 | else |
124 | m_barAxis = new QValueAxis; |
125 | |
126 | m_barAxis->setReverse(reverseBar); |
127 | m_valueAxis->setReverse(reverseValue); |
128 | |
129 | for (int i = 0; i < maxSeriesCount; i++) { |
130 | if (horizontal) { |
131 | if (percent) |
132 | m_series.append(t: new QHorizontalPercentBarSeries); |
133 | else if (stacked) |
134 | m_series.append(t: new QHorizontalStackedBarSeries); |
135 | else |
136 | m_series.append(t: new QHorizontalBarSeries); |
137 | } else { |
138 | if (percent) |
139 | m_series.append(t: new QPercentBarSeries); |
140 | else if (stacked) |
141 | m_series.append(t: new QStackedBarSeries); |
142 | else |
143 | m_series.append(t: new QBarSeries); |
144 | } |
145 | QAbstractBarSeries *series = |
146 | qobject_cast<QAbstractBarSeries *>(object: m_series.at(i: m_series.size() - 1)); |
147 | QString seriesNameTemplate = QStringLiteral("bar %1" ); |
148 | series->setName(seriesNameTemplate.arg(a: i)); |
149 | series->setLabelsPosition(QAbstractBarSeries::LabelsInsideEnd); |
150 | series->setLabelsVisible(initialLabels); |
151 | series->setBarWidth(barWidth); |
152 | } |
153 | |
154 | resize(w: 800, h: 300); |
155 | m_horizontalLayout = new QHBoxLayout(this); |
156 | m_horizontalLayout->setSpacing(6); |
157 | m_horizontalLayout->setContentsMargins(left: 11, top: 11, right: 11, bottom: 11); |
158 | m_horizontalLayout->addWidget(m_chartView); |
159 | |
160 | qDebug() << "UI setup time:" << m_elapsedTimer.restart(); |
161 | |
162 | m_chartView->setRenderHint(hint: QPainter::Antialiasing); |
163 | |
164 | createChart(); |
165 | qDebug() << "Chart creation time:" << m_elapsedTimer.restart(); |
166 | |
167 | QObject::connect(sender: &m_timer, SIGNAL(timeout()), receiver: this, SLOT(handleTimeout())); |
168 | |
169 | m_timer.setInterval(interval); |
170 | m_timer.start(); |
171 | } |
172 | |
173 | ChartWidget::~ChartWidget() |
174 | { |
175 | } |
176 | |
177 | void ChartWidget::handleTimeout() |
178 | { |
179 | qDebug() << "Intervening time:" << m_elapsedTimer.restart(); |
180 | |
181 | qDebug() << "----------" << counter << "----------" ; |
182 | |
183 | bool doScroll = false; |
184 | |
185 | if (counter % appendFrequency == 0) { |
186 | for (int i = 0; i < maxSeriesCount; i++) { |
187 | for (int j = 0; j < maxSetCount; j++) { |
188 | QBarSet *set = m_sets.value(akey: m_series.at(i)).at(i: j); |
189 | qreal value = 5 * i + (counter % 100) / qreal(j+1); |
190 | if (negativeValues) |
191 | set->append(value: -value); |
192 | else if (mixedValues) |
193 | set->append(value: counter % 2 ? value : -value); |
194 | else |
195 | set->append(value); |
196 | #ifdef VALUE_LOGGING |
197 | qDebug() << "Appended value:" << set->at(set->count() - 1) |
198 | << "to series:" << i |
199 | << "to set:" << j |
200 | << "at index:" << set->count() - 1; |
201 | #endif |
202 | if (set->count() > storeCount) |
203 | set->remove(index: 0, count: set->count() - storeCount); |
204 | if (set->count() > visibleCount) |
205 | doScroll = true; |
206 | } |
207 | } |
208 | qDebug() << "Append time:" << m_elapsedTimer.restart(); |
209 | } |
210 | |
211 | if (doScroll) { |
212 | doScroll = false; |
213 | qreal scrollAmount = horizontal ? m_chart->plotArea().height() / visibleCount |
214 | : m_chart->plotArea().width() / visibleCount; |
215 | // Charts can't scroll without any series, so store the required scroll in those cases |
216 | if (m_seriesCount == 0) { |
217 | m_extraScroll += scrollAmount; |
218 | } else { |
219 | if (horizontal) |
220 | m_chart->scroll(dx: 0, dy: scrollAmount + m_extraScroll); |
221 | else |
222 | m_chart->scroll(dx: scrollAmount + m_extraScroll, dy: 0); |
223 | m_extraScroll = 0.0; |
224 | } |
225 | qDebug() << "Scroll time:" << m_elapsedTimer.restart(); |
226 | } |
227 | |
228 | if (doRemove || doReplace || doInsert) { |
229 | for (int i = 0; i < maxSeriesCount; i++) { |
230 | for (int j = 0; j < m_setCount; j++) { |
231 | QBarSet *set = m_sets.value(akey: m_series.at(i)).at(i: j); |
232 | qreal value = ((counter + 20) % 100) / qreal(j + 1); |
233 | if (doReplace && (!singleReplace || j == 0)) { |
234 | int index = replaceIndex < 0 ? set->count() + replaceIndex : replaceIndex; |
235 | set->replace(index, value); |
236 | } |
237 | if (doRemove && (!singleRemove || j == 0)) { |
238 | int index = removeIndex < 0 ? set->count() + removeIndex : removeIndex; |
239 | set->remove(index, count: 1); |
240 | } |
241 | if (doInsert && (!singleInsert || j == 0)) { |
242 | int index = insertIndex < 0 ? set->count() + insertIndex : insertIndex; |
243 | set->insert(index, value); |
244 | } |
245 | } |
246 | } |
247 | qDebug() << "R/R time:" << m_elapsedTimer.restart(); |
248 | } |
249 | |
250 | if (counter % extraSeriesFrequency == 0) { |
251 | if (m_seriesCount <= maxSeriesCount) { |
252 | qDebug() << "Adjusting series count, current count:" << m_seriesCount; |
253 | static int seriesCountAdder = 1; |
254 | if (m_seriesCount == maxSeriesCount) { |
255 | if (removeSeries) |
256 | seriesCountAdder = -1; |
257 | else |
258 | seriesCountAdder = 0; |
259 | } else if (m_seriesCount == 0) { |
260 | seriesCountAdder = 1; |
261 | } |
262 | if (seriesCountAdder < 0) |
263 | m_chart->removeSeries(series: m_series.at(i: m_seriesCount - 1)); |
264 | else if (m_seriesCount < maxSeriesCount) |
265 | addSeriesToChart(series: m_series.at(i: m_seriesCount)); |
266 | m_seriesCount += seriesCountAdder; |
267 | } |
268 | } |
269 | |
270 | if (counter % extraSetFrequency == 0) { |
271 | if (m_setCount <= maxSetCount) { |
272 | qDebug() << "Adjusting setcount, current count:" << m_setCount; |
273 | static int setCountAdder = 1; |
274 | if (m_setCount == maxSetCount) { |
275 | if (removeSets) |
276 | setCountAdder = -1; |
277 | else |
278 | setCountAdder = 0; |
279 | } else if (m_setCount == 0) { |
280 | setCountAdder = 1; |
281 | } |
282 | for (int i = 0; i < maxSeriesCount; i++) { |
283 | if (setCountAdder < 0) { |
284 | int barCount = m_sets.value(akey: m_series.at(i)).at(i: m_setCount - 1)->count(); |
285 | m_series.at(i)->remove(set: m_sets.value(akey: m_series.at(i)).at(i: m_setCount - 1)); |
286 | // Since remove deletes the set, recreate it in our list |
287 | QBarSet *set = new QBarSet(nameTemplate.arg(a: i).arg(a: m_setCount - 1)); |
288 | m_sets[m_series.at(i)][m_setCount - 1] = set; |
289 | set->setLabelBrush(QColor("black" )); |
290 | set->setPen(QPen(QColor("black" ), 0.3)); |
291 | QList<qreal> valueList; |
292 | valueList.reserve(alloc: barCount); |
293 | for (int j = 0; j < barCount; ++j) { |
294 | qreal value = counter % 100; |
295 | if (negativeValues) |
296 | valueList.append(t: -value); |
297 | else if (mixedValues) |
298 | valueList.append(t: counter % 2 ? value : -value); |
299 | else |
300 | valueList.append(t: value); |
301 | } |
302 | set->append(values: valueList); |
303 | |
304 | } else if (m_setCount < maxSetCount) { |
305 | m_series.at(i)->append(set: m_sets.value(akey: m_series.at(i)).at(i: m_setCount)); |
306 | } |
307 | } |
308 | m_setCount += setCountAdder; |
309 | } |
310 | } |
311 | |
312 | if (labelsTrigger > 0 && counter % labelsTrigger == 0) { |
313 | m_series.at(i: 0)->setLabelsVisible(!m_series.at(i: 0)->isLabelsVisible()); |
314 | qDebug() << "Label visibility changed" ; |
315 | } |
316 | |
317 | if (visibleTrigger > 0 && counter % visibleTrigger == 0) { |
318 | m_series.at(i: 0)->setVisible(!m_series.at(i: 0)->isVisible()); |
319 | qDebug() << "Series visibility changed" ; |
320 | } |
321 | |
322 | if (animationTrigger > 0 && counter % animationTrigger == 0) { |
323 | if (m_chart->animationOptions() == QChart::SeriesAnimations) |
324 | m_chart->setAnimationOptions(QChart::NoAnimation); |
325 | else |
326 | m_chart->setAnimationOptions(QChart::SeriesAnimations); |
327 | qDebug() << "Series animation changed" ; |
328 | } |
329 | |
330 | qDebug() << "Rest of time:" << m_elapsedTimer.restart(); |
331 | |
332 | qDebug() << "GraphicsItem Count:" << m_chart->scene()->items().size(); |
333 | counter++; |
334 | } |
335 | |
336 | void ChartWidget::createChart() |
337 | { |
338 | qDebug() << "Initial bar count:" << initialCount; |
339 | |
340 | QList<qreal> valueList; |
341 | valueList.reserve(alloc: initialCount); |
342 | for (int j = 0; j < initialCount; ++j) { |
343 | qreal value = counter++ % 100; |
344 | if (negativeValues) |
345 | valueList.append(t: -value); |
346 | else if (mixedValues) |
347 | valueList.append(t: counter % 2 ? value : -value); |
348 | else |
349 | valueList.append(t: value); |
350 | } |
351 | |
352 | for (int i = 0; i < maxSeriesCount; i++) { |
353 | for (int j = 0; j < maxSetCount; j++) { |
354 | QBarSet *set = new QBarSet(nameTemplate.arg(a: i).arg(a: j)); |
355 | m_sets[m_series.at(i)].append(t: set); |
356 | set->setLabelBrush(QColor("black" )); |
357 | set->setPen(QPen(QColor("black" ), 0.3)); |
358 | if (sameNumberOfBars) { |
359 | set->append(values: valueList); |
360 | } else { |
361 | QList<qreal> tempList = valueList; |
362 | for (int k = 0; k < j; k++) |
363 | tempList.removeLast(); |
364 | set->append(values: tempList); |
365 | } |
366 | if (j < m_setCount) |
367 | m_series.at(i)->append(set); |
368 | } |
369 | } |
370 | for (int i = 0; i < initialSeriesCount; i++) |
371 | addSeriesToChart(series: m_series.at(i)); |
372 | |
373 | m_chart->setTitle("Chart" ); |
374 | if (animation) { |
375 | m_chart->setAnimationOptions(QChart::SeriesAnimations); |
376 | m_chart->setAnimationDuration(animationDuration); |
377 | } |
378 | |
379 | if (barCategories) { |
380 | QBarCategoryAxis *barCatAxis = qobject_cast<QBarCategoryAxis *>(object: m_barAxis); |
381 | QStringList categories; |
382 | const int count = qMax(a: initialCount, b: visibleCount); |
383 | for (int i = 0; i < count; i++) |
384 | categories.append(t: QString::number(i)); |
385 | barCatAxis->setCategories(categories); |
386 | } else { |
387 | qobject_cast<QValueAxis *>(object: m_barAxis)->setTickCount(11); |
388 | } |
389 | |
390 | if (initialCount > visibleCount) |
391 | m_barAxis->setRange(min: initialCount - visibleCount, max: initialCount); |
392 | else |
393 | m_barAxis->setRange(min: 0, max: visibleCount); |
394 | |
395 | qreal rangeValue = stacked ? 200.0 : 100.0; |
396 | m_valueAxis->setRange(min: logarithmic ? 1.0 : (negativeValues || mixedValues) ? -rangeValue : 0.0, |
397 | max: (!negativeValues || mixedValues) ? rangeValue : 0.0); |
398 | |
399 | m_chartView->setChart(m_chart); |
400 | } |
401 | |
402 | void ChartWidget::addSeriesToChart(QAbstractBarSeries *series) |
403 | { |
404 | qDebug() << "Adding series:" << series->name(); |
405 | |
406 | // HACK: Temporarily take the sets out of the series until axes are set. |
407 | // This is done because added series defaults to a domain that displays all bars, which can |
408 | // get extremely slow as the bar count increases. |
409 | QList<QBarSet *> sets = series->barSets(); |
410 | for (auto set : sets) |
411 | series->take(set); |
412 | |
413 | m_chart->addSeries(series); |
414 | if (horizontal) { |
415 | m_chart->setAxisX(axis: m_valueAxis,series); |
416 | m_chart->setAxisY(axis: m_barAxis, series); |
417 | } else { |
418 | m_chart->setAxisX(axis: m_barAxis, series); |
419 | m_chart->setAxisY(axis: m_valueAxis, series); |
420 | } |
421 | |
422 | series->append(sets); |
423 | } |
424 | |