/*
 * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
 *               2011 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 "PowerHist.h"
#include "MainWindow.h"
#include "Context.h"
#include "Athlete.h"
#include "RideCache.h"
#include "RideItem.h"
#include "IntervalItem.h"
#include "RideFile.h"
#include "WPrime.h"
#include "RideFileCache.h"
#include "Settings.h"
#include "Zones.h"
#include "HrZones.h"
#include "Colors.h"
#include "Units.h"

#include "ZoneScaleDraw.h"

#include <qpainter.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_zoomer.h>
#include <qwt_scale_engine.h>
#include <qwt_scale_widget.h>
#include <qwt_text.h>
#include <qwt_legend.h>
#include <qwt_series_data.h>

#include "LTMCanvasPicker.h" // for tooltip

PowerHist::PowerHist(Context *context, bool rangemode) :
    minX(0),
    maxX(0),
    rangemode(rangemode),
    rideItem(NULL),
    context(context),
    series(RideFile::watts),
    lny(false),
    shade(false),
    zoned(false),
    cpzoned(false),
    binw(3),
    withz(true),
    dt(1),
    absolutetime(true),
    cache(NULL),
    source(Ride)
{
    binw = appsettings->value(this, GC_HIST_BIN_WIDTH, 5).toInt();
    if (appsettings->value(this, GC_SHADEZONES, true).toBool() == true)
        shade = true;
    else
        shade = false;

    // create a background object for shading
    bg = new PowerHistBackground(this);
    bg->attach(this);

    hrbg = new HrHistBackground(this);
    hrbg->attach(this);

    pacebg = new PaceHistBackground(this);
    pacebg->attach(this);

    setCanvasBackground(Qt::white);
    static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);

    setParameterAxisTitle();
    setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)"));

    curve = new QwtPlotCurve("");
    curve->setStyle(QwtPlotCurve::Steps);
    curve->setRenderHint(QwtPlotItem::RenderAntialiased);
    curve->setZ(20);
    curve->attach(this);

    curveSelected = new QwtPlotCurve("");
    curveSelected->setStyle(QwtPlotCurve::Steps);
    curveSelected->setRenderHint(QwtPlotItem::RenderAntialiased);
    curveSelected->setZ(50);
    curveSelected->attach(this);

    curveHover = new QwtPlotCurve("");
    curveHover->setStyle(QwtPlotCurve::Steps);
    curveHover->setRenderHint(QwtPlotItem::RenderAntialiased);
    curveHover->setZ(100);
    curveHover->attach(this);

    grid = new QwtPlotGrid();
    grid->enableX(false);
    grid->attach(this);

    zoneLabels = QList<PowerHistZoneLabel *>();
    hrzoneLabels = QList<HrHistZoneLabel *>();
    pacezoneLabels = QList<PaceHistZoneLabel *>();

    zoomer = new penTooltip(this->canvas());
    canvasPicker = new LTMCanvasPicker(this);
    connect(canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), this, SLOT(pointHover(QwtPlotCurve*, int)));

    // usually hidden, but shown for compare mode
    //XXX insertLegend(new QwtLegend(), QwtPlot::BottomLegend);

    setAxisMaxMinor(xBottom, 0);
    setAxisMaxMinor(yLeft, 0);

    configChanged(CONFIG_APPEARANCE);
}

void
PowerHist::configChanged(qint32)
{
    // plot background
    if (rangemode) setCanvasBackground(GColor(CTRENDPLOTBACKGROUND));
    else setCanvasBackground(GColor(CPLOTBACKGROUND));

    // curve
    QPen pen;
    QColor brush_color;

    if (source == Metric) {

        pen.setColor(metricColor.darker(200));
        brush_color = metricColor;

    } else {

        switch (series) {
        case RideFile::watts:
        case RideFile::aPower:
        case RideFile::wattsKg:
            pen.setColor(GColor(CPOWER).darker(200));
            brush_color = GColor(CPOWER);
            break;
        case RideFile::nm:
            pen.setColor(GColor(CTORQUE).darker(200));
            brush_color = GColor(CTORQUE);
            break;
        case RideFile::kph:
            pen.setColor(GColor(CSPEED).darker(200));
            brush_color = GColor(CSPEED);
            break;
        case RideFile::cad:
            pen.setColor(GColor(CCADENCE).darker(200));
            brush_color = GColor(CCADENCE);
            break;
        case RideFile::smo2:
            pen.setColor(GColor(CSMO2).darker(200));
            brush_color = GColor(CSMO2);
            break;
        case RideFile::gear:
            pen.setColor(GColor(CGEAR).darker(200));
            brush_color = GColor(CGEAR);
            break;
        case RideFile::wbal:
            pen.setColor(GColor(CWBAL).darker(200));
            brush_color = GColor(CGEAR);
            break;
        default:
        case RideFile::hr:
            pen.setColor(GColor(CHEARTRATE).darker(200));
            brush_color = GColor(CHEARTRATE);
            break;
        }
    }

    double width = appsettings->value(this, GC_LINEWIDTH, 0.5).toDouble();
    if (appsettings->value(this, GC_ANTIALIAS, true).toBool()==true) {
        curve->setRenderHint(QwtPlotItem::RenderAntialiased);
        curveSelected->setRenderHint(QwtPlotItem::RenderAntialiased);
        curveHover->setRenderHint(QwtPlotItem::RenderAntialiased);
    }

    // use a linear gradient
    if (rangemode) brush_color.setAlpha(GColor(CTRENDPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200);
    else brush_color.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200);
    QColor brush_color1 = brush_color.darker();
    QLinearGradient linearGradient(0, 0, 0, height());
    linearGradient.setColorAt(0.0, brush_color);
    linearGradient.setColorAt(1.0, brush_color1);
    linearGradient.setSpread(QGradient::PadSpread);
    curve->setBrush(linearGradient);   // fill below the line

    if (!isZoningEnabled()) {
        pen.setWidth(width);
        curve->setPen(pen);

    } else {
        pen.setWidth(width);
        curve->setPen(QPen(Qt::NoPen));
    }

    // intervalselection
    QPen ivl(GColor(CINTERVALHIGHLIGHTER).darker(200));
    ivl.setWidth(width);
    curveSelected->setPen(ivl);
    QColor ivlbrush = GColor(CINTERVALHIGHLIGHTER);
    if (rangemode) ivlbrush.setAlpha(GColor(CTRENDPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200);
    else ivlbrush.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200);
    curveSelected->setBrush(ivlbrush);   // fill below the line

    // hover curve
    QPen hvl(Qt::darkGray);
    hvl.setWidth(width);
    curveHover->setPen(hvl);
    QColor hvlbrush = QColor(Qt::darkGray);
    if (rangemode) hvlbrush.setAlpha((GColor(CTRENDPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200));
    else hvlbrush.setAlpha((GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200));
    curveHover->setBrush(hvlbrush);   // fill below the line

    // grid
    QPen gridPen(GColor(CPLOTGRID));
    //gridPen.setStyle(Qt::DotLine);
    grid->setPen(gridPen);

    QPalette palette;
    if (rangemode) palette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
    else palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
    palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
    palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
    setPalette(palette);

    axisWidget(QwtPlot::xBottom)->setPalette(palette);
    axisWidget(QwtPlot::yLeft)->setPalette(palette);

    setAutoFillBackground(true);
}

void
PowerHist::hideStandard(bool hide)
{
    bg->setVisible(!hide);
    hrbg->setVisible(!hide);
    pacebg->setVisible(!hide);
    curve->setVisible(!hide);
    curveSelected->setVisible(!hide);
    curveHover->setVisible(!hide);

    // clear if we are showing standard (not comparing) or if the comparisons got cleared
    if (!hide || (rangemode && context->compareDateRanges.count() == 0) || (!rangemode && context->compareIntervals.count() == 0) ) {

        // wipe the compare data 
        compareData.clear();

        // we want normal so zap any compare curves
        foreach(QwtPlotCurve *x, compareCurves) {
            x->detach();
            delete x;
        }
        compareCurves.clear();
    }
}

PowerHist::~PowerHist() {
    delete bg;
    delete hrbg;
    delete pacebg;
    delete curve;
    delete curveSelected;
    delete curveHover;
    delete grid;
}

void
PowerHist::refreshZoneLabels()
{
    // delete any existing power zone labels
    if (zoneLabels.size()) {
        QListIterator<PowerHistZoneLabel *> i(zoneLabels);
        while (i.hasNext()) {
            PowerHistZoneLabel *label = i.next();
            label->detach();
            delete label;
        }
    }
    zoneLabels.clear();

    if (!rideItem) return;

    if (series == RideFile::watts || series == RideFile::wattsKg) {

        const Zones *zones = context->athlete->zones();
        int zone_range = zones->whichRange(rideItem->dateTime.date());

        // generate labels for existing zones
        if (zone_range >= 0) {
            int num_zones = zones->numZones(zone_range);
            for (int z = 0; z < num_zones; z ++) {
                PowerHistZoneLabel *label = new PowerHistZoneLabel(this, z);
                label->attach(this);
                zoneLabels.append(label);
            }
        }
    }
}

void
PowerHist::refreshHRZoneLabels()
{
    // delete any existing power zone labels
    if (hrzoneLabels.size()) {
        QListIterator<HrHistZoneLabel *> i(hrzoneLabels);
        while (i.hasNext()) {
            HrHistZoneLabel *label = i.next();
            label->detach();
            delete label;
        }
    }
    hrzoneLabels.clear();

    if (!rideItem) return;

    if (series == RideFile::hr) {
        const HrZones *zones = context->athlete->hrZones();
        int zone_range = zones->whichRange(rideItem->dateTime.date());

        // generate labels for existing zones
        if (zone_range >= 0) {
            int num_zones = zones->numZones(zone_range);
            for (int z = 0; z < num_zones; z ++) {
                HrHistZoneLabel *label = new HrHistZoneLabel(this, z);
                label->attach(this);
                hrzoneLabels.append(label);
            }
        }
    }
}

void
PowerHist::refreshPaceZoneLabels()
{
    // delete any existing power zone labels
    if (pacezoneLabels.size()) {
        QListIterator<PaceHistZoneLabel *> i(pacezoneLabels);
        while (i.hasNext()) {
            PaceHistZoneLabel *label = i.next();
            label->detach();
            delete label;
        }
    }
    pacezoneLabels.clear();

    if (!rideItem || !(rideItem->isRun || rideItem->isSwim)) return;

    if (series == RideFile::kph && context->athlete->paceZones(rideItem->isSwim)) {
        const PaceZones *zones = context->athlete->paceZones(rideItem->isSwim);
        int zone_range = zones->whichRange(rideItem->dateTime.date());

        // generate labels for existing zones
        if (zone_range >= 0) {
            int num_zones = zones->numZones(zone_range);
            for (int z = 0; z < num_zones; z ++) {
                PaceHistZoneLabel *label = new PaceHistZoneLabel(this, z);
                label->attach(this);
                pacezoneLabels.append(label);
            }
        }
    }
}

void
PowerHist::recalcCompare()
{
    // Set curves .. they will always have been created 
    //               in setDataFromCompareIntervals, but no samples set

    // don't bother if not comparing for our mode or we're not visible
    if (!isVisible() ||
        ((rangemode && !context->isCompareDateRanges)
        || (!rangemode && !context->isCompareIntervals))) return;

    // zap any zone data labels
    foreach (QwtPlotMarker *label, zoneDataLabels) {
        label->detach();
        delete label;
    }
    zoneDataLabels.clear();

    // loop through intervals or dateranges, depending upon mode
    // counting columns
    double ncols = 0;
    for(int i=0; (rangemode && i < context->compareDateRanges.count()) ||
                 (!rangemode && i < context->compareIntervals.count()) ; i++) {

        if (rangemode && context->compareDateRanges[i].isChecked()) ncols++;
        if (!rangemode && context->compareIntervals[i].isChecked()) ncols++;
    }
    int acol = 0;
    int maxX = 0;

    // loop again but now setting up data
    for(int intervalNumber=0; (rangemode && intervalNumber < context->compareDateRanges.count()) ||
                 (!rangemode && intervalNumber < context->compareIntervals.count()) ; intervalNumber++) {

        HistData &cid = compareData[intervalNumber];
        QwtPlotCurve *curve = compareCurves[intervalNumber];
        QVector<unsigned int> *array = NULL;
        int arrayLength = 0;

        if (source == Metric) {

            // we use the metricArray
            array = &cid.metricArray;
            arrayLength = cid.metricArray.size();

        } else if (series == RideFile::watts && zoned == false) {

            array = &cid.wattsArray;
            arrayLength = cid.wattsArray.size();

        } else if ((series == RideFile::watts || series == RideFile::wattsKg) && zoned == true) {

            if (cpzoned) {

                array = &cid.wattsCPZoneArray;
                arrayLength = cid.wattsCPZoneArray.size();

            } else {

                array = &cid.wattsZoneArray;
                arrayLength = cid.wattsZoneArray.size();
            }

        } else if (series == RideFile::wbal && zoned == true) {

            array = &cid.wbalZoneArray;
            arrayLength = cid.wbalZoneArray.size();

        } else if (series == RideFile::aPower && zoned == false) {

            array = &cid.aPowerArray;
            arrayLength = cid.aPowerArray.size();

        } else if (series == RideFile::wattsKg && zoned == false) {

            array = &cid.wattsKgArray;
            arrayLength = cid.wattsKgArray.size();

        } else if (series == RideFile::nm) {

            array = &cid.nmArray;
            arrayLength = cid.nmArray.size();

        } else if (series == RideFile::hr && zoned == false) {

            array = &cid.hrArray;
            arrayLength = cid.hrArray.size();

        } else if (series == RideFile::hr && zoned == true) {

            if (cpzoned) {

                array = &cid.hrCPZoneArray;
                arrayLength = cid.hrCPZoneArray.size();

            } else {

                array = &cid.hrZoneArray;
                arrayLength = cid.hrZoneArray.size();

            }

        } else if (series == RideFile::kph && !(zoned == true && (!rideItem || rideItem->isRun || rideItem->isSwim))) {

            array = &cid.kphArray;
            arrayLength = cid.kphArray.size();

        } else if (series == RideFile::kph && zoned == true && (!rideItem || rideItem->isRun || rideItem->isSwim)) {

            if (cpzoned) {

                array = &cid.paceCPZoneArray;
                arrayLength = cid.paceCPZoneArray.size();

            } else {

                array = &cid.paceZoneArray;
                arrayLength = cid.paceZoneArray.size();

            }

        } else if (series == RideFile::smo2) {

            array = &cid.smo2Array;
            arrayLength = cid.smo2Array.size();

        } else if (series == RideFile::gear) {

            array = &cid.gearArray;
            arrayLength = cid.gearArray.size();


        } else if (series == RideFile::cad) {
            array = &cid.cadArray;
            arrayLength = cid.cadArray.size();
        }

        // UNUSEDRideFile::SeriesType baseSeries = (series == RideFile::wattsKg) ? RideFile::watts : series;

        // null curve please -- we have no data!
        if (!array || arrayLength == 0) {
            // create empty curves when no data
            const double zero = 0;
            curve->setSamples(&zero, &zero, 0);
            continue;
        }

        if (!isZoningEnabled()) {

            // NOT ZONED
            // we add a bin on the end since the last "incomplete" bin
            // will be dropped otherwise
            int count = int(ceil((arrayLength - 1) / (binw)))+1;

            // allocate space for data, plus beginning and ending point
            QVector<double> parameterValue(count+2, 0.0);
            QVector<double> totalTime(count+2, 0.0);
            int i;
            for (i = 1; i <= count; ++i) {
                double high = i * round(binw/delta);
                double low = high - round(binw/delta);
                if (low==0 && !withz) low++;
                parameterValue[i] = high*delta;
                totalTime[i]  = 1e-9;  // nonzero to accommodate log plot
                while (low < high && low<arrayLength) {
                    totalTime[i] += dt * (*array)[low++];
                }
            }

            totalTime[i] = 1e-9;       // nonzero to accommodate log plot
            parameterValue[i] = i * delta * binw;
            totalTime[0] = 1e-9;
            parameterValue[0] = 0;

            // convert vectors from absolute time to percentage
            // if the user has selected that
            if (!absolutetime) {
                percentify(totalTime, 1);
            }

            curve->setSamples(parameterValue.data(), totalTime.data(), count + 2);

            QwtScaleDraw *sd = new QwtScaleDraw;
            sd->setTickLength(QwtScaleDiv::MajorTick, 3);
            setAxisScaleDraw(QwtPlot::xBottom, sd);

            // HR typically starts at 80 or so, rather than zero
            // lets crop the chart so we can focus on the data
            // if we're working with HR data...
            minX=0;
            if (!withz && series == RideFile::hr) {
                for (int i=1; i<cid.hrArray.size(); i++) {
                    if (cid.hrArray[i] > 0.1) {
                        minX = i;
                        break;
                    }
                }
            }

            // only set X-axis to largest value with significant value
            if (series == RideFile::wbal) {
                if (cid.wbalArray.size() > maxX) maxX = cid.wbalArray.size();
            } else {
                int truncate = count;
                while (truncate > 0) {
                    if (!absolutetime && totalTime[truncate] >= 0.1) break;
                    if (absolutetime && totalTime[truncate] >= 0.1) break;
                    truncate--;
                }
                if (parameterValue[truncate] > maxX) maxX = parameterValue[truncate];
            }

            // we only do zone labels when using absolute values
            refreshZoneLabels();
            refreshHRZoneLabels();
            refreshPaceZoneLabels();

        } else { // ZONED

            QFont labelFont;
            labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
            labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());

            // 0.625 = golden ratio for gaps between group of cols
            // 0.9 = 10% space between each col in group

            double width = (0.625 / ncols) * 0.90f;
            double jump = acol * (0.625 / ncols);
            
            // we're not binning instead we are prettyfying the columnar
            // display in much the same way as the weekly summary workds
            // Each zone column will have 4 points
            QVector<double> xaxis (array->size() * 4);
            QVector<double> yaxis (array->size() * 4);

            // so we can calculate percentage for the labels
            double total=0;
            for (int i=0; i<array->size(); i++) total += dt * (double)(*array)[i];

            // samples to time
            for (int i=0, offset=0; i<array->size(); i++) {

                double x = double(i) - (0.625f * 0.90f / 2.0f);
                double y = dt * (double)(*array)[i];

                xaxis[offset] = x +jump;
                yaxis[offset] = 0;
                offset++;
                xaxis[offset] = x +jump;
                yaxis[offset] = y;
                offset++;
                xaxis[offset] = x +jump +width;
                yaxis[offset] = y;
                offset++;
                xaxis[offset] = x +jump +width;
                yaxis[offset] = 0;
                offset++;

                double yval = absolutetime ? y : (y /total * 100.00f);

                if (yval > 0) {

                    QColor color = rangemode ? context->compareDateRanges[intervalNumber].color.darker(200)
                                             : context->compareIntervals[intervalNumber].color.darker(200);

                    // is this one selected ?
                    if ((rangemode && context->compareDateRanges[intervalNumber].isChecked()) ||
                        (!rangemode && context->compareIntervals[intervalNumber].isChecked())) {

                        // now add a label above the bar
                        QwtPlotMarker *label = new QwtPlotMarker();
                        QwtText text(QString("%1%2").arg(round(yval)).arg(absolutetime ? "" : "%"), QwtText::PlainText);
                        text.setFont(labelFont);
                        text.setColor(color);
                        label->setLabel(text);
                        label->setValue(x+jump+(width/2.00f), yval);
                        label->setYAxis(QwtPlot::yLeft);
                        label->setSpacing(5); // not px but by yaxis value !? mad.
                        label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
            
                        // and attach
                        label->attach(this);
                        zoneDataLabels << label;
                    }
                }
            }

            if (!absolutetime) {
                percentify(yaxis, 2);
            }

            // set those curves
            curve->setPen(QPen(Qt::NoPen));
            curve->setSamples(xaxis.data(), yaxis.data(), xaxis.size());

            //
            // POWER ZONES
            //
            if (series == RideFile::wattsKg || series == RideFile::watts) {

                if (cpzoned) {

                    setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
                    setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);

                } else {

                    const Zones *zones = context->athlete->zones();
                    int zone_range = -1;

                    if (zones) {
                        if (context->compareIntervals.count())
                            zone_range = zones->whichRange(context->compareIntervals[0].data->startTime().date());
                        if (zone_range == -1) zone_range = zones->whichRange(QDate::currentDate());

                    }
                    if (zones && zone_range != -1) {
                        if ((series == RideFile::watts || series == RideFile::wattsKg)) {
                            setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(zones, zone_range));
                            setAxisScale(QwtPlot::xBottom, -0.99, zones->numZones(zone_range), 1);
                        }
                    }
                }
            }

            //
            // HR ZONES
            //
            if (!cpzoned) {

                const HrZones *hrzones = context->athlete->hrZones();
                int hrzone_range = -1;

                if (hrzones) {
                    if (context->compareIntervals.count())
                        hrzone_range = hrzones->whichRange(context->compareIntervals[0].data->startTime().date());
                    if (hrzone_range == -1) hrzone_range = hrzones->whichRange(QDate::currentDate());

                }
                if (hrzones && hrzone_range != -1) {
                    if (series == RideFile::hr) {
                        setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(hrzones, hrzone_range));
                        setAxisScale(QwtPlot::xBottom, -0.99, hrzones->numZones(hrzone_range), 1);
                    }
                }

            }

            //
            // PACE ZONES using Run Zones for scale
            //
            if (!cpzoned) {

                const PaceZones *pacezones = context->athlete->paceZones();
                int pacezone_range = -1;

                if (pacezones) {
                    if (context->compareIntervals.count())
                        pacezone_range = pacezones->whichRange(context->compareIntervals[0].data->startTime().date());
                    if (pacezone_range == -1) pacezone_range = pacezones->whichRange(QDate::currentDate());

                }
                if (pacezones && pacezone_range != -1) {
                    if (series == RideFile::kph) {
                        setAxisScaleDraw(QwtPlot::xBottom, new PaceZoneScaleDraw(pacezones, pacezone_range));
                        setAxisScale(QwtPlot::xBottom, -0.99, pacezones->numZones(pacezone_range), 1);
                    }
                }

            }

            //
            // W'bal zones
            //
            if (!cpzoned && series == RideFile::wbal) {

                setAxisScaleDraw(QwtPlot::xBottom, new WbalZoneScaleDraw());
                setAxisScale(QwtPlot::xBottom, -0.99, 4, 1);
            }

            setAxisMaxMinor(QwtPlot::xBottom, 0);

            // keep track of columns visible -- depending upon mode
            if (!rangemode && context->compareIntervals[intervalNumber].isChecked()) acol++;
            if (rangemode && context->compareDateRanges[intervalNumber].isChecked()) acol++;
        }
    }

    // set axis etc
    if (!isZoningEnabled()) {
        //normal
        setAxisScale(xBottom, minX, maxX);
    } else {
        // zoned
    }
    setYMax(); 
    updatePlot();
}

void
PowerHist::recalc(bool force)
{
    if ((!rangemode && context->isCompareIntervals) ||
       (rangemode && context->isCompareDateRanges)) {
        recalcCompare();
        return;
    }

    // lets make sure we need to recalculate
    if (force == false &&
        LASTsource == source &&
        LASTcache == cache &&
        LASTrideItem == rideItem &&
        LASTseries == series &&
        LASTshade == shade &&
        LASTuseMetricUnits == context->athlete->useMetricUnits &&
        LASTlny == lny &&
        LASTzoned == zoned &&
        LASTcpzoned == cpzoned &&
        LASTbinw == binw &&
        LASTwithz == withz &&
        LASTdt == dt &&
        LASTabsolutetime == absolutetime) {
        return; // nothing has changed

    } else {

        // remember for next time
        LASTsource = source;
        LASTcache = cache;
        LASTrideItem = rideItem;
        LASTseries = series;
        LASTshade = shade;
        LASTuseMetricUnits = context->athlete->useMetricUnits;
        LASTlny = lny;
        LASTzoned = zoned;
        LASTcpzoned = cpzoned;
        LASTbinw = binw;
        LASTwithz = withz;
        LASTdt = dt;
        LASTabsolutetime = absolutetime;
    }


    if (source == Ride && !rideItem) { 
        return;
    }

    // make sure the interval length is set if not plotting metrics
    if (source != Metric && dt <= 0) {
        return;
    }

    // zap any zone data labels
    foreach (QwtPlotMarker *label, zoneDataLabels) {
        label->detach();
        delete label;
    }
    zoneDataLabels.clear();

    // bin the data
    QVector<double>x,y,sx,sy;
    binData(standard, x, y, sx, sy);

    if (!isZoningEnabled()) {

        // now draw curves / axis etc
        curve->setSamples(x, y);
        curveSelected->setSamples(sx, sy);

        QwtScaleDraw *sd = new QwtScaleDraw;
        sd->setTickLength(QwtScaleDiv::MajorTick, 3);
        setAxisScaleDraw(QwtPlot::xBottom, sd);

        // HR typically starts at 80 or so, rather than zero
        // lets crop the chart so we can focus on the data
        // if we're working with HR data...
        minX=0;
        if (!withz && series == RideFile::hr) {
            for (int i=1; i<standard.hrArray.size(); i++) {
                if (standard.hrArray[i] > 0.1) {
                    minX = i;
                    break;
                }
            }
        }
        setAxisScale(xBottom, minX, x[x.size()-1]);

        // we only do zone labels when using absolute values
        refreshZoneLabels();
        refreshHRZoneLabels();
        refreshPaceZoneLabels();

    } else {

        QFont labelFont;
        labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
        labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 10).toInt());

        // zoned labels
        for(int i=1; i<x.count(); i += 4) {

            double yval = y[i];
            double xval = x[i];

            if (yval > 0) {

                // now add a label above the bar
                QwtPlotMarker *label = new QwtPlotMarker();
                QwtText text(QString("%1%2").arg(round(yval)).arg(absolutetime ? "" : "%"), QwtText::PlainText);
                text.setFont(labelFont);
                if (series == RideFile::watts)
                    text.setColor(GColor(CPOWER).darker(200));
                else if (series == RideFile::hr)
                    text.setColor(GColor(CHEARTRATE).darker(200));
                else
                    text.setColor(GColor(CSPEED).darker(200));
                label->setLabel(text);
                label->setValue(xval+0.312f, yval);
                label->setYAxis(QwtPlot::yLeft);
                label->setSpacing(5); // not px but by yaxis value !? mad.
                label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
            
                // and attach
                label->attach(this);
                zoneDataLabels << label;
            }
        }

        // set those curves
        curve->setSamples(x, y);
        curveSelected->setSamples(sx, sy);

        // zone scale draw
        if ((series == RideFile::watts || series == RideFile::wattsKg) && zoned && rideItem && context->athlete->zones()) {

            if (cpzoned) {
                setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
                setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
            } else {
                int zone_range = context->athlete->zones()->whichRange(rideItem->dateTime.date());
                setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(context->athlete->zones(), zone_range));
                if (zone_range >= 0)
                    setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->zones()->numZones(zone_range), 1);
                else
                    setAxisScale(QwtPlot::xBottom, -0.99, 0, 1);
            }
        }

        // hr scale draw
        int hrRange;
        if (series == RideFile::hr && zoned && rideItem && context->athlete->hrZones() &&
            (hrRange=context->athlete->hrZones()->whichRange(rideItem->dateTime.date())) != -1) {

            if (cpzoned) {
                setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
                setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
            } else {
                setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(context->athlete->hrZones(), hrRange));
                if (hrRange >= 0)
                    setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->hrZones()->numZones(hrRange), 1);
                else
                    setAxisScale(QwtPlot::xBottom, -0.99, 0, 1);
            }
        }

        // pace scale draw
        int paceRange;
        if (series == RideFile::kph && zoned && rideItem &&
            (rideItem->isRun || rideItem->isSwim) && context->athlete->paceZones(rideItem->isSwim) &&
            (paceRange=context->athlete->paceZones(rideItem->isSwim)->whichRange(rideItem->dateTime.date())) != -1) {

            if (cpzoned) {
                setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
                setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
            } else {
                setAxisScaleDraw(QwtPlot::xBottom, new PaceZoneScaleDraw(context->athlete->paceZones(rideItem->isSwim), paceRange));

                if (paceRange >= 0)
                    setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->paceZones(rideItem->isSwim)->numZones(paceRange), 1);
                else
                    setAxisScale(QwtPlot::xBottom, -0.99, 0, 1);
            }
        }

        // watts zoned for a time range
        if (source == Cache && zoned && (series == RideFile::watts || series == RideFile::wattsKg) && context->athlete->zones()) {
            if (cpzoned) {
                setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
                setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
            } else {
                setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(context->athlete->zones(), 0));
                if (context->athlete->zones()->getRangeSize())
                    setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->zones()->numZones(0), 1); // use zones from first defined range
            }
        }

        // hr zoned for a time range
        if (source == Cache && zoned && series == RideFile::hr && context->athlete->hrZones()) {
            if (cpzoned) {
                setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
                setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
            } else {
                setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(context->athlete->hrZones(), 0));
                if (context->athlete->hrZones()->getRangeSize())
                    setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->hrZones()->numZones(0), 1); // use zones from first defined range
            }
        }

        // pace zoned for a time range using run zones for scale
        if (source == Cache && zoned && series == RideFile::kph && context->athlete->paceZones()) {
            if (cpzoned) {
                setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
                setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
            } else {
                setAxisScaleDraw(QwtPlot::xBottom, new PaceZoneScaleDraw(context->athlete->paceZones(), 0));
                if (context->athlete->paceZones()->getRangeSize())
                    setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->paceZones()->numZones(0), 1); // use zones from first defined range
            }
        }

        // w'bal zoned 
        if (zoned && series == RideFile::wbal) { 
            setAxisScaleDraw(QwtPlot::xBottom, new WbalZoneScaleDraw());
            setAxisScale(QwtPlot::xBottom, -0.99, 4, 1);
        }
        setAxisMaxMinor(QwtPlot::xBottom, 0);
    }

    setYMax();
    configChanged(CONFIG_APPEARANCE); // setup the curve colors to appropriate values
    updatePlot();
}

// bin the data into x/y for ride and selected intervals
void
PowerHist::binData(HistData &standard, QVector<double>&x, // x-axis for data
                                       QVector<double>&y, // y-axis for data
                                       QVector<double>&sx, // x-axis for selected data
                                       QVector<double>&sy) // y-axis for selected data
{
    QVector<unsigned int> *array = NULL;
    QVector<unsigned int> *selectedArray = NULL;
    int arrayLength = 0;

    if (source == Metric) {

        // we use the metricArray
        array = &standard.metricArray;
        arrayLength = standard.metricArray.size();
        selectedArray = NULL;

    } else if (series == RideFile::watts && zoned == false) {

        array = &standard.wattsArray;
        arrayLength = standard.wattsArray.size();
        selectedArray = &standard.wattsSelectedArray;

    } else if (series == RideFile::wbal && zoned == false) {

        array = &standard.wbalArray;
        arrayLength = standard.wbalArray.size();
        selectedArray = &standard.wbalSelectedArray;

    } else if (series == RideFile::wbal && zoned == true) {

            array = &standard.wbalZoneArray;
            arrayLength = standard.wbalZoneArray.size();
            selectedArray = &standard.wbalZoneSelectedArray;

    } else if ((series == RideFile::watts || series == RideFile::wattsKg) && zoned == true) {
        if (cpzoned) {
            array = &standard.wattsCPZoneArray;
            arrayLength = standard.wattsCPZoneArray.size();
            selectedArray = &standard.wattsCPZoneSelectedArray;
        } else {
            array = &standard.wattsZoneArray;
            arrayLength = standard.wattsZoneArray.size();
            selectedArray = &standard.wattsZoneSelectedArray;
        }

    } else if (series == RideFile::aPower && zoned == false) {

        array = &standard.aPowerArray;
        arrayLength = standard.aPowerArray.size();
        selectedArray = &standard.aPowerSelectedArray;

    } else if (series == RideFile::wattsKg && zoned == false) {

        array = &standard.wattsKgArray;
        arrayLength = standard.wattsKgArray.size();
        selectedArray = &standard.wattsKgSelectedArray;

    } else if (series == RideFile::nm) {

        array = &standard.nmArray;
        arrayLength = standard.nmArray.size();
        selectedArray = &standard.nmSelectedArray;

    } else if (series == RideFile::hr && zoned == false) {

        array = &standard.hrArray;
        arrayLength = standard.hrArray.size();
        selectedArray = &standard.hrSelectedArray;

    } else if (series == RideFile::hr && zoned == true) {

        if (cpzoned) {
            array = &standard.hrCPZoneArray;
            arrayLength = standard.hrCPZoneArray.size();
            selectedArray = &standard.hrCPZoneSelectedArray;
        } else {
            array = &standard.hrZoneArray;
            arrayLength = standard.hrZoneArray.size();
            selectedArray = &standard.hrZoneSelectedArray;
        }

    } else if (series == RideFile::kph && !(zoned == true && (!rideItem || rideItem->isRun || rideItem->isSwim))) {

        array = &standard.kphArray;
        arrayLength = standard.kphArray.size();
        selectedArray = &standard.kphSelectedArray;

    } else if (series == RideFile::kph && zoned == true && (!rideItem || rideItem->isRun || rideItem->isSwim)) {

        if (cpzoned) {
            array = &standard.paceCPZoneArray;
            arrayLength = standard.paceCPZoneArray.size();
            selectedArray = &standard.paceCPZoneSelectedArray;
        } else {
            array = &standard.paceZoneArray;
            arrayLength = standard.paceZoneArray.size();
            selectedArray = &standard.paceZoneSelectedArray;
        }

    } else if (series == RideFile::smo2) {
        array = &standard.smo2Array;
        arrayLength = standard.smo2Array.size();
        selectedArray = &standard.smo2SelectedArray;

    } else if (series == RideFile::gear) {
        array = &standard.gearArray;
        arrayLength = standard.gearArray.size();
        selectedArray = &standard.gearSelectedArray;

    } else if (series == RideFile::cad) {
        array = &standard.cadArray;
        arrayLength = standard.cadArray.size();
        selectedArray = &standard.cadSelectedArray;
    }

    // binning of data when not zoned
    if (!isZoningEnabled()) {

        // we add a bin on the end since the last "incomplete" bin
        // will be dropped otherwise
        int count = qMax(0, int(ceil((arrayLength - 1) / (binw)))+1);

        // allocate space for data, plus beginning and ending point
        x.resize(count+2);
        y.resize(count+2);
        sy.resize(count+2);
        x.fill(0.0);
        y.fill(0.0);
        sx.fill(0.0);

        int i;
        for (i = 1; i <= count; ++i) {
            double high = i * round(binw/delta);
            double low = high - round(binw/delta);
            if (low==0 && !withz) low++;
            x[i] = high*delta;
            y[i]  = 1e-9;  // nonzero to accommodate log plot
            sy[i] = 1e-9;  // nonzero to accommodate log plot
            while (low < high && low<arrayLength) {
                if (selectedArray && (*selectedArray).size()>low)
                    sy[i] += dt * (*selectedArray)[low];
                y[i] += dt * (*array)[low++];
            }
        }
        y[i] = 1e-9;       // nonzero to accommodate log plot
        sy[i] = 1e-9;       // nonzero to accommodate log plot
        x[i] = i * delta * binw;
        y[0] = 1e-9;
        sy[0] = 1e-9;
        x[0] = 0;

        // convert vectors from absolute time to percentage
        // if the user has selected that
        if (!absolutetime) {
            percentify(y, 1);
            percentify(sy, 1);
        }

        // the selected parameter values are the same as the 
        // actual values
        sx = x;

    } else {

        // we're not binning instead we are prettyfing the columnar
        // display in much the same way as the weekly summary workds
        // Each zone column will have 4 points
        x.resize(array->size() * 4);
        y.resize(array->size() * 4);
        sx.resize(selectedArray->size() * 4);
        sy.resize(selectedArray->size() * 4);

        // so we can calculate percentage for the labels
        double total=0;
        for (int i=0; i<array->size(); i++) total += dt * (double)(*array)[i];

        // samples to time
        for (int i=0, offset=0; i<array->size(); i++) {

            double xn = (double) i - (0.625f / 2.0f);
            double yn = dt * (double)(*array)[i];

            x[offset] = xn;
            y[offset] = 0;
            offset++;
            x[offset] = xn;
            y[offset] = yn;
            offset++;
            x[offset] = xn+0.625;
            y[offset] = yn;
            offset++;
            x[offset] = xn +0.625;
            y[offset] = 0;
            offset++;
        }

        for (int i=0, offset=0; i<selectedArray->size(); i++) {
            double xn = (double)i - (0.625f / 2.0f);
            double yn = dt * (double)(*selectedArray)[i];

            sx[offset] = xn;
            sy[offset] = 0;
            offset++;
            sx[offset] = xn;
            sy[offset] = yn;
            offset++;
            sx[offset] = xn+0.625;
            sy[offset] = yn;
            offset++;
            sx[offset] = xn +0.625;
            sy[offset] = 0;
            offset++;

        }

        if (!absolutetime) {
            percentify(y, 2);
            percentify(sy, 2);
        }
    }
}

void
PowerHist::setYMax()
{
    double MaxY=0;

    if (!rangemode && context->isCompareIntervals) {
        int i=0;
        foreach (QwtPlotCurve *p, compareCurves) {

            // if its not visible don't set for it
            if (context->compareIntervals[i].isChecked()) {
                double my = p->maxYValue();
                if (my > MaxY) MaxY = my;
            }
            i++;
        }

    } else if (rangemode && context->isCompareDateRanges) {

        int i=0;
        foreach (QwtPlotCurve *p, compareCurves) {

            // if its not visible don't set for it
            if (context->compareDateRanges[i].isChecked()) {
                double my = p->maxYValue();
                if (my > MaxY) MaxY = my;
            }
            i++;
        }

    } else {

        MaxY = curve->maxYValue();
        if (MaxY < curveSelected->maxYValue()) MaxY = curveSelected->maxYValue();

    }

    static const double tmin = 1.0/60;
    setAxisScale(yLeft, (lny ? tmin : 0.0), MaxY * 1.1);

    QwtScaleDraw *sd = new QwtScaleDraw;
    sd->setTickLength(QwtScaleDiv::MajorTick, 3);
    sd->enableComponent(QwtScaleDraw::Ticks, false);
    sd->enableComponent(QwtScaleDraw::Backbone, false);
    setAxisScaleDraw(QwtPlot::yLeft, sd);
}

static void
longFromDouble(QVector<unsigned int>&here, QVector<double>&there)
{
    int highest = 0;
    here.resize(there.size());
    for (int i=0; i<here.size(); i++) {
        here[i] = there[i];
        if (here[i] != 0) highest = i;
    }
    here.resize(highest);
}

void
PowerHist::setData(RideFileCache *cache)
{
    source = Cache;
    this->cache = cache;
    dt = 1.0f / 60.0f; // rideFileCache is normalised to 1secs

    // we set with this data already?
    if (cache == LASTcache && source == LASTsource) return;

    // Now go set all those tedious arrays from
    // the ride cache
    standard.wattsArray.resize(0);
    standard.wattsZoneArray.resize(10);
    standard.wattsCPZoneArray.resize(3);
    standard.hrZoneArray.resize(10);
    standard.hrCPZoneArray.resize(3);
    standard.wattsKgArray.resize(0);
    standard.aPowerArray.resize(0);
    standard.nmArray.resize(0);
    standard.hrArray.resize(0);
    standard.kphArray.resize(0);
    standard.paceZoneArray.resize(10);
    standard.paceCPZoneArray.resize(3);
    standard.gearArray.resize(0);
    standard.smo2Array.resize(0);
    standard.cadArray.resize(0);
    standard.wbalArray.resize(0);
    standard.wbalZoneArray.resize(4);

    // we do not use the selected array since it is
    // not meaningful to overlay interval selection
    // with long term data
    standard.wattsSelectedArray.resize(0);
    standard.wattsZoneSelectedArray.resize(0);
    standard.wattsKgSelectedArray.resize(0);
    standard.aPowerSelectedArray.resize(0);
    standard.nmSelectedArray.resize(0);
    standard.hrSelectedArray.resize(0);
    standard.hrZoneSelectedArray.resize(0);
    standard.kphSelectedArray.resize(0);
    standard.paceZoneSelectedArray.resize(0);
    standard.gearSelectedArray.resize(0);
    standard.smo2SelectedArray.resize(0);
    standard.cadSelectedArray.resize(0);
    standard.wbalSelectedArray.resize(0);
    standard.wbalZoneSelectedArray.resize(0);

    longFromDouble(standard.wattsArray, cache->distributionArray(RideFile::watts));
    longFromDouble(standard.wattsKgArray, cache->distributionArray(RideFile::wattsKg));
    longFromDouble(standard.aPowerArray, cache->distributionArray(RideFile::aPower));
    longFromDouble(standard.hrArray, cache->distributionArray(RideFile::hr));
    longFromDouble(standard.nmArray, cache->distributionArray(RideFile::nm));
    longFromDouble(standard.cadArray, cache->distributionArray(RideFile::cad));
    longFromDouble(standard.gearArray, cache->distributionArray(RideFile::gear));
    longFromDouble(standard.smo2Array, cache->distributionArray(RideFile::smo2));
    longFromDouble(standard.kphArray, cache->distributionArray(RideFile::kph));
    longFromDouble(standard.wbalArray, cache->distributionArray(RideFile::wbal));

    if (!context->athlete->useMetricUnits) {
        double torque_factor = (context->athlete->useMetricUnits ? 1.0 : 0.73756215);
        double speed_factor  = (context->athlete->useMetricUnits ? 1.0 : 0.62137119);

        for(int i=0; i<standard.nmArray.size(); i++) standard.nmArray[i] = standard.nmArray[i] * torque_factor;
        for(int i=0; i<standard.kphArray.size(); i++) standard.kphArray[i] = standard.kphArray[i] * speed_factor;
    }

    // zone array
    for (int i=0; i<10; i++) {
        standard.wattsZoneArray[i] = cache->wattsZoneArray()[i];
        standard.hrZoneArray[i] = cache->hrZoneArray()[i];
        standard.paceZoneArray[i] = cache->paceZoneArray()[i];
    }

    // polarised zones
    standard.wattsCPZoneArray[0] = cache->wattsCPZoneArray()[1];
    standard.hrCPZoneArray[0] = cache->hrCPZoneArray()[1];
    standard.paceCPZoneArray[0] = cache->paceCPZoneArray()[1];
    if (withz) {
        standard.wattsCPZoneArray[0] += cache->wattsCPZoneArray()[0]; // add in zero watts
        standard.hrCPZoneArray[0] += cache->hrCPZoneArray()[0]; // add in zero bpm
        standard.paceCPZoneArray[0] += cache->paceCPZoneArray()[0]; // add in zero kph
    }
    standard.wattsCPZoneArray[1] = cache->wattsCPZoneArray()[2];
    standard.hrCPZoneArray[1] = cache->hrCPZoneArray()[2];
    standard.paceCPZoneArray[1] = cache->paceCPZoneArray()[2];
    standard.wattsCPZoneArray[2] = cache->wattsCPZoneArray()[3];
    standard.hrCPZoneArray[2] = cache->hrCPZoneArray()[3];
    standard.paceCPZoneArray[2] = cache->paceCPZoneArray()[3];

    // w'bal zones
    for(int i=0; i<4; i++) standard.wbalZoneArray[i] = cache->wbalZoneArray()[i];

    curveSelected->hide();
    curveHover->hide();
}

void
PowerHist::intervalHover(IntervalItem *x)
{
    // telling me to hide
    if (x == NULL) {
        curveHover->hide();
        return;
    }

    if (!isVisible()) return; // noone can see us

    if ((rangemode && context->isCompareDateRanges)
        || (!rangemode && context->isCompareIntervals)) return; // not in compare mode

    if (rideItem && rideItem->ride()) {

        // set data
        HistData hoverData;
        setArraysFromRide(rideItem->ride(), hoverData, context->athlete->zones(), x);

        // set curve
        QVector<double>x,y,sx,sy;
        binData(hoverData, x,y,sx,sy);

        curveHover->setSamples(sx,sy);
        curveHover->show();

        replot();
    }
}

void
PowerHist::setDataFromCompare()
{
    // not for metric plots sonny
    if (source == Metric) return;

    double width = appsettings->value(this, GC_LINEWIDTH, 0.5).toDouble();

    // set all the curves based upon whats in the compare intervals array
    // first lets clear the old data
    compareData.clear();

    // and remove the old curves
    foreach(QwtPlotCurve *x, compareCurves) {
        x->detach();
        delete x;
    }
    compareCurves.clear();

    // now lets setup a HistData for each CompareInterval
    for(int intervalNumber=0; (rangemode && intervalNumber < context->compareDateRanges.count()) ||
                 (!rangemode && intervalNumber < context->compareIntervals.count()) ; intervalNumber++) {

        // get the bits we need from either the interval or daterange compare object
        RideFileCache *s = rangemode ? context->compareDateRanges[intervalNumber].rideFileCache() :
                                       context->compareIntervals[intervalNumber].rideFileCache();

        QColor color = rangemode ? context->compareDateRanges[intervalNumber].color :
                                       context->compareIntervals[intervalNumber].color;

        bool ischecked = rangemode ? context->compareDateRanges[intervalNumber].isChecked() :
                                       context->compareIntervals[intervalNumber].isChecked();

        // set the data even if not checked
        HistData add;

        // Now go set all those tedious arrays from
        // the ride cache
        add.wattsArray.resize(0);
        add.wattsZoneArray.resize(10);
        add.wattsCPZoneArray.resize(3);
        add.wattsKgArray.resize(0);
        add.aPowerArray.resize(0);
        add.nmArray.resize(0);
        add.hrArray.resize(0);
        add.hrZoneArray.resize(10);
        add.hrCPZoneArray.resize(3);
        add.kphArray.resize(0);
        add.paceZoneArray.resize(10);
        add.paceCPZoneArray.resize(3);
        add.gearArray.resize(0);
        add.smo2Array.resize(0);
        add.cadArray.resize(0);
        add.wbalArray.resize(0);
        add.wbalZoneArray.resize(4);

        longFromDouble(add.wattsArray, s->distributionArray(RideFile::watts));
        longFromDouble(add.wattsKgArray, s->distributionArray(RideFile::wattsKg));
        longFromDouble(add.aPowerArray, s->distributionArray(RideFile::aPower));
        longFromDouble(add.hrArray, s->distributionArray(RideFile::hr));
        longFromDouble(add.nmArray, s->distributionArray(RideFile::nm));
        longFromDouble(add.gearArray, s->distributionArray(RideFile::gear));
        longFromDouble(add.smo2Array, s->distributionArray(RideFile::smo2));
        longFromDouble(add.cadArray, s->distributionArray(RideFile::cad));
        longFromDouble(add.kphArray, s->distributionArray(RideFile::kph));
        longFromDouble(add.wbalArray, s->distributionArray(RideFile::wbal));

        // always 0-100 percent minimum
        if (add.wbalArray.size() < 101) add.wbalArray.resize(101);

        // convert for metric imperial types
        if (!context->athlete->useMetricUnits) {
            double torque_factor = (context->athlete->useMetricUnits ? 1.0 : 0.73756215);
            double speed_factor  = (context->athlete->useMetricUnits ? 1.0 : 0.62137119);

            for(int i=0; i<add.nmArray.size(); i++) add.nmArray[i] = add.nmArray[i] * torque_factor;
            for(int i=0; i<add.kphArray.size(); i++) add.kphArray[i] = add.kphArray[i] * speed_factor;
        }

        // zone array
        for (int i=0; i<10; i++) {
            add.wattsZoneArray[i] = s->wattsZoneArray()[i];
            add.hrZoneArray[i] = s->hrZoneArray()[i];
            add.paceZoneArray[i] = s->paceZoneArray()[i];
        }
        // polarised zones
        add.wattsCPZoneArray[0] = s->wattsCPZoneArray()[1];
        add.hrCPZoneArray[0] = s->hrCPZoneArray()[1];
        add.paceCPZoneArray[0] = s->paceCPZoneArray()[1];
        if (withz) {
            add.wattsCPZoneArray[0] += s->wattsCPZoneArray()[0]; // add in zero watts
            add.hrCPZoneArray[0] += s->hrCPZoneArray()[0]; // add in zero bpm
            add.paceCPZoneArray[0] += s->paceCPZoneArray()[0]; // add in zero kph
        }
        add.wattsCPZoneArray[1] = s->wattsCPZoneArray()[2];
        add.hrCPZoneArray[1] = s->hrCPZoneArray()[2];
        add.paceCPZoneArray[1] = s->paceCPZoneArray()[2];
        add.wattsCPZoneArray[2] = s->wattsCPZoneArray()[3];
        add.hrCPZoneArray[2] = s->hrCPZoneArray()[3];
        add.paceCPZoneArray[2] = s->paceCPZoneArray()[3];

        // w'bal zones
        for(int i=0; i<4; i++) add.wbalZoneArray[i] = s->wbalZoneArray()[i];

        // add to the list
        compareData << add;

        // now add a curve for recalc to play with
        QwtPlotCurve *newCurve = new QwtPlotCurve("");
        newCurve->setStyle(QwtPlotCurve::Steps);

        if (appsettings->value(this, GC_ANTIALIAS, true).toBool()==true)
            newCurve->setRenderHint(QwtPlotItem::RenderAntialiased);

        // curve has no brush .. too confusing...
        QPen pen;
        pen.setColor(color);
        pen.setWidth(width);
        newCurve->setPen(pen);

        QColor brush_color = color;
        if (rangemode) brush_color.setAlpha(GColor(CTRENDPLOTBACKGROUND) == QColor(Qt::white) ? 120 : 200);
        else brush_color.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 120 : 200);
        QColor brush_color1 = brush_color.darker();
        //QLinearGradient linearGradient(0, 0, 0, height());
        //linearGradient.setColorAt(0.0, brush_color);
        //linearGradient.setColorAt(1.0, brush_color1);
        //linearGradient.setSpread(QGradient::PadSpread);
        newCurve->setBrush(brush_color1);   // fill below the line

        // hide and show, but always attach
        newCurve->setVisible(ischecked);
        newCurve->attach(this);

        // we do want a legend in compare mode
        //XXX newCurve->setItemAttribute(QwtPlotItem::Legend, true);

        // add to our collection
        compareCurves << newCurve;
    }

    // show legend in compare mode
    //XXX legend()->show();
    //XXX updateLegend();
}

void
PowerHist::setComparePens()
{
    // no compare? don't bother
    if ((!rangemode && !context->isCompareIntervals) ||
        (rangemode && !context->isCompareDateRanges)) return;

    double width = appsettings->value(this, GC_LINEWIDTH, 0.5).toDouble();
    for (int i=0; (!rangemode && i<context->compareIntervals.count()) ||
                  (rangemode && i<context->compareDateRanges.count()); i++) {

        if (!isZoningEnabled()) {

            // NOT ZONED
            if (compareCurves.count() > i) {

                // set pen back
                QPen pen;
                pen.setColor(rangemode ? context->compareDateRanges[i].color :
                                         context->compareIntervals[i].color);
                pen.setWidth(width);
                compareCurves[i]->setPen(pen);
            }

        } else {

            if (compareCurves.count() > i) {

                // set pen back
                compareCurves[i]->setPen(QPen(Qt::NoPen));
            }
        }
    }
}

// set the metric arrays up for each date range using the
// summary metrics in the Context::CompareDateRange array
void
PowerHist::setDataFromCompare(QString totalMetric, QString distMetric)
{

    // set the data for each compare date range using
    // the metric results in the compare data range
    // and create the HistData and curves associated
    double width = appsettings->value(this, GC_LINEWIDTH, 0.5).toDouble();

    // remove old data and curves
    compareData.clear();
    foreach(QwtPlotCurve *x, compareCurves) {
        x->detach();
        delete x;
    }
    compareCurves.clear();

    // now lets setup a HistData for each CompareDate Range
    foreach(CompareDateRange cd, context->compareDateRanges) {

        // set the data even if not checked
        HistData add;

        // set the specification
        FilterSet fs;
        fs.addFilter(context->isfiltered, context->filters);
        fs.addFilter(context->ishomefiltered, context->homeFilters);
        Specification spec;
        spec.setDateRange(DateRange(cd.start,cd.end));
        spec.setFilterSet(fs);

        // set it from the metric
        setData(spec,  totalMetric, distMetric, &add);

        // add to the list
        compareData << add;

        // now add a curve for recalc to play with
        QwtPlotCurve *newCurve = new QwtPlotCurve("");
        newCurve->setStyle(QwtPlotCurve::Steps);

        if (appsettings->value(this, GC_ANTIALIAS, true).toBool()==true)
            newCurve->setRenderHint(QwtPlotItem::RenderAntialiased);

        // curve has no brush .. too confusing...
        QPen pen;
        pen.setColor(cd.color);
        pen.setWidth(width);
        newCurve->setPen(pen);

        QColor brush_color = cd.color;
        if (rangemode) brush_color.setAlpha(GColor(CTRENDPLOTBACKGROUND) == QColor(Qt::white) ? 120 : 200);
        else brush_color.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 120 : 200);
        QColor brush_color1 = brush_color.darker();
        //QLinearGradient linearGradient(0, 0, 0, height());
        //linearGradient.setColorAt(0.0, brush_color);
        //linearGradient.setColorAt(1.0, brush_color1);
        //linearGradient.setSpread(QGradient::PadSpread);
        newCurve->setBrush(brush_color1);   // fill below the line

        // hide and show, but always attach
        newCurve->setVisible(cd.isChecked());
        newCurve->attach(this);

        // we do want a legend in compare mode
        //XXX newCurve->setItemAttribute(QwtPlotItem::Legend, true);

        // add to our collection
        compareCurves << newCurve;
    }
}

void 
PowerHist::setData(Specification specification, QString totalMetric, QString distMetric, HistData *data)
{
    // what metrics are we plotting?
    source = Metric;
    const RideMetricFactory &factory = RideMetricFactory::instance();
    const RideMetric *m = factory.rideMetric(distMetric);
    const RideMetric *tm = factory.rideMetric(totalMetric);
    if (m == NULL || tm == NULL) return;

    // metricX, metricY
    metricX = distMetric;
    metricY = totalMetric;

    // how big should the array be?
    double multiplier = pow(10, m->precision());
    double max = 0, min = 0;

    // LOOP THRU VALUES -- REPEATED WITH CUT AND PASTE BELOW
    // SO PLEASE MAKE SAME CHANGES TWICE (SORRY)
    foreach(RideItem *x, context->athlete->rideCache->rides()) { 

        // not passed
        if (!specification.pass(x)) continue;

        // get computed value
        double v = x->getForSymbol(distMetric, context->athlete->useMetricUnits);

        // ignore no temp files
        if ((distMetric == "average_temp" || distMetric == "max_temp") && v == RideFile::NoTemp) continue;

        // clean up dodgy values
        if (std::isnan(v) || std::isinf(v)) v = 0;

        // seconds to minutes
        if (m->units(context->athlete->useMetricUnits) == "seconds" ||
            m->units(context->athlete->useMetricUnits) == tr("seconds")) v /= 60;

        // apply multiplier
        v *= multiplier;

        if (v>max) max = v;
        if (v<min) min = v;
    }

    // lets truncate the data if there are very high
    // or very low max/min values, to ensure we don't exhaust memory
    if (max > 100000) max = 100000;
    if (min < -100000) min = -100000;

    // now run thru the data again, but this time
    // populate the metricArray
    // we add 1 to account for possible rounding up
    data->metricArray.resize(1 + (int)(max)-(int)(min));
    data->metricArray.fill(0);

    // LOOP THRU VALUES -- REPEATED WITH CUT AND PASTE ABOVE
    // SO PLEASE MAKE SAME CHANGES TWICE (SORRY)
    foreach(RideItem *x, context->athlete->rideCache->rides()) { 

        // does it match
        if (!specification.pass(x)) continue;

        // get computed value
        double v = x->getForSymbol(distMetric, context->athlete->useMetricUnits);

        // ignore no temp files
        if ((distMetric == "average_temp" || distMetric == "max_temp") && v == RideFile::NoTemp) continue;

        // clean up dodgy values
        if (std::isnan(v) || std::isinf(v)) v = 0;

        // seconds to minutes
        if (m->units(context->athlete->useMetricUnits) == "seconds" ||
            m->units(context->athlete->useMetricUnits) == tr("seconds")) v /= 60;

        // apply multiplier
        v *= multiplier;

        // ignore out of bounds data
        if ((int)(v)<min || (int)(v)>max) continue;

        // increment value, are inititialised to zero above
        // there will be some loss of precision due to totalising
        // a double in an int, but frankly that should be minimal
        // since most values of note are integer based anyway.
        double t = x->getForSymbol(totalMetric, context->athlete->useMetricUnits);

        // totalise in minutes
        if (tm->units(context->athlete->useMetricUnits) == "seconds" ||
            tm->units(context->athlete->useMetricUnits) == tr("seconds")) t /= 60;

        // sum up
        data->metricArray[(int)(v)-min] += t;
    }

    // we certainly don't want the interval curve when plotting
    // metrics across rides!
    curveSelected->hide();

    // now set all the plot parameters to match the data
    source = Metric;
    zoned = false;
    rideItem = NULL;
    lny = false;
    shade = false;
    withz = false;
    dt = 1;
    absolutetime = true;

    // and the plot itself
    QString yunits = tm->units(context->athlete->useMetricUnits);
    if (yunits == "seconds" || yunits == tr("seconds")) yunits = tr("minutes");
    QString xunits = m->units(context->athlete->useMetricUnits);
    if (xunits == "seconds" || xunits == tr("seconds")) xunits = tr("minutes");

    if (tm->units(context->athlete->useMetricUnits) != "")
        setAxisTitle(yLeft, QString(tr("Total %1 (%2)")).arg(tm->name()).arg(yunits));
    else
        setAxisTitle(yLeft, QString(tr("Total %1")).arg(tm->name()));

    if (m->units(context->athlete->useMetricUnits) != "")
        setAxisTitle(xBottom, QString(tr("%1 of Activity (%2)")).arg(m->name()).arg(xunits));
    else
        setAxisTitle(xBottom, QString(tr("%1 of Activity")).arg(m->name()));

    // dont show legend in metric mode
    //XXX legend()->hide();
    //XXX updateLegend();
}

void
PowerHist::updatePlot()
{
    replot();
    zoomer->setZoomBase();
}

void
PowerHist::setData(RideItem *_rideItem, bool force)
{
    source = Ride;

    // hide hover curve just in case its there
    // from previous ride
    curveHover->hide();

    // we set with this data already
    if (force == false && _rideItem == LASTrideItem && source == LASTsource) return;

    rideItem = _rideItem;
    if (!rideItem) return;

    RideFile *ride = rideItem->ride();

    bool hasData = ((series == RideFile::watts || series == RideFile::wattsKg) && ride->areDataPresent()->watts) ||
                   (series == RideFile::nm && ride->areDataPresent()->nm) ||
                   (series == RideFile::kph && ride->areDataPresent()->kph) ||
                   (series == RideFile::gear && ride->areDataPresent()->gear) ||
                   (series == RideFile::smo2 && ride->areDataPresent()->smo2) ||
                   (series == RideFile::cad && ride->areDataPresent()->cad) ||
                   (series == RideFile::aPower && ride->areDataPresent()->apower) ||
                   (series == RideFile::wbal && ride->areDataPresent()->watts) ||
                   (series == RideFile::hr && ride->areDataPresent()->hr);

    if (ride && hasData) {
        //setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
        setArraysFromRide(ride, standard, context->athlete->zones(), NULL);

    } else {

        // create empty curves when no data
        const double zero = 0;
        curve->setSamples(&zero, &zero, 0);
        curveSelected->setSamples(&zero, &zero, 0);
        updatePlot();
    }
    curveSelected->show();
    zoomer->setZoomBase();

    // dont show legend in metric mode
    //XXX legend()->hide();
    //XXX updateLegend();
}

void
PowerHist::setArraysFromRide(RideFile *ride, HistData &standard, const Zones *zones, IntervalItem *hover)
{
    // predefined deltas for each series
    static const double wattsDelta = 1.0;
    static const double wattsKgDelta = 0.01;
    static const double nmDelta    = 0.1;
    static const double hrDelta    = 1.0;
    static const double kphDelta   = 0.1;
    static const double cadDelta   = 1.0;
    static const double gearDelta  = 0.01; //RideFileCache creates POW(10) * decimals section
    static const double smo2Delta  = 1;
    static const double wbalDelta  = 1;
    static const int maxSize = 4096;

    // recording interval in minutes
    dt = ride->recIntSecs() / 60.0;

    standard.wattsArray.resize(0);
    standard.wattsZoneArray.resize(0);
    standard.wattsCPZoneArray.resize(0);
    standard.wattsKgArray.resize(0);
    standard.aPowerArray.resize(0);
    standard.nmArray.resize(0);
    standard.hrArray.resize(0);
    standard.hrZoneArray.resize(0);
    standard.hrCPZoneArray.resize(0);
    standard.kphArray.resize(0);
    standard.paceZoneArray.resize(0);
    standard.paceCPZoneArray.resize(0);
    standard.gearArray.resize(0);
    standard.smo2Array.resize(0);
    standard.cadArray.resize(0);
    standard.wbalArray.resize(0);
    standard.wbalZoneArray.resize(0);

    standard.wattsSelectedArray.resize(0);
    standard.wattsZoneSelectedArray.resize(0);
    standard.wattsKgSelectedArray.resize(0);
    standard.aPowerSelectedArray.resize(0);
    standard.nmSelectedArray.resize(0);
    standard.hrSelectedArray.resize(0);
    standard.hrZoneSelectedArray.resize(0);
    standard.kphSelectedArray.resize(0);
    standard.paceZoneSelectedArray.resize(0);
    standard.gearSelectedArray.resize(0);
    standard.smo2SelectedArray.resize(0);
    standard.cadSelectedArray.resize(0);
    standard.wbalSelectedArray.resize(0);
    standard.wbalZoneSelectedArray.resize(0);

    // unit conversion factor for imperial units for selected parameters
    double torque_factor = (context->athlete->useMetricUnits ? 1.0 : 0.73756215);
    double speed_factor  = (context->athlete->useMetricUnits ? 1.0 : 0.62137119);

    // cp and zones
    int zoneRange = zones ? zones->whichRange(ride->startTime().date()) : -1;
    int CP = zoneRange != -1 ? zones->getCP(zoneRange) : 0;
    double WPRIME = zoneRange != -1 ? zones->getWprime(zoneRange) : 22000;

    int hrZoneRange = context->athlete->hrZones() ? context->athlete->hrZones()->whichRange(ride->startTime().date()) : -1;
    int LTHR = hrZoneRange != -1 ? context->athlete->hrZones()->getLT(hrZoneRange) : 0;

    int paceZoneRange = context->athlete->paceZones(ride->isSwim()) ? context->athlete->paceZones(ride->isSwim())->whichRange(ride->startTime().date()) : -1;
    double CV = (paceZoneRange != -1) ? context->athlete->paceZones(ride->isSwim())->getCV(paceZoneRange) : 0.0;

    // get it from the wprimeData()->ydata() if we're plotting
    // w'bal, otherwise its from the ride data
    if (series == RideFile::wbal) {

        // always do 0-100%
        standard.wbalArray.resize(101);
        standard.wbalSelectedArray.resize(101);

        // fill with zeroes
        standard.wbalZoneArray.resize(4);
        standard.wbalZoneSelectedArray.resize(4);

        // t is time in seconds
        for(int t=0; t< ride->wprimeData()->ydata().count(); t++) {

            // get the value
            double value = ride->wprimeData()->ydata()[t];

            // percent left
            double percent = 100.0f - ((double (value) / WPRIME) * 100.0f);
            if (percent < 0.0f) percent = 0.0f;
            if (percent > 100.0f) percent = 100.0f;

            // is this point selected in an interval ?
            bool selected = false; 
            if (hover && t >= hover->start && t <= hover->stop) selected = true; 
            if (!selected) selected = isSelected(t, ride->recIntSecs());

            // watts array
            int wbalIndex = int(floor(percent / wbalDelta));
            if (wbalIndex >= 0 && wbalIndex < maxSize) {
                if (wbalIndex >= standard.wbalArray.size()) {
                    standard.wbalArray.resize(wbalIndex + 1);
                    standard.wbalArray[wbalIndex]=0;
                }
                standard.wbalArray[wbalIndex]++;

                if (selected) {
                    if (wbalIndex >= standard.wbalSelectedArray.size()) {
                        standard.wbalSelectedArray.resize(wbalIndex + 1);
                        standard.wbalSelectedArray[wbalIndex] = 0;
                    }
                    standard.wbalSelectedArray[wbalIndex]++;
                }
            }

            // zones
            if (percent < 25.0f) standard.wbalZoneArray[0] += ride->recIntSecs();
            else if (percent < 50.0f) standard.wbalZoneArray[1] += ride->recIntSecs();
            else if (percent < 75.0f) standard.wbalZoneArray[2] += ride->recIntSecs();
            else if (percent >= 75.0f) standard.wbalZoneArray[3] += ride->recIntSecs();

            if (selected) {
                if (percent < 25.0f) standard.wbalZoneSelectedArray[0] += ride->recIntSecs();
                else if (percent < 50.0f) standard.wbalZoneSelectedArray[1] += ride->recIntSecs();
                else if (percent < 75.0f) standard.wbalZoneSelectedArray[2] += ride->recIntSecs();
                else if (percent >= 75.0f) standard.wbalZoneSelectedArray[3] += ride->recIntSecs();
            }
        }

    } else {

        foreach(const RideFilePoint *p1, ride->dataPoints()) {

            // selected if hovered -or- selected depending on
            // whether we were passed a blank or real RideFileInterval
            bool selected = false; 
            if (hover) {
                if (p1->secs >= hover->start && p1->secs <= hover->stop) { selected = true; }
            } else {
                selected = isSelected(p1, ride->recIntSecs());
            }

            // watts array
            int wattsIndex = int(floor(p1->watts / wattsDelta));
            if (wattsIndex >= 0 && wattsIndex < maxSize) {
                if (wattsIndex >= standard.wattsArray.size())
                    standard.wattsArray.resize(wattsIndex + 1);
                standard.wattsArray[wattsIndex]++;

                if (selected) {
                    if (wattsIndex >= standard.wattsSelectedArray.size())
                        standard.wattsSelectedArray.resize(wattsIndex + 1);
                    standard.wattsSelectedArray[wattsIndex]++;
                }
            }

            // watts zoned array
            // Only calculate zones if we have a valid range and check zeroes
            if (zoneRange > -1 && (withz || (!withz && p1->watts))) {

                // cp zoned
                if (standard.wattsCPZoneArray.size() < 3) {
                    standard.wattsCPZoneArray.resize(3);
                }

                if (p1->watts < 1 && withz) { // I zero watts
                    standard.wattsCPZoneArray[0] ++;
                } else if (p1->watts < (CP * 0.85f)) { // I
                    standard.wattsCPZoneArray[0] ++;
                } else if (p1->watts < CP) { // II
                    standard.wattsCPZoneArray[1] ++;
                } else { // III
                    standard.wattsCPZoneArray[2] ++;
                }

                // get the zone
                wattsIndex = zones->whichZone(zoneRange, p1->watts);

                // zoned
                if (wattsIndex >= 0 && wattsIndex < maxSize) {
                    if (wattsIndex >= standard.wattsZoneArray.size())
                        standard.wattsZoneArray.resize(wattsIndex + 1);
                    standard.wattsZoneArray[wattsIndex]++;

                    if (selected) {
                        if (wattsIndex >= standard.wattsZoneSelectedArray.size())
                            standard.wattsZoneSelectedArray.resize(wattsIndex + 1);
                        standard.wattsZoneSelectedArray[wattsIndex]++;
                    }
                }
            }

            // aPower array
            int aPowerIndex = int(floor(p1->apower / wattsDelta));
            if (aPowerIndex >= 0 && aPowerIndex < maxSize) {
                if (aPowerIndex >= standard.aPowerArray.size())
                    standard.aPowerArray.resize(aPowerIndex + 1);
                standard.aPowerArray[aPowerIndex]++;

                if (selected) {
                    if (aPowerIndex >= standard.aPowerSelectedArray.size())
                        standard.aPowerSelectedArray.resize(aPowerIndex + 1);
                    standard.aPowerSelectedArray[aPowerIndex]++;
                }
            }

            // wattsKg array
            int wattsKgIndex = int(floor(p1->watts / ride->getWeight() / wattsKgDelta));
            if (wattsKgIndex >= 0 && wattsKgIndex < maxSize) {
                if (wattsKgIndex >= standard.wattsKgArray.size())
                    standard.wattsKgArray.resize(wattsKgIndex + 1);
                standard.wattsKgArray[wattsKgIndex]++;

                if (selected) {
                    if (wattsKgIndex >= standard.wattsKgSelectedArray.size())
                        standard.wattsKgSelectedArray.resize(wattsKgIndex + 1);
                    standard.wattsKgSelectedArray[wattsKgIndex]++;
                }
            }

            int nmIndex = int(floor(p1->nm * torque_factor / nmDelta));
            if (nmIndex >= 0 && nmIndex < maxSize) {
                if (nmIndex >= standard.nmArray.size())
                    standard.nmArray.resize(nmIndex + 1);
                standard.nmArray[nmIndex]++;

                if (selected) {
                    if (nmIndex >= standard.nmSelectedArray.size())
                        standard.nmSelectedArray.resize(nmIndex + 1);
                    standard.nmSelectedArray[nmIndex]++;
                }
            }

            int hrIndex = int(floor(p1->hr / hrDelta));
            if (hrIndex >= 0 && hrIndex < maxSize) {
                if (hrIndex >= standard.hrArray.size())
                    standard.hrArray.resize(hrIndex + 1);
                standard.hrArray[hrIndex]++;

                if (selected) {
                    if (hrIndex >= standard.hrSelectedArray.size())
                        standard.hrSelectedArray.resize(hrIndex + 1);
                    standard.hrSelectedArray[hrIndex]++;
                }
            }

            // hr zoned array
            // Only calculate zones if we have a valid range
            if (hrZoneRange > -1 && (withz || (!withz && p1->hr))) {
                // cp zoned
                if (standard.hrCPZoneArray.size() < 3) {
                    standard.hrCPZoneArray.resize(3);
                }

                if (p1->hr < 1 && withz) { // I zero bpm
                    standard.hrCPZoneArray[0] ++;
                } else if (p1->hr < (LTHR * 0.9f)) { // I
                    standard.hrCPZoneArray[0] ++;
                } else if (p1->hr < LTHR) { // II
                    standard.hrCPZoneArray[1] ++;
                } else { // III
                    standard.hrCPZoneArray[2] ++;
                }

                hrIndex = context->athlete->hrZones()->whichZone(hrZoneRange, p1->hr);

                if (hrIndex >= 0 && hrIndex < maxSize) {
                    if (hrIndex >= standard.hrZoneArray.size())
                        standard.hrZoneArray.resize(hrIndex + 1);
                    standard.hrZoneArray[hrIndex]++;

                    if (selected) {
                        if (hrIndex >= standard.hrZoneSelectedArray.size())
                            standard.hrZoneSelectedArray.resize(hrIndex + 1);
                        standard.hrZoneSelectedArray[hrIndex]++;
                    }
                }
            }

            int kphIndex = int(floor(p1->kph * speed_factor / kphDelta));
            if (kphIndex >= 0 && kphIndex < maxSize) {
                if (kphIndex >= standard.kphArray.size())
                    standard.kphArray.resize(kphIndex + 1);
                standard.kphArray[kphIndex]++;

                if (selected) {
                    if (kphIndex >= standard.kphSelectedArray.size())
                        standard.kphSelectedArray.resize(kphIndex + 1);
                    standard.kphSelectedArray[kphIndex]++;
                }
            }

            // pace zoned array
            // Only calculate zones if we have a running activity with a valid range
            if ((ride->isRun() || ride->isSwim()) && paceZoneRange > -1 && (withz || (!withz && p1->kph))) {
                // cp zoned
                if (standard.paceCPZoneArray.size() < 3) {
                    standard.paceCPZoneArray.resize(3);
                }

                if (p1->kph < 0.1 && withz) { // I zero kph
                    standard.paceCPZoneArray[0] ++;
                } else if (ride->isRun() && p1->kph < (CV * 0.9f)) { // I Run
                    standard.paceCPZoneArray[0] ++;
                } else if (ride->isSwim() && p1->kph < (CV * 0.975f)) { // I Swim
                    standard.paceCPZoneArray[0] ++;
                } else if (p1->kph < CV) { // II
                    standard.paceCPZoneArray[1] ++;
                } else { // III
                    standard.paceCPZoneArray[2] ++;
                }

                kphIndex = context->athlete->paceZones(ride->isSwim())->whichZone(paceZoneRange, p1->kph);

                if (kphIndex >= 0 && kphIndex < maxSize) {
                    if (kphIndex >= standard.paceZoneArray.size())
                        standard.paceZoneArray.resize(kphIndex + 1);
                    standard.paceZoneArray[kphIndex]++;

                    if (selected) {
                        if (kphIndex >= standard.paceZoneSelectedArray.size())
                            standard.paceZoneSelectedArray.resize(kphIndex + 1);
                        standard.paceZoneSelectedArray[kphIndex]++;
                    }
                }
            }

            int smo2Index = int(floor(p1->smo2 / smo2Delta));
            if (smo2Index >= 0 && smo2Index < maxSize) {
                if (smo2Index >= standard.smo2Array.size())
                    standard.smo2Array.resize(smo2Index + 1);
                standard.smo2Array[smo2Index]++;

                if (selected) {
                    if (smo2Index >= standard.smo2SelectedArray.size())
                        standard.smo2SelectedArray.resize(smo2Index + 1);
                    standard.smo2SelectedArray[smo2Index]++;
                }
            }

            int gearIndex = int(floor(p1->gear / gearDelta));
            if (gearIndex >= 0 && gearIndex < maxSize) {
                if (gearIndex >= standard.gearArray.size())
                    standard.gearArray.resize(gearIndex + 1);
                standard.gearArray[gearIndex]++;

                if (selected) {
                    if (gearIndex >= standard.gearSelectedArray.size())
                        standard.gearSelectedArray.resize(gearIndex + 1);
                    standard.gearSelectedArray[gearIndex]++;
                }
            }

            int cadIndex = int(floor(p1->cad / cadDelta));
            if (cadIndex >= 0 && cadIndex < maxSize) {
                if (cadIndex >= standard.cadArray.size())
                    standard.cadArray.resize(cadIndex + 1);
                standard.cadArray[cadIndex]++;

                if (selected) {
                    if (cadIndex >= standard.cadSelectedArray.size())
                        standard.cadSelectedArray.resize(cadIndex + 1);
                    standard.cadSelectedArray[cadIndex]++;
                }
            }
        }
    }
}

void
PowerHist::setBinWidth(double value)
{
    if (!value) value = 1; // binwidth must be nonzero
    binw = value;

    // hide the hover curve as it will now be wrong
    if (!rangemode) curveHover->hide();
}

void
PowerHist::setCPZoned(bool value)
{
    cpzoned = value;
    setComparePens();
}

void
PowerHist::setZoned(bool value)
{
    zoned = value;
    setComparePens();
}

void
PowerHist::setWithZeros(bool value)
{
    withz = value;
}

void
PowerHist::setlnY(bool value)
{
    // note: setAxisScaleEngine deletes the old ScaleEngine, so specifying
    // "new" in the argument list is not a leak

    lny=value;
    if (lny && !zoned) {

        setAxisScaleEngine(yLeft, new QwtLogScaleEngine);
        curve->setBaseline(1e-6);
        curveSelected->setBaseline(1e-6);

    } else {

        setAxisScaleEngine(yLeft, new QwtLinearScaleEngine);
        curve->setBaseline(0);
        curveSelected->setBaseline(0);

    }
    setYMax();
    updatePlot();
}

void
PowerHist::setSumY(bool value)
{
    absolutetime = value;
    setParameterAxisTitle();
}

void
PowerHist::setParameterAxisTitle()
{
    QString axislabel;
    switch (series) {

        case RideFile::wbal:
            if (zoned) axislabel = tr("W'bal zone");
            else axislabel = tr("W'Bal Consumed (%)");
            break;

        case RideFile::watts:
            if (zoned) axislabel = tr("Power zone");
            else axislabel = tr("Power (watts)");
            break;

        case RideFile::wattsKg:
            if (zoned) axislabel = tr("Power zone");
            else axislabel = tr("Power (watts/kg)");
            break;

        case RideFile::hr:
            if (zoned) axislabel = tr("Heartrate zone");
            else axislabel = tr("Heartrate (bpm)");
            break;

        case RideFile::aPower:
            axislabel = tr("aPower (watts)");
            break;

        case RideFile::cad:
            axislabel = tr("Cadence (rpm)");
            break;

        case RideFile::kph:
            if (zoned && (!rideItem || rideItem->isRun || rideItem->isSwim))
                axislabel = tr("Pace zone");
            else
                axislabel = QString(tr("Speed (%1)")).arg(context->athlete->useMetricUnits ? tr("kph") : tr("mph"));
            break;

        case RideFile::nm:
            axislabel = QString(tr("Torque (%1)")).arg(context->athlete->useMetricUnits ? tr("N-m") : tr("ft-lbf"));
            break;

        case RideFile::gear:
            axislabel = tr("Gear Ratio");
            break;

        case RideFile::smo2:
            axislabel = tr("SmO2");
            break;

        default:
            axislabel = QString(tr("Unknown data series"));
            break;
    }
    setAxisTitle(xBottom, axislabel);
    setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)"));
}

void
PowerHist::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
PowerHist::setSeries(RideFile::SeriesType x)
{
    // user selected a different series to plot
    series = x;
    configChanged(CONFIG_APPEARANCE); // set colors
    setParameterAxisTitle();
}

bool PowerHist::shadeZones() const
{
    return (rideItem && rideItem->ride() && (series == RideFile::aPower || series == RideFile::watts || series == RideFile::wattsKg) && !zoned && shade == true);
}

bool PowerHist::shadeHRZones() const
{
    return (rideItem && rideItem->ride() && series == RideFile::hr && !zoned && shade == true);
}

bool PowerHist::shadePaceZones() const
{
    return (rideItem && rideItem->ride() && series == RideFile::kph && !zoned && shade == true);
}

bool PowerHist::isSelected(const RideFilePoint *p, double sample)
{
    if (!rideItem) {

        foreach (IntervalItem *interval, rideItem->intervalsSelected()) {
            if (interval->selected && p->secs+sample>interval->start && p->secs<interval->stop) 
                return true;
        }
    }
    return false;
}

bool PowerHist::isSelected(const double t, double sample)
{
    if (!rideItem) {

        foreach (IntervalItem *interval, rideItem->intervalsSelected()) {
            if (interval->selected && t+sample>interval->start && t<interval->stop) 
                return true;
        }
    }
    return false;
}

void
PowerHist::pointHover(QwtPlotCurve *curve, int index)
{
    if (index >= 0) {

        double xvalue = curve->sample(index).x();
        double yvalue = curve->sample(index).y();
        QString text;

        if (zoned && yvalue > 0) {
            // output the tooltip
            text = QString("%1 %2").arg(yvalue, 0, 'f', 1).arg(absolutetime ? tr("minutes") : tr("%"));

            // set that text up
            zoomer->setText(text);
            return;

        } else if (yvalue > 0) {

            if (source != Metric) {
                // for speed series add pace with units according to settings
                // only when there is no ride (home) or the activity is a run/swim.
                QString runPaceStr, swimPaceStr;
                if (series == RideFile::kph && (!rideItem || rideItem->isRun)) {
                    bool metricPace = appsettings->value(this, GC_PACE, true).toBool();
                    QString paceunit = metricPace ? tr("min/km") : tr("min/mile");
                    runPaceStr = tr("\n%1 Pace (%2)").arg(context->athlete->useMetricUnits ? kphToPace(xvalue, metricPace, false) : mphToPace(xvalue, metricPace, false)).arg(paceunit);
                }
                if (series == RideFile::kph && (!rideItem || rideItem->isSwim)) {
                    bool metricPace = appsettings->value(this, GC_SWIMPACE, true).toBool();
                    QString paceunit = metricPace ? tr("min/100m") : tr("min/100yd");
                    swimPaceStr = tr("\n%1 Pace (%2)").arg(context->athlete->useMetricUnits ? kphToPace(xvalue, metricPace, true) : mphToPace(xvalue, metricPace, true)).arg(paceunit);
                }
                // output the tooltip
                text = QString("%1 %2%5%6\n%3 %4")
                            .arg(xvalue, 0, 'f', digits)
                            .arg(this->axisTitle(curve->xAxis()).text())
                            .arg(yvalue, 0, 'f', 1)
                            .arg(absolutetime ? tr("minutes") : tr("%"))
                            .arg(runPaceStr)
                            .arg(swimPaceStr);
            } else {
                text = QString("%1 %2\n%3 %4")
                            .arg(xvalue, 0, 'f', digits)
                            .arg(this->axisTitle(curve->xAxis()).text())
                            .arg(yvalue, 0, 'f', 1)
                            .arg(this->axisTitle(curve->yAxis()).text());
            }

            // set that text up
            zoomer->setText(text);
            return;
        }
    }
    // no point
    zoomer->setText("");
}

// because we need to effectively draw bars when showing
// time in zone (i.e. for every zone there are 2 points for each
// zone - top left and top right) we need to multiply the percentage
// values by 2 to take this into account
void
PowerHist::percentify(QVector<double> &array, double factor)
{
    double total=0;
    foreach (double current, array) total += current;

    if (total > 0)
        for (int i=0; i< array.size(); i++)
            if (array[i] > 0.01) // greater than 0.8s (i.e. not a double storage issue)
                array[i] = factor * (array[i] / total) * (double)100.00;
}

// Conditions to enable zoning, we can't zone for series besides watts, hr and
// kph only for running activities, so ignore zoning for those data series
bool
PowerHist::isZoningEnabled()
{
    // zoning valid for power, w/kg, hr, wbal and also
    // for speed (aka Pace; but only for swims and runs)
    return (zoned == true &&
            (series == RideFile::watts || series == RideFile::wattsKg || 
             series == RideFile::hr || series == RideFile::wbal ||
            (series == RideFile::kph && (!rideItem || rideItem->isRun || rideItem->isSwim))));
}
