diff --git a/CMakeLists.txt b/CMakeLists.txt
index f359aa823..d25313e96 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -301,7 +301,7 @@ endif()
#set up the subsurface_link_libraries variable
set(SUBSURFACE_LINK_LIBRARIES ${SUBSURFACE_LINK_LIBRARIES} ${LIBDIVECOMPUTER_LIBRARIES} ${LIBGIT2_LIBRARIES} ${LIBUSB_LIBRARIES} ${LIBMTP_LIBRARIES})
if (NOT SUBSURFACE_TARGET_EXECUTABLE MATCHES "DownloaderExecutable")
- qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc map-widget/qml/map-widget.qrc)
+ qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc map-widget/qml/map-widget.qrc stats/qml/statsview.qrc)
endif()
# hack to build successfully on LGTM
@@ -391,6 +391,7 @@ elseif (SUBSURFACE_TARGET_EXECUTABLE MATCHES "DesktopExecutable")
subsurface_models_desktop
subsurface_commands
subsurface_corelib
+ subsurface_stats
${SUBSURFACE_LINK_LIBRARIES}
)
add_dependencies(subsurface_desktop_preferences subsurface_generated_ui)
diff --git a/stats/CMakeLists.txt b/stats/CMakeLists.txt
index 594bc6fd2..31e809270 100644
--- a/stats/CMakeLists.txt
+++ b/stats/CMakeLists.txt
@@ -27,6 +27,8 @@ set(SUBSURFACE_STATS_SRCS
statsstate.cpp
statsvariables.h
statsvariables.cpp
+ statsview.h
+ statsview.cpp
zvalues.h
)
diff --git a/stats/qml/statsview.qml b/stats/qml/statsview.qml
new file mode 100644
index 000000000..24f1fe9d3
--- /dev/null
+++ b/stats/qml/statsview.qml
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: GPL-2.0
+import QtQuick 2.0
+import QtCharts 2.0
+
+ChartView {
+ antialiasing: true
+ localizeNumbers: true
+}
diff --git a/stats/qml/statsview.qrc b/stats/qml/statsview.qrc
new file mode 100644
index 000000000..aeb65167e
--- /dev/null
+++ b/stats/qml/statsview.qrc
@@ -0,0 +1,5 @@
+
+
+ statsview.qml
+
+
diff --git a/stats/statsview.cpp b/stats/statsview.cpp
new file mode 100644
index 000000000..ba5e8c24e
--- /dev/null
+++ b/stats/statsview.cpp
@@ -0,0 +1,984 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "statsview.h"
+#include "barseries.h"
+#include "boxseries.h"
+#include "legend.h"
+#include "pieseries.h"
+#include "scatterseries.h"
+#include "statsaxis.h"
+#include "statsstate.h"
+#include "statstranslations.h"
+#include "statsvariables.h"
+#include "zvalues.h"
+#include "core/divefilter.h"
+#include "core/subsurface-qt/divelistnotifier.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Constants that control the graph layouts
+static const QColor quartileMarkerColor(Qt::red);
+static const double quartileMarkerSize = 15;
+
+static const QUrl urlStatsView = QUrl(QStringLiteral("qrc:/qml/statsview.qml"));
+
+// We use QtQuick's ChartView so that we can show the statistics on mobile.
+// However, accessing the ChartView from C++ is maliciously cumbersome and
+// the full QChart interface is not exported. Fortunately, the interface
+// leaks the QChart object: We can create a dummy-series and access the chart
+// object via the chart() accessor function. By creating a "PieSeries", the
+// ChartView does not automatically add axes.
+static QtCharts::QChart *getChart(QQuickItem *item)
+{
+ QtCharts::QAbstractSeries *abstract_series;
+ if (!item)
+ return nullptr;
+ if (!QMetaObject::invokeMethod(item, "createSeries", Qt::AutoConnection,
+ Q_RETURN_ARG(QtCharts::QAbstractSeries *, abstract_series),
+ Q_ARG(int, QtCharts::QAbstractSeries::SeriesTypePie),
+ Q_ARG(QString, QString()))) {
+ qWarning("Couldn't call createSeries()");
+ return nullptr;
+ }
+ QtCharts::QChart *res = abstract_series->chart();
+ res->removeSeries(abstract_series);
+ delete abstract_series;
+ return res;
+}
+
+bool StatsView::EventFilter::eventFilter(QObject *o, QEvent *event)
+{
+ if (event->type() == QEvent::GraphicsSceneHoverMove) {
+ QGraphicsSceneHoverEvent *hover = static_cast(event);
+ view->hover(hover->pos());
+ return true;
+ }
+ return QObject::eventFilter(o, event);
+}
+
+StatsView::StatsView(QWidget *parent) : QQuickWidget(parent),
+ highlightedSeries(nullptr),
+ eventFilter(this)
+{
+ setResizeMode(QQuickWidget::SizeRootObjectToView);
+ setSource(urlStatsView);
+ chart = getChart(rootObject());
+ connect(chart, &QtCharts::QChart::plotAreaChanged, this, &StatsView::plotAreaChanged);
+ connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible);
+
+ chart->installEventFilter(&eventFilter);
+ chart->setAcceptHoverEvents(true);
+ chart->legend()->setVisible(false);
+}
+
+StatsView::~StatsView()
+{
+}
+
+void StatsView::plotAreaChanged(const QRectF &)
+{
+ for (auto &axis: axes)
+ axis->updateLabels(chart);
+ for (auto &series: series)
+ series->updatePositions();
+ for (QuartileMarker &marker: quartileMarkers)
+ marker.updatePosition();
+ for (LineMarker &marker: lineMarkers)
+ marker.updatePosition();
+ if (legend)
+ legend->resize();
+}
+
+void StatsView::replotIfVisible()
+{
+ if (isVisible())
+ plot(state);
+}
+
+void StatsView::hover(QPointF pos)
+{
+ for (auto &series: series) {
+ if (series->hover(pos)) {
+ if (series.get() != highlightedSeries) {
+ if (highlightedSeries)
+ highlightedSeries->unhighlight();
+ highlightedSeries = series.get();
+ }
+ return;
+ }
+ }
+
+ // No series was highlighted -> unhighlight any previously highlighted series.
+ if (highlightedSeries) {
+ highlightedSeries->unhighlight();
+ highlightedSeries = nullptr;
+ }
+}
+
+template
+T *StatsView::createSeries(Args&&... args)
+{
+ StatsAxis *xAxis = axes.size() >= 2 ? axes[0].get() : nullptr;
+ StatsAxis *yAxis = axes.size() >= 2 ? axes[1].get() : nullptr;
+ T *res = new T(chart, xAxis, yAxis, std::forward(args)...);
+ series.emplace_back(res);
+ series.back()->updatePositions();
+ return res;
+}
+
+void StatsView::setTitle(const QString &s)
+{
+ chart->setTitle(s);
+}
+
+template
+T *StatsView::createAxis(const QString &title, Args&&... args)
+{
+ T *res = new T(std::forward(args)...);
+ axes.emplace_back(res);
+ axes.back()->updateLabels(chart);
+ axes.back()->qaxis()->setTitleText(title);
+ return res;
+}
+
+void StatsView::addAxes(StatsAxis *x, StatsAxis *y)
+{
+ chart->addAxis(x->qaxis(), Qt::AlignBottom);
+ chart->addAxis(y->qaxis(), Qt::AlignLeft);
+}
+
+void StatsView::reset()
+{
+ if (!chart)
+ return;
+ highlightedSeries = nullptr;
+ legend.reset();
+ series.clear();
+ quartileMarkers.clear();
+ lineMarkers.clear();
+ chart->removeAllSeries();
+ axes.clear();
+}
+
+void StatsView::plot(const StatsState &stateIn)
+{
+ state = stateIn;
+ if (!chart || !state.var1)
+ return;
+ reset();
+
+ const std::vector dives = DiveFilter::instance()->visibleDives();
+ switch (state.type) {
+ case ChartType::DiscreteBar:
+ return plotBarChart(dives, state.subtype, state.var1, state.var1Binner, state.var2,
+ state.var2Binner, state.labels, state.legend);
+ case ChartType::DiscreteValue:
+ return plotValueChart(dives, state.subtype, state.var1, state.var1Binner, state.var2,
+ state.var2Operation, state.labels);
+ case ChartType::DiscreteCount:
+ return plotDiscreteCountChart(dives, state.subtype, state.var1, state.var1Binner, state.labels);
+ case ChartType::Pie:
+ return plotPieChart(dives, state.var1, state.var1Binner, state.labels, state.legend);
+ case ChartType::DiscreteBox:
+ return plotDiscreteBoxChart(dives, state.var1, state.var1Binner, state.var2);
+ case ChartType::DiscreteScatter:
+ return plotDiscreteScatter(dives, state.var1, state.var1Binner, state.var2, state.quartiles);
+ case ChartType::HistogramCount:
+ return plotHistogramCountChart(dives, state.subtype, state.var1, state.var1Binner,
+ state.labels, state.median, state.mean);
+ case ChartType::HistogramValue:
+ return plotHistogramValueChart(dives, state.subtype, state.var1, state.var1Binner, state.var2,
+ state.var2Operation, state.labels);
+ case ChartType::HistogramStacked:
+ return plotHistogramStackedChart(dives, state.subtype, state.var1, state.var1Binner,
+ state.var2, state.var2Binner, state.labels, state.legend);
+ case ChartType::HistogramBox:
+ return plotHistogramBoxChart(dives, state.var1, state.var1Binner, state.var2);
+ case ChartType::ScatterPlot:
+ return plotScatter(dives, state.var1, state.var2);
+ default:
+ qWarning("Unknown chart type: %d", (int)state.type);
+ return;
+ }
+}
+
+template
+CategoryAxis *StatsView::createCategoryAxis(const QString &name, const StatsBinner &binner,
+ const std::vector &bins, bool isHorizontal)
+{
+ std::vector labels;
+ labels.reserve(bins.size());
+ for (const auto &[bin, dummy]: bins)
+ labels.push_back(binner.format(*bin));
+ return createAxis(name, labels, isHorizontal);
+}
+
+CountAxis *StatsView::createCountAxis(int maxVal, bool isHorizontal)
+{
+ return createAxis(StatsTranslations::tr("No. dives"), maxVal, isHorizontal);
+}
+
+// For "two-dimensionally" binned plots (eg. stacked bar or grouped bar):
+// Counts for each bin on the independent variable, including the total counts for that bin.
+struct BinCounts {
+ StatsBinPtr bin;
+ std::vector counts;
+ int total;
+};
+
+// The problem with bar plots is that for different category
+// bins, we might get different value bins. So we have to keep track
+// of our counts and adjust accordingly. That's a bit annoying.
+// Perhaps we should determine the bins of all dives first and then
+// query the counts for precisely those bins?
+struct BarPlotData {
+ std::vector hbin_counts; // For each category bin the counts for all value bins
+ std::vector vbins;
+ std::vector vbinNames;
+ int maxCount; // Highest count of any bin-combination
+ int maxCategoryCount; // Highest count of any category bin
+ // Attention: categoryBin argument will be consumed!
+ BarPlotData(std::vector &categoryBins, const StatsBinner &valuebinner);
+};
+
+BarPlotData::BarPlotData(std::vector &categoryBins, const StatsBinner &valueBinner) :
+ maxCount(0), maxCategoryCount(0)
+{
+ for (auto &[bin, dives]: categoryBins) {
+ // This moves the bin - the original pointer is invalidated
+ hbin_counts.push_back({ std::move(bin), std::vector(vbins.size(), 0), 0 });
+ for (auto &[vbin, count]: valueBinner.count_dives(dives, false)) {
+ // Note: we assume that the bins are sorted!
+ auto it = std::lower_bound(vbins.begin(), vbins.end(), vbin,
+ [] (const StatsBinPtr &p, const StatsBinPtr &bin)
+ { return *p < *bin; });
+ ssize_t pos = it - vbins.begin();
+ if (it == vbins.end() || **it != *vbin) {
+ // Add a new value bin.
+ // Attn: this invalidates "vbin", which must not be used henceforth!
+ vbins.insert(it, std::move(vbin));
+ // Fix the old arrays
+ for (auto &[bin, v, total]: hbin_counts)
+ v.insert(v.begin() + pos, 0);
+ }
+ hbin_counts.back().counts[pos] = count;
+ hbin_counts.back().total += count;
+ if (count > maxCount)
+ maxCount = count;
+ }
+ maxCategoryCount = std::max(maxCategoryCount, hbin_counts.back().total);
+ }
+
+ vbinNames.reserve(vbins.size());
+ for (const auto &vbin: vbins)
+ vbinNames.push_back(valueBinner.formatWithUnit(*vbin));
+}
+
+// Formats "x (y%)" as either a single or two strings for horizontal and non-horizontal cases, respectively.
+static std::vector makePercentageLabels(int count, int total, bool isHorizontal)
+{
+ double percentage = count * 100.0 / total;
+ QString countString = QString("%L1").arg(count);
+ QString percentageString = QString("%L1%").arg(percentage, 0, 'f', 1);
+ if (isHorizontal)
+ return { QString("%1 %2").arg(countString, percentageString) };
+ else
+ return { countString, percentageString };
+}
+
+// From a list of counts, make (count, label) pairs, where the label
+// formats the total number and the percentage of dives.
+static std::vector>> makeCountLabels(const std::vector &counts, int total,
+ bool labels, bool isHorizontal)
+{
+ std::vector>> count_labels;
+ count_labels.reserve(counts.size());
+ for (int count: counts) {
+ std::vector label = labels ? makePercentageLabels(count, total, isHorizontal)
+ : std::vector();
+ count_labels.push_back(std::make_pair(count, label));
+ }
+ return count_labels;
+}
+
+void StatsView::plotBarChart(const std::vector &dives,
+ ChartSubType subType,
+ const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
+ const StatsVariable *valueVariable, const StatsBinner *valueBinner, bool labels, bool showLegend)
+{
+ if (!categoryBinner || !valueBinner)
+ return;
+
+ setTitle(valueVariable->nameWithBinnerUnit(*valueBinner));
+
+ std::vector categoryBins = categoryBinner->bin_dives(dives, false);
+
+ bool isStacked = subType == ChartSubType::VerticalStacked || subType == ChartSubType::HorizontalStacked;
+ bool isHorizontal = subType == ChartSubType::HorizontalGrouped || subType == ChartSubType::HorizontalStacked;
+
+ // Construct the histogram axis now, because the pointers to the bins
+ // will be moved away when constructing BarPlotData below.
+ CategoryAxis *catAxis = createCategoryAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
+ *categoryBinner, categoryBins, !isHorizontal);
+
+ BarPlotData data(categoryBins, *valueBinner);
+
+ int maxVal = isStacked ? data.maxCategoryCount : data.maxCount;
+ CountAxis *valAxis = createCountAxis(maxVal, isHorizontal);
+
+ if (isHorizontal)
+ addAxes(valAxis, catAxis);
+ else
+ addAxes(catAxis, valAxis);
+
+ // Paint legend first, because the bin-names will be moved away from.
+ if (showLegend)
+ legend = std::make_unique