Add QChart to Python Chart (3f of 5)

Added hover code to line chart, so get legend values on hover
and marker points as you mouse over the series and an x-axis
label at the bottom to show current x value.

Also added Utils::removeDP() to remove decimal places from a
number string e.g. 24.000 becomes 24, 987.3440500 becomes
987.34405. This is a hack to avoid handling decimal places
in the user settings, but will keep as reduces the amount
of "digital ink" regardless.
This commit is contained in:
Mark Liversedge
2020-02-24 09:22:17 +00:00
parent 0035530f29
commit ad3024ac7f
5 changed files with 312 additions and 141 deletions

View File

@@ -139,7 +139,7 @@ public:
//! Layout attributes
typedef QFlags<LayoutAttribute> LayoutAttributes;
QwtText( const QString & = QString::null,
QwtText( const QString & = QString(),
TextFormat textFormat = AutoText );
QwtText( const QwtText & );
~QwtText();

View File

@@ -21,6 +21,9 @@
#include "Colors.h"
#include "TabView.h"
#include "RideFileCommand.h"
#include "Utils.h"
#include <limits>
GenericPlot::GenericPlot(QWidget *parent, Context *context) : QWidget(parent), context(context)
{
@@ -96,9 +99,9 @@ GenericLegendItem::GenericLegendItem(Context *context, QWidget *parent, QString
void
GenericLegendItem::configChanged(qint32)
{
static const double gl_margin = 6 * dpiXFactor;
static const double gl_spacer = 6 * dpiXFactor;
static const double gl_block = 10 * dpiXFactor;
static const double gl_margin = 3 * dpiXFactor;
static const double gl_spacer = 3 * dpiXFactor;
static const double gl_block = 7 * dpiXFactor;
static const double gl_linewidth = 1 * dpiXFactor;
// we just set geometry for now.
@@ -121,7 +124,7 @@ GenericLegendItem::configChanged(qint32)
// calculate all the rects used by the painter now since static
blockrect = QRectF(gl_margin, gl_margin, gl_block, height-gl_margin);
linerect = QRectF(gl_margin, height-gl_linewidth, width-gl_margin, gl_linewidth);
linerect = QRectF(gl_margin+gl_block, height-gl_linewidth, width-gl_margin, gl_linewidth);
namerect = QRectF(gl_margin + gl_block + gl_spacer, gl_margin, fm.boundingRect(name).width(), fm.boundingRect(name).height());
valuerect =QRectF(namerect.x() + namerect.width() + gl_spacer, gl_margin, fm.boundingRect(valuelabel).width(), fm.boundingRect(valuelabel).height());
@@ -144,7 +147,7 @@ GenericLegendItem::paintEvent(QPaintEvent *)
// block and line
painter.setBrush(QBrush(color));
painter.setPen(Qt::NoPen);
painter.drawRect(blockrect);
//painter.drawRect(blockrect);
painter.drawRect(linerect);
// just paint the value for now
@@ -152,6 +155,9 @@ GenericLegendItem::paintEvent(QPaintEvent *)
if (hasvalue) string=QString("%1").arg(value, 0, 'f', 2);
else string=" ";
// remove redundat dps (e.g. trailing zeroes)
string = Utils::removeDP(string);
// set pen to series color for now
painter.setPen(GCColor::invertColor(GColor(CPLOTBACKGROUND))); // use invert - usually black or white
painter.setFont(QFont());
@@ -160,6 +166,7 @@ GenericLegendItem::paintEvent(QPaintEvent *)
painter.drawText(namerect, name, Qt::AlignHCenter|Qt::AlignVCenter);
painter.drawText(valuerect, string, Qt::AlignHCenter|Qt::AlignVCenter);
painter.restore();
}
GenericLegend::GenericLegend(Context *context, GenericPlot *plot) : context(context), plot(plot)
@@ -244,12 +251,64 @@ void SelectionTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QW
painter->save();
painter->setClipRect(mapRectFromScene(host->qchart->plotArea()));
// min max texts
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
stGiles.fromString(appsettings->value(NULL, GC_FONT_CHARTLABELS, QFont().toString()).toString());
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
switch (mode) {
case CIRCLE:
{
}
break;
case XRANGE:
{
// current position for each series - we only do first, coz only interested in x axis anyway
foreach(QAbstractSeries *series, host->qchart->series()) {
// convert screen position to value for series
QPointF v = host->qchart->mapToValue(spos,series);
double miny=0;
foreach (QAbstractAxis *axis, series->attachedAxes()) {
if (axis->orientation() == Qt::Vertical && axis->type()==QAbstractAxis::AxisTypeValue) {
miny=static_cast<QValueAxis*>(axis)->min();
break;
}
}
QPointF posxp = mapFromScene(host->qchart->mapToPosition(QPointF(v.x(),miny),series));
QPen markerpen(GColor(CPLOTMARKER));
painter->setPen(markerpen);
painter->setBrush(QBrush(GColor(CPLOTBACKGROUND)));
QFontMetrics fm(stGiles); // adjust position to align centre
painter->setFont(stGiles);
// x value
QString label=QString("%1").arg(v.x(),0,'f',0); // no decimal places XXX fixup on series info
label = Utils::removeDP(label); // remove unneccessary decimal places
painter->drawText(posxp-(QPointF(fm.tightBoundingRect(label).width()/2.0,4)), label);
break;
}
// draw the points we are hovering over
foreach(SeriesPoint p, hoverpoints) {
QPointF pos = mapFromScene(host->qchart->mapToPosition(p.xy,p.series));
QColor invert = GCColor::invertColor(GColor(CPLOTBACKGROUND));
painter->setBrush(invert);
painter->setPen(invert);
QRectF circle(0,0,5*dpiXFactor,5*dpiYFactor);
circle.moveCenter(pos);
painter->drawEllipse(circle);
painter->setBrush(Qt::NoBrush);
}
}
break;
case RECTANGLE:
{
if (state == ACTIVE || state == INACTIVE) {
@@ -257,28 +316,25 @@ void SelectionTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QW
if (host->charttype == GC_CHART_LINE || host->charttype == GC_CHART_SCATTER) {
// min max texts
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
stGiles.fromString(appsettings->value(NULL, GC_FONT_CHARTLABELS, QFont().toString()).toString());
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
painter->setFont(stGiles);
// current position for each series
foreach(QAbstractSeries *series, host->qchart->series()) {
// hovering around - draw label for current position in axis
if (hoverpoint != QPointF()) {
// draw a circle using marker color
painter->setBrush(GColor(CPLOTMARKER));
painter->setPen(GColor(CPLOTMARKER));
QRectF circle(0,0,25,25);
QColor invert = GCColor::invertColor(GColor(CPLOTBACKGROUND));
painter->setBrush(invert);
painter->setPen(invert);
QRectF circle(0,0,10*dpiXFactor,10*dpiYFactor);
circle.moveCenter(hoverpoint);
painter->drawEllipse(circle);
painter->setBrush(Qt::NoBrush);
}
// current position for each series
foreach(QAbstractSeries *series, host->qchart->series()) {
// convert screen position to value for series
QPointF v = host->qchart->mapToValue(spos,series);
@@ -293,6 +349,7 @@ void SelectionTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QW
// x value
QString label=QString("%1").arg(v.x(),0,'f',0); // no decimal places XXX fixup on series info
label = Utils::removeDP(label); // remove unneccessary decimal places
painter->drawText(posxp-(QPointF(fm.tightBoundingRect(label).width()/2.0,4)), label);
if (series->type() == QAbstractSeries::SeriesTypeScatter) {
@@ -305,14 +362,15 @@ void SelectionTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QW
// y value
label=QString("%1").arg(v.y(),0,'f',0); // no decimal places XXX fixup on series info
label = Utils::removeDP(label); // remove unneccessary decimal places
painter->drawText(posyp+QPointF(0,fm.tightBoundingRect(label).height()/2.0), label);
// tell the legend or whoever else is listening
//fprintf(stderr,"cursor (%f,%f) @(%f,%f) for series %s\n", spos.x(), spos.y(),v.x(),v.y(),series->name().toStdString().c_str()); fflush(stderr);
}
}
}
if (state != INACTIVE) {
// there is a rectangle to draw on the screen
@@ -444,6 +502,7 @@ SelectionTool::reset()
rect = QRectF(0,0,0,0);
hoverpoint = QPointF();
hoverseries = NULL;
hoverpoints.clear();
resetSelections();
update();
return true;
@@ -453,6 +512,7 @@ SelectionTool::reset()
bool
SelectionTool::clicked(QPointF pos)
{
if (mode == RECTANGLE) {
if (state==ACTIVE && sceneBoundingRect().contains(pos)) {
// are we moving?
@@ -485,12 +545,15 @@ SelectionTool::clicked(QPointF pos)
return true;
}
}
return false;
}
bool
SelectionTool::released(QPointF)
{
if (mode == RECTANGLE) {
// width and heights can be negative if dragged in reverse
if (state == DRAGGING) {
@@ -513,12 +576,14 @@ SelectionTool::released(QPointF)
update(rect);
return true;
}
}
return false;
}
void
SelectionTool::dragStart()
{
if (mode == RECTANGLE) {
// check still right state for it?
if (state == SIZING) {
fprintf(stderr, "drag mode!\n"); fflush(stderr);
@@ -526,11 +591,22 @@ SelectionTool::dragStart()
state = DRAGGING;
}
}
}
// for std::lower_bound search of x QPointF value
struct CompareQPointFX {
bool operator()(const QPointF p1, const QPointF p2) {
return p1.x() < p2.x();
}
};
bool
SelectionTool::moved(QPointF pos)
{
// only care if we are sizing
if (mode == RECTANGLE) {
// user hovers over points, but can select using a rectangle
if (state == SIZING) {
// cancel the timer to trigger drag
@@ -616,12 +692,66 @@ SelectionTool::moved(QPointF pos)
update(rect);
return true;
}
// END OF RECTANLGE MODE
} else if (mode == XRANGE) {
// xxx just hover for now, will do sizing shortly
// 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
double nearestx=-9999;
foreach(QAbstractSeries *series, host->qchart->series()) {
// get x value to search
double xvalue=host->qchart->mapToValue(spos,series).x();
// pointsVector
if (series->type() == QAbstractSeries::SeriesTypeLine) {
// we take a copy, would love to avoid this.
QVector<QPointF> p = static_cast<QLineSeries*>(series)->pointsVector();
// value we want
QPointF x= QPointF(xvalue,0);
// lower_bound to value near x
QVector<QPointF>::const_iterator i = std::lower_bound(p.begin(), p.end(), x, CompareQPointFX());
// collect them away
vals.insert(series, QPointF(*i));
// nearest x?
if (nearestx == -9999 || (i->x()-xvalue) < (nearestx-xvalue)) nearestx = i->x();
}
}
// run over what we found, updating paint points and signal (for legend)
hoverpoints.clear();
QMapIterator<QAbstractSeries*, QPointF> i(vals);
while (i.hasNext()) {
i.next();
if (i.value().x() == nearestx) {
SeriesPoint add;
add.series = i.key();
add.xy = i.value();
emit hover(i.value(), i.key()->name(), i.key());
if (add.xy.y()) hoverpoints << add; // ignore zeroes
} else emit unhover(i.key()->name());
}
if (vals.count()) return true;
}
return false;
}
bool
SelectionTool::wheel(int delta)
{
if (mode == RECTANGLE) {
// mouse wheel resizes selection rect if it is active
if (state == ACTIVE) {
if (delta < 0) {
@@ -631,6 +761,7 @@ SelectionTool::wheel(int delta)
}
return true;
}
}
return false;
}
@@ -904,7 +1035,7 @@ SelectionTool::updateScene()
QPointF point = scatter->at(i); // avoid deep copy
if (point.y() >= miny && point.y() <= maxy &&
point.x() >= minx && point.x() <= maxx) {
points << point;
if (!points.contains(point)) points << point; // avoid dupes
calc.addPoint(point);
}
}
@@ -1048,6 +1179,10 @@ GenericPlot::initialiseChart(QString title, int type, bool animate)
// by default they are disabled anyway
qchart->setAnimationOptions(animate ? QChart::SeriesAnimations : QChart::NoAnimation);
// what kind of selector do we use?
if (charttype==GC_CHART_LINE) selector->setMode(SelectionTool::XRANGE);
else selector->setMode(SelectionTool::RECTANGLE);
return true;
}

View File

@@ -182,6 +182,12 @@ class Calculator
QAbstractSeries *series;
};
// hover points etc
struct SeriesPoint {
QAbstractSeries *series; // series this is a point for
QPointF xy; // the actual xy value (not screen position)
};
// for watcing scene events
class SelectionTool : public QObject, public QGraphicsItem
{
@@ -193,8 +199,13 @@ class SelectionTool : public QObject, public QGraphicsItem
public:
SelectionTool(GenericPlot *);
enum { INACTIVE, SIZING, MOVING, DRAGGING, ACTIVE } state; // what state are we in?
enum { RECTANGLE, LASSOO, CIRCLE } mode; // what mode are we in?
enum stateType { INACTIVE, SIZING, MOVING, DRAGGING, ACTIVE } state; // what state are we in?
enum modeType { RECTANGLE, XRANGE, LASSOO, CIRCLE } mode; // what mode are we in?
typedef modeType SelectionMode;
typedef stateType SelectionState;
// set mode
void setMode(SelectionMode mode) { this->mode=mode; }
// is invisible and tiny. we are just an observer
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event);
@@ -235,8 +246,12 @@ class SelectionTool : public QObject, public QGraphicsItem
GenericPlot *host;
QPointF start, startingpos, finish; // when calculating distances during transitions
QPointF spos; // last point we saw
// scatter does this xxx TODO refactor into hoverpoints
QPointF hoverpoint;
QAbstractSeries *hoverseries;
// line plot uses this
QList<SeriesPoint> hoverpoints;
// selections from original during selection
QMap<QAbstractSeries*, QAbstractSeries*> selections;

View File

@@ -253,5 +253,25 @@ searchPath(QString path, QString binary, bool isexec)
return returning;
}
QString
removeDP(QString in)
{
QString out;
if (in.contains('.')) {
int n=in.indexOf('.');
out += in.mid(0,n);
int i=in.length()-1;
for(; in[i] != '.'; i--)
if (in[i] != '0')
break;
if (in[i]=='.') return out;
else out += in.mid(n, i-n+1);
return out;
} else {
return in;
}
}
};

View File

@@ -36,6 +36,7 @@ namespace Utils
QString jsonprotect(const QString &buffer);
QString jsonunprotect(const QString &buffer);
QStringList searchPath(QString path, QString binary, bool isexec=true);
QString removeDP(QString);
};