mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
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:
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
1029
src/LTMPlot.cpp
1029
src/LTMPlot.cpp
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user