Files
GoldenCheetah/src/ScatterPlot.cpp
Mark Liversedge f109d10c78 Fix Scatterplot SEGV interval
.. lots of issues introduced by refactor
2015-05-10 14:32:53 +01:00

984 lines
34 KiB
C++

/*
* Copyright (c) 2009 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 "ScatterPlot.h"
#include "ScatterWindow.h"
#include "Statistic.h"
#include "IntervalItem.h"
#include "Context.h"
#include "Context.h"
#include "Athlete.h"
#include "Settings.h"
#include "Zones.h"
#include "Colors.h"
#include "RideFile.h"
#include "Units.h" // for MILES_PER_KM
#include <QWidget>
#include <qwt_series_data.h>
#include <qwt_legend.h>
#include <qwt_scale_widget.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_marker.h>
#include <qwt_plot_canvas.h>
#include <qwt_scale_draw.h>
#include <qwt_symbol.h>
#define PI M_PI
// side 0=left, 1=right, usually 0
static double
pointType(const RideFilePoint *point, int type, int side, bool metric, double cranklength)
{
// return the point value for the given type
switch(type) {
case MODEL_POWER : return point->watts;
case MODEL_CADENCE : return point->cad;
case MODEL_HEARTRATE : return point->hr;
case MODEL_SPEED :
if (metric == true){
return point->kph;
}else {
return point->kph * MILES_PER_KM;
}
case MODEL_ALT :
if (metric == true){
return point->alt;
}else {
return point->alt * FEET_PER_METER;
}
case MODEL_TORQUE : return point->nm;
case MODEL_TIME : return point->secs;
case MODEL_DISTANCE :
if (metric == true){
return point->km;
}else {
return point->km * MILES_PER_KM;
}
case MODEL_INTERVAL : return point->interval;
case MODEL_LAT : return point->lat;
case MODEL_LONG : return point->lon;
case MODEL_AEPF :
if (point->watts == 0 || point->cad == 0) return 0;
else return ((point->watts * 60.0) / (point->cad * cranklength * 2.0 * PI));
case MODEL_CPV :
return ((point->cad * cranklength * 2.0 * PI) / 60.0);
// these you need to do yourself cause there is some
// logic needed and I'm just lookup table!
case MODEL_XYTIME : return 1;
case MODEL_POWERZONE : return point->watts;
case MODEL_HEADWIND : return point->headwind;
case MODEL_SLOPE : return point->slope;
case MODEL_TEMP : return point->temp;
case MODEL_LRBALANCE : return !side ? point->lrbalance : (100 - point->lrbalance);
case MODEL_TE : return side ? point->rte : point->lte;
case MODEL_PS : return side ? point->rps : point->lps;
case MODEL_RV : return point->rvert;
case MODEL_RGCT : return point->rcontact;
case MODEL_RCAD : return point->rcad;
case MODEL_GEAR : return point->gear;
case MODEL_SMO2 : return point->smo2;
case MODEL_THB : return point->thb;
case MODEL_HHB : return point->hhb;
case MODEL_O2HB : return point->o2hb;
}
return 0; // ? unknown channel ?
}
QString ScatterPlot::describeType(int type, bool longer, bool metric)
{
// return the point value for the given type
if (longer == true) {
switch(type) {
case MODEL_POWER : return (tr("Power (watts)"));
case MODEL_CADENCE : return (tr("Cadence (rpm)"));
case MODEL_HEARTRATE : return (tr("Heartrate (bpm)"));
case MODEL_SPEED :
if (metric == true){
return (tr("Speed (kph)"));
}else {
return (tr("Speed (mph)"));
}
case MODEL_ALT :
if (metric == true){
return (tr("Altitude (meters)"));
}else {
return (tr("Altitude (feet)"));
}
case MODEL_TORQUE : return (tr("Torque (N)"));
case MODEL_TIME : return (tr("Elapsed Time (secs)"));
case MODEL_DISTANCE :
if (metric == true){
return (tr("Elapsed Distance (km)"));
}else {
return (tr("Elapsed Distance (mi)"));
}
case MODEL_INTERVAL : return (tr("Interval Number"));
case MODEL_LAT : return (tr("Latitude (degree offset)"));
case MODEL_LONG : return (tr("Longitude (degree offset)"));
case MODEL_CPV : return (tr("Circumferential Pedal Velocity (cm/s)"));
case MODEL_AEPF : return (tr("Average Effective Pedal Force (N)"));
// these you need to do yourself cause there is some
// logic needed and I'm just lookup table!
case MODEL_XYTIME : return (tr("Time at X/Y (%)"));
case MODEL_POWERZONE : return (tr("Power Zone"));
case MODEL_HEADWIND : return (tr("Headwind (kph)"));
case MODEL_SLOPE : return (tr("Slope (gradient)"));
case MODEL_TEMP : return (tr("Temperature (C)"));
case MODEL_LRBALANCE : return (tr("L/R Balance"));
case MODEL_TE : return (tr("Torque Efficiency"));
case MODEL_PS : return (tr("Pedal Smoothness"));
case MODEL_RV : return (tr("Running Vertical Oscillation"));
case MODEL_RGCT : return (tr("Running Ground Contact Time"));
case MODEL_RCAD : return (tr("Running Cadence"));
case MODEL_GEAR : return (tr("Gear Ratio"));
case MODEL_SMO2 : return (tr("Muscle Oxygen"));
case MODEL_THB : return (tr("Haemoglobin Mass"));
case MODEL_HHB : return (tr("Deoxygenated Haemoglobin"));
case MODEL_O2HB : return (tr("Oxygenated Haemoglobin"));
}
return (tr("Unknown"));; // ? unknown channel ?
} else {
switch(type) {
case MODEL_POWER : return (tr("Power"));
case MODEL_CADENCE : return (tr("Cadence"));
case MODEL_HEARTRATE : return (tr("Heartrate"));
case MODEL_SPEED : return (tr("Speed"));
case MODEL_ALT : return (tr("Altitude"));
case MODEL_TORQUE : return (tr("Pedal Force"));
case MODEL_TIME : return (tr("Time"));
case MODEL_DISTANCE : return (tr("Distance"));
case MODEL_INTERVAL : return (tr("Interval"));
case MODEL_LAT : return (tr("Latitude"));
case MODEL_LONG : return (tr("Longitude"));
case MODEL_XYTIME : return (tr("Time at X/Y"));
case MODEL_POWERZONE : return (tr("Zone"));
case MODEL_CPV : return (tr("CPV"));
case MODEL_AEPF : return (tr("AEPF"));
case MODEL_HEADWIND : return (tr("Headwind"));
case MODEL_SLOPE : return (tr("Slope"));
case MODEL_TEMP : return (tr("Temp"));
case MODEL_LRBALANCE : return (tr("Balance"));
case MODEL_TE : return (tr("TE"));
case MODEL_PS : return (tr("PS"));
case MODEL_RV : return (tr("RV"));
case MODEL_RGCT : return (tr("GCT"));
case MODEL_RCAD : return (tr("Run Cad"));
case MODEL_GEAR : return (tr("Gear"));
case MODEL_SMO2 : return (tr("SmO2"));
case MODEL_THB : return (tr("tHb"));
case MODEL_O2HB : return (tr("O2Hb"));
case MODEL_HHB : return (tr("HHb"));
}
return (tr("None")); // ? unknown channel ?
}
}
ScatterPlot::ScatterPlot(Context *context) : context(context)
{
setAutoDelete(false); // no don't delete on detach !
curve = NULL;
curve2 = NULL;
hover = NULL;
hover2 = NULL;
grid = NULL;
ride = NULL;
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
setAxisMaxMinor(xBottom, 0);
setAxisMaxMinor(yLeft, 0);
QwtScaleDraw *sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
setAxisScaleDraw(QwtPlot::xBottom, sd);
sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
sd->enableComponent(QwtScaleDraw::Ticks, false);
sd->enableComponent(QwtScaleDraw::Backbone, false);
setAxisScaleDraw(QwtPlot::yLeft, sd);
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
connect(context, SIGNAL(intervalHover(IntervalItem*)), this, SLOT(intervalHover(IntervalItem*)));
// lets watch the mouse move...
new mouseTracker(this);
configChanged(CONFIG_APPEARANCE | CONFIG_GENERAL); // use latest wheelsize/cranklength and colors
}
void ScatterPlot::setData (ScatterSettings *settings)
{
// if there are no settings or incomplete settings
// create a null data plot
if (settings == NULL || settings->ride == NULL || settings->ride->ride() == NULL ||
settings->x == 0 || settings->y == 0 ) {
ride = NULL;
return;
} else {
ride = settings->ride;
}
// clear the hover curve
if (hover) {
hover->detach();
delete hover;
hover = NULL;
}
// wipe away existing curves -- we decide what to do below
if (curve) {
curve->detach();
delete curve;
curve = NULL;
}
if (curve2) {
curve2->detach();
delete curve2;
curve2 = NULL;
}
// clear out any interval curves
foreach(QwtPlotCurve *c, intervalCurves) {
c->detach();
delete c;
}
intervalCurves.clear();
// clear out any label
foreach(QwtPlotMarker *c, labels) {
c->detach();
delete c;
}
labels.clear();
// count the currently selected intervals
QVector<int> intervals;
QMap<int,int> displaySequence;
for (int child=0; child<settings->ride->intervals().count(); child++) {
IntervalItem *current = settings->ride->intervals().at(child);
if (current->selected) {
intervals.append(child);
displaySequence.insert(current->displaySequence, child);
}
}
// what are the settings
xseries = settings->x;
yseries = settings->y;
// for setting boundaries of axis
double minX, maxX;
double minY, maxY;
maxY = maxX = -65535;
minY = minX = 65535;
// get application settings
cranklength = appsettings->value(this, GC_CRANKLENGTH, 175.00).toDouble() / 1000.0;
// how many curves do we need ?
if (xseries == MODEL_LRBALANCE || xseries == MODEL_TE || xseries == MODEL_PS ||
yseries == MODEL_LRBALANCE || yseries == MODEL_TE || yseries == MODEL_PS) {
// left and right side needed for each ride/interval
curves = 2;
} else {
// just one curve per ride/interval
curves = 1;
}
for (int side = 0; side < curves; side++) {
if (context->isCompareIntervals == false) {
//
// GET ALL DATA AND MIN / MAX
//
int points=0;
QVector<double> x;
QVector<double> y;
foreach(const RideFilePoint *point, settings->ride->ride()->dataPoints()) {
double xv = pointType(point, settings->x, side, context->athlete->useMetricUnits, cranklength);
double yv = pointType(point, settings->y, side, context->athlete->useMetricUnits, cranklength);
// skip values ? Like zeroes...
if (skipValues(xv, yv, settings))
continue;
// add it
x <<xv;
y <<yv;
points++;
if (yv > maxY) maxY = yv;
if (yv < minY) minY = yv;
if (xv > maxX) maxX = xv;
if (xv < minX) minX = xv;
}
//
// CREATE INTERVAL CURVES (and use framing data from above if needed)
//
if (intervals.count() > 0) {
// interval data in here
QVector<QVector<double> > xvals(settings->ride->intervals().count()); // array of curve x arrays
QVector<QVector<double> > yvals(settings->ride->intervals().count()); // array of curve x arrays
QVector<int> points(settings->ride->intervals().count()); // points in eac curve
// extract interval data
foreach(const RideFilePoint *point, settings->ride->ride()->dataPoints()) {
double x = pointType(point, settings->x, side, context->athlete->useMetricUnits, cranklength);
double y = pointType(point, settings->y, side, context->athlete->useMetricUnits, cranklength);
if (!(settings->ignore && (x == 0 && y ==0))) {
// which interval is it in?
for (int idx=0; idx<settings->ride->intervals().count(); idx++) {
IntervalItem *current = settings->ride->intervals().at(idx);
if (current->selected && point->secs+settings->ride->ride()->recIntSecs() > current->start && point->secs< current->stop) {
xvals[idx].append(x);
yvals[idx].append(y);
points[idx]++;
}
}
}
}
// now we have the interval data lets create the curves
QMapIterator<int, int> order(displaySequence);
while (order.hasNext()) {
order.next();
int idx = order.value();
QColor intervalColor;
intervalColor.setHsv((255/settings->ride->intervals().count()) * idx, 255,255);
// left / right are darker lighter
if (side) intervalColor = intervalColor.lighter(50);
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Ellipse);
sym->setSize(4);
sym->setBrush(QBrush(intervalColor));
sym->setPen(QPen(intervalColor));
QwtPlotCurve *ic = new QwtPlotCurve();
ic->setSymbol(sym);
ic->setStyle(QwtPlotCurve::Dots);
ic->setRenderHint(QwtPlotItem::RenderAntialiased);
ic->setSamples(xvals[idx].constData(), yvals[idx].constData(), points[idx]);
ic->attach(this);
intervalCurves.append(ic);
// show as a line too, if not framed
#if 0 // a bit ugly tbh
if (!settings->frame) {
QwtPlotCurve *icl = new QwtPlotCurve();
icl->setStyle(QwtPlotCurve::Lines);
icl->setPen(QPen(intervalColor, Qt::SolidLine));
icl->setRenderHint(QwtPlotItem::RenderAntialiased);
icl->setSamples(xvals[idx].constData(), yvals[idx].constData(), points[idx]);
icl->attach(this);
intervalCurves.append(icl);
}
#endif
}
}
// setup the framing curve
if (intervals.count() == 0 || settings->frame) {
smooth(x, y, points, settings->smoothing);
if (side) {
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Ellipse);
sym->setSize(4);
sym->setPen(QPen(Qt::cyan));
sym->setBrush(QBrush(Qt::cyan));
curve2 = new QwtPlotCurve();
curve2->setSymbol(sym);
curve2->setStyle(QwtPlotCurve::Dots);
curve2->setRenderHint(QwtPlotItem::RenderAntialiased);
curve2->setSamples(x.constData(), y.constData(), points);
curve2->setZ(-1);
curve2->attach(this);
if (settings->trendLine>0) {
addTrendLine(x, y, points, Qt::cyan);
}
} else {
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Ellipse);
sym->setSize(4);
sym->setPen(QPen(Qt::red));
sym->setBrush(QBrush(Qt::red));
curve = new QwtPlotCurve();
curve->setSymbol(sym);
curve->setStyle(QwtPlotCurve::Dots);
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
curve->setSamples(x.constData(), y.constData(), points);
curve->setZ(-1);
curve->attach(this);
if (settings->trendLine>0) {
addTrendLine(x, y, points, Qt::red);
}
}
}
}
else {
if (context->compareIntervals.count() > 0) {
//
// CREATE COMPARE CURVES
//
int i=0;
if (settings->compareMode > 0)
i++;
for (; i< context->compareIntervals.count(); i++) {
CompareInterval interval = context->compareIntervals.at(i);
if (interval.checked == false)
continue;
// interval data in here
QVector<double> xval;
QVector<double> yval;
int nbPoints = 0;
// extract interval data
foreach(const RideFilePoint *point, interval.data->dataPoints()) {
double x;
double y;
if (settings->compareMode == 1) {
const RideFilePoint *refPoint = context->compareIntervals.at(0).data->dataPoints().at(context->compareIntervals.at(0).data->timeIndex(point->secs));
x = pointType(refPoint, settings->x, side, context->athlete->useMetricUnits, cranklength);
y = pointType(point, settings->y, side, context->athlete->useMetricUnits, cranklength);
}
else if (settings->compareMode == 2) {
const RideFilePoint *refPoint = context->compareIntervals.at(0).data->dataPoints().at(context->compareIntervals.at(0).data->timeIndex(point->secs));
x = pointType(point, settings->x, side, context->athlete->useMetricUnits, cranklength);
y = pointType(refPoint, settings->y, side, context->athlete->useMetricUnits, cranklength);
}
else {
x = pointType(point, settings->x, side, context->athlete->useMetricUnits, cranklength);
y = pointType(point, settings->y, side, context->athlete->useMetricUnits, cranklength);
}
// skip values ?
if (skipValues(x, y, settings))
continue;
if (y > maxY) maxY = y;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (x < minX) minX = x;
xval.append(x);
yval.append(y);
nbPoints++;
}
smooth(xval, yval, nbPoints, settings->smoothing);
QColor intervalColor = interval.color;
// left / right are darker lighter
if (side) intervalColor = intervalColor.lighter(50);
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Ellipse);
sym->setSize(4);
sym->setBrush(QBrush(intervalColor));
sym->setPen(QPen(intervalColor));
QwtPlotCurve *ic = new QwtPlotCurve();
ic->setSymbol(sym);
ic->setStyle(QwtPlotCurve::Dots);
ic->setRenderHint(QwtPlotItem::RenderAntialiased);
ic->setSamples(xval.constData(), yval.constData(), nbPoints);
ic->attach(this);
intervalCurves.append(ic);
if (settings->trendLine>0) {
addTrendLine(xval, yval, nbPoints, intervalColor);
}
}
}
}
}
// redraw grid
if (grid) {
grid->detach();
delete grid;
}
if (settings->gridlines) {
QPen gridPen(GColor(CPLOTGRID));
grid = new QwtPlotGrid();
grid->setPen(gridPen);
grid->enableX(true);
grid->enableY(true);
grid->setZ(-10);
grid->attach(this);
} else {
grid = NULL;
}
// axis titles
setAxisTitle(yLeft, describeType(settings->y, true, context->athlete->useMetricUnits));
setAxisTitle(xBottom, describeType(settings->x, true, context->athlete->useMetricUnits));
// axis scale
if (settings->y == MODEL_AEPF) setAxisScale(yLeft, 0, 600);
else setAxisScale(yLeft, minY, maxY);
if (settings->x == MODEL_CPV) setAxisScale(xBottom, 0, 3);
else setAxisScale(xBottom, minX, maxX);
// gear
if (settings->x == MODEL_GEAR) setAxisScale(xBottom, 0, 7);
if (settings->y == MODEL_GEAR) setAxisScale(yLeft, 0, 7);
// and those interval markers
refreshIntervalMarkers(settings);
replot();
}
void ScatterPlot::mouseMoved()
{
if (!isVisible()) return;
if (ride && ride->ride() && ride->intervals().count() >= intervalMarkers.count()) {
// where is the mouse ?
QPoint pos = QCursor::pos();
// are we hovering "close" to an interval marker ?
int index = 0;
foreach (QwtPlotMarker *is, intervalMarkers) {
QPoint mpos = canvas()->mapToGlobal(QPoint(transform(xBottom, is->value().x()), transform(yLeft, is->value().y())));
int dx = mpos.x() - pos.x();
int dy = mpos.y() - pos.y();
if ((dx > -6 && dx < 6) && (dy > -6 && dy < 6))
context->notifyIntervalHover(ride->intervals()[index]);
index++;
}
}
return;
}
void
ScatterPlot::intervalHover(IntervalItem *ri)
{
if (!isVisible()) return;
if (context->isCompareIntervals) return;
if (!ride) return;
if (!ride->ride()) return;
// zap the old one
if (hover) {
hover->detach();
delete hover;
hover = NULL;
}
if (hover2) {
hover2->detach();
delete hover2;
hover2 = NULL;
}
// null so just clear hover
if (!ri) return;
// how many curves do we need ?
if (xseries == MODEL_LRBALANCE || xseries == MODEL_TE || xseries == MODEL_PS ||
yseries == MODEL_LRBALANCE || yseries == MODEL_TE || yseries == MODEL_PS) {
// left and right side needed for each ride/interval
curves = 2;
} else {
// just one curve per ride/interval
curves = 1;
}
for (int side = 0; side < curves; side++) {
// collect the data
QVector<double> xArray, yArray;
foreach(const RideFilePoint *p1, ride->ride()->dataPoints()) {
double y = pointType(p1, xseries, side, context->athlete->useMetricUnits, cranklength);
double x = pointType(p1, yseries, side, context->athlete->useMetricUnits, cranklength);
if (p1->secs < ri->start || p1->secs > ri->stop) continue;
xArray << x;
yArray << y;
}
// which interval is it or how many ?
int count = 0;
int ours = 0;
foreach(IntervalItem *p, ride->intervals()) {
if (p->start == ri->start && p->stop == ri->stop) ours = count;
count++;
}
// any data ?
if (xArray.size()) {
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Ellipse);
sym->setSize(4);
QColor color;
color.setHsv(ours * 255/count, 255,255);
color.setAlpha(128);
// lighter for rhs
if (side) color = color.lighter(50);
sym->setPen(QPen(color));
sym->setBrush(QBrush(color));
if (side) {
hover2 = new QwtPlotCurve();
hover2->setSymbol(sym);
hover2->setStyle(QwtPlotCurve::Dots);
hover2->setRenderHint(QwtPlotItem::RenderAntialiased);
hover2->setSamples(yArray, xArray);
hover2->attach(this);
} else {
hover = new QwtPlotCurve();
hover->setSymbol(sym);
hover->setStyle(QwtPlotCurve::Dots);
hover->setRenderHint(QwtPlotItem::RenderAntialiased);
hover->setSamples(yArray, xArray);
hover->attach(this);
}
}
}
replot(); // refresh
}
// for accumulating averages when refreshing interval markers
class saccum { public: saccum() : count(0), x(0), y(0) {} int count; double x; double y; };
void
ScatterPlot::refreshIntervalMarkers(ScatterSettings *settings)
{
// zap what we got ...
foreach(QwtPlotMarker *is, intervalMarkers) {
is->detach();
delete is;
}
intervalMarkers.clear();
if (context->isCompareIntervals) return;
// do we have a ride with intervals to refresh ?
int count=0;
if (settings->ride && settings->ride->ride() && settings->ride->ride()->dataPoints().count() && (count = settings->ride->intervals().count())) {
// accumulating...
QVector<saccum> intervalAccumulator(count);
foreach (RideFilePoint *point, settings->ride->ride()->dataPoints()) {
double x0 = pointType(point, settings->x, 0, context->athlete->useMetricUnits, cranklength);
double y0 = pointType(point, settings->y, 0, context->athlete->useMetricUnits, cranklength);
double x1 = pointType(point, settings->x, 1, context->athlete->useMetricUnits, cranklength);
double y1 = pointType(point, settings->y, 1, context->athlete->useMetricUnits, cranklength);
// average of left and right (even if both the same)
double x = (x0 + x1) / 2.00f;
double y = (y0 + y1) / 2.00f;
// accumulate values for each interval here ....
for(int i=0; i < count; i++) {
IntervalItem *v = settings->ride->intervals()[i];
// in our interval ?
if (point->secs >= v->start && point->secs <= v->stop) {
intervalAccumulator[i].x += x;
intervalAccumulator[i].y += y;
intervalAccumulator[i].count++;
}
}
}
// ok, so now add markers for the average position for each interval
int index = 0;
foreach (saccum a, intervalAccumulator) {
QColor color;
color.setHsv(index * 255/count, 255,255);
double x = a.x/double(a.count);
double y = a.y/double(a.count);
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Diamond);
sym->setSize(8);
sym->setPen(QPen(GColor(CPLOTMARKER)));
sym->setBrush(QBrush(color));
QwtPlotMarker *p = new QwtPlotMarker();
p->setValue(x, y);
p->setYAxis(yLeft);
p->setZ(100);
p->setSymbol(sym);
p->attach(this);
intervalMarkers << p;
index++;
}
}
}
void
ScatterPlot::configChanged(qint32)
{
// setColors bg
setCanvasBackground(GColor(CPLOTBACKGROUND));
QPalette palette;
palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
axisWidget(QwtPlot::xBottom)->setPalette(palette);
axisWidget(QwtPlot::yLeft)->setPalette(palette);
replot();
}
void
ScatterPlot::setAxisTitle(int axis, QString label)
{
// setup the default fonts
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
QwtText title(label);
title.setFont(stGiles);
QwtPlot::setAxisFont(axis, stGiles);
QwtPlot::setAxisTitle(axis, title);
}
void
ScatterPlot::addTrendLine(QVector<double> xval, QVector<double> yval, int nbPoints, QColor intervalColor)
{
QwtPlotCurve *trend = new QwtPlotCurve();
// cosmetics
QPen cpen = QPen(intervalColor.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(yLeft);
trend->setStyle(QwtPlotCurve::Lines);
// perform linear regression
Statistic regress(xval.data(), yval.data(), nbPoints);
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] = regress.maxX;
ytrend[1] = regress.getYforX(regress.maxX);
trend->setSamples(xtrend,ytrend, 2);
trend->attach(this);
intervalCurves.append(trend);
// make that mark -- always above with topN
QwtPlotMarker *label = new QwtPlotMarker();
QString labelString = regress.label();
QwtText text(labelString);
text.setColor(intervalColor.darker(200));
label->setLabel(text);
label->setValue(xtrend[1]*0.8, ytrend[1]*0.9);
label->setYAxis(yLeft);
label->setSpacing(6); // not px but by yaxis value !? mad.
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
// and attach
label->attach(this);
labels << label;
}
void
ScatterPlot::smooth(QVector<double> &xval, QVector<double> &yval, int count, int applySmooth)
{
if (applySmooth<2 || count < applySmooth)
return;
QVector<double> smoothX(count);
QVector<double> smoothY(count);
for (int i=0; i<count; i++) {
smoothX[i] = xval.value(i);
smoothY[i] = yval.value(i);
}
// initialise rolling average
double rXtot = 0;
double rYtot = 0;
for (int i=applySmooth; i>0 && count-i >=0; i--) {
rXtot += smoothX[count-i];
rYtot += smoothY[count-i];
}
// now run backwards setting the rolling average
for (int i=count-1; i>=applySmooth; i--) {
int hereX = smoothX[i];
smoothX[i] = rXtot / applySmooth;
rXtot -= hereX;
rXtot += smoothX[i-applySmooth];
int hereY = smoothY[i];
smoothY[i] = rYtot / applySmooth;
rYtot -= hereY;
rYtot += smoothY[i-applySmooth];
}
// Finish with smaller rolling average
for (int i=applySmooth-1; i>0; i--) {
int hereX = smoothX[i];
smoothX[i] = rXtot / (i+2);
rXtot -= hereX;
int hereY = smoothY[i];
smoothY[i] = rYtot / (i+2);
rYtot -= hereY;
}
xval = smoothX;
yval = smoothY;
}
void
ScatterPlot::resample(QVector<double> &xval, QVector<double> &yval, int &count, double recInterval, int applySmooth)
{
if (recInterval >= applySmooth)
return;
int newcount=0;
QVector<double> newxval;
QVector<double> newyval;
if (applySmooth > recInterval) {
double totalX = 0.0;
double totalY = 0.0;
int j=0;
for (int i = 0; i < count; i++) {
j++;
if (j<applySmooth) {
totalX += xval.at(i)*recInterval;
totalY += yval.at(i)*recInterval;
}
else {
double part = recInterval-j+applySmooth;
totalX += xval.at(i)*part;
totalY += yval.at(i)*part;
double smoothX = totalX/applySmooth;
double smoothY = totalY/applySmooth;
newxval.append(smoothX);
newyval.append(smoothY);
newcount++;
j=0;
totalX = xval.at(i)*(recInterval-part);
totalY = yval.at(i)*(recInterval-part);
}
}
}
count = newcount;
xval = newxval;
yval = newyval;
}
bool
ScatterPlot::skipValues(double xv, double yv, ScatterSettings *settings) {
// skip zeroes? - special logic for Model Gear, since there value between 0.01 and 1 happen and are relevant
if ((settings->x != MODEL_GEAR && settings->y != MODEL_GEAR)
&& settings->ignore && (int(xv) == 0 || int(yv) == 0)) return true;
// Model Gear
if ((settings->x == MODEL_GEAR)
&& settings->ignore && (xv == 0.0f || int(yv) == 0)) return true;
if ((settings->y == MODEL_GEAR)
&& settings->ignore && (int(xv) == 0 || yv == 0.0f)) return true;
// Temp 0 values are relevant
if ((settings->x == MODEL_TEMP && xv == RideFile::NoTemp) || (settings->y == MODEL_TEMP && yv == RideFile::NoTemp)) return true;
// LR Balance : if skip 0% skip also 100%
if ((settings->x == MODEL_LRBALANCE && settings->ignore && xv == 100) || (settings->y == MODEL_LRBALANCE && settings->ignore && yv == 100)) return true;
return false;
}