File indexing completed on 2024-12-15 03:44:59
0001 /* 0002 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: MIT 0005 */ 0006 0007 #include "analyticsview.h" 0008 #include "ui_analyticsview.h" 0009 0010 #include "aggregator.h" 0011 #include "categoryaggregator.h" 0012 #include "chartexportdialog.h" 0013 #include "chartutil.h" 0014 #include "numericaggregator.h" 0015 #include "ratiosetaggregator.h" 0016 #include "totalaggregator.h" 0017 0018 #include <model/aggregateddatamodel.h> 0019 #include <model/datamodel.h> 0020 #include <model/timeaggregationmodel.h> 0021 #include <rest/restapi.h> 0022 #include <core/aggregation.h> 0023 #include <core/sample.h> 0024 0025 #include <QtCharts/QChart> 0026 0027 #include <QActionGroup> 0028 #include <QClipboard> 0029 #include <QFile> 0030 #include <QFileDialog> 0031 #include <QImage> 0032 #include <QMenu> 0033 #include <QMessageBox> 0034 #include <QNetworkReply> 0035 #include <QPdfWriter> 0036 #include <QSettings> 0037 #include <QSvgGenerator> 0038 0039 using namespace KUserFeedback::Console; 0040 0041 AnalyticsView::AnalyticsView(QWidget* parent) : 0042 QWidget(parent), 0043 ui(new Ui::AnalyticsView), 0044 m_dataModel(new DataModel(this)), 0045 m_timeAggregationModel(new TimeAggregationModel(this)), 0046 m_aggregatedDataModel(new AggregatedDataModel(this)), 0047 m_nullSingularChart(new QChart), 0048 m_nullTimelineChart(new QChart) 0049 { 0050 ui->setupUi(this); 0051 0052 ChartUtil::applyTheme(m_nullSingularChart.get()); 0053 ChartUtil::applyTheme(m_nullTimelineChart.get()); 0054 ui->singularChartView->setChart(m_nullSingularChart.get()); 0055 ui->timelineChartView->setChart(m_nullTimelineChart.get()); 0056 0057 ui->dataView->setModel(m_dataModel); 0058 ui->aggregatedDataView->setModel(m_aggregatedDataModel); 0059 0060 m_timeAggregationModel->setSourceModel(m_dataModel); 0061 connect(m_timeAggregationModel, &QAbstractItemModel::modelReset, this, &AnalyticsView::updateTimeSliderRange); 0062 0063 ui->actionAggregateYear->setData(TimeAggregationModel::AggregateYear); 0064 ui->actionAggregateMonth->setData(TimeAggregationModel::AggregateMonth); 0065 ui->actionAggregateWeek->setData(TimeAggregationModel::AggregateWeek); 0066 ui->actionAggregateDay->setData(TimeAggregationModel::AggregateDay); 0067 auto aggrGroup = new QActionGroup(this); 0068 aggrGroup->addAction(ui->actionAggregateYear); 0069 aggrGroup->addAction(ui->actionAggregateMonth); 0070 aggrGroup->addAction(ui->actionAggregateWeek); 0071 aggrGroup->addAction(ui->actionAggregateDay); 0072 aggrGroup->setExclusive(true); 0073 connect(aggrGroup, &QActionGroup::triggered, this, [this, aggrGroup]() { 0074 m_timeAggregationModel->setAggregationMode(static_cast<TimeAggregationModel::AggregationMode>(aggrGroup->checkedAction()->data().toInt())); 0075 }); 0076 0077 auto timeAggrMenu = new QMenu(tr("&Time Interval"), this); 0078 timeAggrMenu->addAction(ui->actionAggregateDay); 0079 timeAggrMenu->addAction(ui->actionAggregateWeek); 0080 timeAggrMenu->addAction(ui->actionAggregateMonth); 0081 timeAggrMenu->addAction(ui->actionAggregateYear); 0082 0083 auto chartModeGroup = new QActionGroup(this); 0084 chartModeGroup->addAction(ui->actionSingularChart); 0085 chartModeGroup->addAction(ui->actionTimelineChart); 0086 connect(chartModeGroup, &QActionGroup::triggered, this, &AnalyticsView::updateChart); 0087 0088 auto chartMode = new QMenu(tr("&Chart Mode"), this); 0089 chartMode->addAction(ui->actionSingularChart); 0090 chartMode->addAction(ui->actionTimelineChart); 0091 0092 ui->actionReload->setShortcut(QKeySequence::Refresh); 0093 connect(ui->actionReload, &QAction::triggered, m_dataModel, &DataModel::reload); 0094 connect(ui->actionExportChart, &QAction::triggered, this, &AnalyticsView::exportChart); 0095 connect(ui->actionExportData, &QAction::triggered, this, &AnalyticsView::exportData); 0096 connect(ui->actionImportData, &QAction::triggered, this, &AnalyticsView::importData); 0097 0098 addActions({ 0099 timeAggrMenu->menuAction(), 0100 chartMode->menuAction(), 0101 ui->actionReload, 0102 ui->actionExportChart, 0103 ui->actionExportData, 0104 ui->actionImportData 0105 }); 0106 0107 QSettings settings; 0108 settings.beginGroup(QStringLiteral("Analytics")); 0109 const auto aggrSetting = settings.value(QStringLiteral("TimeAggregationMode"), TimeAggregationModel::AggregateMonth).toInt(); 0110 foreach (auto act, aggrGroup->actions()) 0111 act->setChecked(act->data().toInt() == aggrSetting); 0112 m_timeAggregationModel->setAggregationMode(static_cast<TimeAggregationModel::AggregationMode>(aggrSetting)); 0113 settings.endGroup(); 0114 0115 connect(ui->chartType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &AnalyticsView::chartSelected); 0116 connect(ui->timeSlider, &QSlider::valueChanged, this, [this](int value) { 0117 auto aggr = ui->chartType->currentData().value<Aggregator*>(); 0118 if (!aggr) 0119 return; 0120 aggr->setSingularTime(value); 0121 ui->timeLabel->setText(aggr->singularAggregationModel()->index(0, 0).data(TimeAggregationModel::TimeDisplayRole).toString()); 0122 }); 0123 0124 connect(ui->dataView, &QWidget::customContextMenuRequested, this, [this](QPoint pos) { 0125 QMenu menu; 0126 menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy"), [this, pos]() { 0127 const auto idx = ui->dataView->indexAt(pos); 0128 QGuiApplication::clipboard()->setText(idx.data().toString()); 0129 }); 0130 menu.exec(ui->dataView->viewport()->mapToGlobal(pos)); 0131 }); 0132 } 0133 0134 AnalyticsView::~AnalyticsView() 0135 { 0136 QSettings settings; 0137 settings.beginGroup(QStringLiteral("Analytics")); 0138 settings.setValue(QStringLiteral("TimeAggregationMode"), m_timeAggregationModel->aggregationMode()); 0139 settings.endGroup(); 0140 0141 // the chart views can't handle null or deleted charts, so set them to something safe 0142 ui->singularChartView->setChart(m_nullSingularChart.get()); 0143 ui->timelineChartView->setChart(m_nullTimelineChart.get()); 0144 qDeleteAll(m_aggregators); 0145 } 0146 0147 void AnalyticsView::setRESTClient(RESTClient* client) 0148 { 0149 m_client = client; 0150 m_dataModel->setRESTClient(client); 0151 } 0152 0153 void AnalyticsView::setProduct(const Product& product) 0154 { 0155 // the chart views can't handle null or deleted charts, so set them to something safe 0156 ui->singularChartView->setChart(m_nullSingularChart.get()); 0157 ui->timelineChartView->setChart(m_nullTimelineChart.get()); 0158 0159 m_dataModel->setProduct(product); 0160 0161 ui->chartType->clear(); 0162 m_aggregatedDataModel->clear(); 0163 0164 qDeleteAll(m_aggregators); 0165 m_aggregators.clear(); 0166 0167 m_aggregatedDataModel->addSourceModel(m_timeAggregationModel); 0168 auto totalsAggr = new TotalAggregator; 0169 m_aggregators.push_back(totalsAggr); 0170 totalsAggr->setSourceModel(m_timeAggregationModel); 0171 ui->chartType->addItem(totalsAggr->displayName(), QVariant::fromValue<Aggregator*>(totalsAggr)); 0172 0173 foreach (const auto &aggr, product.aggregations()) { 0174 auto aggregator = createAggregator(aggr); 0175 if (!aggregator) 0176 continue; 0177 m_aggregators.push_back(aggregator); 0178 if (auto model = aggregator->timeAggregationModel()) { 0179 m_aggregatedDataModel->addSourceModel(model, aggregator->displayName()); 0180 } 0181 if (aggregator->chartModes() != Aggregator::None) 0182 ui->chartType->addItem(aggregator->displayName(), QVariant::fromValue(aggregator)); 0183 } 0184 } 0185 0186 void AnalyticsView::chartSelected() 0187 { 0188 auto aggr = ui->chartType->currentData().value<Aggregator*>(); 0189 if (!aggr) 0190 return; 0191 0192 const auto chartMode = aggr->chartModes(); 0193 ui->actionSingularChart->setEnabled(chartMode & Aggregator::Singular); 0194 ui->actionTimelineChart->setEnabled(chartMode & Aggregator::Timeline); 0195 if (chartMode != (Aggregator::Timeline | Aggregator::Singular)) { 0196 ui->actionSingularChart->setChecked(chartMode & Aggregator::Singular); 0197 ui->actionTimelineChart->setChecked(chartMode & Aggregator::Timeline); 0198 } 0199 0200 updateChart(); 0201 } 0202 0203 void AnalyticsView::updateChart() 0204 { 0205 auto aggr = ui->chartType->currentData().value<Aggregator*>(); 0206 if (!aggr) 0207 return; 0208 0209 if (ui->actionTimelineChart->isChecked()) { 0210 ui->timelineChartView->setChart(aggr->timelineChart()); 0211 ui->chartStack->setCurrentWidget(ui->timelinePage); 0212 } else if (ui->actionSingularChart->isChecked()) { 0213 ui->singularChartView->setChart(aggr->singlularChart()); 0214 ui->chartStack->setCurrentWidget(ui->singularPage); 0215 aggr->setSingularTime(ui->timeSlider->value()); 0216 } 0217 } 0218 0219 void AnalyticsView::updateTimeSliderRange() 0220 { 0221 if (m_timeAggregationModel->rowCount() > 0) { 0222 ui->timeSlider->setRange(0, m_timeAggregationModel->rowCount() - 1); 0223 ui->timeLabel->setText(m_timeAggregationModel->index(ui->timeSlider->value(), 0).data(TimeAggregationModel::TimeDisplayRole).toString()); 0224 auto aggr = ui->chartType->currentData().value<Aggregator*>(); 0225 if (aggr) 0226 aggr->setSingularTime(ui->timeSlider->value()); 0227 } 0228 } 0229 0230 Aggregator* AnalyticsView::createAggregator(const Aggregation& aggr) const 0231 { 0232 Aggregator *aggregator = nullptr; 0233 0234 switch (aggr.type()) { 0235 case Aggregation::None: 0236 break; 0237 case Aggregation::Category: 0238 aggregator = new CategoryAggregator; 0239 break; 0240 case Aggregation::Numeric: 0241 aggregator = new NumericAggregator; 0242 break; 0243 case Aggregation::RatioSet: 0244 aggregator = new RatioSetAggregator; 0245 break; 0246 } 0247 0248 if (!aggregator) 0249 return nullptr; 0250 0251 aggregator->setAggregation(aggr); 0252 aggregator->setSourceModel(m_timeAggregationModel); 0253 return aggregator; 0254 } 0255 0256 void AnalyticsView::exportData() 0257 { 0258 const auto fileName = QFileDialog::getSaveFileName(this, tr("Export Data")); 0259 if (fileName.isEmpty()) 0260 return; 0261 0262 QFile f(fileName); 0263 if (!f.open(QFile::WriteOnly)) { 0264 QMessageBox::critical(this, tr("Export Failed"), tr("Could not open file: %1").arg(f.errorString())); 0265 return; 0266 } 0267 0268 const auto samples = m_dataModel->index(0, 0).data(DataModel::AllSamplesRole).value<QVector<Sample>>(); 0269 f.write(Sample::toJson(samples, m_dataModel->product())); 0270 Q_EMIT logMessage(tr("Data samples of %1 exported to %2.").arg(m_dataModel->product().name(), f.fileName())); 0271 } 0272 0273 void AnalyticsView::importData() 0274 { 0275 const auto fileName = QFileDialog::getOpenFileName(this, tr("Import Data")); 0276 if (fileName.isEmpty()) 0277 return; 0278 0279 QFile f(fileName); 0280 if (!f.open(QFile::ReadOnly)) { 0281 QMessageBox::critical(this, tr("Import Failed"), tr("Could not open file: %1").arg(f.errorString())); 0282 return; 0283 } 0284 const auto samples = Sample::fromJson(f.readAll(), m_dataModel->product()); 0285 if (samples.isEmpty()) { 0286 QMessageBox::critical(this, tr("Import Failed"), tr("Selected file contains no valid data.")); 0287 return; 0288 } 0289 0290 auto reply = RESTApi::addSamples(m_client, m_dataModel->product(), samples); 0291 connect(reply, &QNetworkReply::finished, this, [this, reply]() { 0292 if (reply->error() == QNetworkReply::NoError) { 0293 Q_EMIT logMessage(tr("Data samples imported.")); 0294 m_dataModel->reload(); 0295 } 0296 }); 0297 } 0298 0299 void AnalyticsView::exportChart() 0300 { 0301 ChartExportDialog dlg(this); 0302 if (dlg.exec() != QDialog::Accepted) 0303 return; 0304 0305 QChart *chart = nullptr; 0306 QGraphicsScene *scene = nullptr; 0307 if (ui->actionTimelineChart->isChecked()) { 0308 chart = ui->timelineChartView->chart(); 0309 scene = ui->timelineChartView->scene(); 0310 } else if (ui->actionSingularChart->isChecked()) { 0311 chart = ui->singularChartView->chart(); 0312 scene = ui->singularChartView->scene(); 0313 } 0314 Q_ASSERT(chart); 0315 Q_ASSERT(scene); 0316 0317 chart->setTheme(QChart::ChartThemeLight); 0318 0319 switch (dlg.type()) { 0320 case ChartExportDialog::Image: 0321 { 0322 QImage img(dlg.size(), QImage::Format_ARGB32_Premultiplied); 0323 img.fill(Qt::transparent); 0324 QPainter p(&img); 0325 p.setRenderHint(QPainter::Antialiasing); 0326 scene->render(&p, QRectF(QPoint(), dlg.size()), scene->sceneRect()); 0327 img.save(dlg.filename()); 0328 break; 0329 } 0330 case ChartExportDialog::SVG: 0331 { 0332 QSvgGenerator svg; 0333 svg.setFileName(dlg.filename()); 0334 svg.setSize(scene->sceneRect().size().toSize()); 0335 svg.setViewBox(scene->sceneRect()); 0336 svg.setTitle(ui->chartType->currentText()); 0337 0338 QPainter p(&svg); 0339 p.setRenderHint(QPainter::Antialiasing); 0340 scene->render(&p); 0341 break; 0342 } 0343 case ChartExportDialog::PDF: 0344 { 0345 QPdfWriter pdf(dlg.filename()); 0346 pdf.setCreator(QStringLiteral("UserFeedbackConsole")); 0347 pdf.setTitle(ui->chartType->currentText()); 0348 if (scene->sceneRect().width() > scene->sceneRect().height()) 0349 pdf.setPageOrientation(QPageLayout::Landscape); 0350 0351 QPainter p(&pdf); 0352 p.setRenderHint(QPainter::Antialiasing); 0353 scene->render(&p); 0354 break; 0355 } 0356 } 0357 0358 ChartUtil::applyTheme(chart); 0359 } 0360 0361 #include "moc_analyticsview.cpp"