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"