diff --git a/autotests/categoryaggregationmodeltest.cpp b/autotests/categoryaggregationmodeltest.cpp index 963df27..b898e21 100644 --- a/autotests/categoryaggregationmodeltest.cpp +++ b/autotests/categoryaggregationmodeltest.cpp @@ -1,217 +1,228 @@ /* Copyright (C) 2016 Volker Krause 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 . */ #include #include #include #include #include #include <3rdparty/qt/modeltest.h> #include #include #include #include using namespace UserFeedback::Console; class CategoryAggregationModelTest : public QObject { Q_OBJECT private slots: void initTestCase() { Q_INIT_RESOURCE(schematemplates); QStandardPaths::setTestModeEnabled(true); } void testEmptyModel() { CategoryAggregationModel model; ModelTest modelTest(&model); model.setAggregation(Aggregation()); AggregationElement aggrElem; { SchemaEntry entry; entry.setName(QLatin1String("applicationVersion")); aggrElem.setSchemaEntry(entry); SchemaEntryElement elem; elem.setName(QLatin1String("value")); aggrElem.setSchemaEntryElement(elem); aggrElem.setType(AggregationElement::Value); } Aggregation aggr; aggr.setType(Aggregation::Category); aggr.setElements({aggrElem}); model.setAggregation(aggr); TimeAggregationModel timeModel; model.setSourceModel(&timeModel); DataModel srcModel; timeModel.setSourceModel(&srcModel); srcModel.setProduct({}); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.columnCount(), 1); Product p; for (const auto &tpl : SchemaEntryTemplates::availableTemplates()) p.addTemplate(tpl); p.setName(QStringLiteral("org.kde.UserFeedback.UnitTest")); srcModel.setProduct(p); QCOMPARE(model.columnCount(), 1); QCOMPARE(model.rowCount(), 0); } void testModelContentDepth1() { CategoryAggregationModel model; ModelTest modelTest(&model); AggregationElement aggrElem; { SchemaEntry entry; entry.setName(QLatin1String("applicationVersion")); aggrElem.setSchemaEntry(entry); SchemaEntryElement elem; elem.setName(QLatin1String("value")); aggrElem.setSchemaEntryElement(elem); aggrElem.setType(AggregationElement::Value); } Aggregation aggr; aggr.setType(Aggregation::Category); aggr.setElements({aggrElem}); model.setAggregation(aggr); TimeAggregationModel timeModel; model.setSourceModel(&timeModel); DataModel srcModel; timeModel.setSourceModel(&srcModel); timeModel.setAggregationMode(TimeAggregationModel::AggregateDay); Product p; for (const auto &tpl : SchemaEntryTemplates::availableTemplates()) p.addTemplate(tpl); p.setName(QStringLiteral("org.kde.UserFeedback.UnitTest")); srcModel.setProduct(p); auto samples = Sample::fromJson(R"([ { "timestamp": "2016-11-27 12:00:00", "applicationVersion": { "value": "1.0" } }, { "timestamp": "2016-11-27 12:00:00", "applicationVersion": { "value": "1.9.84" } }, { "timestamp": "2016-11-27 12:00:00", "applicationVersion": { "value": "1.9.84" } }, { "timestamp": "2016-11-28 12:00:00", "applicationVersion": { "value": "1.9.84" } }, { "timestamp": "2016-11-28 12:00:00", "applicationVersion": { "value": "1.9.84" } }, { "timestamp": "2016-11-28 12:00:00" } ])", p); QCOMPARE(samples.size(), 6); srcModel.setSamples(samples); QCOMPARE(model.columnCount(), 4); QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("[empty]")); QCOMPARE(model.headerData(2, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("1.0")); QCOMPARE(model.headerData(3, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("1.9.84")); QCOMPARE(model.rowCount(), 2); QCOMPARE(model.index(0, 0).data(TimeAggregationModel::TimeDisplayRole).toString(), QLatin1String("2016-11-27")); QCOMPARE(model.index(0, 1).data(Qt::DisplayRole).toInt(), 0); QCOMPARE(model.index(0, 2).data(Qt::DisplayRole).toInt(), 1); QCOMPARE(model.index(0, 3).data(Qt::DisplayRole).toInt(), 2); QCOMPARE(model.index(0, 3).data(TimeAggregationModel::AccumulatedDisplayRole).toInt(), 3); QCOMPARE(model.index(1, 0).data(TimeAggregationModel::TimeDisplayRole).toString(), QLatin1String("2016-11-28")); QCOMPARE(model.index(1, 1).data(Qt::DisplayRole).toInt(), 1); QCOMPARE(model.index(1, 2).data(Qt::DisplayRole).toInt(), 0); QCOMPARE(model.index(0, 3).data(Qt::DisplayRole).toInt(), 2); QCOMPARE(model.index(0, 3).data(TimeAggregationModel::AccumulatedDisplayRole).toInt(), 3); QCOMPARE(model.index(0, 0).data(TimeAggregationModel::MaximumValueRole).toInt(), 3); } void testModelContentDepth2() { const auto p = Product::fromJson(R"({ "name": "depth2test", "schema": [{ "name": "platform", "type": "scalar", "elements": [ { "name": "os", "type": "string" }, { "name": "version", "type": "string" } ] }], "aggregation": [{ "type": "category", "name": "OS Details", "elements": [ { "type": "value", "schemaEntry": "platform", "schemaEntryElement": "os" }, { "type": "value", "schemaEntry": "platform", "schemaEntryElement": "version" } ] }] })").at(0); QVERIFY(p.isValid()); QCOMPARE(p.aggregations().size(), 1); CategoryAggregationModel model; ModelTest modelTest(&model); model.setAggregation(p.aggregations().at(0)); TimeAggregationModel timeModel; model.setSourceModel(&timeModel); DataModel srcModel; timeModel.setSourceModel(&srcModel); timeModel.setAggregationMode(TimeAggregationModel::AggregateDay); srcModel.setProduct(p); auto samples = Sample::fromJson(R"([ { "timestamp": "2016-11-27 12:00:00", "platform": { "os": "windows", "version": "10" } }, { "timestamp": "2016-11-27 12:00:00", "platform": { "os": "linux", "version": "10" } }, { "timestamp": "2016-11-27 12:00:00", "platform": { "os": "linux", "version": "10" } }, { "timestamp": "2016-11-28 12:00:00", "platform": { "os": "windows", "version": "10" } }, { "timestamp": "2016-11-28 12:00:00", "platform": { "os": "linux", "version": "42" } }, { "timestamp": "2016-11-28 12:00:00" } ])", p); QCOMPARE(samples.size(), 6); srcModel.setSamples(samples); QCOMPARE(model.columnCount(), 5); QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("[empty]")); QCOMPARE(model.headerData(2, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("10")); // linux QCOMPARE(model.headerData(3, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("42")); QCOMPARE(model.headerData(4, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("10")); // windows QCOMPARE(model.rowCount(), 2); QCOMPARE(model.index(0, 0).data(TimeAggregationModel::TimeDisplayRole).toString(), QLatin1String("2016-11-27")); QCOMPARE(model.index(0, 1).data(Qt::DisplayRole).toInt(), 0); QCOMPARE(model.index(0, 2).data(Qt::DisplayRole).toInt(), 2); QCOMPARE(model.index(0, 3).data(Qt::DisplayRole).toInt(), 0); QCOMPARE(model.index(0, 4).data(Qt::DisplayRole).toInt(), 1); QCOMPARE(model.index(1, 0).data(TimeAggregationModel::TimeDisplayRole).toString(), QLatin1String("2016-11-28")); QCOMPARE(model.index(1, 1).data(Qt::DisplayRole).toInt(), 1); QCOMPARE(model.index(1, 2).data(Qt::DisplayRole).toInt(), 0); QCOMPARE(model.index(1, 3).data(Qt::DisplayRole).toInt(), 1); QCOMPARE(model.index(1, 4).data(Qt::DisplayRole).toInt(), 1); QCOMPARE(model.index(0, 0).data(TimeAggregationModel::MaximumValueRole).toInt(), 3); + + model.setDepth(1); + QCOMPARE(model.columnCount(), 4); + QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("[empty]")); + QCOMPARE(model.headerData(2, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("linux")); + QCOMPARE(model.headerData(3, Qt::Horizontal, Qt::DisplayRole).toString(), QLatin1String("windows")); + + QCOMPARE(model.rowCount(), 2); + QCOMPARE(model.index(0, 1).data(Qt::DisplayRole).toInt(), 0); + QCOMPARE(model.index(0, 2).data(Qt::DisplayRole).toInt(), 2); + QCOMPARE(model.index(0, 3).data(Qt::DisplayRole).toInt(), 1); } }; QTEST_MAIN(CategoryAggregationModelTest) #include "categoryaggregationmodeltest.moc" diff --git a/src/console/model/categoryaggregationmodel.cpp b/src/console/model/categoryaggregationmodel.cpp index 0d5555c..0d57780 100644 --- a/src/console/model/categoryaggregationmodel.cpp +++ b/src/console/model/categoryaggregationmodel.cpp @@ -1,202 +1,210 @@ /* Copyright (C) 2016 Volker Krause 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 . */ #include "categoryaggregationmodel.h" #include #include #include #include #include #include using namespace UserFeedback::Console; CategoryAggregationModel::CategoryAggregationModel(QObject *parent) : QAbstractTableModel(parent) { } CategoryAggregationModel::~CategoryAggregationModel() { delete[] m_data; } void CategoryAggregationModel::setSourceModel(QAbstractItemModel* model) { Q_ASSERT(model); m_sourceModel = model; connect(model, &QAbstractItemModel::modelReset, this, &CategoryAggregationModel::recompute); recompute(); } void CategoryAggregationModel::setAggregation(const Aggregation& aggr) { m_aggr = aggr; + m_depth = m_aggr.elements().size(); + recompute(); +} + +void CategoryAggregationModel::setDepth(int depth) +{ + if (depth == m_depth) + return; + m_depth = std::min(depth, m_aggr.elements().size()); recompute(); } int CategoryAggregationModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_categories.size() + 1; } int CategoryAggregationModel::rowCount(const QModelIndex& parent) const { if (parent.isValid() || !m_sourceModel) return 0; return m_sourceModel->rowCount(); } QVariant CategoryAggregationModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || !m_sourceModel) return {}; if (role == TimeAggregationModel::MaximumValueRole) return m_maxValue; if (index.column() == 0) { const auto srcIdx = m_sourceModel->index(index.row(), 0); return m_sourceModel->data(srcIdx, role); } const auto idx = index.row() * m_categories.size() + index.column() - 1; switch (role) { case TimeAggregationModel::AccumulatedDisplayRole: return m_data[idx]; case Qt::DisplayRole: case TimeAggregationModel::DataDisplayRole: if (index.column() == 1) return m_data[idx]; return m_data[idx] - m_data[idx - 1]; } return {}; } QVariant CategoryAggregationModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && m_sourceModel) { if (section == 0) return m_sourceModel->headerData(section, orientation, role); if (role == Qt::DisplayRole) { const auto cat = m_categories.at(section - 1); if (cat.isEmpty()) return tr("[empty]"); return cat; } } return QAbstractTableModel::headerData(section, orientation, role); } void CategoryAggregationModel::recompute() { if (!m_sourceModel) return; const auto rowCount = m_sourceModel->rowCount(); beginResetModel(); m_categories.clear(); delete[] m_data; m_data = nullptr; m_maxValue = 0; - if (rowCount <= 0 || !m_aggr.isValid()) { + if (rowCount <= 0 || !m_aggr.isValid() || m_depth <= 0) { endResetModel(); return; } // scan all samples to find all categories - const auto depth = m_aggr.elements().size(); const auto allSamples = m_sourceModel->index(0, 0).data(TimeAggregationModel::AllSamplesRole).value>(); QVector>> depthSamples{{allSamples}}; // depth -> parent category index -> samples - depthSamples.resize(depth + 1); + depthSamples.resize(m_depth + 1); QVector>> depthCategories{{{{}}}}; // depth -> parent category index -> category values - depthCategories.resize(depth + 1); + depthCategories.resize(m_depth + 1); QVector> depthOffsets{{0}}; // depth -> parent category index -> column offset - depthOffsets.resize(depth + 1); - for (int i = 0; i < depth; ++i) { // for each depth layer... + depthOffsets.resize(m_depth + 1); + for (int i = 0; i < m_depth; ++i) { // for each depth layer... depthOffsets[i + 1] = { 0 }; for (int j = 0; j < depthCategories.at(i).size(); ++j) { // ... and for each parent category ... int prevSize = 0; for (int k = 0; k < depthCategories.at(i).at(j).size(); ++k) { // ... and for each category value... const auto sampleSubSet = depthSamples.at(i).at(j + k); QHash> catHash; for (const auto &s : sampleSubSet) // ... and for each sample catHash[sampleValue(s, i).toString()].push_back(s); QVector cats; cats.reserve(catHash.size()); for (auto it = catHash.cbegin(); it != catHash.cend(); ++it) cats.push_back(it.key()); std::sort(cats.begin(), cats.end()); depthCategories[i + 1].push_back(cats); for (const auto &cat : cats) depthSamples[i + 1].push_back(catHash.value(cat)); if (k > 0) depthOffsets[i + 1].push_back(depthOffsets.at(i + 1).constLast() + prevSize); prevSize = cats.size(); } } } - for (const auto &cats : depthCategories.at(depth)) + for (const auto &cats : depthCategories.at(m_depth)) m_categories += cats; const auto colCount = m_categories.size(); // compute the counts per cell, we could do that on demand, but we need the maximum for QtCharts... m_data = new int[colCount * rowCount]; memset(m_data, 0, sizeof(int) * colCount * rowCount); for (int row = 0; row < rowCount; ++row) { const auto samples = m_sourceModel->index(row, 0).data(TimeAggregationModel::SamplesRole).value>(); for (const auto &sample : samples) { int parentIdx = 0; - for (int i = 1; i <= depth; ++i) { + for (int i = 1; i <= m_depth; ++i) { const auto cats = depthCategories.at(i).at(parentIdx); const auto catIt = std::lower_bound(cats.constBegin(), cats.constEnd(), sampleValue(sample, i - 1).toString()); Q_ASSERT(catIt != cats.constEnd()); parentIdx = std::distance(cats.constBegin(), catIt) + depthOffsets.at(i).at(parentIdx); } m_data[colCount * row + parentIdx]++; } // accumulate per row for stacked plots for (int col = 1; col < colCount; ++col) { const auto idx = colCount * row + col; m_data[idx] += m_data[idx - 1]; } m_maxValue = std::max(m_maxValue, m_data[row * colCount + colCount - 1]); } endResetModel(); } QVariant CategoryAggregationModel::sampleValue(const Sample& s, int depth) const { const auto elem = m_aggr.elements().at(depth); switch (elem.type()) { case AggregationElement::Value: return s.value(elem.schemaEntry().name() + QLatin1String(".") + elem.schemaEntryElement().name()); case AggregationElement::Size: const auto l = s.value(elem.schemaEntry().name()); return l.value().size(); break; } return {}; } diff --git a/src/console/model/categoryaggregationmodel.h b/src/console/model/categoryaggregationmodel.h index 0725560..8732627 100644 --- a/src/console/model/categoryaggregationmodel.h +++ b/src/console/model/categoryaggregationmodel.h @@ -1,61 +1,64 @@ /* Copyright (C) 2016 Volker Krause 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 . */ #ifndef USERFEEDBACK_CONSOLE_CATEGORYAGGREGATIONMODEL_H #define USERFEEDBACK_CONSOLE_CATEGORYAGGREGATIONMODEL_H #include #include #include namespace UserFeedback { namespace Console { class Sample; /** Aggregate by time and one string category value (e.g. version. platform, etc). */ class CategoryAggregationModel : public QAbstractTableModel { Q_OBJECT public: explicit CategoryAggregationModel(QObject *parent = nullptr); ~CategoryAggregationModel(); void setSourceModel(QAbstractItemModel *model); void setAggregation(const Aggregation &aggr); + /*! Limits depth to @p depth, even if the aggregation has a higher one. */ + void setDepth(int depth); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; private: void recompute(); QVariant sampleValue(const Sample &s, int depth) const; QAbstractItemModel *m_sourceModel = nullptr; Aggregation m_aggr; QVector m_categories; int *m_data = nullptr; int m_maxValue; + int m_depth; }; } } #endif // USERFEEDBACK_CONSOLE_CATEGORYAGGREGATIONMODEL_H