Compare mode for LTM

You can now compare seasons / date ranges across or between
athletes on the LTM charts.

This is only shown on the stack chart as we need one chart
per data series - in a similar vein to the AllPlot chart.

There are some tidy ups left to do over the next few days;

- Data table needs updating to support compare mode
- Event markers need to be shown and in the right color
- PMC curve data is slow, needs some kind of optimisation
- The tooltip is missing and needs to be put back
- Ensure the new stack frame looks correct when using a
  dark plot background (or anything other than white)
- Consider how to handle zooming when there is only one
  data series and hence only one chart for compare
This commit is contained in:
Mark Liversedge
2014-02-05 20:40:24 +00:00
parent 9bb8e932c7
commit 7a83da2ee6
7 changed files with 1270 additions and 28 deletions

View File

@@ -181,6 +181,48 @@ class RelativeIntensity : public RideMetric {
RideMetric *clone() const { return new RelativeIntensity(*this); }
};
class CriticalPower : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(CriticalPower)
public:
CriticalPower()
{
setSymbol("cp_setting");
setInternalName("CP setting");
}
void initialize() {
setName(tr("Critical Power"));
setType(RideMetric::Average);
setMetricUnits(tr(""));
setImperialUnits(tr(""));
setPrecision(0);
}
void compute(const RideFile *r, const Zones *zones, int zoneRange,
const HrZones *, int,
const QHash<QString,RideMetric*> &,
const Context *) {
// did user override for this ride?
int cp = r->getTag("CP","0").toInt();
// not overriden so use the set value
// if it has been set at all
if (!cp && zones && zoneRange >= 0)
cp = zones->getCP(zoneRange);
setValue(cp);
}
bool canAggregate() { return true; }
void aggregateWith(const RideMetric &other) {
assert(symbol() == other.symbol());
setValue(other.value(true) > value(true) ? other.value(true) : value(true));
}
RideMetric *clone() const { return new CriticalPower(*this); }
};
class BikeScore : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(BikeScore)
double score;
@@ -258,7 +300,8 @@ class ResponseIndex : public RideMetric {
RideMetric *clone() const { return new ResponseIndex(*this); }
};
static bool addAllFive() {
static bool addAllSix() {
RideMetricFactory::instance().addMetric(CriticalPower());
RideMetricFactory::instance().addMetric(XPower());
QVector<QString> deps;
deps.append("skiba_xpower");
@@ -276,5 +319,5 @@ static bool addAllFive() {
return true;
}
static bool allFiveAdded = addAllFive();
static bool allSixAdded = addAllSix();

View File

@@ -78,8 +78,9 @@
// 57 20 Jan 2014 Mark Liversedge Added W' Expenditure for total energy spent above CP
// 58 23 Jan 2014 Mark Liversedge W' work rename and calculate without reference to WPrime class (speed)
// 59 24 Jan 2014 Mark Liversedge Added Maximum W' exp which is same as W'bal bur expressed as used not left
// 60 05 Feb 2014 Mark Liversedge Added Critical Power as a metric -- retreives from settings for now
int DBSchemaVersion = 59;
int DBSchemaVersion = 60;
DBAccess::DBAccess(Context* context) : context(context), db(NULL)
{

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@ class LTMPlotBackground;
class LTMWindow;
class LTMPlotZoneLabel;
class LTMScaleDraw;
class CompareScaleDraw;
class StressCalculator;
class LTMPlot : public QwtPlot
@@ -50,6 +51,7 @@ class LTMPlot : public QwtPlot
LTMPlot(LTMWindow *, Context *context);
~LTMPlot();
void setData(LTMSettings *);
void setCompareData(LTMSettings *);
void setAxisTitle(QwtAxisId axis, QString label);
public slots:
@@ -68,7 +70,7 @@ class LTMPlot : public QwtPlot
LTMWindow *parent;
double minY[10], maxY[10], maxX; // for all possible 10 curves
void resetPMC();
void createPMCCurveData(LTMSettings *, MetricDetail, QList<SummaryMetrics> &);
void createPMCCurveData(Context *,LTMSettings *, MetricDetail, QList<SummaryMetrics> &);
private:
Context *context;
@@ -94,10 +96,8 @@ class LTMPlot : public QwtPlot
QVector< QVector<double>* > stackY;
int groupForDate(QDate , int);
void createCurveData(LTMSettings *, MetricDetail,
QVector<double>&, QVector<double>&, int&);
void createTODCurveData(LTMSettings *, MetricDetail,
QVector<double>&, QVector<double>&, int&);
void createCurveData(Context *,LTMSettings *, MetricDetail, QVector<double>&, QVector<double>&, int&);
void createTODCurveData(Context *,LTMSettings *, MetricDetail, QVector<double>&, QVector<double>&, int&);
void aggregateCurves(QVector<double> &a, QVector<double>&w); // aggregate a with w, updates a
QwtAxisId chooseYAxis(QString);
void refreshZoneLabels(QwtAxisId);
@@ -110,6 +110,17 @@ class LTMPlot : public QwtPlot
QList<QwtAxisId> supportedAxes;
};
class CompareScaleDraw: public QwtScaleDraw
{
public:
CompareScaleDraw() {}
virtual QwtText label(double v) const {
return QwtText(QString("%1%2").arg(v ? "+" : "").arg(int(v)));
}
};
// Produce Labels for X-Axis
class LTMScaleDraw: public QwtScaleDraw
{

View File

@@ -41,7 +41,7 @@
#include <qwt_plot_marker.h>
LTMWindow::LTMWindow(Context *context) :
GcChartWindow(context), context(context), dirty(true), stackDirty(true)
GcChartWindow(context), context(context), dirty(true), stackDirty(true), compareDirty(true)
{
useToToday = useCustom = false;
plotted = DateRange(QDate(01,01,01), QDate(01,01,01));
@@ -79,12 +79,27 @@ LTMWindow::LTMWindow(Context *context) :
dataSummary->settings()->setFontSize(QWebSettings::DefaultFontSize, defaultFont.pointSize()+1);
dataSummary->settings()->setFontFamily(QWebSettings::StandardFont, defaultFont.family());
// compare plot page
compareplotsWidget = new QWidget(this);
compareplotsWidget->setPalette(palette);
compareplotsLayout = new QVBoxLayout(compareplotsWidget);
compareplotsLayout->setSpacing(0);
compareplotsLayout->setContentsMargins(0,0,0,0);
compareplotArea = new QScrollArea(this);
compareplotArea->setAutoFillBackground(false);
compareplotArea->setWidgetResizable(true);
compareplotArea->setWidget(compareplotsWidget);
compareplotArea->setFrameStyle(QFrame::NoFrame);
compareplotArea->setContentsMargins(0,0,0,0);
compareplotArea->setPalette(palette);
// the stack
stackWidget = new QStackedWidget(this);
stackWidget->addWidget(ltmPlot);
stackWidget->addWidget(dataSummary);
stackWidget->addWidget(plotArea);
stackWidget->addWidget(compareplotArea);
stackWidget->setCurrentIndex(0);
mainLayout->addWidget(stackWidget);
setChartLayout(mainLayout);
@@ -184,6 +199,10 @@ LTMWindow::LTMWindow(Context *context) :
connect(ltmTool, SIGNAL(curvesChanged()), this, SLOT(refresh()));
connect(context, SIGNAL(filterChanged()), this, SLOT(refresh()));
// comparing things
connect(context, SIGNAL(compareDateRangesStateChanged(bool)), this, SLOT(compareChanged()));
connect(context, SIGNAL(compareDateRangesChanged()), this, SLOT(compareChanged()));
// connect pickers to ltmPlot
connect(_canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), ltmPlot, SLOT(pointHover(QwtPlotCurve*, int)));
connect(_canvasPicker, SIGNAL(pointClicked(QwtPlotCurve*, int)), ltmPlot, SLOT(pointClicked(QwtPlotCurve*, int)));
@@ -198,6 +217,28 @@ LTMWindow::~LTMWindow()
delete popup;
}
void
LTMWindow::compareChanged()
{
if (!amVisible()) {
compareDirty = true;
return;
}
if (isCompare()) {
// refresh plot handles the compare case
refreshPlot();
} else {
// forced refresh back to normal
stackDirty = dirty = true;
filterChanged(); // forces reread etc
}
repaint();
}
void
LTMWindow::rideSelected() { } // deprecated
@@ -206,7 +247,13 @@ LTMWindow::refreshPlot()
{
if (amVisible() == true) {
if (ltmTool->showData->isChecked()) {
if (isCompare()) {
// COMPARE PLOTS
stackWidget->setCurrentIndex(3);
refreshCompare();
} else if (ltmTool->showData->isChecked()) {
// DATA TABLE
stackWidget->setCurrentIndex(1);
@@ -234,9 +281,101 @@ LTMWindow::refreshPlot()
}
}
void
LTMWindow::refreshCompare()
{
// not if in compare mode
if (!isCompare()) return;
// setup stacks but only if needed
//if (!stackDirty) return; // lets come back to that!
setUpdatesEnabled(false);
// delete old and create new...
// QScrollArea *plotArea;
// QWidget *plotsWidget;
// QVBoxLayout *plotsLayout;
// QList<LTMSettings> plotSettings;
foreach (LTMPlot *p, compareplots) {
compareplotsLayout->removeWidget(p);
delete p;
}
compareplots.clear();
compareplotSettings.clear();
if (compareplotsLayout->count() == 1) {
compareplotsLayout->takeAt(0); // remove the stretch
}
// now lets create them all again
// based upon the current setttings
// we create a plot for each curve
// but where they are stacked we put
// them all in the SAME plot
// so we go once through picking out
// the stacked items and once through
// for all the rest of the curves
LTMSettings plotSetting = settings;
plotSetting.metrics.clear();
foreach(MetricDetail m, settings.metrics) {
if (m.stack) plotSetting.metrics << m;
}
// create ltmPlot with this
if (plotSetting.metrics.count()) {
compareplotSettings << plotSetting;
// create and setup the plot
LTMPlot *stacked = new LTMPlot(this, context);
stacked->setCompareData(&compareplotSettings.last()); // setData using the compare data
stacked->setFixedHeight(200); // maybe make this adjustable later
// now add
compareplotsLayout->addWidget(stacked);
compareplots << stacked;
}
// OK, now one plot for each curve
// that isn't stacked!
foreach(MetricDetail m, settings.metrics) {
// ignore stacks
if (m.stack) continue;
plotSetting = settings;
plotSetting.metrics.clear();
plotSetting.metrics << m;
compareplotSettings << plotSetting;
// create and setup the plot
LTMPlot *plot = new LTMPlot(this, context);
plot->setCompareData(&compareplotSettings.last()); // setData using the compare data
// now add
compareplotsLayout->addWidget(plot);
compareplots << plot;
}
// squash em up
compareplotsLayout->addStretch();
// resize to choice
zoomSliderChanged();
// we no longer dirty
compareDirty = false;
setUpdatesEnabled(true);
}
void
LTMWindow::refreshStackPlots()
{
// not if in compare mode
if (isCompare()) return;
// setup stacks but only if needed
//if (!stackDirty) return; // lets come back to that!
@@ -328,9 +467,16 @@ LTMWindow::zoomSliderChanged()
settings.stackWidth = ltmTool->stackSlider->value();
setUpdatesEnabled(false);
// do the compare and the noncompare plots
// at the same time, as we don't need to worry
// about optimising out as its fast anyway
foreach(LTMPlot *plot, plots) {
plot->setFixedHeight(150 + add[index]);
}
foreach(LTMPlot *plot, compareplots) {
plot->setFixedHeight(150 + add[index]);
}
setUpdatesEnabled(true);
}
@@ -366,6 +512,8 @@ LTMWindow::useThruToday()
void
LTMWindow::refresh()
{
// not if in compare mode
if (isCompare()) return;
// refresh for changes to ridefiles / zones
if (amVisible() == true && context->athlete->metricDB != NULL) {
@@ -396,14 +544,30 @@ LTMWindow::dateRangeChanged(DateRange range)
settings.measures = &measures;
settings.bests = &bestsresults;
// apply filter to new date range too -- will also refresh plot
filterChanged();
// we let all the state get updated, but lets not actually plot
// whilst in compare mode -- but when compare mode ends we will
// call filterChanged, so need to record the fact that the date
// range changed whilst we were in compare mode
if (!isCompare()) {
// apply filter to new date range too -- will also refresh plot
filterChanged();
} else {
// we've been told to redraw so maybe
// compare mode was switched whilst we were
// not visible, lets refresh
if (compareDirty) compareChanged();
}
}
}
void
LTMWindow::filterChanged()
{
// ignore in compare mode
if (isCompare()) return;
if (amVisible() == false || context->athlete->metricDB == NULL) return;
if (useCustom) {
@@ -720,7 +884,7 @@ LTMWindow::refreshDataTable()
data = settings.measures;
} else if (metricDetail.type == METRIC_PM) {
// PMC fixup later
ltmPlot->createPMCCurveData(&settings, metricDetail, PMCdata);
ltmPlot->createPMCCurveData(context, &settings, metricDetail, PMCdata);
data = &PMCdata;
} else if (metricDetail.type == METRIC_BEST) {
data = settings.bests;

View File

@@ -119,6 +119,9 @@ class LTMWindow : public GcChartWindow
bool isFiltered() const { return (ltmTool->isFiltered() || context->ishomefiltered || context->isfiltered); }
#endif
// comparing things
bool isCompare() const { return context->isCompareDateRanges; }
// used by children
Context *context;
@@ -179,9 +182,11 @@ class LTMWindow : public GcChartWindow
void rideSelected(); // notification to refresh
void refreshPlot(); // normal mode
void refreshCompare(); // compare mode
void refreshStackPlots(); // stacked plots
void refreshDataTable(); // data table
void compareChanged();
void dateRangeChanged(DateRange);
void filterChanged();
void groupBySelected(int);
@@ -227,6 +232,7 @@ class LTMWindow : public GcChartWindow
// local state
bool dirty;
bool stackDirty;
bool compareDirty;
LTMSettings settings; // all the plot settings
QList<SummaryMetrics> results;
@@ -240,6 +246,14 @@ class LTMWindow : public GcChartWindow
QList<LTMSettings> plotSettings;
QList<LTMPlot *> plots;
// when comparing things we have a plot for each data series
// with a curve for each date range on the plot
QScrollArea *compareplotArea;
QWidget *compareplotsWidget;
QVBoxLayout *compareplotsLayout;
QList<LTMSettings> compareplotSettings;
QList<LTMPlot *> compareplots;
// Widgets
LTMPlot *ltmPlot;
LTMTool *ltmTool;

View File

@@ -92,10 +92,10 @@ class PeakPower : public RideMetric {
RideMetric *clone() const { return new PeakPower(*this); }
};
class CriticalPower : public PeakPower {
Q_DECLARE_TR_FUNCTIONS(CriticalPower)
class PeakPower60m : public PeakPower {
Q_DECLARE_TR_FUNCTIONS(PeakPower60m)
public:
CriticalPower()
PeakPower60m()
{
setSecs(3600);
setSymbol("60m_critical_power");
@@ -106,7 +106,7 @@ class CriticalPower : public PeakPower {
setMetricUnits(tr("watts"));
setImperialUnits(tr("watts"));
}
RideMetric *clone() const { return new CriticalPower(*this); }
RideMetric *clone() const { return new PeakPower60m(*this); }
};
class PeakPower1s : public PeakPower {
@@ -530,7 +530,7 @@ static bool addAllPeaks() {
RideMetricFactory::instance().addMetric(PeakPower10m());
RideMetricFactory::instance().addMetric(PeakPower20m());
RideMetricFactory::instance().addMetric(PeakPower30m());
RideMetricFactory::instance().addMetric(CriticalPower());
RideMetricFactory::instance().addMetric(PeakPower60m());
RideMetricFactory::instance().addMetric(PeakPower90m());
RideMetricFactory::instance().addMetric(PeakPowerHr1m());
RideMetricFactory::instance().addMetric(PeakPowerHr5m());