statistics: implement rectangle selection in scatter plot

Allow the user to select regions of the scatter plot using
a rectangular selection. When shift is pressed, do an
incremental selection.

Unfortunately, the list-selection code is so slow that this
becomes unusable for a large number of selected dives.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2021-02-01 23:17:04 +01:00 committed by Dirk Hohndel
parent e38b78b2aa
commit d63d4cd3c3
16 changed files with 207 additions and 36 deletions

View file

@ -406,12 +406,15 @@ void BarSeries::unhighlight()
highlighted = Index(); highlighted = Index();
} }
void BarSeries::selectItemsUnderMouse(const QPointF &pos, bool) bool BarSeries::selectItemsUnderMouse(const QPointF &pos, bool)
{ {
Index index = getItemUnderMouse(pos); Index index = getItemUnderMouse(pos);
if (index.bar < 0) if (index.bar < 0) {
return setSelection({}, nullptr); setSelection({}, nullptr);
return false;
}
const std::vector<dive *> &dives = items[index.bar].subitems[index.subitem].dives; const std::vector<dive *> &dives = items[index.bar].subitems[index.subitem].dives;
setSelection(dives, dives.empty() ? nullptr : dives.front()); setSelection(dives, dives.empty() ? nullptr : dives.front());
return true;
} }

View file

@ -69,7 +69,7 @@ public:
void updatePositions() override; void updatePositions() override;
bool hover(QPointF pos) override; bool hover(QPointF pos) override;
void unhighlight() override; void unhighlight() override;
void selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override; bool selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override;
private: private:
BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,

View file

@ -143,12 +143,15 @@ void BoxSeries::unhighlight()
highlighted = -1; highlighted = -1;
} }
void BoxSeries::selectItemsUnderMouse(const QPointF &pos, bool) bool BoxSeries::selectItemsUnderMouse(const QPointF &pos, bool)
{ {
int index = getItemUnderMouse(pos); int index = getItemUnderMouse(pos);
if (index < 0) if (index < 0) {
return setSelection({}, nullptr); setSelection({}, nullptr);
return false;
}
const std::vector<dive *> &dives = items[index]->q.dives; const std::vector<dive *> &dives = items[index]->q.dives;
setSelection(dives, dives.empty() ? nullptr : dives.front()); setSelection(dives, dives.empty() ? nullptr : dives.front());
return true;
} }

View file

@ -23,7 +23,7 @@ public:
void updatePositions() override; void updatePositions() override;
bool hover(QPointF pos) override; bool hover(QPointF pos) override;
void unhighlight() override; void unhighlight() override;
void selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override; bool selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override;
// Note: this expects that all items are added with increasing pos // Note: this expects that all items are added with increasing pos
// and that no bar is inside another bar, i.e. lowerBound and upperBound // and that no bar is inside another bar, i.e. lowerBound and upperBound

View file

@ -196,6 +196,12 @@ bool ChartScatterItem::contains(QPointF point) const
return squareDist(point, rect.center()) <= (scatterItemDiameter / 2.0) * (scatterItemDiameter / 2.0); return squareDist(point, rect.center()) <= (scatterItemDiameter / 2.0) * (scatterItemDiameter / 2.0);
} }
// For rectangular selections, we are more crude: simply check whether the center is in the selection.
bool ChartScatterItem::inRect(const QRectF &selection) const
{
return selection.contains(rect.center());
}
void ChartScatterItem::setHighlight(Highlight highlightIn) void ChartScatterItem::setHighlight(Highlight highlightIn)
{ {
if (highlight == highlightIn) if (highlight == highlightIn)
@ -301,15 +307,23 @@ void ChartPieItem::resize(QSizeF size)
img->fill(Qt::transparent); img->fill(Qt::transparent);
} }
ChartLineItem::ChartLineItem(StatsView &v, ChartZValue z, QColor color, double width) : HideableChartItem(v, z), ChartLineItemBase::ChartLineItemBase(StatsView &v, ChartZValue z, QColor color, double width) : HideableChartItem(v, z),
color(color), width(width), positionDirty(false), materialDirty(false) color(color), width(width), positionDirty(false), materialDirty(false)
{ {
} }
ChartLineItem::~ChartLineItem() ChartLineItemBase::~ChartLineItemBase()
{ {
} }
void ChartLineItemBase::setLine(QPointF fromIn, QPointF toIn)
{
from = fromIn;
to = toIn;
positionDirty = true;
markDirty();
}
// Helper function to set points // Helper function to set points
void setPoint(QSGGeometry::Point2D &v, const QPointF &p) void setPoint(QSGGeometry::Point2D &v, const QPointF &p)
{ {
@ -347,12 +361,37 @@ void ChartLineItem::render()
positionDirty = materialDirty = false; positionDirty = materialDirty = false;
} }
void ChartLineItem::setLine(QPointF fromIn, QPointF toIn) void ChartRectLineItem::render()
{ {
from = fromIn; if (!node) {
to = toIn; geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4));
positionDirty = true; geometry->setDrawingMode(QSGGeometry::DrawLineLoop);
markDirty(); material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
view.addQSGNode(node.get(), zValue);
positionDirty = materialDirty = true;
}
updateVisible();
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], from);
setPoint(vertices[1], QPointF(from.x(), to.y()));
setPoint(vertices[2], to);
setPoint(vertices[3], QPointF(to.x(), from.y()));
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
} }
ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal) : HideableChartItem(v, z), ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal) : HideableChartItem(v, z),

View file

@ -112,23 +112,35 @@ private:
double borderWidth; double borderWidth;
}; };
class ChartLineItem : public HideableChartItem<HideableQSGNode<QSGGeometryNode>> { // Common data for line and rect items. Both are represented by two points.
class ChartLineItemBase : public HideableChartItem<HideableQSGNode<QSGGeometryNode>> {
public: public:
ChartLineItem(StatsView &v, ChartZValue z, QColor color, double width); ChartLineItemBase(StatsView &v, ChartZValue z, QColor color, double width);
~ChartLineItem(); ~ChartLineItemBase();
void setLine(QPointF from, QPointF to); void setLine(QPointF from, QPointF to);
void render() override; // Only call on render thread! protected:
private:
QPointF from, to; QPointF from, to;
QColor color; QColor color;
double width; double width;
bool horizontal;
bool positionDirty; bool positionDirty;
bool materialDirty; bool materialDirty;
std::unique_ptr<QSGFlatColorMaterial> material; std::unique_ptr<QSGFlatColorMaterial> material;
std::unique_ptr<QSGGeometry> geometry; std::unique_ptr<QSGGeometry> geometry;
}; };
class ChartLineItem : public ChartLineItemBase {
public:
using ChartLineItemBase::ChartLineItemBase;
void render() override; // Only call on render thread!
};
// A simple rectangle without fill. Specified by any two opposing vertices.
class ChartRectLineItem : public ChartLineItemBase {
public:
using ChartLineItemBase::ChartLineItemBase;
void render() override; // Only call on render thread!
};
// A bar in a bar chart: a rectangle bordered by lines. // A bar in a bar chart: a rectangle bordered by lines.
class ChartBarItem : public HideableChartProxyItem<QSGRectangleNode> { class ChartBarItem : public HideableChartProxyItem<QSGRectangleNode> {
public: public:
@ -185,6 +197,7 @@ public:
void render() override; // Only call on render thread! void render() override; // Only call on render thread!
QRectF getRect() const; QRectF getRect() const;
bool contains(QPointF point) const; bool contains(QPointF point) const;
bool inRect(const QRectF &rect) const;
private: private:
QSGTexture *getTexture() const; QSGTexture *getTexture() const;
QRectF rect; QRectF rect;

View file

@ -265,12 +265,15 @@ void PieSeries::unhighlight()
highlighted = -1; highlighted = -1;
} }
void PieSeries::selectItemsUnderMouse(const QPointF &pos, bool) bool PieSeries::selectItemsUnderMouse(const QPointF &pos, bool)
{ {
int index = getItemUnderMouse(pos); int index = getItemUnderMouse(pos);
if (index < 0) if (index < 0) {
return setSelection({}, nullptr); setSelection({}, nullptr);
return false;
}
const std::vector<dive *> &dives = items[index].dives; const std::vector<dive *> &dives = items[index].dives;
setSelection(dives, dives.empty() ? nullptr : dives.front()); setSelection(dives, dives.empty() ? nullptr : dives.front());
return true;
} }

View file

@ -28,7 +28,7 @@ public:
void updatePositions() override; void updatePositions() override;
bool hover(QPointF pos) override; bool hover(QPointF pos) override;
void unhighlight() override; void unhighlight() override;
void selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override; bool selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override;
std::vector<QString> binNames(); std::vector<QString> binNames();

View file

@ -78,7 +78,25 @@ std::vector<int> ScatterSeries::getItemsUnderMouse(const QPointF &point) const
return res; return res;
} }
void ScatterSeries::selectItemsUnderMouse(const QPointF &point, bool shiftPressed) std::vector<int> ScatterSeries::getItemsInRect(const QRectF &rect) const
{
std::vector<int> res;
auto low = std::lower_bound(items.begin(), items.end(), rect.left(),
[] (const Item &item, double x) { return item.item->getRect().right() < x; });
auto high = std::upper_bound(low, items.end(), rect.right(),
[] (double x, const Item &item) { return x < item.item->getRect().left(); });
// 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);
for (auto it = low; it < high; ++it) {
if (it->item->inRect(rect))
res.push_back(it - items.begin());
}
return res;
}
bool ScatterSeries::selectItemsUnderMouse(const QPointF &point, bool shiftPressed)
{ {
std::vector<struct dive *> selected; std::vector<struct dive *> selected;
std::vector<int> indices = getItemsUnderMouse(point); std::vector<int> indices = getItemsUnderMouse(point);
@ -87,6 +105,7 @@ void ScatterSeries::selectItemsUnderMouse(const QPointF &point, bool shiftPresse
// When shift is pressed, add the items under the mouse to the selection // When shift is pressed, add the items under the mouse to the selection
// or, if all items under the mouse are selected, remove them. // or, if all items under the mouse are selected, remove them.
selected = getDiveSelection(); selected = getDiveSelection();
selected.reserve(indices.size() + selected.size());
bool allSelected = std::all_of(indices.begin(), indices.end(), bool allSelected = std::all_of(indices.begin(), indices.end(),
[this] (int idx) { return items[idx].d->selected; }); [this] (int idx) { return items[idx].d->selected; });
if (allSelected) { if (allSelected) {
@ -108,10 +127,38 @@ void ScatterSeries::selectItemsUnderMouse(const QPointF &point, bool shiftPresse
} }
} }
} else { } else {
selected.reserve(indices.size());
for(int idx: indices) for(int idx: indices)
selected.push_back(items[idx].d); selected.push_back(items[idx].d);
} }
setSelection(selected, selected.empty() ? nullptr : selected.front());
return !indices.empty();
}
bool ScatterSeries::supportsLassoSelection() const
{
return true;
}
void ScatterSeries::selectItemsInRect(const QRectF &rect, bool shiftPressed, const std::vector<dive *> &oldSelection)
{
std::vector<struct dive *> selected;
std::vector<int> indices = getItemsInRect(rect);
selected.reserve(oldSelection.size() + indices.size());
if (shiftPressed) {
selected = oldSelection;
// Ouch - this primitive merging of the selections grows with O(n^2). Fix this.
for (int idx: indices) {
if (std::find(selected.begin(), selected.end(), items[idx].d) == selected.end())
selected.push_back(items[idx].d);
}
} else {
for (int idx: indices)
selected.push_back(items[idx].d);
}
setSelection(selected, selected.empty() ? nullptr : selected.front()); setSelection(selected, selected.empty() ? nullptr : selected.front());
} }

View file

@ -27,11 +27,14 @@ public:
// Note: this expects that all items are added with increasing pos! // Note: this expects that all items are added with increasing pos!
void append(dive *d, double pos, double value); void append(dive *d, double pos, double value);
void selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override; bool selectItemsUnderMouse(const QPointF &point, bool shiftPressed) override;
bool supportsLassoSelection() const override;
void selectItemsInRect(const QRectF &rect, bool shiftPressed, const std::vector<dive *> &oldSelection) override;
private: private:
// Get items under mouse. // Get items under mouse.
std::vector<int> getItemsUnderMouse(const QPointF &f) const; std::vector<int> getItemsUnderMouse(const QPointF &f) const;
std::vector<int> getItemsInRect(const QRectF &f) const;
struct Item { struct Item {
ChartItemPtr<ChartScatterItem> item; ChartItemPtr<ChartScatterItem> item;

View file

@ -24,6 +24,7 @@ inline const QColor quartileMarkerColor(Qt::red);
inline const QColor regressionItemColor(Qt::red); inline const QColor regressionItemColor(Qt::red);
inline const QColor meanMarkerColor(Qt::green); inline const QColor meanMarkerColor(Qt::green);
inline const QColor medianMarkerColor(Qt::red); inline const QColor medianMarkerColor(Qt::red);
inline const QColor selectionLassoColor(Qt::black);
QColor binColor(int bin, int numBins); QColor binColor(int bin, int numBins);
QColor labelColor(int bin, size_t numBins); QColor labelColor(int bin, size_t numBins);

View file

@ -20,3 +20,12 @@ QPointF StatsSeries::toScreen(QPointF p)
void StatsSeries::divesSelected(const QVector<dive *> &) void StatsSeries::divesSelected(const QVector<dive *> &)
{ {
} }
bool StatsSeries::supportsLassoSelection() const
{
return false;
}
void StatsSeries::selectItemsInRect(const QRectF &, bool, const std::vector<dive *> &)
{
}

View file

@ -4,11 +4,13 @@
#ifndef STATS_SERIES_H #ifndef STATS_SERIES_H
#define STATS_SERIES_H #define STATS_SERIES_H
#include <vector>
#include <QPointF> #include <QPointF>
class StatsAxis; class StatsAxis;
class StatsView; class StatsView;
struct dive; struct dive;
class QRectF;
class StatsSeries { class StatsSeries {
public: public:
@ -17,7 +19,11 @@ public:
virtual void updatePositions() = 0; // Called if chart geometry changes. virtual void updatePositions() = 0; // Called if chart geometry changes.
virtual bool hover(QPointF pos) = 0; // Called on mouse movement. Return true if an item of this series is highlighted. virtual bool hover(QPointF pos) = 0; // Called on mouse movement. Return true if an item of this series is highlighted.
virtual void unhighlight() = 0; // Unhighlight any highlighted item. virtual void unhighlight() = 0; // Unhighlight any highlighted item.
virtual void selectItemsUnderMouse(const QPointF &pos, bool shiftPressed) = 0; // Returns true if an item was under the mouse.
virtual bool selectItemsUnderMouse(const QPointF &pos, bool shiftPressed) = 0;
virtual bool supportsLassoSelection() const;
// Needs only be defined if supportsLassoSelection() returns true.
virtual void selectItemsInRect(const QRectF &rect, bool shiftPressed, const std::vector<dive *> &oldSelection);
virtual void divesSelected(const QVector<dive *> &dives); virtual void divesSelected(const QVector<dive *> &dives);
protected: protected:

View file

@ -18,6 +18,7 @@
#include "zvalues.h" #include "zvalues.h"
#include "core/divefilter.h" #include "core/divefilter.h"
#include "core/subsurface-qt/divelistnotifier.h" #include "core/subsurface-qt/divelistnotifier.h"
#include "core/selection.h"
#include "core/trip.h" #include "core/trip.h"
#include <cmath> #include <cmath>
@ -30,6 +31,7 @@
// Constants that control the graph layouts // Constants that control the graph layouts
static const double sceneBorder = 5.0; // Border between scene edges and statitistics view static const double sceneBorder = 5.0; // Border between scene edges and statitistics view
static const double titleBorder = 2.0; // Border between title and chart static const double titleBorder = 2.0; // Border between title and chart
static const double selectionLassoWidth = 2.0; // Border between title and chart
StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent), StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
backgroundDirty(true), backgroundDirty(true),
@ -37,6 +39,7 @@ StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
xAxis(nullptr), xAxis(nullptr),
yAxis(nullptr), yAxis(nullptr),
draggedItem(nullptr), draggedItem(nullptr),
shiftSelection(false),
rootNode(nullptr) rootNode(nullptr)
{ {
setFlag(ItemHasContents, true); setFlag(ItemHasContents, true);
@ -82,8 +85,25 @@ void StatsView::mousePressEvent(QMouseEvent *event)
} }
bool shiftPressed = event->modifiers() & Qt::ShiftModifier; bool shiftPressed = event->modifiers() & Qt::ShiftModifier;
bool itemSelected = false;
for (auto &series: series) for (auto &series: series)
series->selectItemsUnderMouse(pos, shiftPressed); itemSelected |= series->selectItemsUnderMouse(pos, shiftPressed);
// The user clicked in "empty" space. If there is a series supporting lasso-select,
// got into lasso mode. For now, we only support a rectangular lasso.
if (!itemSelected && std::any_of(series.begin(), series.end(),
[] (const std::unique_ptr<StatsSeries> &s)
{ return s->supportsLassoSelection(); })) {
if (selectionRect)
deleteChartItem(selectionRect); // Ooops. Already a selection in place.
dragStartMouse = pos;
selectionRect = createChartItem<ChartRectLineItem>(ChartZValue::Selection, selectionLassoColor, selectionLassoWidth);
shiftSelection = shiftPressed;
oldSelection = shiftPressed ? getDiveSelection() : std::vector<dive *>();
grabMouse();
setKeepMouseGrab(true); // don't allow Qt to steal the grab
update();
}
} }
void StatsView::mouseReleaseEvent(QMouseEvent *) void StatsView::mouseReleaseEvent(QMouseEvent *)
@ -92,6 +112,12 @@ void StatsView::mouseReleaseEvent(QMouseEvent *)
draggedItem = nullptr; draggedItem = nullptr;
ungrabMouse(); ungrabMouse();
} }
if (selectionRect) {
deleteChartItem(selectionRect);
ungrabMouse();
update();
}
} }
// Define a hideable dummy QSG node that is used as a parent node to make // Define a hideable dummy QSG node that is used as a parent node to make
@ -358,14 +384,26 @@ void StatsView::divesSelected(const QVector<dive *> &dives)
void StatsView::mouseMoveEvent(QMouseEvent *event) void StatsView::mouseMoveEvent(QMouseEvent *event)
{ {
if (!draggedItem) if (draggedItem) {
return; QSizeF sceneSize = size();
if (sceneSize.width() <= 1.0 || sceneSize.height() <= 1.0)
return;
draggedItem->setPos(event->pos() - dragStartMouse + dragStartItem);
update();
}
QSizeF sceneSize = size(); if (selectionRect) {
if (sceneSize.width() <= 1.0 || sceneSize.height() <= 1.0) QPointF p1 = event->pos();
return; QPointF p2 = dragStartMouse;
draggedItem->setPos(event->pos() - dragStartMouse + dragStartItem); selectionRect->setLine(p1, p2);
update(); QRectF rect(std::min(p1.x(), p2.x()), std::min(p1.y(), p2.y()),
fabs(p2.x() - p1.x()), fabs(p2.y() - p1.y()));
for (auto &series: series) {
if (series->supportsLassoSelection())
series->selectItemsInRect(rect, shiftSelection, oldSelection);
}
update();
}
} }
void StatsView::hoverEnterEvent(QHoverEvent *) void StatsView::hoverEnterEvent(QHoverEvent *)
@ -446,6 +484,7 @@ void StatsView::reset()
regressionItem.reset(); regressionItem.reset();
meanMarker.reset(); meanMarker.reset();
medianMarker.reset(); medianMarker.reset();
selectionRect.reset();
// Mark clean and dirty chart items for deletion // Mark clean and dirty chart items for deletion
cleanItems.splice(deletedItems); cleanItems.splice(deletedItems);

View file

@ -19,6 +19,7 @@ struct StatsVariable;
class StatsSeries; class StatsSeries;
class CategoryAxis; class CategoryAxis;
class ChartItem; class ChartItem;
class ChartRectLineItem;
class ChartTextItem; class ChartTextItem;
class CountAxis; class CountAxis;
class HistogramAxis; class HistogramAxis;
@ -142,7 +143,10 @@ private:
ChartItemPtr<Legend> legend; ChartItemPtr<Legend> legend;
Legend *draggedItem; Legend *draggedItem;
ChartItemPtr<RegressionItem> regressionItem; ChartItemPtr<RegressionItem> regressionItem;
ChartItemPtr<ChartRectLineItem> selectionRect;
QPointF dragStartMouse, dragStartItem; QPointF dragStartMouse, dragStartItem;
bool shiftSelection;
std::vector<dive *> oldSelection;
void hoverEnterEvent(QHoverEvent *event) override; void hoverEnterEvent(QHoverEvent *event) override;
void hoverMoveEvent(QHoverEvent *event) override; void hoverMoveEvent(QHoverEvent *event) override;

View file

@ -11,6 +11,7 @@ enum class ChartZValue {
Axes, Axes,
SeriesLabels, SeriesLabels,
ChartFeatures, // quartile markers and regression lines ChartFeatures, // quartile markers and regression lines
Selection,
InformationBox, InformationBox,
Legend, Legend,
Count Count