mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-04-15 05:32:21 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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,24 +576,37 @@ 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);
|
||||
host->setCursor(Qt::ClosedHandCursor);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user