mirror of
https://github.com/subsurface/subsurface.git
synced 2025-01-31 22:53:23 +00:00
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:
parent
e38b78b2aa
commit
d63d4cd3c3
16 changed files with 207 additions and 36 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 *> &)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue