Generic Chart Click-thru 2a of 2b

.. Scatter chart click thru from trends.

.. Need to decide how click thru will work from a line chart as the UX
   doesn't quite work as the auto hover points are elusive (they move
   when you go to click on them). Will review and fixup shortly.
This commit is contained in:
Mark Liversedge
2020-07-17 17:59:53 +01:00
parent 3bf2f13764
commit 947393914f
8 changed files with 91 additions and 39 deletions

View File

@@ -279,7 +279,7 @@ GenericLegend::removeAllSeries()
}
void
GenericLegend::setValue(QPointF value, QString name)
GenericLegend::setValue(GPointF value, QString name)
{
GenericLegendItem *call = items.value(name, NULL);
if (call) call->setValue(value.y());

View File

@@ -100,7 +100,7 @@ class GenericLegend : public QWidget {
public slots:
void setValue(QPointF value, QString name);
void setValue(GPointF value, QString name);
void unhover(QString name);
void unhoverx();
void setClickable(bool x);

View File

@@ -22,6 +22,7 @@
#include "Colors.h"
#include "TabView.h"
#include "RideFileCommand.h"
#include "RideCache.h"
#include "Utils.h"
#include <limits>
@@ -85,7 +86,8 @@ GenericPlot::GenericPlot(QWidget *parent, Context *context) : QWidget(parent), c
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
// get notifications when values change
connect(selector, SIGNAL(hover(QPointF,QString,QAbstractSeries*)), legend, SLOT(setValue(QPointF,QString)));
connect(selector, SIGNAL(seriesClicked(QAbstractSeries*,GPointF)), this, SLOT(seriesClicked(QAbstractSeries*,GPointF)));
connect(selector, SIGNAL(hover(GPointF,QString,QAbstractSeries*)), legend, SLOT(setValue(GPointF,QString)));
connect(selector, SIGNAL(unhover(QString)), legend, SLOT(unhover(QString)));
connect(selector, SIGNAL(unhoverx()), legend, SLOT(unhoverx()));
connect(legend, SIGNAL(clicked(QString,bool)), this, SLOT(setSeriesVisible(QString,bool)));
@@ -95,6 +97,19 @@ GenericPlot::GenericPlot(QWidget *parent, Context *context) : QWidget(parent), c
configChanged(0);
}
void
GenericPlot::seriesClicked(QAbstractSeries*series, GPointF point)
{
// user clicked on a point, do we need to click thru?
QVector<QString> fseries = filenames.value(series, QVector<QString>());
if (point.index >= 0 && point.index < fseries.count()) {
// click thru
RideItem *item = context->athlete->rideCache->getRide(fseries.at(point.index));
if (item) context->notifyRideSelected(item);
}
}
bool GenericPlot::eventFilter(QObject *obj, QEvent *e) { return eventHandler(1, obj, e); }
// source 0=scene, 1=widget
bool
GenericPlot::eventHandler(int, void *, QEvent *e)
@@ -245,7 +260,7 @@ void
GenericPlot::pieHover(QPieSlice *slice, bool state)
{
if (havelegend.count() == 0) return;
if (state == true) legend->setValue(QPointF(0, round(slice->percentage()*1000)/10), havelegend.first());
if (state == true) legend->setValue(GPointF(0, round(slice->percentage()*1000)/10, -1), havelegend.first());
else legend->unhover(havelegend.first());
}
@@ -253,7 +268,7 @@ GenericPlot::pieHover(QPieSlice *slice, bool state)
void GenericPlot::barsetHover(bool status, int index, QBarSet *)
{
foreach(QBarSet *barset, barsets) {
if (status) legend->setValue(QPointF(0, barset->at(index)), barset->label());
if (status) legend->setValue(GPointF(0, barset->at(index), -1), barset->label());
else legend->unhover(barset->label());
}
}
@@ -371,6 +386,7 @@ GenericPlot::initialiseChart(QString title, int type, bool animate, int legpos)
if (charttype != type) {
qchart->removeAllSeries();
curves.clear();
filenames.clear();
barseries=NULL;
}
@@ -443,7 +459,7 @@ GenericPlot::initialiseChart(QString title, int type, bool animate, int legpos)
// rendering to qt chart
bool
GenericPlot::addCurve(QString name, QVector<double> xseries, QVector<double> yseries, QVector<QString> /** UNUSED fseries **/, QString xname, QString yname,
GenericPlot::addCurve(QString name, QVector<double> xseries, QVector<double> yseries, QVector<QString> fseries, QString xname, QString yname,
QStringList labels, QStringList colors,
int linestyle, int symbol, int size, QString color, int opacity, bool opengl, bool legend, bool datalabels, bool fill)
{
@@ -627,6 +643,10 @@ GenericPlot::addCurve(QString name, QVector<double> xseries, QVector<double> yse
QScatterSeries *add = new QScatterSeries();
add->setName(name);
// handle click thru
connect(add, SIGNAL(clicked(QPointF)), selector, SLOT(seriesClicked())); // catch series clicks
filenames.insert(add, fseries);
// aesthetics
if (symbol == 0) add->setVisible(false); // no marker !
else if (symbol == 1) add->setMarkerShape(QScatterSeries::MarkerShapeCircle);
@@ -661,7 +681,7 @@ GenericPlot::addCurve(QString name, QVector<double> xseries, QVector<double> yse
Quadtree *tree = new Quadtree(QPointF(calc.x.min, calc.y.min), QPointF(calc.x.max, calc.y.max));
for (int i=0; i<xseries.size() && i<yseries.size(); i++)
if (xseries.at(i) != 0 && yseries.at(i) != 0) // 0,0 is common and lets ignore (usually means no data)
tree->insert(QPointF(xseries.at(i), yseries.at(i)));
tree->insert(GPointF(xseries.at(i), yseries.at(i), i));
if (tree->nodes.count() || tree->root->contents.count()) quadtrees.insert(add, tree);

View File

@@ -109,6 +109,7 @@ class GenericPlot : public QWidget {
void setSeriesVisible(QString name, bool visible);
// watching scene events and managing interaction
void seriesClicked(QAbstractSeries*series, GPointF point);
bool eventHandler(int eventsource, void *obj, QEvent *event);
void barsetHover(bool status, int index, QBarSet *barset);
void plotAreaChanged();
@@ -139,7 +140,6 @@ class GenericPlot : public QWidget {
// quadtrees
QMap<QAbstractSeries*, Quadtree*> quadtrees;
// annotation labels
QList<QLabel *> labels;
@@ -150,6 +150,9 @@ class GenericPlot : public QWidget {
// curves
QMap<QString, QAbstractSeries *>curves;
// filenames
QMap<QAbstractSeries*, QVector<QString> > filenames;
// decorations (symbols for line charts, lines for scatter)
QMap<QAbstractSeries*, QAbstractSeries *>decorations;

View File

@@ -32,7 +32,7 @@ GenericSelectTool::GenericSelectTool(GenericPlot *host) : QObject(host), QGraphi
mode = RECTANGLE;
setVisible(true); // always visible - paints on axis
setZValue(100); // always on top.
hoverpoint = QPointF();
hoverpoint = GPointF();
hoverseries = NULL;
hoveraxis = NULL;
rect = QRectF(0,0,0,0);
@@ -111,7 +111,7 @@ void GenericSelectTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *
//
// SCATTER PLOT HOVERED POINT
//
if (hoverpoint != QPointF()) {
if (hoverpoint != GPointF()) {
// draw a circle using marker color
QColor invert = GCColor::invertColor(GColor(CPLOTBACKGROUND));
painter->setBrush(invert);
@@ -380,7 +380,6 @@ QRectF GenericSelectTool::boundingRect() const { return rect; }
// trap events and redirect to plot event handler
bool GenericSelectTool::sceneEventFilter(QGraphicsItem *watched, QEvent *event) { return host->eventHandler(0, watched,event); }
bool GenericPlot::eventFilter(QObject *obj, QEvent *e) { return eventHandler(1, obj, e); }
bool
GenericSelectTool::reset()
@@ -390,7 +389,7 @@ GenericSelectTool::reset()
start=QPointF(0,0);
finish=QPointF(0,0);
rect = QRectF(0,0,0,0);
hoverpoint = QPointF();
hoverpoint = GPointF();
hoverseries = NULL;
hoverpoints.clear();
hoveraxis = NULL;
@@ -402,17 +401,36 @@ GenericSelectTool::reset()
}
// handle mouse events in selector
bool
GenericSelectTool::seriesClicked()
{
// clicked on a point in the series
return clicked(QPointF()); // ignore mostly
}
bool
GenericSelectTool::clicked(QPointF pos)
{
bool updatescene = false;
// click on a point to click-thru
if (hoverpoint.index != -1) { // hovering and clicked
emit seriesClicked(hoverseries, hoverpoint);
// not sure need to do this....
hoverpoints.clear();
hoverpoint=GPointF();
return false;
} else if (pos == QPointF()) { // series clicked and not hovering
return false;
}
if (mode == XRANGE || mode == RECTANGLE) {
if (hoveraxis) return false;
hoverpoints.clear();
hoverpoint=QPointF();
if (state==ACTIVE && sceneBoundingRect().contains(pos)) {
@@ -506,7 +524,7 @@ GenericSelectTool::released(QPointF pos)
// finishing move/resize
state = ACTIVE;
hoverpoint=QPointF();
hoverpoint=GPointF();
hoverpoints.clear();
rectchanged = true;
update(rect);
@@ -600,8 +618,8 @@ GenericSelectTool::moved(QPointF pos)
// this needs to be super quick as mouse
// movements are very fast, so we use a
// quadtree to find the nearest points
QPointF hoverv; // value // series x,y co-ord used in signal (and legend later)
hoverpoint = QPointF(); // screen coordinates
GPointF hoverv; // value // series x,y co-ord used in signal (and legend later)
hoverpoint = GPointF(); // screen coordinates
QAbstractSeries *originalhoverseries = hoverseries;
hoverseries = NULL;
foreach(QAbstractSeries *series, host->qchart->series()) {
@@ -618,18 +636,18 @@ GenericSelectTool::moved(QPointF pos)
//QPointF vpos = host->qchart->mapToValue(pos, series);
// find candidates all close by using paint co-ords
QList<QPointF> tohere;
QList<GPointF> tohere;
tree->candidates(vrect, tohere);
QPointF cursorpos=mapFromScene(pos);
foreach(QPointF p, tohere) {
foreach(GPointF p, tohere) {
QPointF scpos = mapFromScene(host->qchart->mapToPosition(p, series));
if (hoverpoint == QPointF()) {
hoverpoint = scpos;
if (hoverpoint == GPointF()) {
hoverpoint = GPointF(scpos.x(), scpos.y(), p.index);
hoverseries = series;
hoverv = p;
} else if ((cursorpos-scpos).manhattanLength() < (cursorpos-hoverpoint).manhattanLength()) {
hoverpoint=scpos; // not happy with this XXX needs more work
hoverpoint=GPointF(scpos.x(), scpos.y(), p.index);
hoverseries = series;
hoverv = p;
}
@@ -641,13 +659,13 @@ GenericSelectTool::moved(QPointF pos)
}
// hoverpoint changed - either a new series selected, a new point, or no point at all
if (originalhoverseries != hoverseries || hoverv != QPointF()) {
if (originalhoverseries != hoverseries || hoverv != GPointF()) {
if (hoverseries != originalhoverseries && originalhoverseries != NULL) emit (unhover(originalhoverseries->name())); // old hover changed
if (hoverseries != NULL) emit hover(hoverv, hoverseries->name(), hoverseries); // new hover changed
}
// we need to clear x-axis if we aren't hovering on anything at all
if (hoverv == QPointF()) {
if (hoverv == GPointF()) {
emit unhoverx();
}
@@ -660,7 +678,7 @@ GenericSelectTool::moved(QPointF pos)
// user hovers with a vertical line, but can select a range on the x axis
// lets get x axis value (any old series will do as they should have a common x axis
spos = pos;
QMap<QAbstractSeries*,QPointF> vals; // what values were found
QMap<QAbstractSeries*,GPointF> vals; // what values were found
double nearestx=-9999;
foreach(QAbstractSeries *series, host->qchart->series()) {
@@ -689,7 +707,7 @@ GenericSelectTool::moved(QPointF pos)
QVector<QPointF>::const_iterator i = std::lower_bound(p.begin(), p.end(), x, CompareQPointFX());
// collect them away
vals.insert(series, QPointF(*i));
vals.insert(series, GPointF(i->x(), i->y(), i-p.begin()));
// nearest x?
if (i->x() != 0 && (nearestx == -9999 || (std::fabs(i->x()-xvalue)) < std::fabs((nearestx-xvalue)))) nearestx = i->x();
@@ -699,7 +717,7 @@ GenericSelectTool::moved(QPointF pos)
// run over what we found, updating paint points and signal (for legend)
hoverpoints.clear();
QMapIterator<QAbstractSeries*, QPointF> i(vals);
QMapIterator<QAbstractSeries*, GPointF> i(vals);
while (i.hasNext()) {
i.next();
if (i.value().x() == nearestx) {
@@ -918,7 +936,7 @@ GenericSelectTool::setSeriesVisible(QString name, bool visible)
// but for now this is the quickes and simplest way
// to avoid artefacts
hoverpoints.clear();
hoverpoint=QPointF();
hoverpoint=GPointF();
// hide/show and updatescenes may overlap/get out of sync
// so we update scene to be absolutely sure the scene

View File

@@ -134,9 +134,11 @@ class GenericSelectTool : public QObject, public QGraphicsItem
public slots:
void dragStart();
bool seriesClicked();
Q_SIGNALS:
void hover(QPointF value, QString name, QAbstractSeries*series); // mouse cursor is over a point on the chart
void seriesClicked(QAbstractSeries*,GPointF);
void hover(GPointF value, QString name, QAbstractSeries*series); // mouse cursor is over a point on the chart
void unhover(QString name); // mouse cursor is no longer over a point on the chart
void unhoverx(); // when we aren't hovering on anything at all
@@ -150,7 +152,7 @@ class GenericSelectTool : public QObject, public QGraphicsItem
QPointF spos; // last point we saw
// scatter does this xxx TODO refactor into hoverpoints
QPointF hoverpoint;
GPointF hoverpoint;
QAbstractSeries *hoverseries;
// line plot uses this

View File

@@ -20,7 +20,7 @@
// add a node
bool
QuadtreeNode::insert(Quadtree *root, QPointF value)
QuadtreeNode::insert(Quadtree *root, GPointF value)
{
if (!contains(value)) return false;
@@ -44,7 +44,7 @@ QuadtreeNode::insert(Quadtree *root, QPointF value)
// get candidates
int
QuadtreeNode::candidates(QRectF rect, QList<QPointF> &here)
QuadtreeNode::candidates(QRectF rect, QList<GPointF> &here)
{
// nope
if (!intersect(rect)) return 0;
@@ -125,7 +125,7 @@ Quadtree::newnode(QPointF topleft, QPointF bottomright)
return add;
}
bool Quadtree::insert(QPointF point)
bool Quadtree::insert(GPointF point)
{
bool result= root->insert(this, point);
if (result == false) {

View File

@@ -23,6 +23,15 @@
#include <QRectF>
#include <QList>
class GPointF : public QPointF
{
public:
GPointF() : QPointF(), index(-1) {}
GPointF(double x, double y, int index) : QPointF(x,y), index(index) {}
int index;
};
class GenericPlot;
class Quadtree;
class QuadtreeNode
@@ -40,17 +49,17 @@ class QuadtreeNode
topleft(topleft), bottomright(bottomright), mid((topleft+bottomright)/2.0), leaf(true) {}
// is the point in our space - when inserting
bool contains(QPointF p) { return ((p.x() >= topleft.x() && p.x() <= bottomright.x()) &&
bool contains(GPointF p) { return ((p.x() >= topleft.x() && p.x() <= bottomright.x()) &&
p.y() >= topleft.y() && p.y() <= bottomright.y()); }
// do we overlap with the search space - when looking
bool intersect(QRectF r) { return r.intersects(QRectF(topleft,bottomright)); }
// add a point - return false if not added
bool insert(Quadtree *root, QPointF value);
bool insert(Quadtree *root, GPointF value);
// get candidates in same quadrant (might be miles away for big quadrant).
int candidates(QRectF,QList<QPointF>&tohere);
int candidates(QRectF,QList<GPointF>&tohere);
protected:
@@ -64,7 +73,7 @@ class QuadtreeNode
QuadtreeNode *aabb[4];
// the points in this quadrant
QList<QPointF> contents;
QList<GPointF> contents;
// if no children in aabb leaf==true
bool leaf;
@@ -79,10 +88,10 @@ class Quadtree
~Quadtree();
// add a point - returns false if not in range
bool insert(QPointF x);
bool insert(GPointF x);
// find points in boundg rect, of course might be long way away...
int candidates(QRectF rect, QList<QPointF>&tohere) { return root->candidates(rect, tohere); }
int candidates(QRectF rect, QList<GPointF>&tohere) { return root->candidates(rect, tohere); }
// manage the entire child tree on a single qvector to delete quickly
QuadtreeNode *newnode(QPointF topleft, QPointF bottomright);