/* * 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 #include #include #include #include #include #include #include #include #include #include // 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(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(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.."<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 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.."<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*p, stackX) delete p; foreach(QVector*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 *xdata = new QVector(); QVector *ydata = new QVector(); 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.."<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 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 xaxis (xdata.size() * 4); QVector yaxis (ydata.size() * 4); // samples to time for (int i=0, offset=0; isetSamples(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.."<metrics.count(); m++) { MetricDetail metricDetail = settings->metrics[m]; // // *ONLY* PLOT NON-STACKS // if (metricDetail.stack == true) continue; QVector 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.."<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 xtrend; QVector 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 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 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 sortedList; // copy the yvalues, retaining the offset for(int i=0; i 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 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 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; iprecision(); 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 xaxis (xdata.size() * 4); QVector yaxis (ydata.size() * 4); // samples to time for (int i=0, offset=0; isetSize(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; iprecision(); 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.."<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.."<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 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.."< 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 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.."<*p, stackX) delete p; foreach(QVector*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 *xdata = new QVector(); QVector *ydata = new QVector(); 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.."<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 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 xaxis (xdata.size() * 4); QVector yaxis (ydata.size() * 4); // samples to time for (int i=0, offset=0; isetSamples(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.."<metrics) { // // *ONLY* PLOT NON-STACKS // if (metricDetail.stack == true) continue; QVector 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.."<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 xtrend; QVector 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 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; isetRenderHint(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 sortedList; for(int i=0; i 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 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 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; iprecision(); 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 xaxis (xdata.size() * 4); QVector yaxis (ydata.size() * 4); // samples to time for (int i=0, offset=0; isetSize(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; iprecision(); 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.."<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.."<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 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.."<&x,QVector&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&x,QVector&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&x,QVector&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 (lastDaystart.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&x,QVector&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 (lastDaystart.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&x,QVector&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 xCount; QVector 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&x,QVector&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 (lastDaystart.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(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="<(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()<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 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 &a, QVector&w) { if (a.size() != w.size()) return; // ignore silently // add them in! for(int i=0; iparent->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 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 zone_lows = zones->getZoneLows(zone_range); QList 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); }