KPI overview item

.. uses  a datafilter program to calculate a kpi and displayed alongside
   a progressbar that shows how the value is progressing to a goal.

.. its really useful to compute across estimates, mmp etc without having
   to write a custom metric

.. one simple example is to show CP estimates and progress towards
   a target CP or 5 minute best, or perhaps weight.

   A program to display the internally generated CP estimate using
   the Morton 3 parameter model would be:

   { round(estimate(cp3,cp)); }
This commit is contained in:
Mark Liversedge
2020-06-09 12:42:53 +01:00
parent 6e8bad3d19
commit 47d79bd872
4 changed files with 462 additions and 14 deletions

View File

@@ -20,6 +20,7 @@
#include "ChartSpace.h"
#include "OverviewItems.h"
#include "AddChartWizard.h"
#include "Utils.h"
static QIcon grayConfig, whiteConfig, accentConfig;
@@ -88,7 +89,7 @@ OverviewWindow::getConfiguration() const
switch(item->type) {
case OverviewItemType::RPE:
{
RPEOverviewItem *rpe = reinterpret_cast<RPEOverviewItem*>(item);
//UNUSED RPEOverviewItem *rpe = reinterpret_cast<RPEOverviewItem*>(item);
}
break;
case OverviewItemType::METRIC:
@@ -111,7 +112,7 @@ OverviewWindow::getConfiguration() const
break;
case OverviewItemType::ROUTE:
{
RouteOverviewItem *route = reinterpret_cast<RouteOverviewItem*>(item);
// UNUSED RouteOverviewItem *route = reinterpret_cast<RouteOverviewItem*>(item);
}
break;
case OverviewItemType::INTERVAL:
@@ -128,6 +129,15 @@ OverviewWindow::getConfiguration() const
config += "\"series\":" + QString("%1").arg(static_cast<int>(zone->series)) + ",";
}
break;
case OverviewItemType::KPI:
{
KPIOverviewItem *kpi = reinterpret_cast<KPIOverviewItem*>(item);
config += "\"program\":\"" + QString("%1").arg(Utils::jsonprotect(kpi->program)) + "\",";
config += "\"units\":\"" + QString("%1").arg(kpi->units) + "\",";
config += "\"start\":" + QString("%1").arg(kpi->start) + ",";
config += "\"stop\":" + QString("%1").arg(kpi->stop) + ",";
}
break;
}
config += "\"name\":\"" + item->name + "\"";
@@ -338,6 +348,18 @@ OverviewWindow::setConfiguration(QString config)
space->addItem(order,column,deep, add);
}
break;
case OverviewItemType::KPI :
{
QString program=Utils::jsonunprotect(obj["program"].toString());
double start=obj["start"].toDouble();
double stop =obj["stop"].toDouble();
QString units =obj["units"].toString();
add = new KPIOverviewItem(space, name, start, stop, program, units);
space->addItem(order,column,deep, add);
}
break;
}
}
}
@@ -379,7 +401,15 @@ void
OverviewConfigDialog::close()
{
// remove from the layout- unless we just deleted it !
if (item) main->removeWidget(item->config());
if (item) {
main->removeWidget(item->config()); // doesn't work xxx todo !
// update after config changed
if (item->parent->context->currentRideItem())
item->setData(const_cast<RideItem*>(item->parent->context->currentRideItem()));
}
accept();
}

View File

@@ -30,6 +30,12 @@
#include "PMCData.h"
#include "RideMetadata.h"
#include "DataFilter.h"
#include "Utils.h"
#include "Tab.h"
#include "LTMTool.h"
#include "RideNavigator.h"
#include <cmath>
#include <QGraphicsSceneMouseEvent>
#include <QGLWidget>
@@ -44,14 +50,15 @@ static bool _registerItems()
// get the factory
ChartSpaceItemRegistry &registry = ChartSpaceItemRegistry::instance();
// Register TYPE SHORT DESCRIPTION SCOPE CREATOR
registry.addItem(OverviewItemType::METRIC, QObject::tr("Metric"), QObject::tr("Metric and Sparkline"), 0, MetricOverviewItem::create);
registry.addItem(OverviewItemType::META, QObject::tr("Metadata"), QObject::tr("Metadata and Sparkline"), 0, MetaOverviewItem::create);
registry.addItem(OverviewItemType::ZONE, QObject::tr("Zones"), QObject::tr("Zone Histogram"), 0, ZoneOverviewItem::create);
registry.addItem(OverviewItemType::RPE, QObject::tr("RPE"), QObject::tr("RPE Widget"), 0, RPEOverviewItem::create);
registry.addItem(OverviewItemType::INTERVAL, QObject::tr("Intervals"), QObject::tr("Interval Bubble Chart"), 0, IntervalOverviewItem::create);
registry.addItem(OverviewItemType::PMC, QObject::tr("PMC"), QObject::tr("PMC Status Summary"), 0, PMCOverviewItem::create);
registry.addItem(OverviewItemType::ROUTE, QObject::tr("Route"), QObject::tr("Route Summary"), 0, RouteOverviewItem::create);
// Register TYPE SHORT DESCRIPTION SCOPE CREATOR
registry.addItem(OverviewItemType::METRIC, QObject::tr("Metric"), QObject::tr("Metric and Sparkline"), 0, MetricOverviewItem::create);
registry.addItem(OverviewItemType::KPI, QObject::tr("KPI"), QObject::tr("KPI calculation and progress bar"), 0, KPIOverviewItem::create);
registry.addItem(OverviewItemType::META, QObject::tr("Metadata"), QObject::tr("Metadata and Sparkline"), 0, MetaOverviewItem::create);
registry.addItem(OverviewItemType::ZONE, QObject::tr("Zones"), QObject::tr("Zone Histogram"), 0, ZoneOverviewItem::create);
registry.addItem(OverviewItemType::RPE, QObject::tr("RPE"), QObject::tr("RPE Widget"), 0, RPEOverviewItem::create);
registry.addItem(OverviewItemType::INTERVAL, QObject::tr("Intervals"), QObject::tr("Interval Bubble Chart"), 0, IntervalOverviewItem::create);
registry.addItem(OverviewItemType::PMC, QObject::tr("PMC"), QObject::tr("PMC Status Summary"), 0, PMCOverviewItem::create);
registry.addItem(OverviewItemType::ROUTE, QObject::tr("Route"), QObject::tr("Route Summary"), 0, RouteOverviewItem::create);
return true;
}
@@ -74,6 +81,25 @@ RPEOverviewItem::~RPEOverviewItem()
delete sparkline;
}
KPIOverviewItem::KPIOverviewItem(ChartSpace *parent, QString name, double start, double stop, QString program, QString units) : ChartSpaceItem(parent, name)
{
this->type = OverviewItemType::KPI;
this->start = start;
this->stop = stop;
this->program = program;
this->units = units;
value ="0";
progressbar = new ProgressBar(this, start, stop, value.toDouble());
}
KPIOverviewItem::~KPIOverviewItem()
{
// delete progress bar; //XXX todo
delete progressbar;
}
RouteOverviewItem::RouteOverviewItem(ChartSpace *parent, QString name) : ChartSpaceItem(parent, name)
{
this->type = OverviewItemType::ROUTE;
@@ -325,6 +351,28 @@ IntervalOverviewItem::~IntervalOverviewItem()
delete bubble;
}
void
KPIOverviewItem::setData(RideItem *item)
{
if (item == NULL || item->ride() == NULL) return;
// calculate the value...
DataFilter parser(this, item->context, program);
Result res = parser.evaluate(item, NULL);
// set to zero for daft values
value = QString("%1").arg(res.number);
if (value == "nan") value ="";
value=Utils::removeDP(value);
// now set the progressbar
if (start == 0 and stop == 0) progressbar->hide();
else {
progressbar->show();
progressbar->setValue(start, stop, value.toDouble());
}
}
void
RPEOverviewItem::setData(RideItem *item)
{
@@ -812,6 +860,26 @@ RPEOverviewItem::itemGeometryChanged() {
}
}
void
KPIOverviewItem::itemGeometryChanged() {
QRectF geom = geometry();
if (progressbar) {
// make space for the progress bar
int minh=6;
// space enough?
if (!drag && geom.height() > (ROWHEIGHT*minh)) {
if (start != 0 || stop != 0) progressbar->show();
progressbar->setGeometry(20, ROWHEIGHT*(minh-2), geom.width()-40, geom.height()-20-(ROWHEIGHT*(minh-2)));
} else {
progressbar->hide();
}
}
}
void
MetricOverviewItem::itemGeometryChanged() {
@@ -901,6 +969,76 @@ ZoneOverviewItem::itemGeometryChanged() {
chart->setGeometry(20,20+(ROWHEIGHT*2), geom.width()-40, geom.height()-(40+(ROWHEIGHT*2)));
}
void
KPIOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) {
double addy = 0;
if (units != "" && units != tr("seconds")) addy = QFontMetrics(parent->smallfont).height();
// mid is slightly higher to account for space around title, move mid up
double mid = (ROWHEIGHT*1.5f) + ((geometry().height() - (ROWHEIGHT*2)) / 2.0f) - (addy/2);
// if we're deep enough to show the sparkline then stop
if (geometry().height() > (ROWHEIGHT*6)) mid=((ROWHEIGHT*1.5f) + (ROWHEIGHT*3) / 2.0f) - (addy/2);
// we align centre and mid
QFontMetrics fm(parent->bigfont);
QRectF rect = QFontMetrics(parent->bigfont, parent->device()).boundingRect(value);
painter->setPen(GColor(CPLOTMARKER));
painter->setFont(parent->bigfont);
painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f,
mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font
// now units
if (units != "" && addy > 0) {
painter->setPen(QColor(100,100,100));
painter->setFont(parent->smallfont);
painter->drawText(QPointF((geometry().width() - QFontMetrics(parent->smallfont).width(units)) / 2.0f,
mid + (fm.ascent() / 3.0f) + addy), units); // divided by 3 to account for "gap" at top of font
}
// paint the range and mean if the chart is shown
if (progressbar->isVisible()) {
//sparkline->paint(painter, option, widget);
// in small font max min at top bottom right of chart
double bottom = progressbar->geometry().top() + (ROWHEIGHT*2.5);
double left = ROWHEIGHT*2;
double right = geometry().width()-(ROWHEIGHT*2);
painter->setPen(QColor(100,100,100));
painter->setFont(parent->smallfont);
QString stoptext = Utils::removeDP(QString("%1").arg(stop));
QString starttext = Utils::removeDP(QString("%1").arg(start));
painter->drawText(QPointF(right - QFontMetrics(parent->smallfont).width(stoptext), bottom), stoptext);
painter->drawText(QPointF(left, bottom), starttext);
// percentage in mid font...
if (geometry().height() >= (ROWHEIGHT*8)) {
double percent = round((value.toDouble()-start)/(stop-start) * 100.0);
QString percenttext = Utils::removeDP(QString("%1%").arg(percent));
QFontMetrics mfm(parent->midfont);
QRectF mrect = QFontMetrics(parent->midfont, parent->device()).boundingRect(percenttext);
if (!percenttext.startsWith("nan") && !percenttext.startsWith("inf") && percenttext != "0%") {
// title color, copied code from chartspace.cpp, should really be a cleaner way to get these
if (GCColor::luminance(GColor(CCARDBACKGROUND)) < 127) painter->setPen(QColor(200,200,200));
else painter->setPen(QColor(70,70,70));
painter->setFont(parent->midfont);
painter->drawText(QPointF((geometry().width() - mrect.width()) / 2.0f, (ROWHEIGHT * 4.5) + mid + (mfm.ascent() / 3.0f)), percenttext); // divided by 3 to account for "gap" at top of font
}
}
}
}
void
RPEOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) {
@@ -1249,6 +1387,10 @@ void ZoneOverviewItem::itemPaint(QPainter *, const QStyleOptionGraphicsItem *, Q
//
// OverviewItem Configuration Widget
//
static bool insensitiveLessThan(const QString &a, const QString &b)
{
return a.toLower() < b.toLower();
}
OverviewItemConfig::OverviewItemConfig(ChartSpaceItem *item) : QWidget(item->parent), item(item), block(false)
{
QFormLayout *layout = new QFormLayout(this);
@@ -1294,10 +1436,125 @@ OverviewItemConfig::OverviewItemConfig(ChartSpaceItem *item) : QWidget(item->par
layout->addRow(tr("Zone Series"), series1);
}
if (item->type == OverviewItemType::KPI) {
double1 = new QDoubleSpinBox(this);
double2 = new QDoubleSpinBox(this);
double1->setMinimum(-9999999);
double1->setMaximum(9999999);
double2->setMinimum(-9999999);
double2->setMaximum(9999999);
layout->addRow(tr("Start"), double1);
layout->addRow(tr("Stop"), double2);
connect(double1, SIGNAL(valueChanged(double)), this, SLOT(dataChanged()));
connect(double2, SIGNAL(valueChanged(double)), this, SLOT(dataChanged()));
//
// Program editor... bit of a faff needs refactoring!!
//
QList<QString> list;
QString last;
SpecialFields sp;
// get sorted list
QStringList names = item->parent->context->tab->rideNavigator()->logicalHeadings;
// start with just a list of functions
list = DataFilter::builtins(item->parent->context);
// ridefile data series symbols
list += RideFile::symbols();
// add special functions (older code needs fixing !)
list << "config(cranklength)";
list << "config(cp)";
list << "config(ftp)";
list << "config(w')";
list << "config(pmax)";
list << "config(cv)";
list << "config(scv)";
list << "config(height)";
list << "config(weight)";
list << "config(lthr)";
list << "config(maxhr)";
list << "config(rhr)";
list << "config(units)";
list << "const(e)";
list << "const(pi)";
list << "daterange(start)";
list << "daterange(stop)";
list << "ctl";
list << "tsb";
list << "atl";
list << "sb(BikeStress)";
list << "lts(BikeStress)";
list << "sts(BikeStress)";
list << "rr(BikeStress)";
list << "tiz(power, 1)";
list << "tiz(hr, 1)";
list << "best(power, 3600)";
list << "best(hr, 3600)";
list << "best(cadence, 3600)";
list << "best(speed, 3600)";
list << "best(torque, 3600)";
list << "best(isopower, 3600)";
list << "best(xpower, 3600)";
list << "best(vam, 3600)";
list << "best(wpk, 3600)";
qSort(names.begin(), names.end(), insensitiveLessThan);
foreach(QString name, names) {
// handle dups
if (last == name) continue;
last = name;
// Handle bikescore tm
if (name.startsWith("BikeScore")) name = QString("BikeScore");
// Always use the "internalNames" in Filter expressions
name = sp.internalName(name);
// we do very little to the name, just space to _ and lower case it for now...
name.replace(' ', '_');
list << name;
}
// program editor
QVBoxLayout *pl= new QVBoxLayout();
editor = new DataFilterEdit(this, item->parent->context);
editor->setMinimumHeight(50 * dpiXFactor); // give me some space!
DataFilterCompleter *completer = new DataFilterCompleter(list, this);
editor->setCompleter(completer);
errors = new QLabel(this);
errors->setWordWrap(true);
errors->setStyleSheet("color: red;");
pl->addWidget(editor);
pl->addWidget(errors);
layout->addRow(tr("Program"), (QWidget*)NULL);
layout->addRow(pl);
connect(editor, SIGNAL(syntaxErrors(QStringList&)), this, SLOT(setErrors(QStringList&)));
connect(editor, SIGNAL(textChanged()), this, SLOT(dataChanged()));
// units
string1 = new QLineEdit(this);
layout->addRow(tr("Units"), string1);
connect(string1, SIGNAL(textChanged(QString)), this, SLOT(dataChanged()));
}
// reflect current config
setWidgets();
}
void
OverviewItemConfig::setErrors(QStringList &list)
{
errors->setText(list.join(";"));
}
OverviewItemConfig::~OverviewItemConfig() {}
void
@@ -1360,6 +1617,16 @@ OverviewItemConfig::setWidgets()
metric1->setSymbol(mi->symbol);
}
break;
case OverviewItemType::KPI:
{
KPIOverviewItem *mi = reinterpret_cast<KPIOverviewItem*>(item);
name->setText(mi->name);
editor->setText(mi->program);
double1->setValue(mi->start);
double2->setValue(mi->stop);
string1->setText(mi->units);
}
}
block = false;
}
@@ -1432,6 +1699,16 @@ OverviewItemConfig::dataChanged()
if (metric1->isValid()) mi->symbol = metric1->rideMetric()->symbol();
}
break;
case OverviewItemType::KPI:
{
KPIOverviewItem *mi = reinterpret_cast<KPIOverviewItem*>(item);
mi->name = name->text();
mi->units = string1->text();
mi->program = editor->toPlainText();
mi->start = double1->value();
mi->stop = double2->value();
}
}
}
@@ -2306,3 +2583,68 @@ Routeline::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*)
painter->restore();
return;
}
ProgressBar::ProgressBar(QGraphicsWidget *parent, double start, double stop, double value)
: QGraphicsItem(NULL), parent(parent), start(start), stop(stop), value(value)
{
setGeometry(20,20,100,100);
setZValue(11);
animator=new QPropertyAnimation(this, "value");
}
ProgressBar::~ProgressBar()
{
animator->stop();
delete animator;
}
void
ProgressBar::setValue(double start, double stop, double newvalue)
{
this->start=start;
this->stop = stop;
animator->stop();
animator->setStartValue(this->value);
animator->setEndValue(newvalue);
animator->setEasingCurve(QEasingCurve::OutQuad);
animator->setDuration(1000);
animator->start();
}
QVariant ProgressBar::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == ItemPositionChange && parent->scene()) prepareGeometryChange();
return QGraphicsItem::itemChange(change, value);
}
void
ProgressBar::setGeometry(double x, double y, double width, double height)
{
geom = QRectF(x,y,width,height);
// we need to go onto the scene !
if (scene() == NULL && parent->scene()) parent->scene()->addItem(this);
// set our geom
prepareGeometryChange();
}
void
ProgressBar::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*)
{
QRectF box(boundingRect().left() + (ROWHEIGHT*2), boundingRect().top() + ROWHEIGHT, geom.width()-(ROWHEIGHT*4), ROWHEIGHT/3.0);
painter->fillRect(box, QBrush(QColor(100,100,100,100)));
// width of bar
double factor = (value-start) / (stop-start);
QString percent = Utils::removeDP(QString("%1").arg(factor * 100.0));
if (factor > 1) factor = 1;
if (factor < 0) factor = 0;
QRectF bar(box.left(), box.top(), box.width() * factor, ROWHEIGHT/3.0);
painter->fillRect(bar, QBrush(GColor(CPLOTMARKER)));
}

View File

@@ -22,6 +22,7 @@
// basics
#include "ChartSpace.h"
#include "MetricSelect.h"
#include "DataFilter.h"
#include <QGraphicsItem>
// qt charts for zone chart
@@ -36,6 +37,7 @@ class BPointF;
class Sparkline;
class BubbleViz;
class Routeline;
class ProgressBar;
// sparklines number of points - look back 6 weeks
#define SPARKDAYS 42
@@ -44,7 +46,7 @@ class Routeline;
#define ROUTEPOINTS 250
// types we use start from 100 to avoid clashing with main chart types
enum OverviewItemType { RPE=100, METRIC, META, ZONE, INTERVAL, PMC, ROUTE };
enum OverviewItemType { RPE=100, METRIC, META, ZONE, INTERVAL, PMC, ROUTE, KPI };
//
// Configuration widget for ALL Overview Items
@@ -66,21 +68,57 @@ class OverviewItemConfig : public QWidget
// set the config widgets to reflect current config
void setWidgets();
// program editor
void setErrors(QStringList &errors);
private:
// the widget we are configuring
ChartSpaceItem *item;
// editor for program
DataFilterEdit *editor;
QLabel *errors;
// block updates during initialisation
bool block;
QLineEdit *name; // all of them
QLineEdit *name, *string1; // all of them
QDoubleSpinBox *double1, *double2; // KPI
MetricSelect *metric1, *metric2, *metric3; // Metric/Interval/PMC
MetricSelect *meta1; // Meta
SeriesSelect *series1; // Zone Histogram
};
class KPIOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
KPIOverviewItem(ChartSpace *parent, QString name, double start, double stop, QString program, QString units);
~KPIOverviewItem();
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
QWidget *config() { return new OverviewItemConfig(this); }
// create and config
static ChartSpaceItem *create(ChartSpace *parent) { return new KPIOverviewItem(parent, "CP", 0, 261, "{ 200; }", "watts"); }
// settings
double start, stop;
QString program, units;
// computed and ready for painting
QString value;
// progress bar viz
ProgressBar *progressbar;
};
class RPEOverviewItem : public ChartSpaceItem
{
Q_OBJECT
@@ -414,7 +452,6 @@ class Routeline : public QObject, public QGraphicsItem
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
Q_PROPERTY(int transition READ getTransition WRITE setTransition)
public:
@@ -452,4 +489,41 @@ class Routeline : public QObject, public QGraphicsItem
double owidth, oheight; // size of painterpath, so we scale to fit on paint
};
// progress bar to show percentage progress from start to goal
class ProgressBar : public QObject, public QGraphicsItem
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
Q_PROPERTY(double value READ getCurrentValue WRITE setCurrentValue)
public:
ProgressBar(QGraphicsWidget *parent, double start, double stop, double value);
~ProgressBar();
void setValue(double start, double stop, double value);
// we monkey around with this *A LOT*
void setGeometry(double x, double y, double width, double height);
QRectF geometry() { return geom; }
// needed as pure virtual in QGraphicsItem
QVariant itemChange(GraphicsItemChange change, const QVariant &value);
void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*);
QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(),
parent->geometry().y() + geom.y(),
geom.width(), geom.height());
}
// transition animation between old and new values
double getCurrentValue() const {return value;}
void setCurrentValue(double x) { if (value !=x) {value=x; update();}}
private:
QGraphicsWidget *parent;
QRectF geom;
double start, stop, value;
QPropertyAnimation *animator;
};
#endif // _GC_OverviewItem_h

View File

@@ -50,6 +50,7 @@ class EditMetricDetailDialog;
class EditUserDataDialog;
class EditUserMetricDialog;
class EditUserSeriesDialog;
class OverviewItemConfig;
//
// The RideNavigator
@@ -80,6 +81,7 @@ class RideNavigator : public GcChartWindow
friend class ::EditUserDataDialog;
friend class ::EditUserMetricDialog;
friend class ::EditUserSeriesDialog;
friend class ::OverviewItemConfig;
public:
RideNavigator(Context *, bool mainwindow = false);