mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
It TREBLES the amount of time required to refresh the metrics, so will need to be optmised before 3.1 is released. But it should only need to run once. I've also added a 'RideMetric::Low' type which we could also apply to weight.
1410 lines
51 KiB
C++
1410 lines
51 KiB
C++
/*
|
|
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
|
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "Athlete.h"
|
|
#include "Context.h"
|
|
#include "LTMPlot.h"
|
|
#include "LTMTool.h"
|
|
#include "LTMTrend.h"
|
|
#include "LTMOutliers.h"
|
|
#include "LTMWindow.h"
|
|
#include "MetricAggregator.h"
|
|
#include "SummaryMetrics.h"
|
|
#include "RideMetric.h"
|
|
#include "Settings.h"
|
|
#include "Colors.h"
|
|
|
|
#include "StressCalculator.h" // for LTS/STS calculation
|
|
|
|
#include <QSettings>
|
|
|
|
#include <qwt_series_data.h>
|
|
#include <qwt_legend.h>
|
|
#include <qwt_plot_curve.h>
|
|
#include <qwt_curve_fitter.h>
|
|
#include <qwt_plot_grid.h>
|
|
#include <qwt_symbol.h>
|
|
|
|
#include <math.h> // for isinf() isnan()
|
|
|
|
static int supported_axes[] = { QwtPlot::yLeft, QwtPlot::yRight, QwtPlot::yLeft1, QwtPlot::yRight1, QwtPlot::yLeft2, QwtPlot::yRight2, QwtPlot::yLeft3, QwtPlot::yRight3 };
|
|
|
|
LTMPlot::LTMPlot(LTMWindow *parent, Context *context) :
|
|
bg(NULL), parent(parent), context(context), highlighter(NULL)
|
|
{
|
|
setInstanceName("Metric Plot");
|
|
|
|
// get application settings
|
|
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
|
|
setAxisTitle(yLeft, tr(""));
|
|
setAxisTitle(xBottom, tr("Date"));
|
|
setAxisMaxMinor(QwtPlot::xBottom,-1);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(QDateTime::currentDateTime(), 0, LTM_DAY));
|
|
|
|
canvas()->setFrameStyle(QFrame::NoFrame);
|
|
|
|
grid = new QwtPlotGrid();
|
|
grid->enableX(false);
|
|
grid->attach(this);
|
|
|
|
settings = NULL;
|
|
|
|
configUpdate(); // set basic colors
|
|
|
|
connect(context, SIGNAL(configChanged()), this, SLOT(configUpdate()));
|
|
}
|
|
|
|
LTMPlot::~LTMPlot()
|
|
{
|
|
}
|
|
|
|
void
|
|
LTMPlot::configUpdate()
|
|
{
|
|
// set basic plot colors
|
|
setCanvasBackground(GColor(CPLOTBACKGROUND));
|
|
QPen gridPen(GColor(CPLOTGRID));
|
|
gridPen.setStyle(Qt::DotLine);
|
|
grid->setPen(gridPen);
|
|
}
|
|
|
|
void
|
|
LTMPlot::setAxisTitle(int axis, QString label)
|
|
{
|
|
// setup the default fonts
|
|
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
|
|
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
|
|
|
QwtText title(label);
|
|
title.setFont(stGiles);
|
|
QwtPlot::setAxisFont(axis, stGiles);
|
|
QwtPlot::setAxisTitle(axis, title);
|
|
}
|
|
|
|
void
|
|
LTMPlot::setData(LTMSettings *set)
|
|
{
|
|
settings = set;
|
|
|
|
// For each metric in chart, translate units and name if default uname
|
|
LTMTool::translateMetrics(context, settings);
|
|
|
|
// crop dates to at least within a year of the data available, but only if we have some data
|
|
if (settings->data != NULL && (*settings->data).count() != 0) {
|
|
// if dates are null we need to set them from the available data
|
|
|
|
// end
|
|
if (settings->end == QDateTime() ||
|
|
settings->end > (*settings->data).last().getRideDate().addDays(365)) {
|
|
if (settings->end < QDateTime::currentDateTime()) {
|
|
settings->end = QDateTime::currentDateTime();
|
|
} else {
|
|
settings->end = (*settings->data).last().getRideDate();
|
|
}
|
|
}
|
|
|
|
// start
|
|
if (settings->start == QDateTime() ||
|
|
settings->start < (*settings->data).first().getRideDate().addDays(-365)) {
|
|
settings->start = (*settings->data).first().getRideDate();
|
|
}
|
|
}
|
|
|
|
//setTitle(settings->title);
|
|
if (settings->groupBy != LTM_TOD)
|
|
setAxisTitle(xBottom, tr("Date"));
|
|
else
|
|
setAxisTitle(xBottom, tr("Time of Day"));
|
|
|
|
// wipe existing curves/axes details
|
|
QHashIterator<QString, QwtPlotCurve*> c(curves);
|
|
while (c.hasNext()) {
|
|
c.next();
|
|
QString symbol = c.key();
|
|
QwtPlotCurve *current = c.value();
|
|
//current->detach(); // the destructor does this for you
|
|
delete current;
|
|
}
|
|
curves.clear();
|
|
if (highlighter) {
|
|
highlighter->detach();
|
|
delete highlighter;
|
|
highlighter = NULL;
|
|
}
|
|
|
|
// disable all y axes until we have populated
|
|
for (int i=0; i<8; i++) enableAxis(supported_axes[i], false);
|
|
axes.clear();
|
|
|
|
// reset all min/max Y values
|
|
for (int i=0; i<10; i++) minY[i]=0, maxY[i]=0;
|
|
|
|
// no data to display so that all folks
|
|
if (settings->data == NULL || (*settings->data).count() == 0) {
|
|
|
|
// tidy up the bottom axis
|
|
maxX = groupForDate(settings->end.date(), settings->groupBy) -
|
|
groupForDate(settings->start.date(), settings->groupBy);
|
|
|
|
setAxisScale(xBottom, 0, maxX);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
|
|
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
|
|
|
|
// remove the shading if it exists
|
|
refreshZoneLabels(-1);
|
|
|
|
// remove the old markers
|
|
refreshMarkers(settings->start.date(), settings->end.date(), settings->groupBy);
|
|
|
|
replot();
|
|
return;
|
|
}
|
|
|
|
// count the bars since we format them side by side and need
|
|
// to now how to offset them from each other
|
|
// unset stacking if not a bar chart too since we don't support
|
|
// that yet, but would be good to add in the future (stacked
|
|
// area plot).
|
|
int barnum=0;
|
|
int bars = 0;
|
|
int stacknum = -1;
|
|
// index through rather than foreach so we can modify
|
|
for (int v=0; v<settings->metrics.count(); v++) {
|
|
if (settings->metrics[v].curveStyle == QwtPlotCurve::Steps) {
|
|
if (settings->metrics[v].stack && stacknum < 0) stacknum = bars++; // starts from 1 not zero
|
|
else if (settings->metrics[v].stack == false) bars++;
|
|
} else if (settings->metrics[v].stack == true)
|
|
settings->metrics[v].stack = false; // we only support stack on bar charts
|
|
}
|
|
|
|
// aggregate the stack curves - backwards since
|
|
// we plot forwards overlaying to create the illusion
|
|
// of a stack, when in fact its just bars of descending
|
|
// order (with values aggregated)
|
|
|
|
// free stack memory
|
|
foreach(QVector<double>*p, stackX) delete p;
|
|
foreach(QVector<double>*q, stackY) delete q;
|
|
stackX.clear();
|
|
stackY.clear();
|
|
stacks.clear();
|
|
|
|
int r=0;
|
|
foreach (MetricDetail metricDetail, settings->metrics) {
|
|
if (metricDetail.stack == true) {
|
|
|
|
// register this data
|
|
QVector<double> *xdata = new QVector<double>();
|
|
QVector<double> *ydata = new QVector<double>();
|
|
stackX.append(xdata);
|
|
stackY.append(ydata);
|
|
|
|
int count;
|
|
if (settings->groupBy != LTM_TOD)
|
|
createCurveData(settings, metricDetail, *xdata, *ydata, count);
|
|
else
|
|
createTODCurveData(settings, metricDetail, *xdata, *ydata, count);
|
|
|
|
// we add in the last curve for X axis values
|
|
if (r) {
|
|
aggregateCurves(*stackY[r], *stackY[r-1]);
|
|
}
|
|
r++;
|
|
}
|
|
}
|
|
|
|
// setup the curves
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble();
|
|
bool donestack = false;
|
|
|
|
// now we iterate over the metric details AGAIN
|
|
// but this time in reverse and only plot the
|
|
// stacked values. This is because we overcome the
|
|
// lack of a stacked plot in QWT by painting decreasing
|
|
// bars, with the values aggregated previously
|
|
// so if we plot L1 time in zone 1hr and L2 time in zone 1hr
|
|
// it plots as L2 time in zone 2hr and then paints over that
|
|
// with a L1 time in zone of 1hr.
|
|
//
|
|
// The tooltip has to unpick the aggregation to ensure
|
|
// that it subtracts other data series in the stack from
|
|
// the value plotted... all nasty but heck, it works
|
|
int stackcounter = stackX.size()-1;
|
|
for (int m=settings->metrics.count()-1; m>=0; m--) {
|
|
|
|
int count=0;
|
|
MetricDetail metricDetail = settings->metrics[m];
|
|
|
|
if (metricDetail.stack == false) continue;
|
|
|
|
QVector<double> xdata, ydata;
|
|
|
|
// use the aggregated values
|
|
xdata = *stackX[stackcounter];
|
|
ydata = *stackY[stackcounter];
|
|
stackcounter--;
|
|
count = xdata.size()-2;
|
|
|
|
// no data to plot!
|
|
if (count <= 0) continue;
|
|
|
|
// Create a curve
|
|
QwtPlotCurve *current = new QwtPlotCurve(metricDetail.uname);
|
|
if (metricDetail.type == METRIC_BEST)
|
|
curves.insert(metricDetail.bestSymbol, current);
|
|
else
|
|
curves.insert(metricDetail.symbol, current);
|
|
stacks.insert(current, stackcounter+1);
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true)
|
|
current->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen cpen = QPen(metricDetail.penColor);
|
|
cpen.setWidth(width);
|
|
current->setPen(cpen);
|
|
current->setStyle(metricDetail.curveStyle);
|
|
|
|
QwtSymbol sym;
|
|
sym.setStyle(metricDetail.symbolStyle);
|
|
|
|
// choose the axis
|
|
int axisid = chooseYAxis(metricDetail.uunits);
|
|
current->setYAxis(axisid);
|
|
|
|
// left and right offset for bars
|
|
double left = 0;
|
|
double right = 0;
|
|
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
int barn = metricDetail.stack ? stacknum : barnum;
|
|
|
|
double space = double(0.9) / bars;
|
|
double gap = space * 0.10;
|
|
double width = space * 0.90;
|
|
left = (space * barn) + (gap / 2) + 0.1;
|
|
right = left + width;
|
|
|
|
if (metricDetail.stack && donestack == false) {
|
|
barnum++;
|
|
donestack = true;
|
|
} else if (metricDetail.stack == false) barnum++;
|
|
}
|
|
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// fill the bars
|
|
QColor brushColor = metricDetail.penColor;
|
|
if (metricDetail.stack == true) {
|
|
brushColor.setAlpha(255);
|
|
QBrush brush = QBrush(brushColor);
|
|
current->setBrush(brush);
|
|
} else {
|
|
brushColor.setAlpha(64); // now side by side, less transparency required
|
|
QColor brushColor1 = brushColor.darker();
|
|
|
|
QLinearGradient linearGradient(0, 0, 0, height());
|
|
linearGradient.setColorAt(0.0, brushColor1);
|
|
linearGradient.setColorAt(1.0, brushColor);
|
|
linearGradient.setSpread(QGradient::PadSpread);
|
|
current->setBrush(linearGradient);
|
|
}
|
|
|
|
current->setPen(Qt::NoPen);
|
|
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
|
|
|
|
sym.setStyle(QwtSymbol::NoSymbol);
|
|
current->setSymbol(new QwtSymbol(sym));
|
|
|
|
// fudge for date ranges, not for time of day graph
|
|
// and fudge qwt'S lack of a decent bar chart
|
|
// add a zero point at the head and tail so the
|
|
// histogram columns look nice.
|
|
// and shift all the x-values left by 0.5 so that
|
|
// they centre over x-axis labels
|
|
int i=0;
|
|
for (i=0; i<count; i++) xdata[i] -= 0.5;
|
|
// now add a final 0 value to get the last
|
|
// column drawn - no resize neccessary
|
|
// since it is always sized for 1 + maxnumber of entries
|
|
xdata[i] = xdata[i-1] + 1;
|
|
ydata[i] = 0;
|
|
count++;
|
|
|
|
QVector<double> xaxis (xdata.size() * 4);
|
|
QVector<double> yaxis (ydata.size() * 4);
|
|
|
|
// samples to time
|
|
for (int i=0, offset=0; i<xdata.size(); i++) {
|
|
|
|
double x = (double) xdata[i];
|
|
double y = (double) ydata[i];
|
|
|
|
xaxis[offset] = x +left;
|
|
yaxis[offset] = metricDetail.baseline; // use baseline not 0, default is 0
|
|
offset++;
|
|
xaxis[offset] = x+left;
|
|
yaxis[offset] = y;
|
|
offset++;
|
|
xaxis[offset] = x+right;
|
|
yaxis[offset] = y;
|
|
offset++;
|
|
xaxis[offset] = x +right;
|
|
yaxis[offset] = metricDetail.baseline;; // use baseline not 0, default is 0
|
|
offset++;
|
|
}
|
|
xdata = xaxis;
|
|
ydata = yaxis;
|
|
count *= 4;
|
|
// END OF FUDGE
|
|
|
|
}
|
|
|
|
// set the data series
|
|
current->setData(xdata.data(),ydata.data(), count + 1);
|
|
current->setBaseline(metricDetail.baseline);
|
|
|
|
// update stack data so we can index off them
|
|
// in tooltip
|
|
*stackX[stackcounter+1] = xdata;
|
|
*stackY[stackcounter+1] = ydata;
|
|
|
|
// update min/max Y values for the chosen axis
|
|
if (current->maxYValue() > maxY[axisid]) maxY[axisid] = current->maxYValue();
|
|
if (current->minYValue() < minY[axisid]) minY[axisid] = current->minYValue();
|
|
|
|
current->attach(this);
|
|
|
|
} // end of reverse for stacked plots
|
|
|
|
|
|
// do all curves excepts stacks in order
|
|
// we skip stacked entries because they
|
|
// are painted in reverse order in a
|
|
// loop before this one.
|
|
stackcounter= 0;
|
|
foreach (MetricDetail metricDetail, settings->metrics) {
|
|
|
|
if (metricDetail.stack == true) continue;
|
|
|
|
QVector<double> xdata, ydata;
|
|
|
|
int count;
|
|
if (settings->groupBy != LTM_TOD)
|
|
createCurveData(settings, metricDetail, xdata, ydata, count);
|
|
else
|
|
createTODCurveData(settings, metricDetail, xdata, ydata, count);
|
|
|
|
// Create a curve
|
|
QwtPlotCurve *current = new QwtPlotCurve(metricDetail.uname);
|
|
if (metricDetail.type == METRIC_BEST)
|
|
curves.insert(metricDetail.bestSymbol, current);
|
|
else
|
|
curves.insert(metricDetail.symbol, current);
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true)
|
|
current->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen cpen = QPen(metricDetail.penColor);
|
|
cpen.setWidth(width);
|
|
current->setPen(cpen);
|
|
current->setStyle(metricDetail.curveStyle);
|
|
|
|
QwtSymbol sym;
|
|
sym.setStyle(metricDetail.symbolStyle);
|
|
|
|
// choose the axis
|
|
int axisid = chooseYAxis(metricDetail.uunits);
|
|
current->setYAxis(axisid);
|
|
|
|
// left and right offset for bars
|
|
double left = 0;
|
|
double right = 0;
|
|
double middle = 0;
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// we still worry about stacked bars, since we
|
|
// need to take into account the space it will
|
|
// consume when plotted in the second iteration
|
|
// below this one
|
|
int barn = metricDetail.stack ? stacknum : barnum;
|
|
|
|
double space = double(0.9) / bars;
|
|
double gap = space * 0.10;
|
|
double width = space * 0.90;
|
|
left = (space * barn) + (gap / 2) + 0.1;
|
|
right = left + width;
|
|
middle = ((left+right) / double(2)) - 0.5;
|
|
if (metricDetail.stack && donestack == false) {
|
|
barnum++;
|
|
donestack = true;
|
|
} else if (metricDetail.stack == false) barnum++;
|
|
}
|
|
|
|
// trend - clone the data for the curve and add a curvefitted
|
|
// curve with no symbols and use a dashed pen
|
|
// need more than 2 points for a trend line
|
|
if (metricDetail.trend == true && count > 2) {
|
|
|
|
QString trendName = QString(tr("%1 trend")).arg(metricDetail.uname);
|
|
QString trendSymbol = QString("%1_trend")
|
|
.arg(metricDetail.type == METRIC_BEST ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
QwtPlotCurve *trend = new QwtPlotCurve(trendName);
|
|
|
|
// cosmetics
|
|
QPen cpen = QPen(metricDetail.penColor.darker(200));
|
|
cpen.setWidth(width*2); // double thickness for trend lines
|
|
cpen.setStyle(Qt::DotLine);
|
|
trend->setPen(cpen);
|
|
if (appsettings->value(this, GC_ANTIALIAS, false).toBool()==true)
|
|
trend->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
trend->setBaseline(0);
|
|
trend->setYAxis(axisid);
|
|
trend->setStyle(QwtPlotCurve::Lines);
|
|
|
|
// perform linear regression
|
|
LTMTrend regress(xdata.data(), ydata.data(), count);
|
|
double xtrend[2], ytrend[2];
|
|
xtrend[0] = 0.0;
|
|
ytrend[0] = regress.getYforX(0.0);
|
|
// point 2 is at far right of chart, not the last point
|
|
// since we may be forecasting...
|
|
xtrend[1] = maxX;
|
|
ytrend[1] = regress.getYforX(maxX);
|
|
trend->setData(xtrend,ytrend, 2);
|
|
|
|
trend->attach(this);
|
|
curves.insert(trendSymbol, trend);
|
|
}
|
|
|
|
// highlight outliers
|
|
if (metricDetail.topOut > 0 && metricDetail.topOut < count && count > 10) {
|
|
|
|
LTMOutliers outliers(xdata.data(), ydata.data(), count, 10);
|
|
|
|
// the top 5 outliers
|
|
QVector<double> hxdata, hydata;
|
|
hxdata.resize(metricDetail.topOut);
|
|
hydata.resize(metricDetail.topOut);
|
|
|
|
// QMap orders the list so start at the top and work
|
|
// backwards
|
|
for (int i=0; i<metricDetail.topOut; i++) {
|
|
hxdata[i] = outliers.getXForRank(i) + middle;
|
|
hydata[i] = outliers.getYForRank(i);
|
|
}
|
|
|
|
// lets setup a curve with this data then!
|
|
QString outName;
|
|
if (metricDetail.topOut > 1)
|
|
outName = QString(tr("%1 Top %2 Outliers"))
|
|
.arg(metricDetail.uname)
|
|
.arg(metricDetail.topOut);
|
|
else
|
|
outName = QString(tr("%1 Outlier")).arg(metricDetail.uname);
|
|
|
|
QString outSymbol = QString("%1_outlier").arg(metricDetail.type == METRIC_BEST ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
QwtPlotCurve *out = new QwtPlotCurve(outName);
|
|
curves.insert(outSymbol, out);
|
|
|
|
out->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
out->setStyle(QwtPlotCurve::Dots);
|
|
|
|
// we might have hidden the symbols for this curve
|
|
// if its set to none then default to a rectangle
|
|
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol)
|
|
sym.setStyle(QwtSymbol::Rect);
|
|
sym.setSize(20);
|
|
QColor lighter = metricDetail.penColor;
|
|
lighter.setAlpha(50);
|
|
sym.setPen(metricDetail.penColor);
|
|
sym.setBrush(lighter);
|
|
|
|
out->setSymbol(new QwtSymbol(sym));
|
|
out->setData(hxdata.data(),hydata.data(), metricDetail.topOut);
|
|
out->setBaseline(0);
|
|
out->setYAxis(axisid);
|
|
out->attach(this);
|
|
}
|
|
|
|
// highlight top N values
|
|
if (metricDetail.topN > 0) {
|
|
|
|
QMap<double, int> sortedList;
|
|
|
|
// copy the yvalues, retaining the offset
|
|
for(int i=0; i<ydata.count(); i++)
|
|
sortedList.insert(ydata[i], i);
|
|
|
|
// copy the top N values
|
|
QVector<double> hxdata, hydata;
|
|
hxdata.resize(metricDetail.topN);
|
|
hydata.resize(metricDetail.topN);
|
|
|
|
// QMap orders the list so start at the top and work
|
|
// backwards
|
|
QMapIterator<double, int> i(sortedList);
|
|
i.toBack();
|
|
int counter = 0;
|
|
while (i.hasPrevious() && counter < metricDetail.topN) {
|
|
i.previous();
|
|
if (ydata[i.value()]) {
|
|
hxdata[counter] = xdata[i.value()] + middle;
|
|
hydata[counter] = ydata[i.value()];
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
// lets setup a curve with this data then!
|
|
QString topName;
|
|
if (counter > 1)
|
|
topName = QString(tr("%1 Best %2"))
|
|
.arg(metricDetail.uname)
|
|
.arg(counter); // starts from zero
|
|
else
|
|
topName = QString(tr("Best %1")).arg(metricDetail.uname);
|
|
|
|
QString topSymbol = QString("%1_topN")
|
|
.arg(metricDetail.type == METRIC_BEST ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
QwtPlotCurve *top = new QwtPlotCurve(topName);
|
|
curves.insert(topSymbol, top);
|
|
|
|
top->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
top->setStyle(QwtPlotCurve::Dots);
|
|
|
|
// we might have hidden the symbols for this curve
|
|
// if its set to none then default to a rectangle
|
|
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol)
|
|
sym.setStyle(QwtSymbol::Rect);
|
|
sym.setSize(12);
|
|
QColor lighter = metricDetail.penColor;
|
|
lighter.setAlpha(200);
|
|
sym.setPen(metricDetail.penColor);
|
|
sym.setBrush(lighter);
|
|
|
|
top->setSymbol(new QwtSymbol(sym));
|
|
top->setData(hxdata.data(),hydata.data(), counter);
|
|
top->setBaseline(0);
|
|
top->setYAxis(axisid);
|
|
top->attach(this);
|
|
}
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// fill the bars
|
|
QColor brushColor = metricDetail.penColor;
|
|
brushColor.setAlpha(64); // now side by side, less transparency required
|
|
QColor brushColor1 = metricDetail.penColor.darker();
|
|
QLinearGradient linearGradient(0, 0, 0, height());
|
|
linearGradient.setColorAt(0.0, brushColor1);
|
|
linearGradient.setColorAt(1.0, brushColor);
|
|
linearGradient.setSpread(QGradient::PadSpread);
|
|
current->setBrush(linearGradient);
|
|
current->setPen(Qt::NoPen);
|
|
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
|
|
|
|
sym.setStyle(QwtSymbol::NoSymbol);
|
|
current->setSymbol(new QwtSymbol(sym));
|
|
|
|
// fudge for date ranges, not for time of day graph
|
|
// fudge qwt'S lack of a decent bar chart
|
|
// add a zero point at the head and tail so the
|
|
// histogram columns look nice.
|
|
// and shift all the x-values left by 0.5 so that
|
|
// they centre over x-axis labels
|
|
count = xdata.size()-2;
|
|
|
|
int i=0;
|
|
for (i=0; i<count; i++) xdata[i] -= 0.5;
|
|
// now add a final 0 value to get the last
|
|
// column drawn - no resize neccessary
|
|
// since it is always sized for 1 + maxnumber of entries
|
|
xdata[i] = xdata[i-1] + 1;
|
|
ydata[i] = 0;
|
|
count++;
|
|
|
|
QVector<double> xaxis (xdata.size() * 4);
|
|
QVector<double> yaxis (ydata.size() * 4);
|
|
|
|
// samples to time
|
|
for (int i=0, offset=0; i<xdata.size(); i++) {
|
|
|
|
double x = (double) xdata[i];
|
|
double y = (double) ydata[i];
|
|
|
|
xaxis[offset] = x +left;
|
|
yaxis[offset] = metricDetail.baseline;; // use baseline not 0, default is 0
|
|
offset++;
|
|
xaxis[offset] = x+left;
|
|
yaxis[offset] = y;
|
|
offset++;
|
|
xaxis[offset] = x+right;
|
|
yaxis[offset] = y;
|
|
offset++;
|
|
xaxis[offset] = x +right;
|
|
yaxis[offset] = metricDetail.baseline;; // use baseline not 0, default is 0
|
|
offset++;
|
|
}
|
|
xdata = xaxis;
|
|
ydata = yaxis;
|
|
count *= 4;
|
|
// END OF FUDGE
|
|
|
|
} else if (metricDetail.curveStyle == QwtPlotCurve::Lines) {
|
|
|
|
QPen cpen = QPen(metricDetail.penColor);
|
|
cpen.setWidth(width);
|
|
sym.setSize(6);
|
|
sym.setStyle(metricDetail.symbolStyle);
|
|
sym.setPen(QPen(metricDetail.penColor));
|
|
sym.setBrush(QBrush(metricDetail.penColor));
|
|
current->setSymbol(new QwtSymbol(sym));
|
|
current->setPen(cpen);
|
|
|
|
// fill below the line
|
|
if (metricDetail.fillCurve) {
|
|
QColor fillColor = metricDetail.penColor;
|
|
fillColor.setAlpha(60);
|
|
current->setBrush(fillColor);
|
|
}
|
|
|
|
|
|
} else if (metricDetail.curveStyle == QwtPlotCurve::Dots) {
|
|
sym.setSize(6);
|
|
sym.setStyle(metricDetail.symbolStyle);
|
|
sym.setPen(QPen(metricDetail.penColor));
|
|
sym.setBrush(QBrush(metricDetail.penColor));
|
|
current->setSymbol(new QwtSymbol(sym));
|
|
|
|
} else if (metricDetail.curveStyle == QwtPlotCurve::Sticks) {
|
|
|
|
sym.setSize(4);
|
|
sym.setStyle(metricDetail.symbolStyle);
|
|
sym.setPen(QPen(metricDetail.penColor));
|
|
sym.setBrush(QBrush(Qt::white));
|
|
current->setSymbol(new QwtSymbol(sym));
|
|
|
|
}
|
|
|
|
// smoothing
|
|
if (metricDetail.smooth == true) {
|
|
current->setCurveAttribute(QwtPlotCurve::Fitted, true);
|
|
}
|
|
|
|
// set the data series
|
|
current->setData(xdata.data(),ydata.data(), count + 1);
|
|
current->setBaseline(metricDetail.baseline);
|
|
|
|
// update min/max Y values for the chosen axis
|
|
if (current->maxYValue() > maxY[axisid]) maxY[axisid] = current->maxYValue();
|
|
if (current->minYValue() < minY[axisid]) minY[axisid] = current->minYValue();
|
|
|
|
current->attach(this);
|
|
|
|
}
|
|
|
|
if (settings->groupBy != LTM_TOD) {
|
|
|
|
// make start date always fall on a Monday
|
|
if (settings->groupBy == LTM_WEEK) {
|
|
int dow = settings->start.date().dayOfWeek(); // 1-7, where 1=monday
|
|
settings->start.date().addDays(dow-1*-1);
|
|
}
|
|
|
|
// setup the xaxis at the bottom
|
|
int tics;
|
|
maxX = 0.5 + groupForDate(settings->end.date(), settings->groupBy) -
|
|
groupForDate(settings->start.date(), settings->groupBy);
|
|
if (maxX < 14) {
|
|
tics = 1;
|
|
} else {
|
|
tics = 1 + maxX/10;
|
|
}
|
|
setAxisScale(xBottom, -0.5, maxX, tics);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
|
|
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
|
|
|
|
} else {
|
|
setAxisScale(xBottom, 0, 24, 2);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
|
|
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
|
|
}
|
|
|
|
// run through the Y axis
|
|
for (int i=0; i<10; i++) {
|
|
// set the scale on the axis
|
|
if (i != xBottom && i != xTop) {
|
|
maxY[i] *= 1.1; // add 10% headroom
|
|
setAxisScale(i, minY[i], maxY[i]);
|
|
}
|
|
}
|
|
|
|
QString format = axisTitle(yLeft).text();
|
|
parent->toolTip()->setAxis(xBottom, yLeft);
|
|
parent->toolTip()->setFormat(format);
|
|
|
|
// draw zone labels axisid of -1 means delete whats there
|
|
// cause no watts are being displayed
|
|
if (settings->shadeZones == true) {
|
|
int axisid = axes.value("watts", -1);
|
|
if (axisid == -1) axisid = axes.value(tr("watts"), -1); // Try translated version
|
|
refreshZoneLabels(axisid);
|
|
} else {
|
|
refreshZoneLabels(-1); // turn em off
|
|
}
|
|
|
|
// show legend?
|
|
if (settings->legend == false) this->legend()->clear();
|
|
|
|
// markers
|
|
if (settings->groupBy != LTM_TOD)
|
|
refreshMarkers(settings->start.date(), settings->end.date(), settings->groupBy);
|
|
|
|
// plot
|
|
replot();
|
|
}
|
|
|
|
void
|
|
LTMPlot::createTODCurveData(LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n)
|
|
{
|
|
y.clear();
|
|
x.clear();
|
|
|
|
x.resize((24+3));
|
|
y.resize((24+3));
|
|
n = (24);
|
|
|
|
for (int i=0; i<(24); i++) x[i]=i;
|
|
|
|
foreach (SummaryMetrics rideMetrics, *(settings->data)) {
|
|
|
|
// filter out unwanted rides
|
|
if (context->isfiltered && !context->filters.contains(rideMetrics.getFileName())) continue;
|
|
|
|
double value = rideMetrics.getForSymbol(metricDetail.symbol);
|
|
|
|
// check values are bounded to stop QWT going berserk
|
|
if (isnan(value) || isinf(value)) value = 0;
|
|
|
|
// Special computed metrics (LTS/STS) have a null metric pointer
|
|
if (metricDetail.metric) {
|
|
// convert from stored metric value to imperial
|
|
if (context->athlete->useMetricUnits == false) {
|
|
value *= metricDetail.metric->conversion();
|
|
value += metricDetail.metric->conversionSum();
|
|
}
|
|
|
|
// convert seconds to hours
|
|
if (metricDetail.metric->units(true) == "seconds" ||
|
|
metricDetail.metric->units(true) == tr("seconds")) value /= 3600;
|
|
}
|
|
|
|
int array = rideMetrics.getRideDate().time().hour();
|
|
int type = metricDetail.metric ? metricDetail.metric->type() : RideMetric::Average;
|
|
|
|
if (metricDetail.uunits == "Ramp" ||
|
|
metricDetail.uunits == tr("Ramp")) type = RideMetric::Total;
|
|
|
|
switch (type) {
|
|
case RideMetric::Total:
|
|
y[array] += value;
|
|
break;
|
|
case RideMetric::Average:
|
|
{
|
|
// average should be calculated taking into account
|
|
// the duration of the ride, otherwise high value but
|
|
// short rides will skew the overall average
|
|
y[array] = value; //XXX average is broken
|
|
break;
|
|
}
|
|
case RideMetric::Low:
|
|
if (value < y[array]) y[array] = value;
|
|
break;
|
|
case RideMetric::Peak:
|
|
if (value > y[array]) y[array] = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n)
|
|
{
|
|
QList<SummaryMetrics> *data = NULL;
|
|
|
|
// resize the curve array to maximum possible size
|
|
int maxdays = groupForDate(settings->end.date(), settings->groupBy)
|
|
- groupForDate(settings->start.date(), settings->groupBy);
|
|
x.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail
|
|
y.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail
|
|
|
|
// Get metric data, either from metricDB for RideFile metrics
|
|
// or from StressCalculator for PM type metrics
|
|
QList<SummaryMetrics> PMCdata;
|
|
if (metricDetail.type == METRIC_DB || metricDetail.type == METRIC_META) {
|
|
data = settings->data;
|
|
} else if (metricDetail.type == METRIC_MEASURE) {
|
|
data = settings->measures;
|
|
} else if (metricDetail.type == METRIC_PM) {
|
|
createPMCCurveData(settings, metricDetail, PMCdata);
|
|
data = &PMCdata;
|
|
} else if (metricDetail.type == METRIC_BEST) {
|
|
data = settings->bests;
|
|
}
|
|
|
|
n=-1;
|
|
int lastDay=0;
|
|
unsigned long secondsPerGroupBy=0;
|
|
bool wantZero = (metricDetail.curveStyle == QwtPlotCurve::Steps);
|
|
foreach (SummaryMetrics rideMetrics, *data) {
|
|
|
|
// filter out unwanted rides but not for PMC type metrics
|
|
// because that needs to be done in the stress calculator
|
|
if (metricDetail.type != METRIC_PM && context->isfiltered &&
|
|
!context->filters.contains(rideMetrics.getFileName())) continue;
|
|
|
|
// day we are on
|
|
int currentDay = groupForDate(rideMetrics.getRideDate().date(), settings->groupBy);
|
|
|
|
// value for day -- measures are stored differently
|
|
double value;
|
|
if (metricDetail.type == METRIC_MEASURE)
|
|
value = rideMetrics.getText(metricDetail.symbol, "0.0").toDouble();
|
|
else if (metricDetail.type == METRIC_BEST)
|
|
value = rideMetrics.getForSymbol(metricDetail.bestSymbol);
|
|
else
|
|
value = rideMetrics.getForSymbol(metricDetail.symbol);
|
|
|
|
// check values are bounded to stop QWT going berserk
|
|
if (isnan(value) || isinf(value)) value = 0;
|
|
|
|
// Special computed metrics (LTS/STS) have a null metric pointer
|
|
if (metricDetail.type != METRIC_BEST && metricDetail.metric) {
|
|
// convert from stored metric value to imperial
|
|
if (context->athlete->useMetricUnits == false) {
|
|
value *= metricDetail.metric->conversion();
|
|
value += metricDetail.metric->conversionSum();
|
|
}
|
|
|
|
// convert seconds to hours
|
|
if (metricDetail.metric->units(true) == "seconds" ||
|
|
metricDetail.metric->units(true) == tr("seconds")) value /= 3600;
|
|
}
|
|
|
|
if (value || wantZero) {
|
|
unsigned long seconds = rideMetrics.getForSymbol("workout_time");
|
|
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_MEASURE) seconds = 1;
|
|
if (currentDay > lastDay) {
|
|
if (lastDay && wantZero) {
|
|
while (lastDay<currentDay) {
|
|
lastDay++;
|
|
n++;
|
|
x[n]=lastDay - groupForDate(settings->start.date(), settings->groupBy);
|
|
y[n]=0;
|
|
}
|
|
} else {
|
|
n++;
|
|
}
|
|
y[n] = value;
|
|
x[n] = currentDay - groupForDate(settings->start.date(), settings->groupBy);
|
|
secondsPerGroupBy = seconds; // reset for new group
|
|
} else {
|
|
// sum totals, average averages and choose best for Peaks
|
|
int type = metricDetail.metric ? metricDetail.metric->type() : RideMetric::Average;
|
|
|
|
if (metricDetail.uunits == "Ramp" ||
|
|
metricDetail.uunits == tr("Ramp")) type = RideMetric::Total;
|
|
|
|
if (metricDetail.type == METRIC_BEST) type = RideMetric::Peak;
|
|
|
|
switch (type) {
|
|
case RideMetric::Total:
|
|
y[n] += value;
|
|
break;
|
|
case RideMetric::Average:
|
|
{
|
|
// average should be calculated taking into account
|
|
// the duration of the ride, otherwise high value but
|
|
// short rides will skew the overall average
|
|
y[n] = ((y[n]*secondsPerGroupBy)+(seconds*value)) / (secondsPerGroupBy+seconds);
|
|
break;
|
|
}
|
|
case RideMetric::Low:
|
|
if (value < y[n]) y[n] = value;
|
|
break;
|
|
case RideMetric::Peak:
|
|
if (value > y[n]) y[n] = value;
|
|
break;
|
|
}
|
|
secondsPerGroupBy += seconds; // increment for same group
|
|
}
|
|
lastDay = currentDay;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMPlot::createPMCCurveData(LTMSettings *settings, MetricDetail metricDetail,
|
|
QList<SummaryMetrics> &customData)
|
|
{
|
|
QDate earliest, latest; // rides
|
|
QString scoreType;
|
|
|
|
// create a custom set of summary metric data!
|
|
if (metricDetail.symbol.startsWith("skiba")) {
|
|
scoreType = "skiba_bike_score";
|
|
} else if (metricDetail.symbol.startsWith("coggan")) {
|
|
scoreType = "coggan_tss";
|
|
} else if (metricDetail.symbol.startsWith("daniels")) {
|
|
scoreType = "daniels_points";
|
|
} else if (metricDetail.symbol.startsWith("trimp")) {
|
|
scoreType = "trimp_points";
|
|
} else if (metricDetail.symbol.startsWith("work")) {
|
|
scoreType = "total_work";
|
|
} else if (metricDetail.symbol.startsWith("distance")) {
|
|
scoreType = "total_distance";
|
|
}
|
|
|
|
// create the Stress Calculation List
|
|
// FOR ALL RIDE FILES
|
|
StressCalculator *sc = new StressCalculator(
|
|
context->athlete->cyclist,
|
|
settings->start,
|
|
settings->end,
|
|
(appsettings->value(this, GC_STS_DAYS,7)).toInt(),
|
|
(appsettings->value(this, GC_LTS_DAYS,42)).toInt());
|
|
|
|
sc->calculateStress(context, context->athlete->home.absolutePath(), scoreType, settings->ltmTool->isFiltered(), settings->ltmTool->filters());
|
|
|
|
// pick out any data that is in the date range selected
|
|
// convert to SummaryMetric Format used on the plot
|
|
for (int i=0; i< sc->n(); i++) {
|
|
|
|
SummaryMetrics add = SummaryMetrics();
|
|
add.setRideDate(settings->start.addDays(i));
|
|
if (scoreType == "skiba_bike_score") {
|
|
add.setForSymbol("skiba_lts", sc->getLTSvalues()[i]);
|
|
add.setForSymbol("skiba_sts", sc->getSTSvalues()[i]);
|
|
add.setForSymbol("skiba_sb", sc->getSBvalues()[i]);
|
|
add.setForSymbol("skiba_sr", sc->getSRvalues()[i]);
|
|
add.setForSymbol("skiba_lr", sc->getLRvalues()[i]);
|
|
} else if (scoreType == "coggan_tss") {
|
|
add.setForSymbol("coggan_ctl", sc->getLTSvalues()[i]);
|
|
add.setForSymbol("coggan_atl", sc->getSTSvalues()[i]);
|
|
add.setForSymbol("coggan_tsb", sc->getSBvalues()[i]);
|
|
add.setForSymbol("coggan_sr", sc->getSRvalues()[i]);
|
|
add.setForSymbol("coggan_lr", sc->getLRvalues()[i]);
|
|
} else if (scoreType == "daniels_points") {
|
|
add.setForSymbol("daniels_lts", sc->getLTSvalues()[i]);
|
|
add.setForSymbol("daniels_sts", sc->getSTSvalues()[i]);
|
|
add.setForSymbol("daniels_sb", sc->getSBvalues()[i]);
|
|
add.setForSymbol("daniels_sr", sc->getSRvalues()[i]);
|
|
add.setForSymbol("daniels_lr", sc->getLRvalues()[i]);
|
|
} else if (scoreType == "trimp_points") {
|
|
add.setForSymbol("trimp_lts", sc->getLTSvalues()[i]);
|
|
add.setForSymbol("trimp_sts", sc->getSTSvalues()[i]);
|
|
add.setForSymbol("trimp_sb", sc->getSBvalues()[i]);
|
|
add.setForSymbol("trimp_sr", sc->getSRvalues()[i]);
|
|
add.setForSymbol("trimp_lr", sc->getLRvalues()[i]);
|
|
} else if (scoreType == "total_work") {
|
|
add.setForSymbol("work_lts", sc->getLTSvalues()[i]);
|
|
add.setForSymbol("work_sts", sc->getSTSvalues()[i]);
|
|
add.setForSymbol("work_sb", sc->getSBvalues()[i]);
|
|
add.setForSymbol("work_sr", sc->getSRvalues()[i]);
|
|
add.setForSymbol("work_lr", sc->getLRvalues()[i]);
|
|
} else if (scoreType == "total_distance") {
|
|
add.setForSymbol("distance_lts", sc->getLTSvalues()[i]);
|
|
add.setForSymbol("distance_sts", sc->getSTSvalues()[i]);
|
|
add.setForSymbol("distance_sb", sc->getSBvalues()[i]);
|
|
add.setForSymbol("distance_sr", sc->getSRvalues()[i]);
|
|
add.setForSymbol("distance_lr", sc->getLRvalues()[i]);
|
|
}
|
|
add.setForSymbol("workout_time", 1.0); // averaging is per day
|
|
customData << add;
|
|
|
|
}
|
|
delete sc;
|
|
}
|
|
|
|
int
|
|
LTMPlot::chooseYAxis(QString units)
|
|
{
|
|
int chosen;
|
|
|
|
// return the YAxis to use
|
|
if ((chosen = axes.value(units, -1)) != -1) return chosen;
|
|
else if (axes.count() < 8) {
|
|
chosen = supported_axes[axes.count()];
|
|
if (units == "seconds" || units == tr("seconds")) setAxisTitle(chosen, tr("hours")); // we convert seconds to hours
|
|
else setAxisTitle(chosen, units);
|
|
enableAxis(chosen, true);
|
|
axes.insert(units, chosen);
|
|
QwtScaleDraw *sd = new QwtScaleDraw;
|
|
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
setAxisScaleDraw(chosen, sd);
|
|
setAxisMaxMinor(chosen, 0);
|
|
return chosen;
|
|
} else {
|
|
// eek!
|
|
return yLeft; // just re-use the current yLeft axis
|
|
}
|
|
}
|
|
|
|
int
|
|
LTMPlot::groupForDate(QDate date, int groupby)
|
|
{
|
|
switch(groupby) {
|
|
case LTM_WEEK:
|
|
{
|
|
// must start from 1 not zero!
|
|
return 1 + ((date.toJulianDay() - settings->start.date().toJulianDay()) / 7);
|
|
}
|
|
case LTM_MONTH: return (date.year()*12) + date.month();
|
|
case LTM_YEAR: return date.year();
|
|
case LTM_DAY:
|
|
default:
|
|
return date.toJulianDay();
|
|
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMPlot::pointHover(QwtPlotCurve *curve, int index)
|
|
{
|
|
if (index >= 0 && curve != highlighter) {
|
|
|
|
int stacknum = stacks.value(curve, -1);
|
|
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
double value;
|
|
QString units;
|
|
int precision = 0;
|
|
QString datestr;
|
|
|
|
LTMScaleDraw *lsd = new LTMScaleDraw(settings->start, groupForDate(settings->start.date(), settings->groupBy), settings->groupBy);
|
|
QwtText startText = lsd->label((int)(curve->sample(index).x()+0.5));
|
|
|
|
if (settings->groupBy != LTM_WEEK)
|
|
datestr = startText.text();
|
|
else
|
|
datestr = QString(tr("Week Commencing %1")).arg(startText.text());
|
|
|
|
datestr = datestr.replace('\n', ' ');
|
|
|
|
// we reference the metric definitions of name and
|
|
// units to decide on the level of precision required
|
|
QHashIterator<QString, QwtPlotCurve*> c(curves);
|
|
while (c.hasNext()) {
|
|
c.next();
|
|
if (c.value() == curve) {
|
|
const RideMetric *metric =factory.rideMetric(c.key());
|
|
units = metric ? metric->units(context->athlete->useMetricUnits) : "";
|
|
precision = metric ? metric->precision() : 1;
|
|
|
|
// BikeScore, RI and Daniels Points have no units
|
|
if (units == "" && metric != NULL) {
|
|
QTextEdit processHTML(factory.rideMetric(c.key())->name());
|
|
units = processHTML.toPlainText();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// the point value
|
|
value = curve->sample(index).y();
|
|
|
|
// de-aggregate stacked values
|
|
if (stacknum > 0) {
|
|
value = stackY[stacknum]->at(index) - stackY[stacknum-1]->at(index); // de-aggregate
|
|
}
|
|
|
|
// convert seconds to hours for the LTM plot
|
|
if (units == "seconds" || units == tr("seconds")) {
|
|
units = "hours"; // we translate from seconds to hours
|
|
value = ceil(value*10.0)/10.0;
|
|
precision = 1; // need more precision now
|
|
}
|
|
|
|
// output the tooltip
|
|
QString text = QString("%1\n%2\n%3 %4")
|
|
.arg(datestr)
|
|
.arg(curve->title().text())
|
|
.arg(value, 0, 'f', precision)
|
|
.arg(this->axisTitle(curve->yAxis()).text());
|
|
|
|
// set that text up
|
|
parent->toolTip()->setText(text);
|
|
} else {
|
|
// no point
|
|
parent->toolTip()->setText("");
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMPlot::pointClicked(QwtPlotCurve *curve, int index)
|
|
{
|
|
if (index >= 0 && curve != highlighter) {
|
|
// setup the popup
|
|
parent->pointClicked(curve, index);
|
|
}
|
|
}
|
|
|
|
// aggregate curve data, adds w to a and
|
|
// updates a directly. arrays MUST be of
|
|
// equal dimensions
|
|
void
|
|
LTMPlot::aggregateCurves(QVector<double> &a, QVector<double>&w)
|
|
{
|
|
if (a.size() != w.size()) return; // ignore silently
|
|
|
|
// add them in!
|
|
for(int i=0; i<a.size(); i++) a[i] += w[i];
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Draw Power Zone Shading on Background (here to end of source file)
|
|
*
|
|
* THANKS TO DAMIEN GRAUSER FOR GETTING THIS WORKING TO SHOW
|
|
* ZONE SHADING OVER TIME. WHEN CP CHANGES THE ZONE SHADING AND
|
|
* LABELLING CHANGES TOO. NEAT.
|
|
*--------------------------------------------------------------------*/
|
|
class LTMPlotBackground: public QwtPlotItem
|
|
{
|
|
private:
|
|
LTMPlot *parent;
|
|
|
|
public:
|
|
|
|
LTMPlotBackground(LTMPlot *_parent, int axisid)
|
|
{
|
|
//setAxis(QwtPlot::xBottom, axisid);
|
|
setXAxis(axisid);
|
|
setZ(0.0);
|
|
parent = _parent;
|
|
}
|
|
|
|
virtual int rtti() const
|
|
{
|
|
return QwtPlotItem::Rtti_PlotUserItem;
|
|
}
|
|
|
|
virtual void draw(QPainter *painter,
|
|
const QwtScaleMap &xMap, const QwtScaleMap &yMap,
|
|
const QRectF &rect) const
|
|
{
|
|
const Zones *zones = parent->parent->context->athlete->zones();
|
|
int zone_range_size = parent->parent->context->athlete->zones()->getRangeSize();
|
|
|
|
if (zone_range_size >= 0) { //parent->shadeZones() &&
|
|
for (int i = 0; i < zone_range_size; i ++) {
|
|
int zone_range = i;
|
|
int left = xMap.transform(parent->groupForDate(zones->getStartDate(zone_range), parent->settings->groupBy)- parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
|
|
|
|
/* The +50 pixels is for a QWT bug? cover the little gap on the right? */
|
|
int right = xMap.transform(parent->maxX + 0.5) + 50;
|
|
|
|
if (right<0)
|
|
right= xMap.transform(parent->groupForDate(parent->settings->end.date(), parent->settings->groupBy) - parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
|
|
|
|
QList <int> zone_lows = zones->getZoneLows(zone_range);
|
|
int num_zones = zone_lows.size();
|
|
if (num_zones > 0) {
|
|
for (int z = 0; z < num_zones; z ++) {
|
|
QRectF r = rect;
|
|
r.setLeft(left);
|
|
r.setRight(right);
|
|
|
|
QColor shading_color = zoneColor(z, num_zones);
|
|
shading_color.setHsv(
|
|
shading_color.hue(),
|
|
shading_color.saturation() / 4,
|
|
shading_color.value()
|
|
);
|
|
r.setBottom(yMap.transform(zone_lows[z]));
|
|
if (z + 1 < num_zones)
|
|
r.setTop(yMap.transform(zone_lows[z + 1]));
|
|
if (r.top() <= r.bottom())
|
|
painter->fillRect(r, shading_color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// Zone labels are drawn if power zone bands are enabled, automatically
|
|
// at the center of the plot
|
|
class LTMPlotZoneLabel: public QwtPlotItem
|
|
{
|
|
private:
|
|
LTMPlot *parent;
|
|
int zone_number;
|
|
double watts;
|
|
QwtText text;
|
|
|
|
public:
|
|
LTMPlotZoneLabel(LTMPlot *_parent, int _zone_number, int axisid, LTMSettings *settings)
|
|
{
|
|
parent = _parent;
|
|
zone_number = _zone_number;
|
|
|
|
const Zones *zones = parent->parent->context->athlete->zones();
|
|
int zone_range = zones->whichRange(settings->start.addDays((settings->end.date().toJulianDay()-settings->start.date().toJulianDay())/2).date());
|
|
|
|
// which axis has watts?
|
|
setXAxis(axisid);
|
|
|
|
// create new zone labels if we're shading
|
|
if (zone_range >= 0) { //parent->shadeZones()
|
|
QList <int> zone_lows = zones->getZoneLows(zone_range);
|
|
QList <QString> zone_names = zones->getZoneNames(zone_range);
|
|
int num_zones = zone_lows.size();
|
|
if (zone_names.size() != num_zones) return;
|
|
if (zone_number < num_zones) {
|
|
watts =
|
|
(
|
|
(zone_number + 1 < num_zones) ?
|
|
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
|
|
(
|
|
(zone_number > 0) ?
|
|
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
|
|
2.0 * zone_lows[zone_number]
|
|
)
|
|
);
|
|
text = QwtText(zone_names[zone_number]);
|
|
text.setFont(QFont("Helvetica",20, QFont::Bold));
|
|
QColor text_color = zoneColor(zone_number, num_zones);
|
|
text_color.setAlpha(64);
|
|
text.setColor(text_color);
|
|
}
|
|
}
|
|
setZ(1.0 + zone_number / 100.0);
|
|
}
|
|
|
|
virtual int rtti() const
|
|
{
|
|
return QwtPlotItem::Rtti_PlotUserItem;
|
|
}
|
|
|
|
void draw(QPainter *painter,
|
|
const QwtScaleMap &, const QwtScaleMap &yMap,
|
|
const QRectF &rect) const
|
|
{
|
|
if (true) {//parent->shadeZones()
|
|
int x = (rect.left() + rect.right()) / 2;
|
|
int y = yMap.transform(watts);
|
|
|
|
// the following code based on source for QwtPlotMarker::draw()
|
|
QRect tr(QPoint(0, 0), text.textSize(painter->font()).toSize());
|
|
tr.moveCenter(QPoint(x, y));
|
|
text.draw(painter, tr);
|
|
}
|
|
}
|
|
};
|
|
|
|
void
|
|
LTMPlot::refreshMarkers(QDate from, QDate to, int groupby)
|
|
{
|
|
// clear old markers - if there are any
|
|
foreach(QwtPlotMarker *m, markers) {
|
|
m->detach();
|
|
delete m;
|
|
}
|
|
markers.clear();
|
|
|
|
double baseday = groupForDate(from, groupby);
|
|
|
|
// seasons and season events
|
|
if (settings->events) {
|
|
foreach (Season s, context->athlete->seasons->seasons) {
|
|
|
|
if (s.type != Season::temporary && s.name != settings->title && s.getStart() >= from && s.getStart() < to) {
|
|
|
|
QwtPlotMarker *mrk = new QwtPlotMarker;
|
|
markers.append(mrk);
|
|
mrk->attach(this);
|
|
mrk->setLineStyle(QwtPlotMarker::VLine);
|
|
mrk->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
|
|
mrk->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
|
|
|
|
QwtText text(s.getName());
|
|
text.setFont(QFont("Helvetica", 10, QFont::Bold));
|
|
text.setColor(GColor(CPLOTMARKER));
|
|
mrk->setValue(double(groupForDate(s.getStart(), groupby)) - baseday, 0.0);
|
|
mrk->setLabel(text);
|
|
}
|
|
|
|
foreach (SeasonEvent event, s.events) {
|
|
|
|
if (event.date > from && event.date < to) {
|
|
|
|
// and the events...
|
|
QwtPlotMarker *mrk = new QwtPlotMarker;
|
|
markers.append(mrk);
|
|
mrk->attach(this);
|
|
mrk->setLineStyle(QwtPlotMarker::VLine);
|
|
mrk->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
|
|
mrk->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
|
|
|
|
QwtText text(event.name);
|
|
text.setFont(QFont("Helvetica", 10, QFont::Bold));
|
|
text.setColor(GColor(CPLOTMARKER));
|
|
mrk->setValue(double(groupForDate(event.date, groupby)) - baseday, 10.0);
|
|
mrk->setLabel(text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
void
|
|
LTMPlot::refreshZoneLabels(int axisid)
|
|
{
|
|
foreach(LTMPlotZoneLabel *label, zoneLabels) {
|
|
label->detach();
|
|
delete label;
|
|
}
|
|
zoneLabels.clear();
|
|
|
|
if (bg) {
|
|
bg->detach();
|
|
delete bg;
|
|
bg = NULL;
|
|
}
|
|
if (axisid == -1) return; // our job is done - no zones to plot
|
|
|
|
const Zones *zones = context->athlete->zones();
|
|
|
|
if (zones == NULL || zones->getRangeSize()==0) return; // no zones to plot
|
|
|
|
int zone_range = 0; // first range
|
|
|
|
// generate labels for existing zones
|
|
if (zone_range >= 0) {
|
|
int num_zones = zones->numZones(zone_range);
|
|
for (int z = 0; z < num_zones; z ++) {
|
|
LTMPlotZoneLabel *label = new LTMPlotZoneLabel(this, z, axisid, settings);
|
|
label->attach(this);
|
|
zoneLabels.append(label);
|
|
}
|
|
}
|
|
bg = new LTMPlotBackground(this, axisid);
|
|
bg->attach(this);
|
|
}
|