analyticsview.cpp 11 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
    Copyright (C) 2016 Volker Krause <vkrause@kde.org>

    This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Library General Public License as published by
    the Free Software Foundation; either version 2 of the License, or (at your
    option) any later version.

    This program is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
    License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "analyticsview.h"
#include "ui_analyticsview.h"

21
22
#include "aggregator.h"
#include "categoryaggregator.h"
23
#include "chartutil.h"
24
25
#include "numericaggregator.h"
#include "ratiosetaggregator.h"
26
#include "totalaggregator.h"
27

28
#include <model/aggregateddatamodel.h>
Volker Krause's avatar
Volker Krause committed
29
#include <model/datamodel.h>
30
#include <model/timeaggregationmodel.h>
31
#include <rest/restapi.h>
32
#include <core/aggregation.h>
Volker Krause's avatar
Volker Krause committed
33
34
#include <core/sample.h>

35
36
#include <QtCharts/QChart>

Volker Krause's avatar
Volker Krause committed
37
38
#include <QFile>
#include <QFileDialog>
39
#include <QMenu>
Volker Krause's avatar
Volker Krause committed
40
#include <QMessageBox>
41
#include <QNetworkReply>
42
43
44
45
46
47
48
49
50
#include <QSettings>

using namespace UserFeedback::Analyzer;

AnalyticsView::AnalyticsView(QWidget* parent) :
    QWidget(parent),
    ui(new Ui::AnalyticsView),
    m_dataModel(new DataModel(this)),
    m_timeAggregationModel(new TimeAggregationModel(this)),
51
52
53
    m_aggregatedDataModel(new AggregatedDataModel(this)),
    m_nullSingularChart(new QtCharts::QChart),
    m_nullTimelineChart(new QtCharts::QChart)
54
55
56
{
    ui->setupUi(this);

57
58
59
60
61
    ChartUtil::applyTheme(m_nullSingularChart.get());
    ChartUtil::applyTheme(m_nullTimelineChart.get());
    ui->singularChartView->setChart(m_nullSingularChart.get());
    ui->timelineChartView->setChart(m_nullTimelineChart.get());

62
63
64
65
    ui->dataView->setModel(m_dataModel);
    ui->aggregatedDataView->setModel(m_aggregatedDataModel);

    m_timeAggregationModel->setSourceModel(m_dataModel);
66
    connect(m_timeAggregationModel, &QAbstractItemModel::modelReset, this, &AnalyticsView::updateTimeSliderRange);
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

    ui->actionAggregateYear->setData(TimeAggregationModel::AggregateYear);
    ui->actionAggregateMonth->setData(TimeAggregationModel::AggregateMonth);
    ui->actionAggregateWeek->setData(TimeAggregationModel::AggregateWeek);
    ui->actionAggregateDay->setData(TimeAggregationModel::AggregateDay);
    auto aggrGroup = new QActionGroup(this);
    aggrGroup->addAction(ui->actionAggregateYear);
    aggrGroup->addAction(ui->actionAggregateMonth);
    aggrGroup->addAction(ui->actionAggregateWeek);
    aggrGroup->addAction(ui->actionAggregateDay);
    aggrGroup->setExclusive(true);
    connect(aggrGroup, &QActionGroup::triggered, this, [this, aggrGroup]() {
        m_timeAggregationModel->setAggregationMode(static_cast<TimeAggregationModel::AggregationMode>(aggrGroup->checkedAction()->data().toInt()));
    });

    auto timeAggrMenu = new QMenu(tr("&Time interval"), this);
    timeAggrMenu->addAction(ui->actionAggregateDay);
    timeAggrMenu->addAction(ui->actionAggregateWeek);
    timeAggrMenu->addAction(ui->actionAggregateMonth);
    timeAggrMenu->addAction(ui->actionAggregateYear);
Volker Krause's avatar
Volker Krause committed
87

88
89
90
    auto chartModeGroup = new QActionGroup(this);
    chartModeGroup->addAction(ui->actionSingularChart);
    chartModeGroup->addAction(ui->actionTimelineChart);
91
    connect(chartModeGroup, &QActionGroup::triggered, this, &AnalyticsView::updateChart);
92

Volker Krause's avatar
Volker Krause committed
93
    auto chartMode = new QMenu(tr("&Chart mode"), this);
94
95
96
    chartMode->addAction(ui->actionSingularChart);
    chartMode->addAction(ui->actionTimelineChart);

Volker Krause's avatar
Volker Krause committed
97
    connect(ui->actionReload, &QAction::triggered, m_dataModel, &DataModel::reload);
Volker Krause's avatar
Volker Krause committed
98
99
100
    connect(ui->actionExportData, &QAction::triggered, this, &AnalyticsView::exportData);
    connect(ui->actionImportData, &QAction::triggered, this, &AnalyticsView::importData);

Volker Krause's avatar
Volker Krause committed
101
102
    addActions({
        timeAggrMenu->menuAction(),
103
        chartMode->menuAction(),
Volker Krause's avatar
Volker Krause committed
104
105
106
107
        ui->actionReload,
        ui->actionExportData,
        ui->actionImportData
    });
108
109
110
111
112
113
114
115
116

    QSettings settings;
    settings.beginGroup(QStringLiteral("Analytics"));
    const auto aggrSetting = settings.value(QStringLiteral("TimeAggregationMode"), TimeAggregationModel::AggregateMonth).toInt();
    foreach (auto act, aggrGroup->actions())
        act->setChecked(act->data().toInt() == aggrSetting);
    m_timeAggregationModel->setAggregationMode(static_cast<TimeAggregationModel::AggregationMode>(aggrSetting));
    settings.endGroup();

117
    connect(ui->chartType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &AnalyticsView::chartSelected);
118
119
120
121
122
    connect(ui->timeSlider, &QSlider::valueChanged, this, [this](int value) {
        auto aggr = ui->chartType->currentData().value<Aggregator*>();
        if (aggr)
            aggr->setSingularTime(value);
    });
123
124
125
126
}

AnalyticsView::~AnalyticsView()
{
127
128
129
130
    if (ui->singularChartView->chart())
        disconnect(ui->singularChartView->chart(), &QObject::destroyed, this, &AnalyticsView::updateChart);
    if (ui->timelineChartView->chart())
        disconnect(ui->timelineChartView->chart(), &QObject::destroyed, this, &AnalyticsView::updateChart);
131

132
133
134
135
    QSettings settings;
    settings.beginGroup(QStringLiteral("Analytics"));
    settings.setValue(QStringLiteral("TimeAggregationMode"), m_timeAggregationModel->aggregationMode());
    settings.endGroup();
136
137
138
139
140

    // the chart views can't handle null or deleted charts, so set them to something safe
    ui->singularChartView->setChart(m_nullSingularChart.get());
    ui->timelineChartView->setChart(m_nullTimelineChart.get());
    qDeleteAll(m_aggregators);
141
142
143
144
}

void AnalyticsView::setRESTClient(RESTClient* client)
{
145
    m_client = client;
146
147
148
149
150
    m_dataModel->setRESTClient(client);
}

void AnalyticsView::setProduct(const Product& product)
{
151
152
153
154
    // the chart views can't handle null or deleted charts, so set them to something safe
    ui->singularChartView->setChart(m_nullSingularChart.get());
    ui->timelineChartView->setChart(m_nullTimelineChart.get());

155
156
157
158
159
    m_dataModel->setProduct(product);

    ui->chartType->clear();
    m_aggregatedDataModel->clear();

160
161
162
    qDeleteAll(m_aggregators);
    m_aggregators.clear();

163
164
165
166
167
    m_aggregatedDataModel->addSourceModel(m_timeAggregationModel);
    auto totalsAggr = new TotalAggregator;
    totalsAggr->setSourceModel(m_timeAggregationModel);
    ui->chartType->addItem(tr("Samples"), QVariant::fromValue<Aggregator*>(totalsAggr));

168
    foreach (const auto &aggr, product.aggregations()) {
169
170
171
172
173
174
        auto aggregator = createAggregator(aggr);
        if (!aggregator)
            continue;
        m_aggregators.push_back(aggregator);
        if (auto model = aggregator->timeAggregationModel()) {
            m_aggregatedDataModel->addSourceModel(model, aggregator->displayName());
175
        }
176
177
        if (aggregator->chartModes() != Aggregator::None)
            ui->chartType->addItem(aggregator->displayName(), QVariant::fromValue(aggregator));
178
179
    }
}
Volker Krause's avatar
Volker Krause committed
180

181
182
183
void AnalyticsView::chartSelected()
{
    auto aggr = ui->chartType->currentData().value<Aggregator*>();
184
185
    if (!aggr)
        return;
186

187
    const auto chartMode = aggr->chartModes();
188
189
190
191
192
193
194
    ui->actionSingularChart->setEnabled(chartMode & Aggregator::Singular);
    ui->actionTimelineChart->setEnabled(chartMode & Aggregator::Timeline);
    if (chartMode != (Aggregator::Timeline | Aggregator::Singular)) {
        ui->actionSingularChart->setChecked(chartMode & Aggregator::Singular);
        ui->actionTimelineChart->setChecked(chartMode & Aggregator::Timeline);
    }

195
196
197
198
199
200
201
202
203
    updateChart();
}

void AnalyticsView::updateChart()
{
    auto aggr = ui->chartType->currentData().value<Aggregator*>();
    if (!aggr)
        return;

204
205
206
207
    if (ui->singularChartView->chart())
        disconnect(ui->singularChartView->chart(), &QObject::destroyed, this, &AnalyticsView::updateChart);
    if (ui->timelineChartView->chart())
        disconnect(ui->timelineChartView->chart(), &QObject::destroyed, this, &AnalyticsView::updateChart);
208
209

    if (ui->actionTimelineChart->isChecked()) {
210
211
212
        ui->timelineChartView->setChart(aggr->timelineChart());
        connect(ui->timelineChartView->chart(), &QObject::destroyed, this, &AnalyticsView::updateChart);
        ui->chartStack->setCurrentWidget(ui->timelinePage);
213
    } else if (ui->actionSingularChart->isChecked()) {
214
215
216
        ui->singularChartView->setChart(aggr->singlularChart());
        connect(ui->singularChartView->chart(), &QObject::destroyed, this, &AnalyticsView::updateChart);
        ui->chartStack->setCurrentWidget(ui->singularPage);
217
    }
218
}
219

220
221
222
223
224
void AnalyticsView::updateTimeSliderRange()
{
    qDebug() << m_timeAggregationModel->rowCount() << ui->timeSlider->maximum() << ui->timeSlider->minimum();
    if (m_timeAggregationModel->rowCount() > 0)
        ui->timeSlider->setRange(0, m_timeAggregationModel->rowCount() - 1);
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
}

Aggregator* AnalyticsView::createAggregator(const Aggregation& aggr) const
{
    Aggregator *aggregator = nullptr;

    switch (aggr.type()) {
        case Aggregation::None:
            break;
        case Aggregation::Category:
            aggregator = new CategoryAggregator;
            break;
        case Aggregation::Numeric:
            aggregator = new NumericAggregator;
            break;
        case Aggregation::RatioSet:
            aggregator = new RatioSetAggregator;
            break;
        case Aggregation::XY:
            break;
    }

    if (!aggregator)
        return nullptr;

    aggregator->setAggregation(aggr);
    aggregator->setSourceModel(m_timeAggregationModel);
    return aggregator;
}

Volker Krause's avatar
Volker Krause committed
255
256
257
258
259
260
261
262
263
264
265
266
267
268
void AnalyticsView::exportData()
{
    const auto fileName = QFileDialog::getSaveFileName(this, tr("Export Data"));
    if (fileName.isEmpty())
        return;

    QFile f(fileName);
    if (!f.open(QFile::WriteOnly)) {
        QMessageBox::critical(this, tr("Export Failed"), tr("Could not open file: %1").arg(f.errorString()));
        return;
    }

    const auto samples = m_dataModel->index(0, 0).data(DataModel::AllSamplesRole).value<QVector<Sample>>();
    f.write(Sample::toJson(samples, m_dataModel->product()));
269
    emit logMessage(tr("Sample data of %1 exported to %2.").arg(m_dataModel->product().name(), f.fileName()));
Volker Krause's avatar
Volker Krause committed
270
271
272
273
}

void AnalyticsView::importData()
{
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
    const auto fileName = QFileDialog::getOpenFileName(this, tr("Import Data"));
    if (fileName.isEmpty())
        return;

    QFile f(fileName);
    if (!f.open(QFile::ReadOnly)) {
        QMessageBox::critical(this, tr("Import Failed"), tr("Could not open file: %1").arg(f.errorString()));
        return;
    }
    const auto samples = Sample::fromJson(f.readAll(), m_dataModel->product());
    if (samples.isEmpty()) {
        QMessageBox::critical(this, tr("Import Failed"), tr("Selected file contains no valid data."));
        return;
    }

    auto reply = RESTApi::addSamples(m_client, m_dataModel->product(), samples);
    connect(reply, &QNetworkReply::finished, this, [this, reply]() {
Volker Krause's avatar
Volker Krause committed
291
        if (reply->error() == QNetworkReply::NoError) {
292
            emit logMessage(tr("Samples imported."));
Volker Krause's avatar
Volker Krause committed
293
294
            m_dataModel->reload();
        }
295
    });
Volker Krause's avatar
Volker Krause committed
296
}