/* * 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 "MetricAggregator.h" #include "SummaryMetrics.h" #include "RideMetric.h" #include "RideFileCache.h" #include "Settings.h" #include "Colors.h" #include "StressCalculator.h" // for LTS/STS calculation #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) { // don't do this .. setAutoReplot(false); setAutoFillBackground(true); // 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); settings = NULL; cogganPMC = skibaPMC = NULL; // cache when replotting a PMC configUpdate(); // set basic colors connect(context, SIGNAL(configChanged()), this, SLOT(configUpdate())); // 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::configUpdate() { // set basic plot colors setCanvasBackground(GColor(CPLOTBACKGROUND)); QPen gridPen(GColor(CPLOTGRID)); //gridPen.setStyle(Qt::DotLine); grid->setPen(gridPen); QPalette palette; palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND))); palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER)); palette.setColor(QPalette::Text, GColor(CPLOTMARKER)); setPalette(palette); foreach (QwtAxisId x, supportedAxes) { axisWidget(x)->setPalette(palette); axisWidget(x)->setPalette(palette); } axisWidget(QwtPlot::xBottom)->setPalette(palette); QwtLegend *l = static_cast(this->legend()); l->setPalette(palette); foreach(QwtPlotCurve *p, curves) { foreach (QWidget *w, l->legendWidgets(itemToInfo(p))) { w->setPalette(palette); } } updateLegend(); } 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::resetPMC() { if (cogganPMC) { delete cogganPMC; cogganPMC=NULL; } if (skibaPMC) { delete skibaPMC; skibaPMC=NULL; } } void LTMPlot::setData(LTMSettings *set) { QTime timer; timer.start(); //qDebug()<<"Starting.."<data != NULL && (*settings->data).count() != 0) { // if dates are null we need to set them from the available data // end if (settings->end == QDateTime() || settings->end > (*settings->data).last().getRideDate().addDays(365)) { if (settings->end < QDateTime::currentDateTime()) { settings->end = QDateTime::currentDateTime(); } else { settings->end = (*settings->data).last().getRideDate(); } } // start if (settings->start == QDateTime() || settings->start < (*settings->data).first().getRideDate().addDays(-365)) { settings->start = (*settings->data).first().getRideDate(); } } //setTitle(settings->title); if (settings->groupBy != LTM_TOD) setAxisTitle(xBottom, tr("Date")); else setAxisTitle(xBottom, tr("Time of Day")); 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(); // reset all min/max Y values for (int i=0; i<10; i++) minY[i]=0, maxY[i]=0; // no data to display so that all folks if (settings->data == NULL || (*settings->data).count() == 0) { // tidy up the bottom axis maxX = groupForDate(settings->end.date(), settings->groupBy) - groupForDate(settings->start.date(), settings->groupBy); setAxisScale(xBottom, 0, maxX); setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start, groupForDate(settings->start.date(), settings->groupBy), settings->groupBy)); enableAxis(QwtAxis::xBottom, true); setAxisVisible(QwtAxis::xBottom, true); setAxisVisible(QwtAxis::xTop, false); // remove the shading if it exists refreshZoneLabels(-1); // 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, 2.0).toDouble(); bool donestack = false; // now we iterate over the metric details AGAIN // but this time in reverse and only plot the // stacked values. This is because we overcome the // lack of a stacked plot in QWT by painting decreasing // bars, with the values aggregated previously // so if we plot L1 time in zone 1hr and L2 time in zone 1hr // it plots as L2 time in zone 2hr and then paints over that // with a L1 time in zone of 1hr. // // The tooltip has to unpick the aggregation to ensure // that it subtracts other data series in the stack from // the value plotted... all nasty but heck, it works int stackcounter = stackX.size()-1; for (int m=settings->metrics.count()-1; m>=0; m--) { // // *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); if (metricDetail.type == METRIC_BEST) curves.insert(metricDetail.bestSymbol, current); else curves.insert(metricDetail.symbol, current); stacks.insert(current, stackcounter+1); if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true) current->setRenderHint(QwtPlotItem::RenderAntialiased); QPen cpen = QPen(metricDetail.penColor); cpen.setWidth(width); current->setPen(cpen); current->setStyle(metricDetail.curveStyle); // 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(64); // now side by side, less transparency required QColor brushColor1 = brushColor.darker(); QLinearGradient linearGradient(0, 0, 0, height()); linearGradient.setColorAt(0.0, brushColor1); linearGradient.setColorAt(1.0, brushColor); linearGradient.setSpread(QGradient::PadSpread); current->setBrush(linearGradient); } current->setPen(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; if (settings->groupBy != LTM_TOD) createCurveData(context, settings, metricDetail, xdata, ydata, count); else createTODCurveData(context, settings, metricDetail, xdata, ydata, count); //qDebug()<<"Create curve data.."<value(this, GC_ANTIALIAS, false).toBool() == true) current->setRenderHint(QwtPlotItem::RenderAntialiased); QPen cpen = QPen(metricDetail.penColor); cpen.setWidth(width); current->setPen(cpen); current->setStyle(metricDetail.curveStyle); // 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.bestSymbol : metricDetail.symbol); QwtPlotCurve *trend = new QwtPlotCurve(trendName); // 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, false).toBool()==true) trend->setRenderHint(QwtPlotItem::RenderAntialiased); trend->setBaseline(0); trend->setYAxis(axisid); trend->setStyle(QwtPlotCurve::Lines); // perform linear regression LTMTrend regress(xdata.data(), ydata.data(), count); double xtrend[2], ytrend[2]; xtrend[0] = 0.0; ytrend[0] = regress.getYforX(0.0); // point 2 is at far right of chart, not the last point // since we may be forecasting... xtrend[1] = maxX; ytrend[1] = regress.getYforX(maxX); trend->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.bestSymbol : metricDetail.symbol); QwtPlotCurve *trend = new QwtPlotCurve(trendName); // 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, false).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); 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.bestSymbol : metricDetail.symbol); QwtPlotCurve *out = new QwtPlotCurve(outName); curves.insert(outSymbol, out); out->setRenderHint(QwtPlotItem::RenderAntialiased); out->setStyle(QwtPlotCurve::Dots); // we might have hidden the symbols for this curve // if its set to none then default to a rectangle 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 top N values if (metricDetail.topN > 0) { QMap sortedList; // copy the yvalues, retaining the offset for(int i=0; i hxdata, hydata; hxdata.resize(metricDetail.topN); hydata.resize(metricDetail.topN); // QMap orders the list so start at the top and work // backwards 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++; } } // lets setup a curve with this data then! QString topName; if (counter > 1) topName = QString(tr("%1 Best %2")) .arg(metricDetail.uname) .arg(counter); // starts from zero else topName = QString(tr("Best %1")).arg(metricDetail.uname); QString topSymbol = QString("%1_topN") .arg(metricDetail.type == METRIC_BEST ? metricDetail.bestSymbol : metricDetail.symbol); QwtPlotCurve *top = new QwtPlotCurve(topName); curves.insert(topSymbol, top); top->setRenderHint(QwtPlotItem::RenderAntialiased); top->setStyle(QwtPlotCurve::Dots); // we might have hidden the symbols for this curve // if its set to none then default to a rectangle 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") precision=1; if (metricDetail.uunits == "km") precision=0; // we have a metric so lets be precise ... labelString = QString("%1").arg(value * (context->athlete->useMetricUnits ? 1 : m->conversion()) + (context->athlete->useMetricUnits ? 0 : m->conversionSum()), 0, 'f', precision); } else { // no precision labelString = (QString("%1").arg(value, 0, 'f', 0)); } // 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->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(64); // now side by side, less transparency required QColor brushColor1 = metricDetail.penColor.darker(); QLinearGradient linearGradient(0, 0, 0, height()); linearGradient.setColorAt(0.0, brushColor1); linearGradient.setColorAt(1.0, brushColor); linearGradient.setSpread(QGradient::PadSpread); current->setBrush(linearGradient); current->setPen(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(60); 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") precision=1; if (metricDetail.uunits == "km") precision=0; // we have a metric so lets be precise ... labelString = QString("%1").arg(value * (context->athlete->useMetricUnits ? 1 : m->conversion()) + (context->athlete->useMetricUnits ? 0 : m->conversionSum()), 0, 'f', precision); } else { // no precision labelString = (QString("%1").arg(value, 0, 'f', 0)); } // Qwt uses its own text objects QwtText text(labelString); text.setFont(labelFont); text.setColor(metricDetail.penColor); // 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); } //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 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 } // show legend? if (settings->legend == false) { this->legend()->hide(); QHashIterator c(curves); while (c.hasNext()) { c.next(); c.value()->setItemAttribute(QwtPlotItem::Legend, false); } 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(); // 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; // wipe away last cached stress calculator -- it gets redone for each curve // so pretty slow sadly if (cogganPMC) { delete cogganPMC; cogganPMC=NULL; } if (skibaPMC) { delete skibaPMC; skibaPMC=NULL; } settings = set; 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); // set the settings data source to the compare date range // QList metrics, measures; settings->data = &cd.metrics; settings->measures = &cd.measures; // we need to do this for each date range as they are dependant // on the metrics chosen and can't be pre-cached QList herebests; herebests = RideFileCache::getAllBestsFor(cd.sourceContext, settings->metrics, settings->start, settings->end); settings->bests = &herebests; // no data to display so that all folks if (settings->data == NULL || (*settings->data).count() == 0) continue; // crop dates to at least within a year of the data available, but only if we have some data if (settings->data != NULL && (*settings->data).count() != 0) { // end if (settings->end == QDateTime() || settings->end > (*settings->data).last().getRideDate().addDays(365)) { if (settings->end < QDateTime::currentDateTime()) { settings->end = QDateTime::currentDateTime(); } else { settings->end = (*settings->data).last().getRideDate(); } } // start if (settings->start == QDateTime() || settings->start < (*settings->data).first().getRideDate().addDays(-365)) { settings->start = (*settings->data).first().getRideDate(); } } 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; 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, 2.0).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, false).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(64); // now side by side, less transparency required QColor brushColor1 = brushColor.darker(); QLinearGradient linearGradient(0, 0, 0, height()); linearGradient.setColorAt(0.0, brushColor1); linearGradient.setColorAt(1.0, brushColor); linearGradient.setSpread(QGradient::PadSpread); current->setBrush(linearGradient); } current->setPen(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; 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, false).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.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, false).toBool()==true) trend->setRenderHint(QwtPlotItem::RenderAntialiased); trend->setBaseline(0); trend->setYAxis(axisid); trend->setStyle(QwtPlotCurve::Lines); // perform linear regression LTMTrend regress(xdata.data(), ydata.data(), count); double xtrend[2], ytrend[2]; xtrend[0] = 0.0; ytrend[0] = regress.getYforX(0.0); // point 2 is at far right of chart, not the last point // since we may be forecasting... xtrend[1] = maxX; ytrend[1] = regress.getYforX(maxX); trend->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 %2 trend")).arg(cd.name).arg(metricDetail.uname); QString trendSymbol = QString("%1_trend") .arg(metricDetail.type == METRIC_BEST ? 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, false).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); 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(trendName, 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; 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.topN > 0) { QMap sortedList; // copy the yvalues, retaining the offset for(int i=0; i hxdata, hydata; hxdata.resize(metricDetail.topN); hydata.resize(metricDetail.topN); // QMap orders the list so start at the top and work // backwards 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++; } } // 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.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") precision=1; if (metricDetail.uunits == "km") precision=0; // we have a metric so lets be precise ... labelString = QString("%1").arg(value * (context->athlete->useMetricUnits ? 1 : m->conversion()) + (context->athlete->useMetricUnits ? 0 : m->conversionSum()), 0, 'f', precision); } else { // no precision labelString = (QString("%1").arg(value, 0, 'f', 0)); } // 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(64); // 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(60); 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") precision=1; if (metricDetail.uunits == "km") precision=0; // we have a metric so lets be precise ... labelString = QString("%1").arg(value * (context->athlete->useMetricUnits ? 1 : m->conversion()) + (context->athlete->useMetricUnits ? 0 : m->conversionSum()), 0, 'f', precision); } else { // no precision labelString = (QString("%1").arg(value, 0, 'f', 0)); } // 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.value()->setItemAttribute(QwtPlotItem::Legend, false); else p.value()->setItemAttribute(QwtPlotItem::Legend, settings->legend); } // now refresh updateLegend(); // plot replot(); //qDebug()<<"Replot and done.."<&x,QVector&y,int&n) { y.clear(); x.clear(); x.resize((24+3)); y.resize((24+3)); n = (24); for (int i=0; i<(24); i++) x[i]=i; foreach (SummaryMetrics rideMetrics, *(settings->data)) { // filter out unwanted rides if (context->isfiltered && !context->filters.contains(rideMetrics.getFileName())) continue; double value = rideMetrics.getForSymbol(metricDetail.symbol); // check values are bounded to stop QWT going berserk if (isnan(value) || isinf(value)) value = 0; // Special computed metrics (LTS/STS) have a null metric pointer if (metricDetail.metric) { // convert from stored metric value to imperial if (context->athlete->useMetricUnits == false) { value *= metricDetail.metric->conversion(); value += metricDetail.metric->conversionSum(); } // convert seconds to hours if (metricDetail.metric->units(true) == "seconds" || metricDetail.metric->units(true) == tr("seconds")) value /= 3600; } int array = rideMetrics.getRideDate().time().hour(); int type = metricDetail.metric ? metricDetail.metric->type() : RideMetric::Average; if (metricDetail.uunits == "Ramp" || metricDetail.uunits == tr("Ramp")) type = RideMetric::Total; switch (type) { case RideMetric::Total: y[array] += value; break; case RideMetric::Average: { // average should be calculated taking into account // the duration of the ride, otherwise high value but // short rides will skew the overall average y[array] = value; //XXX average is broken break; } case RideMetric::Low: if (value < y[array]) y[array] = value; break; case RideMetric::Peak: if (value > y[array]) y[array] = value; break; } } } void LTMPlot::createCurveData(Context *context, LTMSettings *settings, MetricDetail metricDetail, QVector&x,QVector&y,int&n) { QList *data = NULL; // resize the curve array to maximum possible size int maxdays = groupForDate(settings->end.date(), settings->groupBy) - groupForDate(settings->start.date(), settings->groupBy); x.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail y.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail // Get metric data, either from metricDB for RideFile metrics // or from StressCalculator for PM type metrics QList PMCdata; if (metricDetail.type == METRIC_DB || metricDetail.type == METRIC_META) { data = settings->data; } else if (metricDetail.type == METRIC_MEASURE) { data = settings->measures; } else if (metricDetail.type == METRIC_PM) { createPMCCurveData(context, settings, metricDetail, PMCdata); data = &PMCdata; } else if (metricDetail.type == METRIC_BEST) { data = settings->bests; } n=-1; int lastDay=0; unsigned long secondsPerGroupBy=0; bool wantZero = metricDetail.curveStyle == QwtPlotCurve::Steps; foreach (SummaryMetrics rideMetrics, *data) { // filter out unwanted rides but not for PMC type metrics // because that needs to be done in the stress calculator if (metricDetail.type != METRIC_PM && context->isfiltered && !context->filters.contains(rideMetrics.getFileName())) continue; // day we are on int currentDay = groupForDate(rideMetrics.getRideDate().date(), settings->groupBy); // value for day -- measures are stored differently double value; if (metricDetail.type == METRIC_MEASURE) value = rideMetrics.getText(metricDetail.symbol, "0.0").toDouble(); else if (metricDetail.type == METRIC_BEST) value = rideMetrics.getForSymbol(metricDetail.bestSymbol); else value = rideMetrics.getForSymbol(metricDetail.symbol); // check values are bounded to stop QWT going berserk if (isnan(value) || isinf(value)) value = 0; // Special computed metrics (LTS/STS) have a null metric pointer if (metricDetail.type != METRIC_BEST && metricDetail.metric) { // convert from stored metric value to imperial if (context->athlete->useMetricUnits == false) { value *= metricDetail.metric->conversion(); value += metricDetail.metric->conversionSum(); } // convert seconds to hours if (metricDetail.metric->units(true) == "seconds" || metricDetail.metric->units(true) == tr("seconds")) value /= 3600; } if (value || wantZero) { unsigned long seconds = rideMetrics.getForSymbol("workout_time"); if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_MEASURE) seconds = 1; if (currentDay > lastDay) { if (lastDay && wantZero) { while (lastDaystart.date(), settings->groupBy); y[n]=0; } } else { n++; } y[n] = value; x[n] = currentDay - groupForDate(settings->start.date(), settings->groupBy); secondsPerGroupBy = seconds; // reset for new group } else { // sum totals, average averages and choose best for Peaks int type = metricDetail.metric ? metricDetail.metric->type() : RideMetric::Average; if (metricDetail.uunits == "Ramp" || metricDetail.uunits == tr("Ramp")) type = RideMetric::Total; if (metricDetail.type == METRIC_BEST) type = RideMetric::Peak; // 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; } } } void LTMPlot::createPMCCurveData(Context *context, LTMSettings *settings, MetricDetail metricDetail, QList &customData) { QDate earliest, latest; // rides QString scoreType; // create a custom set of summary metric data! if (metricDetail.symbol.startsWith("skiba")) { scoreType = "skiba_bike_score"; } else if (metricDetail.symbol.startsWith("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"; } // create the Stress Calculation List // FOR ALL RIDE FILES StressCalculator *sc ; if (scoreType == "coggan_tss" && cogganPMC) { sc = cogganPMC; } else if (scoreType == "skiba_bike_score" && skibaPMC) { sc = skibaPMC; } else { sc = new StressCalculator( context->athlete->cyclist, settings->start, settings->end, (appsettings->value(this, GC_STS_DAYS,7)).toInt(), (appsettings->value(this, GC_LTS_DAYS,42)).toInt()); sc->calculateStress(context, context->athlete->home.absolutePath(), scoreType, settings->ltmTool->isFiltered(), settings->ltmTool->filters()); } // pick out any data that is in the date range selected // convert to SummaryMetric Format used on the plot for (int i=0; i< sc->n(); i++) { SummaryMetrics add = SummaryMetrics(); add.setRideDate(settings->start.addDays(i)); if (scoreType == "skiba_bike_score") { add.setForSymbol("skiba_lts", sc->getLTSvalues()[i]); add.setForSymbol("skiba_sts", sc->getSTSvalues()[i]); add.setForSymbol("skiba_sb", sc->getSBvalues()[i]); add.setForSymbol("skiba_sr", sc->getSRvalues()[i]); add.setForSymbol("skiba_lr", sc->getLRvalues()[i]); } else if (scoreType == "atiss_score") { add.setForSymbol("atiss_lts", sc->getLTSvalues()[i]); add.setForSymbol("atiss_sts", sc->getSTSvalues()[i]); add.setForSymbol("atiss_sb", sc->getSBvalues()[i]); add.setForSymbol("atiss_sr", sc->getSRvalues()[i]); add.setForSymbol("atiss_lr", sc->getLRvalues()[i]); } else if (scoreType == "coggan_tss") { add.setForSymbol("coggan_ctl", sc->getLTSvalues()[i]); add.setForSymbol("coggan_atl", sc->getSTSvalues()[i]); add.setForSymbol("coggan_tsb", sc->getSBvalues()[i]); add.setForSymbol("coggan_sr", sc->getSRvalues()[i]); add.setForSymbol("coggan_lr", sc->getLRvalues()[i]); } else if (scoreType == "daniels_points") { add.setForSymbol("daniels_lts", sc->getLTSvalues()[i]); add.setForSymbol("daniels_sts", sc->getSTSvalues()[i]); add.setForSymbol("daniels_sb", sc->getSBvalues()[i]); add.setForSymbol("daniels_sr", sc->getSRvalues()[i]); add.setForSymbol("daniels_lr", sc->getLRvalues()[i]); } else if (scoreType == "trimp_points") { add.setForSymbol("trimp_lts", sc->getLTSvalues()[i]); add.setForSymbol("trimp_sts", sc->getSTSvalues()[i]); add.setForSymbol("trimp_sb", sc->getSBvalues()[i]); add.setForSymbol("trimp_sr", sc->getSRvalues()[i]); add.setForSymbol("trimp_lr", sc->getLRvalues()[i]); } else if (scoreType == "skiba_cp_exp") { add.setForSymbol("cp_lts", sc->getLTSvalues()[i]); add.setForSymbol("cp_sts", sc->getSTSvalues()[i]); add.setForSymbol("cp_sb", sc->getSBvalues()[i]); add.setForSymbol("cp_sr", sc->getSRvalues()[i]); add.setForSymbol("cp_lr", sc->getLRvalues()[i]); } else if (scoreType == "skiba_wprime_exp") { add.setForSymbol("wprime_lts", sc->getLTSvalues()[i]); add.setForSymbol("wprime_sts", sc->getSTSvalues()[i]); add.setForSymbol("wprime_sb", sc->getSBvalues()[i]); add.setForSymbol("wprime_sr", sc->getSRvalues()[i]); add.setForSymbol("wprime_lr", sc->getLRvalues()[i]); } else if (scoreType == "total_work") { add.setForSymbol("work_lts", sc->getLTSvalues()[i]); add.setForSymbol("work_sts", sc->getSTSvalues()[i]); add.setForSymbol("work_sb", sc->getSBvalues()[i]); add.setForSymbol("work_sr", sc->getSRvalues()[i]); add.setForSymbol("work_lr", sc->getLRvalues()[i]); } else if (scoreType == "total_distance") { add.setForSymbol("distance_lts", sc->getLTSvalues()[i]); add.setForSymbol("distance_sts", sc->getSTSvalues()[i]); add.setForSymbol("distance_sb", sc->getSBvalues()[i]); add.setForSymbol("distance_sr", sc->getSRvalues()[i]); add.setForSymbol("distance_lr", sc->getLRvalues()[i]); } add.setForSymbol("workout_time", 1.0); // averaging is per day customData << add; } if (scoreType == "coggan_tss") { cogganPMC = sc; } else if (scoreType == "skiba_bike_score") { skibaPMC = sc; } else { delete sc; } } 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 } } int LTMPlot::groupForDate(QDate date, int groupby) { switch(groupby) { case LTM_WEEK: { // must start from 1 not zero! return 1 + ((date.toJulianDay() - settings->start.date().toJulianDay()) / 7); } case LTM_MONTH: return (date.year()*12) + date.month(); case LTM_YEAR: return date.year(); case LTM_DAY: default: return date.toJulianDay(); } } void LTMPlot::pointHover(QwtPlotCurve *curve, int index) { if (index >= 0 && curve != highlighter) { int stacknum = stacks.value(curve, -1); const RideMetricFactory &factory = RideMetricFactory::instance(); double value; QString units; int precision = 0; QString datestr; 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_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 nothin 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::DashDotLine)); 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::DashDotLine)); 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(color); 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); }