mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
... shift plot if start date is earlier than any Power Estimates exist ... enhance grouping (calculation of averages for month/years/all grouping) ... adjust estimates calculation ... to calculate non-overlapping weeks data ... to not create any "infinite" data for weeks before the first Power based actitivity exists
3373 lines
123 KiB
C++
3373 lines
123 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 "LTMTrend2.h"
|
|
#include "LTMOutliers.h"
|
|
#include "LTMWindow.h"
|
|
#include "RideMetric.h"
|
|
#include "RideCache.h"
|
|
#include "RideFileCache.h"
|
|
#include "Settings.h"
|
|
#include "Colors.h"
|
|
|
|
#include "PMCData.h" // for LTS/STS calculation
|
|
#include "Zones.h"
|
|
#include "HrZones.h"
|
|
#include "PaceZones.h"
|
|
|
|
#include <QSettings>
|
|
|
|
#include <qwt_series_data.h>
|
|
#include <qwt_scale_widget.h>
|
|
#include <qwt_legend.h>
|
|
#include <qwt_legend_label.h>
|
|
#include <qwt_plot_curve.h>
|
|
#include <qwt_plot_canvas.h>
|
|
#include <qwt_curve_fitter.h>
|
|
#include <qwt_plot_grid.h>
|
|
#include <qwt_symbol.h>
|
|
|
|
#include <cmath> // for isinf() isnan()
|
|
|
|
LTMPlot::LTMPlot(LTMWindow *parent, Context *context, bool first) :
|
|
bg(NULL), parent(parent), context(context), highlighter(NULL), first(first), isolation(false)
|
|
{
|
|
// don't do this ..
|
|
setAutoReplot(false);
|
|
setAutoFillBackground(true);
|
|
|
|
// set up the models we support
|
|
models << new CP2Model(context);
|
|
models << new CP3Model(context);
|
|
models << new MultiModel(context);
|
|
models << new ExtendedModel(context);
|
|
|
|
// setup my axes
|
|
// for now we limit to 4 on left and 4 on right
|
|
setAxesCount(QwtAxis::yLeft, 4);
|
|
setAxesCount(QwtAxis::yRight, 4);
|
|
setAxesCount(QwtAxis::xBottom, 1);
|
|
setAxesCount(QwtAxis::xTop, 0);
|
|
|
|
for (int i=0; i<4; i++) {
|
|
|
|
// lefts
|
|
QwtAxisId left(QwtAxis::yLeft, i);
|
|
supportedAxes << left;
|
|
|
|
QwtScaleDraw *sd = new QwtScaleDraw;
|
|
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
sd->enableComponent(QwtScaleDraw::Ticks, false);
|
|
sd->enableComponent(QwtScaleDraw::Backbone, false);
|
|
|
|
setAxisScaleDraw(left, sd);
|
|
setAxisMaxMinor(left, 0);
|
|
setAxisVisible(left, false);
|
|
|
|
QwtAxisId right(QwtAxis::yRight, i);
|
|
supportedAxes << right;
|
|
|
|
// lefts
|
|
sd = new QwtScaleDraw;
|
|
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
sd->enableComponent(QwtScaleDraw::Ticks, false);
|
|
sd->enableComponent(QwtScaleDraw::Backbone, false);
|
|
setAxisScaleDraw(right, sd);
|
|
setAxisMaxMinor(right, 0);
|
|
setAxisVisible(right, false);
|
|
}
|
|
|
|
// get application settings
|
|
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
|
|
setAxisTitle(QwtAxis::xBottom, tr("Date"));
|
|
enableAxis(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xTop, false);
|
|
setAxisMaxMinor(QwtPlot::xBottom,-1);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(QDateTime::currentDateTime(), 0, LTM_DAY));
|
|
|
|
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
|
|
|
|
grid = new QwtPlotGrid();
|
|
grid->enableX(false);
|
|
grid->attach(this);
|
|
|
|
// manage our own picker
|
|
picker = new LTMToolTip(QwtPlot::xBottom, QwtPlot::yLeft, QwtPicker::VLineRubberBand, QwtPicker::AlwaysOn, canvas(), "");
|
|
picker->setMousePattern(QwtEventPattern::MouseSelect1, Qt::LeftButton);
|
|
picker->setTrackerPen(QColor(Qt::black));
|
|
|
|
QColor inv(Qt::white);
|
|
inv.setAlpha(0);
|
|
picker->setRubberBandPen(inv); // make it invisible
|
|
picker->setEnabled(true);
|
|
_canvasPicker = new LTMCanvasPicker(this);
|
|
|
|
curveColors = new CurveColors(this);
|
|
|
|
settings = NULL;
|
|
|
|
configChanged(CONFIG_APPEARANCE); // set basic colors
|
|
|
|
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
|
// connect pickers to ltmPlot
|
|
connect(_canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), this, SLOT(pointHover(QwtPlotCurve*, int)));
|
|
connect(_canvasPicker, SIGNAL(pointClicked(QwtPlotCurve*, int)), this, SLOT(pointClicked(QwtPlotCurve*, int)));
|
|
}
|
|
|
|
LTMPlot::~LTMPlot()
|
|
{
|
|
}
|
|
|
|
void
|
|
LTMPlot::configChanged(qint32)
|
|
{
|
|
// set basic plot colors
|
|
setCanvasBackground(GColor(CTRENDPLOTBACKGROUND));
|
|
QPen gridPen(GColor(CPLOTGRID));
|
|
//gridPen.setStyle(Qt::DotLine);
|
|
grid->setPen(gridPen);
|
|
|
|
QPalette palette;
|
|
palette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
|
|
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
|
|
setPalette(palette);
|
|
|
|
QPalette gray = palette; // same but with gray text for hidden curves
|
|
gray.setColor(QPalette::WindowText, Qt::darkGray);
|
|
gray.setColor(QPalette::Text, Qt::darkGray);
|
|
|
|
axesObject.clear();
|
|
axesId.clear();
|
|
foreach (QwtAxisId x, supportedAxes) {
|
|
axisWidget(x)->setPalette(palette);
|
|
axisWidget(x)->setPalette(palette);
|
|
|
|
// keep track
|
|
axisWidget(x)->removeEventFilter(this);
|
|
axisWidget(x)->installEventFilter(this);
|
|
axesObject << axisWidget(x);
|
|
axesId << x;
|
|
|
|
}
|
|
axisWidget(QwtPlot::xBottom)->setPalette(palette);
|
|
|
|
QwtLegend *l = static_cast<QwtLegend *>(this->legend());
|
|
foreach(QwtPlotCurve *p, curves) {
|
|
foreach (QWidget *w, l->legendWidgets(itemToInfo(p))) {
|
|
for(int m=0; m< settings->metrics.count(); m++) {
|
|
if (settings->metrics[m].curve == p)
|
|
if (settings->metrics[m].hidden == false)
|
|
w->setPalette(palette);
|
|
else
|
|
w->setPalette(gray);
|
|
}
|
|
}
|
|
}
|
|
|
|
// now save state
|
|
curveColors->saveState();
|
|
updateLegend();
|
|
|
|
if (legend()) legend()->installEventFilter(this);
|
|
}
|
|
|
|
void
|
|
LTMPlot::setAxisTitle(QwtAxisId 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)
|
|
{
|
|
QTime timer;
|
|
timer.start();
|
|
|
|
curveColors->isolated = false;
|
|
isolation = false;
|
|
|
|
//qDebug()<<"Starting.."<<timer.elapsed();
|
|
|
|
settings = set;
|
|
|
|
// For each metric in chart, translate units and name if default uname
|
|
//XXX BROKEN XXX LTMTool::translateMetrics(context, settings);
|
|
|
|
// crop dates to at least within a year of the data available, but only if we have some data
|
|
if (context->athlete->rideCache->rides().count()) {
|
|
|
|
QDateTime first = context->athlete->rideCache->rides().first()->dateTime;
|
|
QDateTime last = context->athlete->rideCache->rides().last()->dateTime;
|
|
|
|
// if requested date range is not overlapping with any existing data,
|
|
// just keep it as it is / otherwise crop
|
|
|
|
if ((settings->end.isValid() && settings->start.isValid()) &&
|
|
(settings->end < first || settings->start > last)) {
|
|
|
|
// keep the date range - no code here (by intent) - for easier readability
|
|
|
|
} else {
|
|
|
|
// if dates are null we need to set them from the available data
|
|
|
|
// end
|
|
if (settings->end == QDateTime() || settings->end > last.addDays(365)) {
|
|
if (settings->end < QDateTime::currentDateTime()) {
|
|
settings->end = QDateTime::currentDateTime();
|
|
} else {
|
|
settings->end = last;
|
|
}
|
|
}
|
|
|
|
// start
|
|
if (settings->start == QDateTime() || settings->start < first.addDays(-365)) {
|
|
settings->start = first;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//setTitle(settings->title);
|
|
if (settings->groupBy != LTM_TOD)
|
|
setAxisTitle(xBottom, tr("Date"));
|
|
else
|
|
setAxisTitle(xBottom, tr("Time of Day"));
|
|
enableAxis(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xTop, false);
|
|
|
|
// 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;
|
|
}
|
|
foreach (QwtPlotMarker *label, labels) {
|
|
label->detach();
|
|
delete label;
|
|
}
|
|
labels.clear();
|
|
// clear old markers - if there are any
|
|
foreach(QwtPlotMarker *m, markers) {
|
|
m->detach();
|
|
delete m;
|
|
}
|
|
markers.clear();
|
|
|
|
|
|
|
|
// disable all y axes until we have populated
|
|
for (int i=0; i<8; i++) {
|
|
setAxisVisible(supportedAxes[i], false);
|
|
enableAxis(supportedAxes[i].id, false);
|
|
}
|
|
axes.clear();
|
|
axesObject.clear();
|
|
axesId.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 (context->athlete->rideCache->rides().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));
|
|
enableAxis(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xTop, false);
|
|
|
|
// remove the shading if it exists
|
|
refreshZoneLabels(QwtAxisId(-1,-1)); // turn em off
|
|
|
|
// remove the old markers
|
|
refreshMarkers(settings, settings->start.date(), settings->end.date(), settings->groupBy, GColor(CPLOTMARKER));
|
|
|
|
replot();
|
|
return;
|
|
}
|
|
|
|
//qDebug()<<"Wiped previous.."<<timer.elapsed();
|
|
|
|
// 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(context, settings, metricDetail, *xdata, *ydata, count);
|
|
else
|
|
createTODCurveData(context, settings, metricDetail, *xdata, *ydata, count);
|
|
|
|
// we add in the last curve for X axis values
|
|
if (r) {
|
|
aggregateCurves(*stackY[r], *stackY[r-1]);
|
|
}
|
|
r++;
|
|
}
|
|
}
|
|
|
|
//qDebug()<<"Created curve data.."<<timer.elapsed();
|
|
|
|
// setup the curves
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 0.5).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--) {
|
|
|
|
//
|
|
// *ONLY* PLOT STACKS
|
|
//
|
|
|
|
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);
|
|
current->setVisible(!metricDetail.hidden);
|
|
settings->metrics[m].curve = current;
|
|
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS)
|
|
curves.insert(metricDetail.bestSymbol, current);
|
|
else
|
|
curves.insert(metricDetail.symbol, current);
|
|
stacks.insert(current, stackcounter+1);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true)
|
|
current->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen cpen = QPen(metricDetail.penColor);
|
|
cpen.setWidth(width);
|
|
current->setPen(cpen);
|
|
current->setStyle(metricDetail.curveStyle);
|
|
|
|
// choose the axis
|
|
QwtAxisId 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(200); // 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(QPen(Qt::NoPen));
|
|
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::NoSymbol);
|
|
current->setSymbol(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->setSamples(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[supportedAxes.indexOf(axisid)]) maxY[supportedAxes.indexOf(axisid)] = current->maxYValue();
|
|
if (current->minYValue() < minY[supportedAxes.indexOf(axisid)]) minY[supportedAxes.indexOf(axisid)] = current->minYValue();
|
|
|
|
current->attach(this);
|
|
|
|
} // end of reverse for stacked plots
|
|
|
|
//qDebug()<<"First plotting iteration.."<<timer.elapsed();
|
|
|
|
// 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;
|
|
for(int m=0; m<settings->metrics.count(); m++) {
|
|
|
|
MetricDetail metricDetail = settings->metrics[m];
|
|
|
|
//
|
|
// *ONLY* PLOT NON-STACKS
|
|
//
|
|
if (metricDetail.stack == true) continue;
|
|
|
|
QVector<double> xdata, ydata;
|
|
|
|
int count;
|
|
if (settings->groupBy != LTM_TOD)
|
|
createCurveData(context, settings, metricDetail, xdata, ydata, count);
|
|
else
|
|
createTODCurveData(context, settings, metricDetail, xdata, ydata, count);
|
|
|
|
//qDebug()<<"Create curve data.."<<timer.elapsed();
|
|
|
|
// Create a curve
|
|
QwtPlotCurve *current = new QwtPlotCurve(metricDetail.uname);
|
|
current->setVisible(!metricDetail.hidden);
|
|
settings->metrics[m].curve = current;
|
|
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS)
|
|
curves.insert(metricDetail.bestSymbol, current);
|
|
else
|
|
curves.insert(metricDetail.symbol, current);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true)
|
|
current->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen cpen = QPen(metricDetail.penColor);
|
|
cpen.setWidth(width);
|
|
current->setPen(cpen);
|
|
current->setStyle(metricDetail.curveStyle);
|
|
|
|
// choose the axis
|
|
QwtAxisId 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
|
|
if (metricDetail.trendtype) {
|
|
|
|
// linear regress
|
|
if (metricDetail.trendtype == 1 && count > 2) {
|
|
|
|
// override class variable as doing it temporarily for trend line only
|
|
double maxX = 0.5 + groupForDate(settings->end.date(), settings->groupBy) -
|
|
groupForDate(settings->start.date(), settings->groupBy);
|
|
|
|
QString trendName = QString(tr("%1 trend")).arg(metricDetail.uname);
|
|
QString trendSymbol = QString("%1_trend")
|
|
.arg((metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
|
|
QwtPlotCurve *trend = new QwtPlotCurve(trendName);
|
|
trend->setVisible(!metricDetail.hidden);
|
|
|
|
// cosmetics
|
|
QPen cpen = QPen(metricDetail.penColor.darker(200));
|
|
cpen.setWidth(2); // double thickness for trend lines
|
|
cpen.setStyle(Qt::SolidLine);
|
|
trend->setPen(cpen);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).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->setSamples(xtrend,ytrend, 2);
|
|
|
|
trend->attach(this);
|
|
curves.insert(trendSymbol, trend);
|
|
|
|
}
|
|
|
|
// quadratic lsm regression
|
|
if (metricDetail.trendtype == 2 && count > 3) {
|
|
QString trendName = QString(tr("%1 trend")).arg(metricDetail.uname);
|
|
QString trendSymbol = QString("%1_trend")
|
|
.arg((metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
|
|
QwtPlotCurve *trend = new QwtPlotCurve(trendName);
|
|
trend->setVisible(!metricDetail.hidden);
|
|
|
|
// cosmetics
|
|
QPen cpen = QPen(metricDetail.penColor.darker(200));
|
|
cpen.setWidth(2); // double thickness for trend lines
|
|
cpen.setStyle(Qt::SolidLine);
|
|
trend->setPen(cpen);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).toBool()==true)
|
|
trend->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
trend->setBaseline(0);
|
|
trend->setYAxis(axisid);
|
|
trend->setStyle(QwtPlotCurve::Lines);
|
|
|
|
// perform quadratic curve fit to data
|
|
LTMTrend2 regress(xdata.data(), ydata.data(), count+1);
|
|
|
|
QVector<double> xtrend;
|
|
QVector<double> ytrend;
|
|
|
|
double inc = (regress.maxx - regress.minx) / 100;
|
|
for (double i=regress.minx; i<=(regress.maxx+inc); i+= inc) {
|
|
xtrend << i;
|
|
ytrend << regress.yForX(i);
|
|
}
|
|
|
|
// point 2 is at far right of chart, not the last point
|
|
// since we may be forecasting...
|
|
trend->setSamples(xtrend.data(),ytrend.data(), xtrend.count());
|
|
|
|
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.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
QwtPlotCurve *out = new QwtPlotCurve(outName);
|
|
out->setVisible(!metricDetail.hidden);
|
|
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
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol) {
|
|
sym->setStyle(QwtSymbol::Ellipse);
|
|
sym->setSize(10);
|
|
} else {
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setSize(20);
|
|
}
|
|
QColor lighter = metricDetail.penColor;
|
|
lighter.setAlpha(50);
|
|
sym->setPen(metricDetail.penColor);
|
|
sym->setBrush(lighter);
|
|
|
|
out->setSymbol(sym);
|
|
out->setSamples(hxdata.data(),hydata.data(), metricDetail.topOut);
|
|
out->setBaseline(0);
|
|
out->setYAxis(axisid);
|
|
out->attach(this);
|
|
}
|
|
|
|
// highlight lowest / top N values
|
|
if (metricDetail.lowestN > 0 || metricDetail.topN > 0) {
|
|
|
|
QMap<double, int> sortedList;
|
|
|
|
// copy the yvalues, retaining the offset
|
|
for(int i=0; i<ydata.count(); i++) {
|
|
// pmc metrics we highlight TROUGHS
|
|
if (metricDetail.type == METRIC_STRESS || metricDetail.type == METRIC_PM) {
|
|
if (i && i < (ydata.count()-1) // not at start/end
|
|
&& ((ydata[i-1] > ydata[i] && ydata[i+1] > ydata[i]) || // is a trough
|
|
(ydata[i-1] < ydata[i] && ydata[i+1] < ydata[i]))) // is a peak
|
|
sortedList.insert(ydata[i], i);
|
|
} else
|
|
sortedList.insert(ydata[i], i);
|
|
}
|
|
|
|
// copy the top N values
|
|
QVector<double> hxdata, hydata;
|
|
hxdata.resize(metricDetail.topN + metricDetail.lowestN);
|
|
hydata.resize(metricDetail.topN + metricDetail.lowestN);
|
|
|
|
// QMap orders the list so start at the top and work
|
|
// backwards for topN
|
|
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++;
|
|
}
|
|
}
|
|
i.toFront();
|
|
counter = 0; // and forwards for bottomN
|
|
while (i.hasNext() && counter < metricDetail.lowestN) {
|
|
i.next();
|
|
if (ydata[i.value()]) {
|
|
hxdata[metricDetail.topN + counter] = xdata[i.value()] + middle;
|
|
hydata[metricDetail.topN + counter] = ydata[i.value()];
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
// lets setup a curve with this data then!
|
|
QString topName;
|
|
if (counter > 1)
|
|
topName = QString(tr("%1 Best"))
|
|
.arg(metricDetail.uname);
|
|
else
|
|
topName = QString(tr("Best %1")).arg(metricDetail.uname);
|
|
|
|
QString topSymbol = QString("%1_topN")
|
|
.arg((metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
QwtPlotCurve *top = new QwtPlotCurve(topName);
|
|
top->setVisible(!metricDetail.hidden);
|
|
curves.insert(topName, 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
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol) {
|
|
sym->setStyle(QwtSymbol::Ellipse);
|
|
sym->setSize(6);
|
|
} else {
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setSize(12);
|
|
}
|
|
QColor lighter = metricDetail.penColor;
|
|
lighter.setAlpha(200);
|
|
sym->setPen(metricDetail.penColor);
|
|
sym->setBrush(lighter);
|
|
|
|
top->setSymbol(sym);
|
|
top->setSamples(hxdata.data(),hydata.data(), counter);
|
|
top->setBaseline(0);
|
|
top->setYAxis(axisid);
|
|
top->attach(this);
|
|
|
|
// if we haven't already got data labels selected for this curve
|
|
// then lets put some on, just for the topN, since they are of
|
|
// interest to the user and typically the first thing they do
|
|
// is move mouse over to get a tooltip anyway!
|
|
if (!metricDetail.labels) {
|
|
|
|
QFont labelFont;
|
|
labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
|
|
|
// loop through each NONZERO value and add a label
|
|
for (int i=0; i<hxdata.count(); i++) {
|
|
|
|
double value = hydata[i];
|
|
|
|
// bar headings always need to be centered
|
|
if (value) {
|
|
|
|
// format the label appropriately
|
|
const RideMetric *m = metricDetail.metric;
|
|
QString labelString;
|
|
|
|
if (m != NULL) {
|
|
|
|
// handle precision of 1 for seconds converted to hours
|
|
int precision = m->precision();
|
|
if (metricDetail.uunits == "seconds" || metricDetail.uunits == tr("seconds")) precision=1;
|
|
if (metricDetail.uunits == "km") precision=0;
|
|
|
|
// we have a metric so lets be precise ...
|
|
labelString = QString("%1").arg(value, 0, 'f', precision);
|
|
|
|
} else {
|
|
// no precision
|
|
labelString = (QString("%1").arg(value, 0, 'f', 1));
|
|
}
|
|
|
|
|
|
// Qwt uses its own text objects
|
|
QwtText text(labelString);
|
|
text.setFont(labelFont);
|
|
text.setColor(metricDetail.penColor);
|
|
|
|
// make that mark -- always above with topN
|
|
QwtPlotMarker *label = new QwtPlotMarker();
|
|
label->setVisible(!metricDetail.hidden);
|
|
label->setLabel(text);
|
|
label->setValue(hxdata[i], hydata[i]);
|
|
label->setYAxis(axisid);
|
|
label->setSpacing(6); // not px but by yaxis value !? mad.
|
|
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
|
|
|
|
// and attach
|
|
label->attach(this);
|
|
labels << label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// fill the bars
|
|
QColor brushColor = metricDetail.penColor;
|
|
brushColor.setAlpha(200); // 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(QPen(Qt::NoPen));
|
|
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::NoSymbol);
|
|
current->setSymbol(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);
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setSize(6);
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setPen(QPen(metricDetail.penColor));
|
|
sym->setBrush(QBrush(metricDetail.penColor));
|
|
current->setSymbol(sym);
|
|
current->setPen(cpen);
|
|
|
|
// fill below the line
|
|
if (metricDetail.fillCurve) {
|
|
QColor fillColor = metricDetail.penColor;
|
|
fillColor.setAlpha(100);
|
|
current->setBrush(fillColor);
|
|
}
|
|
|
|
|
|
} else if (metricDetail.curveStyle == QwtPlotCurve::Dots) {
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setSize(6);
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setPen(QPen(metricDetail.penColor));
|
|
sym->setBrush(QBrush(metricDetail.penColor));
|
|
current->setSymbol(sym);
|
|
|
|
} else if (metricDetail.curveStyle == QwtPlotCurve::Sticks) {
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setSize(4);
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setPen(QPen(metricDetail.penColor));
|
|
sym->setBrush(QBrush(Qt::white));
|
|
current->setSymbol(sym);
|
|
|
|
}
|
|
|
|
// add data labels
|
|
if (metricDetail.labels) {
|
|
|
|
QFont labelFont;
|
|
labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
|
|
|
// loop through each NONZERO value and add a label
|
|
for (int i=0; i<xdata.count(); i++) {
|
|
|
|
// we only want to do once per bar, which has 4 points
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps && (i+1)%4) continue;
|
|
|
|
double value = metricDetail.curveStyle == QwtPlotCurve::Steps ? ydata[i-1] : ydata[i];
|
|
|
|
// bar headings always need to be centered
|
|
if (value) {
|
|
|
|
// format the label appropriately
|
|
const RideMetric *m = metricDetail.metric;
|
|
QString labelString;
|
|
|
|
if (m != NULL) {
|
|
|
|
// handle precision of 1 for seconds converted to hours
|
|
int precision = m->precision();
|
|
if (metricDetail.uunits == "seconds" || metricDetail.uunits == tr("seconds")) precision=1;
|
|
if (metricDetail.uunits == "km") precision=0;
|
|
|
|
// we have a metric so lets be precise ...
|
|
labelString = QString("%1").arg(value, 0, 'f', precision);
|
|
|
|
} else {
|
|
// no precision
|
|
labelString = (QString("%1").arg(value, 0, 'f', 1));
|
|
}
|
|
|
|
|
|
// Qwt uses its own text objects
|
|
QwtText text(labelString);
|
|
text.setFont(labelFont);
|
|
text.setColor(metricDetail.penColor);
|
|
|
|
// make that mark
|
|
QwtPlotMarker *label = new QwtPlotMarker();
|
|
label->setVisible(!metricDetail.hidden);
|
|
label->setLabel(text);
|
|
label->setValue(xdata[i], ydata[i]);
|
|
label->setYAxis(axisid);
|
|
label->setSpacing(3); // not px but by yaxis value !? mad.
|
|
|
|
// Bars(steps) / sticks / dots: label above centered
|
|
// but bars have multiple points offset from their actual
|
|
// so need to adjust bars to centre above the top of the bar
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// We only get every fourth point, so center
|
|
// between second and third point of bar "square"
|
|
label->setValue((xdata[i-1]+xdata[i-2])/2.00f, ydata[i-1]);
|
|
}
|
|
|
|
// Lables on a Line curve should be above/below depending upon the shape of the curve
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Lines) {
|
|
|
|
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
|
|
|
|
// we could simplify this into one if clause but it wouldn't be
|
|
// so obvious what we were doing
|
|
if (i && (i == ydata.count()-3) && ydata[i-1] > ydata[i]) {
|
|
|
|
// last point on curve
|
|
label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter);
|
|
|
|
} else if (i && i < ydata.count()) {
|
|
|
|
// is a low / valley
|
|
if (ydata[i-1] > ydata[i] && ydata[i+1] > ydata[i])
|
|
label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter);
|
|
|
|
} else if (i == 0 && ydata[i+1] > ydata[i]) {
|
|
|
|
// first point on curve
|
|
label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter);
|
|
}
|
|
|
|
} else {
|
|
|
|
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
|
|
}
|
|
|
|
// and attach
|
|
label->attach(this);
|
|
labels << label;
|
|
}
|
|
}
|
|
}
|
|
|
|
// smoothing
|
|
if (metricDetail.smooth == true) {
|
|
current->setCurveAttribute(QwtPlotCurve::Fitted, true);
|
|
}
|
|
|
|
// set the data series
|
|
current->setSamples(xdata.data(),ydata.data(), count + 1);
|
|
current->setBaseline(metricDetail.baseline);
|
|
|
|
//qDebug()<<"Set Curve Data.."<<timer.elapsed();
|
|
|
|
// update min/max Y values for the chosen axis
|
|
if (current->maxYValue() > maxY[supportedAxes.indexOf(axisid)]) maxY[supportedAxes.indexOf(axisid)] = current->maxYValue();
|
|
if (current->minYValue() < minY[supportedAxes.indexOf(axisid)]) minY[supportedAxes.indexOf(axisid)] = current->minYValue();
|
|
|
|
current->attach(this);
|
|
|
|
}
|
|
|
|
//qDebug()<<"Second plotting iteration.."<<timer.elapsed();
|
|
|
|
|
|
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));
|
|
}
|
|
enableAxis(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xTop, false);
|
|
|
|
// run through the Y axis
|
|
for (int i=0; i<8; i++) {
|
|
// set the scale on the axis
|
|
if (i != xBottom && i != xTop) {
|
|
maxY[i] *= 1.1; // add 10% headroom
|
|
if (maxY[i] == minY[i] && maxY[i] == 0)
|
|
setAxisScale(supportedAxes[i], 0.0f, 100.0f); // to stop ugly
|
|
else
|
|
setAxisScale(supportedAxes[i], minY[i], maxY[i]);
|
|
}
|
|
}
|
|
|
|
QString format = axisTitle(yLeft).text();
|
|
picker->setAxes(xBottom, yLeft);
|
|
picker->setFormat(format);
|
|
|
|
// draw zone labels axisid of -1 means delete whats there
|
|
// cause no watts are being displayed
|
|
if (settings->shadeZones == true) {
|
|
QwtAxisId axisid = axes.value("watts", QwtAxisId(-1,-1));
|
|
if (axisid == QwtAxisId(-1,-1)) axisid = axes.value(tr("watts"), QwtAxisId(-1,-1)); // Try translated version
|
|
refreshZoneLabels(axisid);
|
|
} else {
|
|
refreshZoneLabels(QwtAxisId(-1,-1)); // turn em off
|
|
}
|
|
|
|
QHashIterator<QString, QwtPlotCurve*> p(curves);
|
|
while (p.hasNext()) {
|
|
p.next();
|
|
|
|
// always hide bollocksy curves
|
|
if (p.key().endsWith(tr("trend")) || p.key().endsWith(tr("Outliers")) || p.key().endsWith(tr("Best")) || p.key().startsWith(tr("Best")))
|
|
p.value()->setItemAttribute(QwtPlotItem::Legend, false);
|
|
else
|
|
p.value()->setItemAttribute(QwtPlotItem::Legend, settings->legend);
|
|
}
|
|
|
|
// show legend?
|
|
if (settings->legend == false) this->legend()->hide();
|
|
else this->legend()->show();
|
|
|
|
|
|
// now refresh
|
|
updateLegend();
|
|
|
|
// markers
|
|
if (settings->groupBy != LTM_TOD)
|
|
refreshMarkers(settings, settings->start.date(), settings->end.date(), settings->groupBy, GColor(CPLOTMARKER));
|
|
|
|
//qDebug()<<"Final tidy.."<<timer.elapsed();
|
|
|
|
// update colours etc for plot chrome will also save state
|
|
configChanged(CONFIG_APPEARANCE);
|
|
|
|
// plot
|
|
replot();
|
|
|
|
//qDebug()<<"Replot and done.."<<timer.elapsed();
|
|
|
|
}
|
|
|
|
void
|
|
LTMPlot::setCompareData(LTMSettings *set)
|
|
{
|
|
QTime timer;
|
|
timer.start();
|
|
|
|
MAXX=0.0; // maximum value for x, always from 0-n
|
|
settings = set;
|
|
|
|
//qDebug()<<"Starting.."<<timer.elapsed();
|
|
|
|
// 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;
|
|
}
|
|
foreach (QwtPlotMarker *label, labels) {
|
|
label->detach();
|
|
delete label;
|
|
}
|
|
labels.clear();
|
|
// clear old markers - if there are any
|
|
foreach(QwtPlotMarker *m, markers) {
|
|
m->detach();
|
|
delete m;
|
|
}
|
|
markers.clear();
|
|
|
|
|
|
// disable all y axes until we have populated
|
|
for (int i=0; i<8; i++) {
|
|
setAxisVisible(supportedAxes[i], false);
|
|
enableAxis(supportedAxes[i].id, false);
|
|
}
|
|
axes.clear();
|
|
axesObject.clear();
|
|
axesId.clear();
|
|
|
|
// reset all min/max Y values
|
|
for (int i=0; i<10; i++) minY[i]=0, maxY[i]=0;
|
|
|
|
// which yAxis did we use (should be yLeft)
|
|
QwtAxisId axisid(QwtPlot::yLeft, 0);
|
|
|
|
// which compare date range are we on?
|
|
int cdCount =0;
|
|
|
|
// how many bars?
|
|
int bars =0;
|
|
foreach(CompareDateRange cd, context->compareDateRanges) if (cd.checked) bars++;
|
|
|
|
//
|
|
// Setup curve for every Date Range being plotted
|
|
//
|
|
foreach(CompareDateRange cd, context->compareDateRanges) {
|
|
|
|
// only plot date ranges selected!
|
|
if (!cd.checked) continue;
|
|
|
|
// increment count of date ranges we have
|
|
cdCount++;
|
|
|
|
//QColor color;
|
|
//QDate start, end;
|
|
//int days;
|
|
//Context *sourceContext;
|
|
|
|
// no data to display so that all folks
|
|
if (context->athlete->rideCache->rides().count() == 0) continue;
|
|
|
|
QDateTime first = context->athlete->rideCache->rides().first()->dateTime;
|
|
QDateTime last = context->athlete->rideCache->rides().last()->dateTime;
|
|
|
|
// end
|
|
if (settings->end == QDateTime() ||
|
|
settings->end > last.addDays(365)) {
|
|
if (settings->end < QDateTime::currentDateTime()) {
|
|
settings->end = QDateTime::currentDateTime();
|
|
} else {
|
|
settings->end = last;
|
|
}
|
|
}
|
|
|
|
// start
|
|
if (settings->start == QDateTime() ||
|
|
settings->start < first.addDays(-365)) {
|
|
settings->start = first;
|
|
}
|
|
|
|
|
|
settings->start = QDateTime(cd.start, QTime());
|
|
settings->end = QDateTime(cd.end, QTime());
|
|
|
|
// For each metric in chart, translate units and name if default uname
|
|
//XXX BROKEN XXX LTMTool::translateMetrics(context, settings);
|
|
|
|
// we need to do this for each date range as they are dependant
|
|
// on the metrics chosen and can't be pre-cached
|
|
settings->specification.setDateRange(DateRange(cd.start, cd.end));
|
|
|
|
// bests...
|
|
QList<RideBest> herebests;
|
|
herebests = RideFileCache::getAllBestsFor(cd.sourceContext, settings->metrics, settings->specification);
|
|
settings->bests = &herebests;
|
|
|
|
switch (settings->groupBy) {
|
|
case LTM_TOD:
|
|
setAxisTitle(xBottom, tr("Time of Day"));
|
|
break;
|
|
case LTM_DAY:
|
|
setAxisTitle(xBottom, tr("Day"));
|
|
break;
|
|
case LTM_WEEK:
|
|
setAxisTitle(xBottom, tr("Week"));
|
|
break;
|
|
case LTM_MONTH:
|
|
setAxisTitle(xBottom, tr("Month"));
|
|
break;
|
|
case LTM_YEAR:
|
|
setAxisTitle(xBottom, tr("Year"));
|
|
break;
|
|
case LTM_ALL:
|
|
setAxisTitle(xBottom, tr("All"));
|
|
break;
|
|
default:
|
|
setAxisTitle(xBottom, tr("Date"));
|
|
break;
|
|
}
|
|
enableAxis(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xTop, false);
|
|
|
|
|
|
//qDebug()<<"Wiped previous.."<<timer.elapsed();
|
|
|
|
// 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).
|
|
|
|
// index through rather than foreach so we can modify
|
|
|
|
// 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(cd.sourceContext, settings, metricDetail, *xdata, *ydata, count);
|
|
else
|
|
createTODCurveData(cd.sourceContext, settings, metricDetail, *xdata, *ydata, count);
|
|
|
|
// lets catch the x-scale
|
|
if (count > MAXX) MAXX=count;
|
|
|
|
// we add in the last curve for X axis values
|
|
if (r) {
|
|
aggregateCurves(*stackY[r], *stackY[r-1]);
|
|
}
|
|
r++;
|
|
}
|
|
}
|
|
|
|
//qDebug()<<"Created curve data.."<<timer.elapsed();
|
|
|
|
// setup the curves
|
|
double width = appsettings->value(this, GC_LINEWIDTH, 0.5).toDouble();
|
|
|
|
// 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--) {
|
|
|
|
//
|
|
// *ONLY* PLOT STACKS
|
|
//
|
|
|
|
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;
|
|
|
|
// name is year and metric
|
|
QString name = QString ("%1 %2").arg(cd.name).arg(metricDetail.uname);
|
|
|
|
// Create a curve
|
|
QwtPlotCurve *current = new QwtPlotCurve(name);
|
|
if (metricDetail.type == METRIC_BEST)
|
|
curves.insert(name, current);
|
|
else
|
|
curves.insert(name, current);
|
|
stacks.insert(current, stackcounter+1);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true)
|
|
current->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen cpen = QPen(cd.color);
|
|
cpen.setWidth(width);
|
|
current->setPen(cpen);
|
|
current->setStyle(metricDetail.curveStyle);
|
|
|
|
// choose the axis
|
|
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 = cdCount-1;
|
|
|
|
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;
|
|
|
|
//left -= 1.00f;
|
|
//right -= 1.00f;
|
|
//left -= 0.5 + gap;
|
|
//right -= 0.5 + gap;
|
|
}
|
|
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// fill the bars
|
|
QColor merge;
|
|
merge.setRed((metricDetail.penColor.red() + cd.color.red()) / 2);
|
|
merge.setGreen((metricDetail.penColor.green() + cd.color.green()) / 2);
|
|
merge.setBlue((metricDetail.penColor.blue() + cd.color.blue()) / 2);
|
|
|
|
QColor brushColor = merge;
|
|
if (metricDetail.stack == true) {
|
|
brushColor.setAlpha(255);
|
|
QBrush brush = QBrush(brushColor);
|
|
current->setBrush(brush);
|
|
} else {
|
|
brushColor.setAlpha(200); // 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(QPen(Qt::NoPen));
|
|
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::NoSymbol);
|
|
current->setSymbol(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->setSamples(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[supportedAxes.indexOf(axisid)]) maxY[supportedAxes.indexOf(axisid)] = current->maxYValue();
|
|
if (current->minYValue() < minY[supportedAxes.indexOf(axisid)]) minY[supportedAxes.indexOf(axisid)] = current->minYValue();
|
|
|
|
current->attach(this);
|
|
|
|
} // end of reverse for stacked plots
|
|
|
|
//qDebug()<<"First plotting iteration.."<<timer.elapsed();
|
|
|
|
// 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) {
|
|
|
|
//
|
|
// *ONLY* PLOT NON-STACKS
|
|
//
|
|
if (metricDetail.stack == true) continue;
|
|
|
|
QVector<double> xdata, ydata;
|
|
|
|
int count=0;
|
|
if (settings->groupBy != LTM_TOD)
|
|
createCurveData(cd.sourceContext, settings, metricDetail, xdata, ydata, count);
|
|
else
|
|
createTODCurveData(cd.sourceContext, settings, metricDetail, xdata, ydata, count);
|
|
|
|
// lets catch the x-scale
|
|
if (count > MAXX) MAXX=count;
|
|
|
|
//qDebug()<<"Create curve data.."<<timer.elapsed();
|
|
|
|
// Create a curve
|
|
QwtPlotCurve *current = new QwtPlotCurve(cd.name);
|
|
if (metricDetail.type == METRIC_BEST)
|
|
curves.insert(cd.name, current);
|
|
else
|
|
curves.insert(cd.name, current);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true)
|
|
current->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
QPen cpen = QPen(cd.color);
|
|
cpen.setWidth(width);
|
|
current->setPen(cpen);
|
|
current->setStyle(metricDetail.curveStyle);
|
|
|
|
// choose the axis
|
|
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 = cdCount-1;
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
// trend - clone the data for the curve and add a curvefitted
|
|
if (metricDetail.trendtype) {
|
|
|
|
// linear regress
|
|
if (metricDetail.trendtype == 1 && count > 2) {
|
|
|
|
// override class variable as doing it temporarily for trend line only
|
|
double maxX = 0.5 + groupForDate(settings->end.date(), settings->groupBy) -
|
|
groupForDate(settings->start.date(), settings->groupBy);
|
|
|
|
QString trendName = QString(tr("%1 %2 trend")).arg(cd.name).arg(metricDetail.uname);
|
|
QString trendSymbol = QString("%1_trend")
|
|
.arg((metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
|
|
QwtPlotCurve *trend = new QwtPlotCurve(trendName);
|
|
curves.insert(trendName, trend);
|
|
|
|
// cosmetics
|
|
QPen cpen = QPen(cd.color.darker(200));
|
|
cpen.setWidth(2); // double thickness for trend lines
|
|
cpen.setStyle(Qt::SolidLine);
|
|
trend->setPen(cpen);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).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->setSamples(xtrend,ytrend, 2);
|
|
|
|
trend->attach(this);
|
|
}
|
|
|
|
// quadratic lsm regression
|
|
if (metricDetail.trendtype == 2 && count > 3) {
|
|
QString trendName = QString(tr("%1 %2 trend")).arg(cd.name).arg(metricDetail.uname);
|
|
QString trendSymbol = QString("%1_trend")
|
|
.arg((metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
|
|
QwtPlotCurve *trend = new QwtPlotCurve(trendName);
|
|
curves.insert(trendName, trend);
|
|
|
|
// cosmetics
|
|
QPen cpen = QPen(cd.color.darker(200));
|
|
cpen.setWidth(2); // double thickness for trend lines
|
|
cpen.setStyle(Qt::SolidLine);
|
|
trend->setPen(cpen);
|
|
if (appsettings->value(this, GC_ANTIALIAS, true).toBool()==true)
|
|
trend->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
trend->setBaseline(0);
|
|
trend->setYAxis(axisid);
|
|
trend->setStyle(QwtPlotCurve::Lines);
|
|
|
|
// perform quadratic curve fit to data
|
|
LTMTrend2 regress(xdata.data(), ydata.data(), count+1);
|
|
|
|
QVector<double> xtrend;
|
|
QVector<double> ytrend;
|
|
|
|
double inc = (regress.maxx - regress.minx) / 100;
|
|
for (double i=regress.minx; i<=(regress.maxx+inc); i+= inc) {
|
|
xtrend << i;
|
|
ytrend << regress.yForX(i);
|
|
}
|
|
|
|
// point 2 is at far right of chart, not the last point
|
|
// since we may be forecasting...
|
|
trend->setSamples(xtrend.data(),ytrend.data(), xtrend.count());
|
|
|
|
trend->attach(this);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
outName = QString(tr("%1 %2 Outliers"))
|
|
.arg(cd.name)
|
|
.arg(metricDetail.uname);
|
|
|
|
QString outSymbol = QString("%1_outlier").arg((metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
QwtPlotCurve *out = new QwtPlotCurve(outName);
|
|
curves.insert(outName, 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
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol) {
|
|
sym->setStyle(QwtSymbol::Ellipse);
|
|
sym->setSize(10);
|
|
} else {
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setSize(20);
|
|
}
|
|
QColor lighter = cd.color;
|
|
lighter.setAlpha(50);
|
|
sym->setPen(cd.color);
|
|
sym->setBrush(lighter);
|
|
|
|
out->setSymbol(sym);
|
|
out->setSamples(hxdata.data(),hydata.data(), metricDetail.topOut);
|
|
out->setBaseline(0);
|
|
out->setYAxis(axisid);
|
|
out->attach(this);
|
|
}
|
|
|
|
// highlight top N values
|
|
if (metricDetail.lowestN > 0 || metricDetail.topN > 0) {
|
|
|
|
QMap<double, int> sortedList;
|
|
|
|
for(int i=0; i<ydata.count(); i++) {
|
|
// pmc metrics we highlight TROUGHS
|
|
if (metricDetail.type == METRIC_STRESS || metricDetail.type == METRIC_PM) {
|
|
if (i && i < (ydata.count()-1) // not at start/end
|
|
&& ((ydata[i-1] > ydata[i] && ydata[i+1] > ydata[i]) || // is a trough
|
|
(ydata[i-1] < ydata[i] && ydata[i+1] < ydata[i]))) // is a peak
|
|
sortedList.insert(ydata[i], i);
|
|
} else
|
|
sortedList.insert(ydata[i], i);
|
|
}
|
|
|
|
|
|
// copy the top N values
|
|
QVector<double> hxdata, hydata;
|
|
hxdata.resize(metricDetail.topN + metricDetail.lowestN);
|
|
hydata.resize(metricDetail.topN + metricDetail.lowestN);
|
|
|
|
// QMap orders the list so start at the top and work
|
|
// backwards for topN
|
|
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++;
|
|
}
|
|
}
|
|
i.toFront();
|
|
counter = 0; // and backwards for bottomN
|
|
while (i.hasNext() && counter < metricDetail.lowestN) {
|
|
i.next();
|
|
if (ydata[i.value()]) {
|
|
hxdata[metricDetail.topN + counter] = xdata[i.value()] + middle;
|
|
hydata[metricDetail.topN + counter] = ydata[i.value()];
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
// lets setup a curve with this data then!
|
|
QString topName = QString(tr("%1 %2 Best")).arg(cd.name).arg(metricDetail.uname);
|
|
QString topSymbol = QString("%1_topN")
|
|
.arg((metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS) ?
|
|
metricDetail.bestSymbol : metricDetail.symbol);
|
|
QwtPlotCurve *top = new QwtPlotCurve(topName);
|
|
curves.insert(topName, 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
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol) {
|
|
sym->setStyle(QwtSymbol::Ellipse);
|
|
sym->setSize(6);
|
|
} else {
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setSize(12);
|
|
}
|
|
QColor lighter = cd.color;
|
|
lighter.setAlpha(200);
|
|
sym->setPen(cd.color);
|
|
sym->setBrush(lighter);
|
|
|
|
top->setSymbol(sym);
|
|
top->setSamples(hxdata.data(),hydata.data(), counter);
|
|
top->setBaseline(0);
|
|
top->setYAxis(axisid);
|
|
top->attach(this);
|
|
|
|
// if we haven't already got data labels selected for this curve
|
|
// then lets put some on, just for the topN, since they are of
|
|
// interest to the user and typically the first thing they do
|
|
// is move mouse over to get a tooltip anyway!
|
|
if (!metricDetail.labels) {
|
|
|
|
QFont labelFont;
|
|
labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
|
|
|
// loop through each NONZERO value and add a label
|
|
for (int i=0; i<hxdata.count(); i++) {
|
|
|
|
double value = hydata[i];
|
|
|
|
// bar headings always need to be centered
|
|
if (value) {
|
|
|
|
// format the label appropriately
|
|
const RideMetric *m = metricDetail.metric;
|
|
QString labelString;
|
|
|
|
if (m != NULL) {
|
|
|
|
// handle precision of 1 for seconds converted to hours
|
|
int precision = m->precision();
|
|
if (metricDetail.uunits == "seconds" || metricDetail.uunits == tr("seconds")) precision=1;
|
|
if (metricDetail.uunits == "km") precision=0;
|
|
|
|
// we have a metric so lets be precise ...
|
|
labelString = QString("%1").arg(value , 0, 'f', precision);
|
|
|
|
} else {
|
|
// no precision
|
|
labelString = (QString("%1").arg(value, 0, 'f', 1));
|
|
}
|
|
|
|
|
|
// Qwt uses its own text objects
|
|
QwtText text(labelString);
|
|
text.setFont(labelFont);
|
|
text.setColor(cd.color);
|
|
|
|
// make that mark -- always above with topN
|
|
QwtPlotMarker *label = new QwtPlotMarker();
|
|
label->setLabel(text);
|
|
label->setValue(hxdata[i], hydata[i]);
|
|
label->setYAxis(axisid);
|
|
label->setSpacing(6); // not px but by yaxis value !? mad.
|
|
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
|
|
|
|
// and attach
|
|
label->attach(this);
|
|
labels << label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// fill the bars
|
|
QColor brushColor = cd.color;
|
|
brushColor.setAlpha(200); // now side by side, less transparency required
|
|
QColor brushColor1 = cd.color.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(QPen(Qt::NoPen));
|
|
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setStyle(QwtSymbol::NoSymbol);
|
|
current->setSymbol(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(cd.color);
|
|
cpen.setWidth(width);
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setSize(6);
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setPen(QPen(cd.color));
|
|
sym->setBrush(QBrush(cd.color));
|
|
current->setSymbol(sym);
|
|
current->setPen(cpen);
|
|
|
|
// fill below the line
|
|
if (metricDetail.fillCurve) {
|
|
QColor fillColor = cd.color;
|
|
fillColor.setAlpha(100);
|
|
current->setBrush(fillColor);
|
|
}
|
|
|
|
|
|
} else if (metricDetail.curveStyle == QwtPlotCurve::Dots) {
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setSize(6);
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setPen(QPen(cd.color));
|
|
sym->setBrush(QBrush(cd.color));
|
|
current->setSymbol(sym);
|
|
|
|
} else if (metricDetail.curveStyle == QwtPlotCurve::Sticks) {
|
|
|
|
QwtSymbol *sym = new QwtSymbol;
|
|
sym->setSize(4);
|
|
sym->setStyle(metricDetail.symbolStyle);
|
|
sym->setPen(QPen(cd.color));
|
|
sym->setBrush(QBrush(Qt::white));
|
|
current->setSymbol(sym);
|
|
|
|
}
|
|
|
|
// add data labels
|
|
if (metricDetail.labels) {
|
|
|
|
QFont labelFont;
|
|
labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
|
|
|
// loop through each NONZERO value and add a label
|
|
for (int i=0; i<xdata.count(); i++) {
|
|
|
|
// we only want to do once per bar, which has 4 points
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps && (i+1)%4) continue;
|
|
|
|
double value = metricDetail.curveStyle == QwtPlotCurve::Steps ? ydata[i-1] : ydata[i];
|
|
|
|
// bar headings always need to be centered
|
|
if (value) {
|
|
|
|
// format the label appropriately
|
|
const RideMetric *m = metricDetail.metric;
|
|
QString labelString;
|
|
|
|
if (m != NULL) {
|
|
|
|
// handle precision of 1 for seconds converted to hours
|
|
int precision = m->precision();
|
|
if (metricDetail.uunits == "seconds" || metricDetail.uunits == tr("seconds")) precision=1;
|
|
if (metricDetail.uunits == "km") precision=0;
|
|
|
|
// we have a metric so lets be precise ...
|
|
labelString = QString("%1").arg(value, 0, 'f', precision);
|
|
|
|
} else {
|
|
// no precision
|
|
labelString = (QString("%1").arg(value, 0, 'f', 1));
|
|
}
|
|
|
|
|
|
// Qwt uses its own text objects
|
|
QwtText text(labelString);
|
|
text.setFont(labelFont);
|
|
text.setColor(cd.color);
|
|
|
|
// make that mark
|
|
QwtPlotMarker *label = new QwtPlotMarker();
|
|
label->setLabel(text);
|
|
label->setValue(xdata[i], ydata[i]);
|
|
label->setYAxis(axisid);
|
|
label->setSpacing(3); // not px but by yaxis value !? mad.
|
|
|
|
// Bars(steps) / sticks / dots: label above centered
|
|
// but bars have multiple points offset from their actual
|
|
// so need to adjust bars to centre above the top of the bar
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
|
|
|
|
// We only get every fourth point, so center
|
|
// between second and third point of bar "square"
|
|
label->setValue((xdata[i-1]+xdata[i-2])/2.00f, ydata[i-1]);
|
|
}
|
|
|
|
// Lables on a Line curve should be above/below depending upon the shape of the curve
|
|
if (metricDetail.curveStyle == QwtPlotCurve::Lines) {
|
|
|
|
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
|
|
|
|
// we could simplify this into one if clause but it wouldn't be
|
|
// so obvious what we were doing
|
|
if (i && (i == ydata.count()-3) && ydata[i-1] > ydata[i]) {
|
|
|
|
// last point on curve
|
|
label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter);
|
|
|
|
} else if (i && i < ydata.count()) {
|
|
|
|
// is a low / valley
|
|
if (ydata[i-1] > ydata[i] && ydata[i+1] > ydata[i])
|
|
label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter);
|
|
|
|
} else if (i == 0 && ydata[i+1] > ydata[i]) {
|
|
|
|
// first point on curve
|
|
label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter);
|
|
}
|
|
|
|
} else {
|
|
|
|
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
|
|
}
|
|
|
|
// and attach
|
|
label->attach(this);
|
|
labels << label;
|
|
}
|
|
}
|
|
}
|
|
|
|
// smoothing
|
|
if (metricDetail.smooth == true) {
|
|
current->setCurveAttribute(QwtPlotCurve::Fitted, true);
|
|
}
|
|
|
|
// set the data series
|
|
current->setSamples(xdata.data(),ydata.data(), count + 1);
|
|
current->setBaseline(metricDetail.baseline);
|
|
|
|
//qDebug()<<"Set Curve Data.."<<timer.elapsed();
|
|
|
|
// update min/max Y values for the chosen axis
|
|
if (current->maxYValue() > maxY[supportedAxes.indexOf(axisid)]) maxY[supportedAxes.indexOf(axisid)] = current->maxYValue();
|
|
if (current->minYValue() < minY[supportedAxes.indexOf(axisid)]) minY[supportedAxes.indexOf(axisid)] = current->minYValue();
|
|
|
|
current->attach(this);
|
|
|
|
}
|
|
|
|
// lastly set markers using the right color
|
|
if (settings->groupBy != LTM_TOD)
|
|
refreshMarkers(settings, settings->start.date(), settings->end.date(), settings->groupBy, cd.color);
|
|
|
|
}
|
|
|
|
//qDebug()<<"Second plotting iteration.."<<timer.elapsed();
|
|
|
|
// axes
|
|
|
|
if (settings->groupBy != LTM_TOD) {
|
|
|
|
int tics;
|
|
if (MAXX < 14) {
|
|
tics = 1;
|
|
} else {
|
|
tics = 1 + MAXX/10;
|
|
}
|
|
setAxisScale(xBottom, -0.498f, MAXX+0.498f, tics);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new CompareScaleDraw());
|
|
|
|
|
|
} else {
|
|
setAxisScale(xBottom, 0, 24, 2);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
|
|
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
|
|
}
|
|
enableAxis(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xBottom, true);
|
|
setAxisVisible(QwtAxis::xTop, false);
|
|
|
|
// run through the Y axis
|
|
for (int i=0; i<8; i++) {
|
|
// set the scale on the axis
|
|
if (i != xBottom && i != xTop) {
|
|
maxY[i] *= 1.2; // add 20% headroom
|
|
setAxisScale(supportedAxes[i], minY[i], maxY[i]);
|
|
}
|
|
}
|
|
|
|
// if not stacked then lets make the yAxis a little
|
|
// more descriptive and use the color of the curve
|
|
if (set->metrics.count() == 1) {
|
|
|
|
// title (units)
|
|
QString units = set->metrics[0].uunits;
|
|
QString name = set->metrics[0].uname;
|
|
|
|
// abbreviate the coggan bullshit everyone loves
|
|
// but god only knows why (sheep?)
|
|
if (name == "Coggan Acute Training Load") name = "ATL";
|
|
if (name == "Coggan Chronic Training Load") name = "CTL";
|
|
if (name == "Coggan Training Stress Balance") name = "TSB";
|
|
|
|
QString title = name ;
|
|
if (units != "" && units != name) title = title + " (" + units + ")";
|
|
|
|
setAxisTitle(axisid, title);
|
|
|
|
// color
|
|
QPalette pal;
|
|
pal.setColor(QPalette::WindowText, set->metrics[0].penColor);
|
|
pal.setColor(QPalette::Text, set->metrics[0].penColor);
|
|
axisWidget(axisid)->setPalette(pal);
|
|
}
|
|
|
|
QString format = axisTitle(yLeft).text();
|
|
picker->setAxes(xBottom, yLeft);
|
|
picker->setFormat(format);
|
|
|
|
// show legend?
|
|
if (settings->legend == false) this->legend()->hide();
|
|
else this->legend()->show();
|
|
|
|
QHashIterator<QString, QwtPlotCurve*> p(curves);
|
|
while (p.hasNext()) {
|
|
p.next();
|
|
|
|
// always hide bollocksy curves
|
|
if (p.key().endsWith(tr("trend")) || p.key().endsWith(tr("Outliers")) || p.key().endsWith(tr("Best")) || p.key().startsWith(tr("Best")))
|
|
p.value()->setItemAttribute(QwtPlotItem::Legend, false);
|
|
else
|
|
p.value()->setItemAttribute(QwtPlotItem::Legend, settings->legend);
|
|
}
|
|
|
|
// now refresh
|
|
updateLegend();
|
|
|
|
// update colours etc for plot chrome
|
|
configChanged(CONFIG_APPEARANCE);
|
|
|
|
// plot
|
|
replot();
|
|
|
|
//qDebug()<<"Replot and done.."<<timer.elapsed();
|
|
|
|
}
|
|
|
|
int
|
|
LTMPlot::getMaxX()
|
|
{
|
|
return MAXX;
|
|
}
|
|
|
|
void
|
|
LTMPlot::setMaxX(int x)
|
|
{
|
|
MAXX = x;
|
|
|
|
int tics;
|
|
if (MAXX < 14) {
|
|
tics = 1;
|
|
} else {
|
|
tics = 1 + MAXX/10;
|
|
}
|
|
setAxisScale(xBottom, -0.498f, MAXX+0.498f, tics);
|
|
setAxisScaleDraw(QwtPlot::xBottom, new CompareScaleDraw());
|
|
}
|
|
|
|
void
|
|
LTMPlot::createTODCurveData(Context *context, LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n,bool)
|
|
{
|
|
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 (RideItem *ride, context->athlete->rideCache->rides()) {
|
|
|
|
if (!settings->specification.pass(ride)) continue;
|
|
|
|
double value = ride->getForSymbol(metricDetail.symbol);
|
|
|
|
// check values are bounded to stop QWT going berserk
|
|
if (std::isnan(value) || std::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 = ride->dateTime.time().hour();
|
|
int type = metricDetail.metric ? metricDetail.metric->type() : RideMetric::Average;
|
|
bool aggZero = metricDetail.metric ? metricDetail.metric->aggregateZero() : false;
|
|
|
|
// set aggZero to false and value to zero if is temperature and -255
|
|
if (metricDetail.metric && metricDetail.metric->symbol() == "average_temp" && value == RideFile::NoTemp) {
|
|
value = 0;
|
|
aggZero = false;
|
|
}
|
|
|
|
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
|
|
if (value || aggZero)
|
|
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(Context *context, LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n, bool forceZero)
|
|
{
|
|
// create curves depending on type ...
|
|
if (metricDetail.type == METRIC_DB || metricDetail.type == METRIC_META) {
|
|
createMetricData(context, settings, metricDetail, x,y,n, forceZero);
|
|
return;
|
|
} else if (metricDetail.type == METRIC_STRESS || metricDetail.type == METRIC_PM) {
|
|
createPMCData(context, settings, metricDetail, x,y,n, forceZero);
|
|
return;
|
|
} else if (metricDetail.type == METRIC_BEST) {
|
|
createBestsData(context,settings,metricDetail,x,y,n, forceZero);
|
|
return;
|
|
} else if (metricDetail.type == METRIC_ESTIMATE) {
|
|
createEstimateData(context, settings, metricDetail, x,y,n, forceZero);
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
void
|
|
LTMPlot::createMetricData(Context *context, LTMSettings *settings, MetricDetail metricDetail,
|
|
QVector<double>&x,QVector<double>&y,int&n, bool forceZero)
|
|
{
|
|
|
|
// 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
|
|
|
|
// do we aggregate ?
|
|
bool aggZero = metricDetail.metric ? metricDetail.metric->aggregateZero() : false;
|
|
|
|
n=-1;
|
|
int lastDay=0;
|
|
unsigned long secondsPerGroupBy=0;
|
|
bool wantZero = forceZero ? 1 : (metricDetail.curveStyle == QwtPlotCurve::Steps);
|
|
|
|
foreach (RideItem *ride, context->athlete->rideCache->rides()) {
|
|
|
|
// filter out unwanted stuff
|
|
if (!settings->specification.pass(ride)) continue;
|
|
|
|
// day we are on
|
|
int currentDay = groupForDate(ride->dateTime.date(), settings->groupBy);
|
|
|
|
// value for day
|
|
double value;
|
|
if (metricDetail.type == METRIC_META)
|
|
value = ride->getText(metricDetail.symbol, "0.0").toDouble();
|
|
else
|
|
value = ride->getForSymbol(metricDetail.symbol);
|
|
|
|
// check values are bounded to stop QWT going berserk
|
|
if (std::isnan(value) || std::isinf(value)) value = 0;
|
|
|
|
// set aggZero to false and value to zero if is temperature and -255
|
|
if (metricDetail.metric && metricDetail.metric->symbol() == "average_temp" && value == RideFile::NoTemp) {
|
|
value = 0;
|
|
aggZero = false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (value || wantZero) {
|
|
unsigned long seconds = ride->getForSymbol("workout_time");
|
|
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++;
|
|
}
|
|
|
|
// first time thru
|
|
if (n<0) n=0;
|
|
|
|
y[n] = value;
|
|
x[n] = currentDay - groupForDate(settings->start.date(), settings->groupBy);
|
|
|
|
// only increment counter if nonzero or we aggregate zeroes
|
|
if (value || aggZero) secondsPerGroupBy = seconds;
|
|
|
|
} 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;
|
|
|
|
// first time thru
|
|
if (n<0) n=0;
|
|
|
|
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
|
|
if (value || aggZero) 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::createBestsData(Context *, LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n, bool forceZero)
|
|
{
|
|
// 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
|
|
|
|
// do we aggregate ?
|
|
bool aggZero = metricDetail.metric ? metricDetail.metric->aggregateZero() : false;
|
|
|
|
n=-1;
|
|
int lastDay=0;
|
|
unsigned long secondsPerGroupBy=0;
|
|
bool wantZero = forceZero ? 1 : (metricDetail.curveStyle == QwtPlotCurve::Steps);
|
|
|
|
foreach (RideBest best, *(settings->bests)) {
|
|
|
|
// filter has already been applied
|
|
|
|
// day we are on
|
|
int currentDay = groupForDate(best.getRideDate().date(), settings->groupBy);
|
|
|
|
// value for day
|
|
double value;
|
|
value = best.getForSymbol(metricDetail.bestSymbol);
|
|
|
|
// check values are bounded to stop QWT going berserk
|
|
if (std::isnan(value) || std::isinf(value)) value = 0;
|
|
|
|
// set aggZero to false and value to zero if is temperature and -255
|
|
if (metricDetail.metric && metricDetail.metric->symbol() == "average_temp" && value == RideFile::NoTemp) {
|
|
value = 0;
|
|
aggZero = false;
|
|
}
|
|
|
|
if (value || wantZero) {
|
|
unsigned long 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);
|
|
|
|
// only increment counter if nonzero or we aggregate zeroes
|
|
if (value || aggZero) secondsPerGroupBy = seconds;
|
|
|
|
} 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;
|
|
|
|
// first time thru
|
|
//if (n<0) n++;
|
|
|
|
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
|
|
if (value || aggZero) 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::createEstimateData(Context *context, LTMSettings *settings, MetricDetail metricDetail,
|
|
QVector<double>&x,QVector<double>&y,int&n, bool)
|
|
{
|
|
// lets refresh the model data if we don't have any
|
|
if (context->athlete->PDEstimates.count() == 0) context->athlete->rideCache->refreshCPModelMetrics();
|
|
|
|
// resize the curve array to maximum possible size (even if we don't need it)
|
|
int maxdays = groupForDate(settings->end.date(), settings->groupBy)
|
|
- groupForDate(settings->start.date(), settings->groupBy);
|
|
|
|
n = 0;
|
|
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
|
|
|
|
// data vectors for averaging in case on Monthly/Yearly/Total grouping
|
|
QVector<double> xCount;
|
|
QVector<double> yTotal;
|
|
|
|
xCount.resize(maxdays+3);
|
|
yTotal.resize(maxdays+3);
|
|
|
|
// what is the first period
|
|
int firstPeriod = groupForDate(settings->start.date(), settings->groupBy);
|
|
|
|
// get first PDEstimate / fillup X/Y with missing time range
|
|
if (!context->athlete->PDEstimates.isEmpty()) {
|
|
PDEstimate firstEst = context->athlete->PDEstimates.first();
|
|
if ((settings->start.date() < firstEst.from) &&
|
|
(settings->end.date() > firstEst.from)){
|
|
int timeforward = groupForDate(firstEst.from, settings->groupBy)
|
|
- groupForDate(settings->start.date(), settings->groupBy);
|
|
for (int i = 0; i < timeforward; i++) {
|
|
x[n] = n;
|
|
y[n] = 0;
|
|
n++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// loop through all the estimate data
|
|
foreach(PDEstimate est, context->athlete->PDEstimates) {
|
|
|
|
// wpk skip for now
|
|
if (est.wpk != metricDetail.wpk) continue;
|
|
|
|
// skip entries for other models
|
|
if (est.model != metricDetail.model) continue;
|
|
|
|
// skip if no in our time period
|
|
if (est.to < settings->start.date() || est.from > settings->end.date()) continue;
|
|
|
|
// get dat for first and last
|
|
QDate from = est.from < settings->start.date() ? settings->start.date() : est.from;
|
|
QDate to = est.to > settings->end.date() ? settings->end.date() : est.to;
|
|
|
|
// what value to plot ?
|
|
double value=0;
|
|
|
|
switch(metricDetail.estimate) {
|
|
case ESTIMATE_WPRIME :
|
|
value = est.WPrime;
|
|
break;
|
|
|
|
case ESTIMATE_CP :
|
|
value = est.CP;
|
|
break;
|
|
|
|
case ESTIMATE_FTP :
|
|
value = est.FTP;
|
|
break;
|
|
|
|
case ESTIMATE_PMAX :
|
|
value = est.PMax;
|
|
break;
|
|
|
|
case ESTIMATE_BEST :
|
|
{
|
|
value = 0;
|
|
|
|
// we need to find the model
|
|
foreach(PDModel *model, models) {
|
|
|
|
// not the one we want
|
|
if (model->code() != metricDetail.model) continue;
|
|
|
|
// set the parameters previously derived
|
|
model->loadParameters(est.parameters);
|
|
|
|
// get the model estimate for our duration
|
|
value = model->y(metricDetail.estimateDuration * metricDetail.estimateDuration_units);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case ESTIMATE_EI :
|
|
value = est.EI;
|
|
break;
|
|
}
|
|
|
|
// PDE estimates are created in Weekly Buckets - so we need to aggregate and average or data fillup
|
|
// depending on the different groupings (day, week, month, year, all)
|
|
switch(settings->groupBy) {
|
|
|
|
case LTM_MONTH:
|
|
case LTM_YEAR:
|
|
case LTM_ALL:
|
|
|
|
// for month, year, all - aggregate the weekly values and build averages
|
|
if (n <= maxdays) {
|
|
int currentPeriod = groupForDate(from, settings->groupBy);
|
|
if (n != (currentPeriod - firstPeriod)) {
|
|
// data of next period of estimates is available,
|
|
// so calcuated the current period and switch forward to next
|
|
x[n] = n;
|
|
if (xCount[n]> 0) {
|
|
y[n] = yTotal[n] / xCount[n];
|
|
} else {
|
|
y[n] = 0;
|
|
}
|
|
n++;
|
|
};
|
|
// store for calcuation
|
|
yTotal[n] += value;
|
|
xCount[n]++;
|
|
}
|
|
break;
|
|
|
|
case LTM_DAY:
|
|
if (n <= maxdays) {
|
|
|
|
// for days - take estimate data from first day and fill the days to end of week
|
|
// since there is no more estimate data available until next week
|
|
x[n] = n;
|
|
y[n] = value;
|
|
n++;
|
|
int currentDay = groupForDate(from, settings->groupBy);
|
|
int nextDay = groupForDate(to, settings->groupBy);
|
|
while (n <= maxdays && nextDay > currentDay) { // i.e. not the same day
|
|
x[n] = n;
|
|
y[n] = value;
|
|
n++;
|
|
currentDay++;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case LTM_WEEK:
|
|
default:
|
|
|
|
// for weeks - just take the data available - no fill,...
|
|
if (n <= maxdays) {
|
|
|
|
x[n] = n;
|
|
y[n] = value;
|
|
n++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// just check if we had data at all
|
|
if (!context->athlete->PDEstimates.isEmpty()) {
|
|
// add the last average value to the output
|
|
switch(settings->groupBy) {
|
|
|
|
case LTM_MONTH:
|
|
case LTM_YEAR:
|
|
case LTM_ALL:
|
|
x[n] = n;
|
|
if (xCount[n]> 0) {
|
|
y[n] = yTotal[n] / xCount[n];
|
|
} else {
|
|
y[n] = 0;
|
|
}
|
|
n++;
|
|
}
|
|
}
|
|
// always seems to be one too many ...
|
|
if (n>0)n--;
|
|
}
|
|
|
|
void
|
|
LTMPlot::createPMCData(Context *context, LTMSettings *settings, MetricDetail metricDetail,
|
|
QVector<double>&x,QVector<double>&y,int&n, bool)
|
|
{
|
|
QString scoreType;
|
|
int stressType = STRESS_LTS;
|
|
|
|
// create a custom set of summary metric data!
|
|
if (metricDetail.type == METRIC_PM) {
|
|
|
|
if (metricDetail.symbol.startsWith("skiba")) {
|
|
scoreType = "skiba_bike_score";
|
|
} else if (metricDetail.symbol.startsWith("antiss")) {
|
|
scoreType = "antiss_score";
|
|
} else if (metricDetail.symbol.startsWith("atiss")) {
|
|
scoreType = "atiss_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("cp_")) {
|
|
scoreType = "skiba_cp_exp";
|
|
} else if (metricDetail.symbol.startsWith("wprime")) {
|
|
scoreType = "skiba_wprime_exp";
|
|
} else if (metricDetail.symbol.startsWith("distance")) {
|
|
scoreType = "total_distance";
|
|
} else if (metricDetail.symbol.startsWith("govss")) {
|
|
scoreType = "govss";
|
|
}
|
|
|
|
stressType = STRESS_LTS; // if in doubt
|
|
if (metricDetail.symbol.endsWith("lts") || metricDetail.symbol.endsWith("ctl"))
|
|
stressType = STRESS_LTS;
|
|
else if (metricDetail.symbol.endsWith("sts") || metricDetail.symbol.endsWith("atl"))
|
|
stressType = STRESS_STS;
|
|
else if (metricDetail.symbol.endsWith("sb"))
|
|
stressType = STRESS_SB;
|
|
else if (metricDetail.symbol.endsWith("lr"))
|
|
stressType = STRESS_RR;
|
|
|
|
} else {
|
|
|
|
scoreType = metricDetail.symbol; // just use the selected metric
|
|
stressType = metricDetail.stressType;
|
|
}
|
|
|
|
|
|
PMCData *athletePMC = NULL;
|
|
PMCData *localPMC = NULL;
|
|
|
|
// create local PMC if filtered
|
|
if (settings->specification.isFiltered()) {
|
|
|
|
// don't filter for date range!!
|
|
Specification allDates = settings->specification;
|
|
allDates.setDateRange(DateRange(QDate(),QDate()));
|
|
localPMC = new PMCData(context, allDates, scoreType);
|
|
}
|
|
|
|
// use global one if not filtered
|
|
if (!localPMC) athletePMC = context->athlete->getPMCFor(scoreType);
|
|
|
|
// point to the right one
|
|
PMCData *pmcData = localPMC ? localPMC : athletePMC;
|
|
|
|
int maxdays = groupForDate(settings->end.date(), settings->groupBy)
|
|
- groupForDate(settings->start.date(), settings->groupBy);
|
|
|
|
// skip for negative or empty time periods.
|
|
if (maxdays <=0) return;
|
|
|
|
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
|
|
|
|
// iterate over it and create curve...
|
|
n=-1;
|
|
int lastDay=0;
|
|
unsigned long secondsPerGroupBy=0;
|
|
bool wantZero = true;
|
|
|
|
for (QDate date=settings->start.date(); date <= settings->end.date(); date = date.addDays(1)) {
|
|
|
|
// day we are on
|
|
int currentDay = groupForDate(date, settings->groupBy);
|
|
|
|
// value for day
|
|
double value = 0.0f;
|
|
|
|
switch (stressType) {
|
|
case STRESS_LTS:
|
|
value = pmcData->lts(date);
|
|
break;
|
|
case STRESS_STS:
|
|
value = pmcData->sts(date);
|
|
break;
|
|
case STRESS_SB:
|
|
value = pmcData->sb(date);
|
|
break;
|
|
case STRESS_RR:
|
|
value = pmcData->rr(date);
|
|
break;
|
|
default:
|
|
value = 0;
|
|
break;
|
|
}
|
|
|
|
if (value || wantZero) {
|
|
unsigned long 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);
|
|
|
|
// only increment counter if nonzero or we aggregate zeroes
|
|
secondsPerGroupBy = seconds;
|
|
|
|
} else {
|
|
// sum totals, average averages and choose best for Peaks
|
|
int type = RideMetric::Average;
|
|
|
|
if (metricDetail.uunits == "Ramp" ||
|
|
metricDetail.uunits == tr("Ramp")) type = RideMetric::Total;
|
|
|
|
// first time thru
|
|
if (n<0) n++;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// wipe away local
|
|
if (localPMC) delete localPMC;
|
|
}
|
|
|
|
QwtAxisId
|
|
LTMPlot::chooseYAxis(QString units)
|
|
{
|
|
QwtAxisId chosen(-1,-1);
|
|
// return the YAxis to use
|
|
if ((chosen = axes.value(units, QwtAxisId(-1,-1))) != QwtAxisId(-1,-1)) return chosen;
|
|
else if (axes.count() < 8) {
|
|
chosen = supportedAxes[axes.count()];
|
|
if (units == "seconds" || units == tr("seconds")) setAxisTitle(chosen, tr("hours")); // we convert seconds to hours
|
|
else setAxisTitle(chosen, units);
|
|
enableAxis(chosen.id, true);
|
|
setAxisVisible(chosen, true);
|
|
axes.insert(units, chosen);
|
|
return chosen;
|
|
|
|
} else {
|
|
// eek!
|
|
return QwtAxis::yLeft; // just re-use the current yLeft axis
|
|
}
|
|
}
|
|
|
|
bool
|
|
LTMPlot::eventFilter(QObject *obj, QEvent *event)
|
|
{
|
|
|
|
// when clicking on a legend item, toggle if the curve is visible
|
|
if (obj == legend() && event->type() == QEvent::MouseButtonPress) {
|
|
|
|
bool replotNeeded = false;
|
|
QwtLegend *l = static_cast<QwtLegend *>(this->legend());
|
|
QPoint pos = QCursor::pos();
|
|
|
|
foreach(QwtPlotCurve *p, curves) {
|
|
foreach (QWidget *w, l->legendWidgets(itemToInfo(p))) {
|
|
if (QRect(l->mapToGlobal(w->geometry().topLeft()),
|
|
l->mapToGlobal(w->geometry().bottomRight())).contains(pos)) {
|
|
|
|
//qDebug()<<"under mouse="<<static_cast<QwtLegendLabel*>(w)->text().text();
|
|
for(int m=0; m< settings->metrics.count(); m++) {
|
|
if (settings->metrics[m].curve == p) {
|
|
settings->metrics[m].hidden = !settings->metrics[m].hidden;
|
|
replotNeeded = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (replotNeeded) setData(settings);
|
|
}
|
|
|
|
// is it for other objects ?
|
|
if (axesObject.contains(obj)) {
|
|
|
|
QwtAxisId id = axesId.at(axesObject.indexOf(obj));
|
|
|
|
// this is an axes widget
|
|
//qDebug()<<this<<"event on="<<id<< static_cast<QwtScaleWidget*>(obj)->title().text() <<"event="<<event->type();
|
|
|
|
// isolate / restore on mouse enter leave
|
|
if (!isolation && event->type() == QEvent::Enter) {
|
|
|
|
// isolate curve on hover
|
|
curveColors->isolateAxis(id);
|
|
replot();
|
|
|
|
} else if (!isolation && event->type() == QEvent::Leave) {
|
|
|
|
// return to normal when leave
|
|
curveColors->restoreState();
|
|
replot();
|
|
|
|
} else if (event->type() == QEvent::MouseButtonRelease) {
|
|
|
|
// click on any axis to toggle isolation
|
|
// if isolation is on, just turns it off
|
|
// if isolation is off, turns it on for the axis clicked
|
|
if (isolation) {
|
|
isolation = false;
|
|
curveColors->restoreState();
|
|
replot();
|
|
} else {
|
|
isolation = true;
|
|
curveColors->isolateAxis(id, true); // with scale adjust
|
|
replot();
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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();
|
|
case LTM_ALL: return 1;
|
|
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
if (!parent->isCompare()) {
|
|
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_ALL)
|
|
datestr = QString(tr("All"));
|
|
else 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;
|
|
if (!parent->isCompare()) {
|
|
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());
|
|
} else {
|
|
text = QString("%1\n%2 %3")
|
|
.arg(curve->title().text())
|
|
.arg(value, 0, 'f', precision)
|
|
.arg(this->axisTitle(curve->yAxis()).text());
|
|
}
|
|
|
|
// set that text up
|
|
picker->setText(text);
|
|
} else {
|
|
// no point
|
|
picker->setText("");
|
|
}
|
|
}
|
|
|
|
void
|
|
LTMPlot::pointClicked(QwtPlotCurve *curve, int index)
|
|
{
|
|
// do nothing on a compare chart
|
|
if (parent->isCompare()) return;
|
|
|
|
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, QwtAxisId 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, QwtAxisId 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(LTMSettings *settings, QDate from, QDate to, int groupby, QColor color)
|
|
{
|
|
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(color, 0, Qt::DashLine));
|
|
mrk->setValue(double(groupForDate(s.getStart(), groupby)) - baseday, 0.0);
|
|
|
|
if (first) {
|
|
QwtText text(s.getName());
|
|
text.setFont(QFont("Helvetica", 10, QFont::Bold));
|
|
text.setColor(color);
|
|
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(color, 0, Qt::SolidLine));
|
|
mrk->setValue(double(groupForDate(event.date, groupby)) - baseday, 10.0);
|
|
|
|
if (first) {
|
|
QwtText text(event.name);
|
|
text.setFont(QFont("Helvetica", 10, QFont::Bold));
|
|
text.setColor(Qt::red);
|
|
mrk->setLabel(text);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
void
|
|
LTMPlot::refreshZoneLabels(QwtAxisId axisid)
|
|
{
|
|
foreach(LTMPlotZoneLabel *label, zoneLabels) {
|
|
label->detach();
|
|
delete label;
|
|
}
|
|
zoneLabels.clear();
|
|
|
|
if (bg) {
|
|
bg->detach();
|
|
delete bg;
|
|
bg = NULL;
|
|
}
|
|
if (axisid == QwtAxisId(-1,-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);
|
|
}
|