File indexing completed on 2024-12-15 03:44:59

0001 /*
0002     SPDX-FileCopyrightText: 2017 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "categoryaggregator.h"
0008 #include "chartutil.h"
0009 
0010 #include <model/categoryaggregationmodel.h>
0011 #include <model/extrarowsproxymodel.h>
0012 #include <model/rolemappingproxymodel.h>
0013 #include <model/singlerowfilterproxymodel.h>
0014 #include <model/timeaggregationmodel.h>
0015 
0016 #include <QtCharts/QAreaSeries>
0017 #include <QtCharts/QChart>
0018 #include <QtCharts/QDateTimeAxis>
0019 #include <QtCharts/QHPieModelMapper>
0020 #include <QtCharts/QLineSeries>
0021 #include <QtCharts/QPieSeries>
0022 #include <QtCharts/QValueAxis>
0023 #include <QtCharts/QVXYModelMapper>
0024 
0025 #include <QDebug>
0026 
0027 using namespace KUserFeedback::Console;
0028 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0029 using namespace QtCharts;
0030 #endif
0031 
0032 CategoryAggregator::CategoryAggregator() = default;
0033 CategoryAggregator::~CategoryAggregator() = default;
0034 
0035 Aggregator::ChartModes CategoryAggregator::chartModes() const
0036 {
0037     Aggregator::ChartModes modes = None;
0038     if (aggregation().elements().size() == 1)
0039         modes |= Timeline;
0040     if (aggregation().elements().size() >= 1)
0041         modes |= Singular;
0042     return modes;
0043 }
0044 
0045 QAbstractItemModel* CategoryAggregator::timeAggregationModel()
0046 {
0047     if (!m_model && !aggregation().elements().isEmpty()) {
0048         m_model.reset(new CategoryAggregationModel);
0049         m_model->setSourceModel(sourceModel());
0050         m_model->setAggregation(aggregation());
0051         QObject::connect(m_model.get(), &QAbstractItemModel::modelReset, [this]() {
0052             updateSingularChart();
0053             updateTimelineChart();
0054         });
0055     }
0056     return m_model.get();
0057 }
0058 
0059 QChart* CategoryAggregator::timelineChart()
0060 {
0061     if (!m_timelineChart) {
0062         m_timelineChart.reset(new QChart);
0063         ChartUtil::applyTheme(m_timelineChart.get());
0064         auto xAxis = new QDateTimeAxis(m_timelineChart.get());
0065         xAxis->setFormat(QStringLiteral("yyyy-MM-dd")); // TODO, follow aggregation mode
0066         auto yAxis = new QValueAxis(m_timelineChart.get());
0067         xAxis->setTickCount(std::min(timeAggregationModel()->rowCount(), 12));
0068         yAxis->setMinorTickCount(4);
0069 
0070         m_timelineChart->addAxis(xAxis, Qt::AlignBottom);
0071         m_timelineChart->addAxis(yAxis, Qt::AlignLeft);
0072         updateTimelineChart();
0073     }
0074 
0075     return m_timelineChart.get();
0076 }
0077 
0078 void CategoryAggregator::updateTimelineChart()
0079 {
0080     if (!m_timelineChart)
0081         return;
0082     m_timelineChart->removeAllSeries();
0083 
0084     QLineSeries *prevSeries = nullptr;
0085     auto model = new RoleMappingProxyModel(m_timelineChart.get());
0086     model->setSourceModel(timeAggregationModel());
0087     model->addRoleMapping(Qt::DisplayRole, TimeAggregationModel::AccumulatedDisplayRole);
0088     for (int i = 1; i < timeAggregationModel()->columnCount(); ++i) {
0089         auto series = new QLineSeries;
0090 
0091         auto mapper = new QVXYModelMapper(series);
0092         mapper->setModel(model);
0093         mapper->setXColumn(0);
0094         mapper->setYColumn(i);
0095         mapper->setFirstRow(0);
0096         mapper->setSeries(series);
0097 
0098         auto areaSeries = new QAreaSeries;
0099         series->setParent(areaSeries); // otherwise series isn't deleted by removeAllSeries!
0100         areaSeries->setLowerSeries(prevSeries);
0101         areaSeries->setUpperSeries(series);
0102         areaSeries->setName(timeAggregationModel()->headerData(i, Qt::Horizontal).toString().toHtmlEscaped());
0103         m_timelineChart->addSeries(areaSeries);
0104 
0105         areaSeries->attachAxis(m_timelineChart->axisX());
0106         areaSeries->attachAxis(m_timelineChart->axisY());
0107 
0108         prevSeries = series;
0109     }
0110 
0111     const auto max = timeAggregationModel()->index(0, 0).data(TimeAggregationModel::MaximumValueRole).toInt();
0112     m_timelineChart->axisY()->setRange(0, max);
0113     qobject_cast<QValueAxis*>(m_timelineChart->axisY())->applyNiceNumbers();
0114 }
0115 
0116 QChart* CategoryAggregator::singlularChart()
0117 {
0118     if (!m_singularChart) {
0119         m_singularChart.reset(new QChart);
0120         ChartUtil::applyTheme(m_singularChart.get());
0121         updateSingularChart();
0122     }
0123 
0124     return m_singularChart.get();
0125 }
0126 
0127 void CategoryAggregator::setSingularTime(int row)
0128 {
0129     Aggregator::setSingularTime(row);
0130     for (auto model : m_hierachicalCategories)
0131         model->setRow(row);
0132 }
0133 
0134 void CategoryAggregator::updateSingularChart()
0135 {
0136     if (!m_singularChart)
0137         return;
0138     m_singularChart->removeAllSeries();
0139     m_hierachicalCategories.clear();
0140 
0141     if (sourceModel()->rowCount() <= 0)
0142         return;
0143 
0144     const auto depth = aggregation().elements().size();
0145     for (int i = 0; i < depth; ++i) {
0146         auto series = new QPieSeries(m_singularChart.get());
0147         auto mapper = new QHPieModelMapper(m_singularChart.get());
0148         auto modelWithLabels = new ExtraRowsProxyModel(mapper);
0149         if (i + 1 == depth) {
0150             modelWithLabels->setSourceModel(singularAggregationModel());
0151         } else {
0152             auto model = new SingleRowFilterProxyModel(mapper);
0153             auto catModel = new CategoryAggregationModel(mapper);
0154             catModel->setSourceModel(sourceModel());
0155             catModel->setAggregation(aggregation());
0156             catModel->setDepth(i + 1);
0157             model->setSourceModel(catModel);
0158             m_hierachicalCategories.push_back(model);
0159             modelWithLabels->setSourceModel(model);
0160         }
0161         mapper->setModel(modelWithLabels);
0162         mapper->setFirstColumn(1);
0163         mapper->setValuesRow(0);
0164         mapper->setLabelsRow(1);
0165         mapper->setSeries(series);
0166 
0167         decorateSeries(series, i);
0168         QObject::connect(series, &QPieSeries::added, [this, series, i]() {
0169             decorateSeries(series, i);
0170         });
0171 
0172         m_singularChart->addSeries(series);
0173     }
0174 }
0175 
0176 void CategoryAggregator::decorateSeries(QPieSeries* series, int ring) const
0177 {
0178     const auto ringCount = aggregation().elements().size();
0179     const auto ringWidth = 0.7 / (ringCount + 1);
0180     const auto holeSize = ringWidth * (ringCount - ring);
0181     series->setPieSize(holeSize + ringWidth);
0182     series->setHoleSize(holeSize);
0183 
0184     for (auto slice : series->slices()) {
0185         if (slice->value() > 0.01)
0186             slice->setLabelVisible(true);
0187     }
0188 }