summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--stats/CMakeLists.txt2
-rw-r--r--stats/scatterseries.cpp214
-rw-r--r--stats/scatterseries.h53
3 files changed, 269 insertions, 0 deletions
diff --git a/stats/CMakeLists.txt b/stats/CMakeLists.txt
index 555e1d443..2065dada3 100644
--- a/stats/CMakeLists.txt
+++ b/stats/CMakeLists.txt
@@ -15,6 +15,8 @@ set(SUBSURFACE_STATS_SRCS
legend.cpp
pieseries.h
pieseries.cpp
+ scatterseries.h
+ scatterseries.cpp
statsaxis.h
statsaxis.cpp
statscolors.h
diff --git a/stats/scatterseries.cpp b/stats/scatterseries.cpp
new file mode 100644
index 000000000..8ef795edc
--- /dev/null
+++ b/stats/scatterseries.cpp
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "scatterseries.h"
+#include "informationbox.h"
+#include "statscolors.h"
+#include "statstranslations.h"
+#include "statsvariables.h"
+#include "zvalues.h"
+#include "core/dive.h"
+#include "core/divelist.h"
+#include "core/qthelper.h"
+
+#include <QChart>
+#include <QGraphicsPixmapItem>
+#include <QPainter>
+
+static const int scatterItemDiameter = 10;
+static const int scatterItemBorder = 1;
+
+ScatterSeries::ScatterSeries(QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis,
+ const StatsVariable &varX, const StatsVariable &varY) :
+ StatsSeries(chart, xAxis, yAxis),
+ varX(varX), varY(varY)
+{
+}
+
+ScatterSeries::~ScatterSeries()
+{
+}
+
+static QPixmap createScatterPixmap(const QColor &color, const QColor &borderColor)
+{
+ QPixmap res(scatterItemDiameter, scatterItemDiameter);
+ res.fill(Qt::transparent);
+ QPainter painter(&res);
+ painter.setPen(Qt::NoPen);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.setBrush(borderColor);
+ painter.drawEllipse(0, 0, scatterItemDiameter, scatterItemDiameter);
+ painter.setBrush(color);
+ painter.drawEllipse(scatterItemBorder, scatterItemBorder,
+ scatterItemDiameter - 2 * scatterItemBorder,
+ scatterItemDiameter - 2 * scatterItemBorder);
+ return res;
+}
+
+// Annoying: we can create a QPixmap only after the application was initialized.
+// Therefore, do this as a on-demand initialized pointer. A function local static
+// variable does unnecesssary (in this case) thread synchronization.
+static std::unique_ptr<QPixmap> scatterPixmapPtr;
+static std::unique_ptr<QPixmap> scatterPixmapHighlightedPtr;
+
+static const QPixmap &scatterPixmap(bool highlight)
+{
+ if (!scatterPixmapPtr) {
+ scatterPixmapPtr.reset(new QPixmap(createScatterPixmap(fillColor, ::borderColor)));
+ scatterPixmapHighlightedPtr.reset(new QPixmap(createScatterPixmap(highlightedColor, highlightedBorderColor)));
+ }
+ return highlight ? *scatterPixmapHighlightedPtr : *scatterPixmapPtr;
+}
+
+ScatterSeries::Item::Item(QtCharts::QChart *chart, ScatterSeries *series, dive *d, double pos, double value) :
+ item(new QGraphicsPixmapItem(scatterPixmap(false), chart)),
+ d(d),
+ pos(pos),
+ value(value)
+{
+ item->setZValue(ZValues::series);
+ updatePosition(chart, series);
+}
+
+void ScatterSeries::Item::updatePosition(QtCharts::QChart *chart, ScatterSeries *series)
+{
+ QPointF center = chart->mapToPosition(QPointF(pos, value), series);
+ item->setPos(center.x() - scatterItemDiameter / 2.0,
+ center.y() - scatterItemDiameter / 2.0);
+}
+
+void ScatterSeries::Item::highlight(bool highlight)
+{
+ item->setPixmap(scatterPixmap(highlight));
+}
+
+void ScatterSeries::append(dive *d, double pos, double value)
+{
+ items.emplace_back(chart(), this, d, pos, value);
+}
+
+void ScatterSeries::updatePositions()
+{
+ QtCharts::QChart *c = chart();
+ for (Item &item: items)
+ item.updatePosition(c, this);
+}
+
+static double sq(double f)
+{
+ return f * f;
+}
+
+static double squareDist(const QPointF &p1, const QPointF &p2)
+{
+ QPointF diff = p1 - p2;
+ return QPointF::dotProduct(diff, diff);
+}
+
+std::vector<int> ScatterSeries::getItemsUnderMouse(const QPointF &point)
+{
+ std::vector<int> res;
+ double x = point.x();
+
+ auto low = std::lower_bound(items.begin(), items.end(), x - scatterItemDiameter,
+ [] (const Item &item, double x) { return item.item->pos().x() < x; });
+ auto high = std::upper_bound(low, items.end(), x + scatterItemDiameter,
+ [] (double x, const Item &item) { return x < item.item->pos().x(); });
+ // Hopefully that narrows it down enough. For discrete scatter plots, we could also partition
+ // by equal x and do a binary search in these partitions. But that's probably not worth it.
+ res.reserve(high - low);
+ double minSquare = sq(scatterItemDiameter / 2.0 + scatterItemBorder);
+ for (auto it = low; it < high; ++it) {
+ QPointF pos = it->item->pos();
+ pos.rx() += scatterItemDiameter / 2.0 + scatterItemBorder;
+ pos.ry() += scatterItemDiameter / 2.0 + scatterItemBorder;
+ if (squareDist(pos, point) <= minSquare)
+ res.push_back(it - items.begin());
+ }
+ return res;
+}
+
+static QString dataInfo(const StatsVariable &var, const dive *d)
+{
+ // For "numeric" variables, we display value and unit.
+ // For "discrete" variables, we display all categories the dive belongs to.
+ // There is only one "continuous" variable, the date, for which we don't display anything,
+ // because the date is displayed anyway.
+ QString val;
+ switch (var.type()) {
+ case StatsVariable::Type::Numeric:
+ val = var.valueWithUnit(d);
+ break;
+ case StatsVariable::Type::Discrete:
+ val = var.diveCategories(d);
+ break;
+ default:
+ return QString();
+ }
+
+ return QString("%1: %2").arg(var.name(), val);
+}
+
+// Highlight item when hovering over item
+bool ScatterSeries::hover(QPointF pos)
+{
+ std::vector<int> newHighlighted = getItemsUnderMouse(pos);
+
+ if (newHighlighted == highlighted) {
+ if (information)
+ information->setPos(pos);
+ return !newHighlighted.empty();
+ }
+
+ // This might be overkill: differential unhighlighting / highlighting of items.
+ for (int idx: highlighted) {
+ if (std::find(newHighlighted.begin(), newHighlighted.end(), idx) == newHighlighted.end())
+ items[idx].highlight(false);
+ }
+ for (int idx: newHighlighted) {
+ if (std::find(highlighted.begin(), highlighted.end(), idx) == highlighted.end())
+ items[idx].highlight(true);
+ }
+ highlighted = std::move(newHighlighted);
+
+ if (highlighted.empty()) {
+ information.reset();
+ return false;
+ } else {
+ if (!information)
+ information.reset(new InformationBox(chart()));
+
+ std::vector<QString> text;
+ text.reserve(highlighted.size() * 5);
+ int show = information->recommendedMaxLines() / 5;
+ int shown = 0;
+ for (int idx: highlighted) {
+ const dive *d = items[idx].d;
+ if (!text.empty())
+ text.push_back(QString(" ")); // Argh. Empty strings are filtered away.
+ // We don't listen to undo-command signals, therefore we have to check whether that dive actually exists!
+ // TODO: fix this.
+ if (get_divenr(d) < 0) {
+ text.push_back(StatsTranslations::tr("Removed dive"));
+ } else {
+ text.push_back(StatsTranslations::tr("Dive #%1").arg(d->number));
+ text.push_back(get_dive_date_string(d->when));
+ text.push_back(dataInfo(varX, d));
+ text.push_back(dataInfo(varY, d));
+ }
+ if (++shown >= show && shown < (int)highlighted.size()) {
+ text.push_back(" ");
+ text.push_back(StatsTranslations::tr("and %1 more").arg((int)highlighted.size() - shown));
+ break;
+ }
+ }
+
+ information->setText(text, pos);
+ return true;
+ }
+}
+
+void ScatterSeries::unhighlight()
+{
+ for (int idx: highlighted)
+ items[idx].highlight(false);
+ highlighted.clear();
+}
diff --git a/stats/scatterseries.h b/stats/scatterseries.h
new file mode 100644
index 000000000..335fb828c
--- /dev/null
+++ b/stats/scatterseries.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0
+// A small custom scatter series, where every item represents a dive
+// The original QScatterSeries was buggy and distinctly slower
+#ifndef SCATTER_SERIES_H
+#define SCATTER_SERIES_H
+
+#include "statsseries.h"
+
+#include <memory>
+#include <vector>
+#include <QGraphicsRectItem>
+
+class QGraphicsPixmapItem;
+class InformationBox;
+struct StatsVariable;
+struct dive;
+
+class ScatterSeries : public StatsSeries {
+public:
+ ScatterSeries(QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis,
+ const StatsVariable &varX, const StatsVariable &varY);
+ ~ScatterSeries();
+
+ void updatePositions() override;
+ bool hover(QPointF pos) override;
+ void unhighlight() override;
+
+ // Note: this expects that all items are added with increasing pos!
+ void append(dive *d, double pos, double value);
+
+private:
+ // Get items under mouse.
+ // Super weird: this function can't be const, because QChart::mapToValue takes
+ // a non-const reference!?
+ std::vector<int> getItemsUnderMouse(const QPointF &f);
+
+ struct Item {
+ std::unique_ptr<QGraphicsPixmapItem> item;
+ dive *d;
+ double pos, value;
+ Item(QtCharts::QChart *chart, ScatterSeries *series, dive *d, double pos, double value);
+ void updatePosition(QtCharts::QChart *chart, ScatterSeries *series);
+ void highlight(bool highlight);
+ };
+
+ std::unique_ptr<InformationBox> information;
+ std::vector<Item> items;
+ std::vector<int> highlighted;
+ const StatsVariable &varX;
+ const StatsVariable &varY;
+};
+
+#endif