| 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 |  |