mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
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:
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user