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 }