From dbecc005b5b7572cacfe8d4333d9959f7a7cb71a Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Sat, 13 Jun 2020 10:10:31 +0100 Subject: [PATCH] Overview Chart on Trends view .. updated the overview chart to support trend view and summarise a season or date range. .. scope now meaningful in the item registry. .. added a new TopNOverviewItem to view a ranked list of activities by metric. .. updated sparkline to plot variable range (>30days) .. sort and multisort datafilter functions adjusted as caused a SEGV during testing (sorry not in separate commit). --- src/Charts/HomeWindow.cpp | 2 +- src/Charts/Overview.cpp | 204 +++++++---- src/Charts/Overview.h | 4 +- src/Charts/OverviewItems.cpp | 578 +++++++++++++++++++++++++++---- src/Charts/OverviewItems.h | 70 +++- src/Core/DataFilter.cpp | 98 +++++- src/Core/DataFilter.h | 1 + src/Gui/AddChartWizard.cpp | 5 +- src/Gui/AddChartWizard.h | 3 +- src/Gui/ChartSpace.cpp | 44 ++- src/Gui/ChartSpace.h | 13 +- src/Gui/GcWindowRegistry.cpp | 7 +- src/Gui/GcWindowRegistry.h | 4 +- src/Metrics/BasicRideMetrics.cpp | 1 + src/Resources/application.qrc | 1 + src/Resources/images/medal.png | Bin 0 -> 28092 bytes 16 files changed, 869 insertions(+), 166 deletions(-) create mode 100644 src/Resources/images/medal.png diff --git a/src/Charts/HomeWindow.cpp b/src/Charts/HomeWindow.cpp index f63aad670..184c8f920 100644 --- a/src/Charts/HomeWindow.cpp +++ b/src/Charts/HomeWindow.cpp @@ -1206,7 +1206,7 @@ GcWindowDialog::GcWindowDialog(GcWinID type, Context *context, GcChartWindow **h } // special case - if (type == GcWindowTypes::Overview) { + if (type == GcWindowTypes::Overview || type == GcWindowTypes::OverviewTrends) { static_cast(win)->setConfiguration(""); } diff --git a/src/Charts/Overview.cpp b/src/Charts/Overview.cpp index fae0a4541..29f4cbce6 100644 --- a/src/Charts/Overview.cpp +++ b/src/Charts/Overview.cpp @@ -24,7 +24,7 @@ static QIcon grayConfig, whiteConfig, accentConfig; -OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), context(context), configured(false) +OverviewWindow::OverviewWindow(Context *context, int scope) : GcChartWindow(context), context(context), configured(false), scope(scope) { setContentsMargins(0,0,0,0); setProperty("color", GColor(COVERVIEWBACKGROUND)); @@ -39,14 +39,15 @@ OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), conte main->setSpacing(0); main->setContentsMargins(0,0,0,0); - space = new ChartSpace(context); + space = new ChartSpace(context, scope); main->addWidget(space); // all the widgets setChartLayout(main); // tell space when a ride is selected - connect(this, SIGNAL(rideItemChanged(RideItem*)), space, SLOT(rideSelected(RideItem*))); + if (scope & ANALYSIS) connect(this, SIGNAL(rideItemChanged(RideItem*)), space, SLOT(rideSelected(RideItem*))); + if (scope & TRENDS) connect(this, SIGNAL(dateRangeChanged(DateRange)), space, SLOT(dateRangeChanged(DateRange))); connect(addTile, SIGNAL(triggered(bool)), this, SLOT(addTile())); connect(space, SIGNAL(itemConfigRequested(ChartSpaceItem*)), this, SLOT(configItem(ChartSpaceItem*))); } @@ -54,7 +55,7 @@ OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), conte void OverviewWindow::addTile() { - AddChartWizard *p = new AddChartWizard(context, space); + AddChartWizard *p = new AddChartWizard(context, space, scope); p->exec(); // no mem leak delete on close dialog } @@ -92,6 +93,8 @@ OverviewWindow::getConfiguration() const //UNUSED RPEOverviewItem *rpe = reinterpret_cast(item); } break; + + case OverviewItemType::TOPN: case OverviewItemType::METRIC: { MetricOverviewItem *metric = reinterpret_cast(item); @@ -172,83 +175,156 @@ OverviewWindow::setConfiguration(QString config) if (config == "") { - // column 0 - ChartSpaceItem *add; - add = new PMCOverviewItem(space, "coggan_tss"); - space->addItem(1,0,9, add); + if (scope == ANALYSIS) { - add = new MetaOverviewItem(space, "Sport", "Sport"); - space->addItem(2,0,5, add); + // column 0 + ChartSpaceItem *add; + add = new PMCOverviewItem(space, "coggan_tss"); + space->addItem(1,0,9, add); - add = new MetaOverviewItem(space, "Workout Code", "Workout Code"); - space->addItem(3,0,5, add); + add = new MetaOverviewItem(space, "Sport", "Sport"); + space->addItem(2,0,5, add); - add = new MetricOverviewItem(space, "Duration", "workout_time"); - space->addItem(4,0,9, add); + add = new MetaOverviewItem(space, "Workout Code", "Workout Code"); + space->addItem(3,0,5, add); - add = new MetaOverviewItem(space, "Notes", "Notes"); - space->addItem(5,0,13, add); + add = new MetricOverviewItem(space, "Duration", "workout_time"); + space->addItem(4,0,9, add); - // column 1 - add = new MetricOverviewItem(space, "HRV rMSSD", "rMSSD"); - space->addItem(1,1,9, add); + add = new MetaOverviewItem(space, "Notes", "Notes"); + space->addItem(5,0,13, add); - add = new MetricOverviewItem(space, "Heartrate", "average_hr"); - space->addItem(2,1,5, add); + // column 1 + add = new MetricOverviewItem(space, "HRV rMSSD", "rMSSD"); + space->addItem(1,1,9, add); - add = new ZoneOverviewItem(space, "Heartrate Zones", RideFile::hr); - space->addItem(3,1,11, add); + add = new MetricOverviewItem(space, "Heartrate", "average_hr"); + space->addItem(2,1,5, add); - add = new MetricOverviewItem(space, "Climbing", "elevation_gain"); - space->addItem(4,1,5, add); + add = new ZoneOverviewItem(space, "Heartrate Zones", RideFile::hr); + space->addItem(3,1,11, add); - add = new MetricOverviewItem(space, "Cadence", "average_cad"); - space->addItem(5,1,5, add); + add = new MetricOverviewItem(space, "Climbing", "elevation_gain"); + space->addItem(4,1,5, add); - add = new MetricOverviewItem(space, "Work", "total_work"); - space->addItem(6,1,5, add); + add = new MetricOverviewItem(space, "Cadence", "average_cad"); + space->addItem(5,1,5, add); - // column 2 - add = new RPEOverviewItem(space, "RPE"); - space->addItem(1,2,9, add); + add = new MetricOverviewItem(space, "Work", "total_work"); + space->addItem(6,1,5, add); - add = new MetricOverviewItem(space, "Stress", "coggan_tss"); - space->addItem(2,2,5, add); + // column 2 + add = new RPEOverviewItem(space, "RPE"); + space->addItem(1,2,9, add); - add = new ZoneOverviewItem(space, "Fatigue Zones", RideFile::wbal); - space->addItem(3,2,11, add); + add = new MetricOverviewItem(space, "Stress", "coggan_tss"); + space->addItem(2,2,5, add); - add = new IntervalOverviewItem(space, "Intervals", "elapsed_time", "average_power", "workout_time"); - space->addItem(4,2,17, add); + add = new ZoneOverviewItem(space, "Fatigue Zones", RideFile::wbal); + space->addItem(3,2,11, add); - // column 3 - add = new MetricOverviewItem(space, "Power", "average_power"); - space->addItem(1,3,9, add); + add = new IntervalOverviewItem(space, "Intervals", "elapsed_time", "average_power", "workout_time"); + space->addItem(4,2,17, add); - add = new MetricOverviewItem(space, "IsoPower", "coggan_np"); - space->addItem(2,3,5, add); + // column 3 + add = new MetricOverviewItem(space, "Power", "average_power"); + space->addItem(1,3,9, add); - add = new ZoneOverviewItem(space, "Power Zones", RideFile::watts); - space->addItem(3,3,11, add); + add = new MetricOverviewItem(space, "IsoPower", "coggan_np"); + space->addItem(2,3,5, add); - add = new MetricOverviewItem(space, "Peak Power Index", "peak_power_index"); - space->addItem(4,3,8, add); + add = new ZoneOverviewItem(space, "Power Zones", RideFile::watts); + space->addItem(3,3,11, add); - add = new MetricOverviewItem(space, "Variability", "coggam_variability_index"); - space->addItem(5,3,8, add); + add = new MetricOverviewItem(space, "Peak Power Index", "peak_power_index"); + space->addItem(4,3,8, add); - // column 4 - add = new MetricOverviewItem(space, "Distance", "total_distance"); - space->addItem(1,4,9, add); + add = new MetricOverviewItem(space, "Variability", "coggam_variability_index"); + space->addItem(5,3,8, add); - add = new MetricOverviewItem(space, "Speed", "average_speed"); - space->addItem(2,4,5, add); + // column 4 + add = new MetricOverviewItem(space, "Distance", "total_distance"); + space->addItem(1,4,9, add); - add = new ZoneOverviewItem(space, "Pace Zones", RideFile::kph); - space->addItem(3,4,11, add); + add = new MetricOverviewItem(space, "Speed", "average_speed"); + space->addItem(2,4,5, add); - add = new RouteOverviewItem(space, "Route"); - space->addItem(4,4,17, add); + add = new ZoneOverviewItem(space, "Pace Zones", RideFile::kph); + space->addItem(3,4,11, add); + + add = new RouteOverviewItem(space, "Route"); + space->addItem(4,4,17, add); + + } + + if (scope == TRENDS) { + + ChartSpaceItem *add; + + // column 0 + add = new KPIOverviewItem(space, tr("Distance"), 0, 10000, "{ round(sum(metrics(Distance))); }", "km"); + space->addItem(0,0,8, add); + + add = new TopNOverviewItem(space, tr("Going Long"), "total_distance"); + space->addItem(1,0,25, add); + + add = new KPIOverviewItem(space, tr("Weekly Hours"), 0, 15, "{ weeks <- (daterange(stop)-daterange(start))/7; round(10*sum(metrics(Duration)/3600)/weeks)/10; }", tr("hours")); + space->addItem(2,0,7, add); + + // column 1 + add = new KPIOverviewItem(space, tr("Peak Power Index"), 0, 150, "{ round(sort(descend, metrics(Power_Index))[0]); }", "%"); + space->addItem(0,1,8, add); + + add = new MetricOverviewItem(space, tr("Max Power"), "max_power"); + space->addItem(1,1,7, add); + + add = new MetricOverviewItem(space, tr("Average Power"), "average_power"); + space->addItem(2,1,7, add); + + add = new ZoneOverviewItem(space, tr("Power Zones"), RideFile::hr); + space->addItem(3,1,9, add); + + add = new MetricOverviewItem(space, tr("Total TSS"), "coggan_tss"); + space->addItem(4,1,7, add); + + // column 2 + add = new KPIOverviewItem(space, tr("Total Hours"), 0, 0, "{ round(sum(metrics(Duration))/3600); }", "hours"); + space->addItem(0,2,8, add); + + add = new TopNOverviewItem(space, tr("Going Hard"), "skiba_wprime_exp"); + space->addItem(1,2,25, add); + + add = new MetricOverviewItem(space, tr("Total W' Work"), "skiba_wprime_exp"); + space->addItem(2,2,7, add); + + // column 3 + add = new KPIOverviewItem(space, tr("W' Ratio"), 0, 100, "{ round((sum(metrics(W'_Work)) / sum(metrics(Work))) * 100); }", "%"); + space->addItem(0,3,8, add); + + add = new KPIOverviewItem(space, tr("Peak CP Estimate "), 0, 360, "{ round(max(estimates(cp3,cp))); }", "watts"); + space->addItem(1,3,7, add); + + add = new KPIOverviewItem(space, tr("Peak W' Estimate "), 0, 25, "{ round(max(estimates(cp3,w')/1000)*10)/10; }", "kJ"); + space->addItem(2,3,7, add); + + + add = new ZoneOverviewItem(space, tr("Fatigue Zones"), RideFile::wbal); + space->addItem(3,3,9, add); + + add = new MetricOverviewItem(space, tr("Total Work"), "total_work"); + space->addItem(4,3,7, add); + + // column 4 + add = new MetricOverviewItem(space, tr("Intensity Factor"), "coggan_if"); + space->addItem(0,4,8, add); + + add = new TopNOverviewItem(space, tr("Going Deep"), "skiba_wprime_low"); + space->addItem(1,4,25, add); + + add = new KPIOverviewItem(space, tr("IF > 0.85"), 0, 0, "{ count(metrics(IF)[x>0.85]); }", "activities"); + space->addItem(2,4,7, add); + + } } else { @@ -298,6 +374,14 @@ OverviewWindow::setConfiguration(QString config) } break; + case OverviewItemType::TOPN : + { + QString symbol=obj["symbol"].toString(); + add = new TopNOverviewItem(space, name,symbol); + space->addItem(order,column,deep, add); + } + break; + case OverviewItemType::METRIC : { QString symbol=obj["symbol"].toString(); @@ -406,8 +490,8 @@ OverviewConfigDialog::close() main->removeWidget(item->config()); // doesn't work xxx todo ! // update after config changed - if (item->parent->context->currentRideItem()) - item->setData(const_cast(item->parent->context->currentRideItem())); + if (item->parent->scope & ANALYSIS && item->parent->currentRideItem) item->setData(item->parent->currentRideItem); + if (item->parent->scope & TRENDS ) item->setDateRange(item->parent->currentDateRange); } accept(); diff --git a/src/Charts/Overview.h b/src/Charts/Overview.h index 991051fee..62e3217cd 100644 --- a/src/Charts/Overview.h +++ b/src/Charts/Overview.h @@ -31,6 +31,7 @@ #include "HrZones.h" #include "ChartSpace.h" +#include "OverviewItems.h" class OverviewWindow : public GcChartWindow { @@ -40,7 +41,7 @@ class OverviewWindow : public GcChartWindow public: - OverviewWindow(Context *context); + OverviewWindow(Context *context, int scope=ANALYSIS); // used by children Context *context; @@ -62,6 +63,7 @@ class OverviewWindow : public GcChartWindow // gui setup ChartSpace *space; bool configured; + int scope; }; class OverviewConfigDialog : public QDialog diff --git a/src/Charts/OverviewItems.cpp b/src/Charts/OverviewItems.cpp index 8d9f853eb..fa3044713 100644 --- a/src/Charts/OverviewItems.cpp +++ b/src/Charts/OverviewItems.cpp @@ -50,15 +50,16 @@ static bool _registerItems() // get the factory ChartSpaceItemRegistry ®istry = ChartSpaceItemRegistry::instance(); - // Register TYPE SHORT DESCRIPTION SCOPE CREATOR - registry.addItem(OverviewItemType::METRIC, QObject::tr("Metric"), QObject::tr("Metric and Sparkline"), 0, MetricOverviewItem::create); - registry.addItem(OverviewItemType::KPI, QObject::tr("KPI"), QObject::tr("KPI calculation and progress bar"), 0, KPIOverviewItem::create); - registry.addItem(OverviewItemType::META, QObject::tr("Metadata"), QObject::tr("Metadata and Sparkline"), 0, MetaOverviewItem::create); - registry.addItem(OverviewItemType::ZONE, QObject::tr("Zones"), QObject::tr("Zone Histogram"), 0, ZoneOverviewItem::create); - registry.addItem(OverviewItemType::RPE, QObject::tr("RPE"), QObject::tr("RPE Widget"), 0, RPEOverviewItem::create); - registry.addItem(OverviewItemType::INTERVAL, QObject::tr("Intervals"), QObject::tr("Interval Bubble Chart"), 0, IntervalOverviewItem::create); - registry.addItem(OverviewItemType::PMC, QObject::tr("PMC"), QObject::tr("PMC Status Summary"), 0, PMCOverviewItem::create); - registry.addItem(OverviewItemType::ROUTE, QObject::tr("Route"), QObject::tr("Route Summary"), 0, RouteOverviewItem::create); + // Register TYPE SHORT DESCRIPTION SCOPE CREATOR + registry.addItem(OverviewItemType::METRIC, QObject::tr("Metric"), QObject::tr("Metric and Sparkline"), ANALYSIS|TRENDS, MetricOverviewItem::create); + registry.addItem(OverviewItemType::KPI, QObject::tr("KPI"), QObject::tr("KPI calculation and progress bar"), ANALYSIS|TRENDS, KPIOverviewItem::create); + registry.addItem(OverviewItemType::TOPN, QObject::tr("Bests"), QObject::tr("Ranked list of bests"), TRENDS, TopNOverviewItem::create); + registry.addItem(OverviewItemType::META, QObject::tr("Metadata"), QObject::tr("Metadata and Sparkline"), ANALYSIS, MetaOverviewItem::create); + registry.addItem(OverviewItemType::ZONE, QObject::tr("Zones"), QObject::tr("Zone Histogram"), ANALYSIS|TRENDS, ZoneOverviewItem::create); + registry.addItem(OverviewItemType::RPE, QObject::tr("RPE"), QObject::tr("RPE Widget"), ANALYSIS, RPEOverviewItem::create); + registry.addItem(OverviewItemType::INTERVAL, QObject::tr("Intervals"), QObject::tr("Interval Bubble Chart"), ANALYSIS, IntervalOverviewItem::create); + registry.addItem(OverviewItemType::PMC, QObject::tr("PMC"), QObject::tr("PMC Status Summary"), ANALYSIS, PMCOverviewItem::create); + registry.addItem(OverviewItemType::ROUTE, QObject::tr("Route"), QObject::tr("Route Summary"), ANALYSIS, RouteOverviewItem::create); return true; } @@ -71,7 +72,7 @@ RPEOverviewItem::RPEOverviewItem(ChartSpace *parent, QString name) : ChartSpaceI // a META widget, "RPE" using the FOSTER modified 0-10 scale this->type = OverviewItemType::RPE; - sparkline = new Sparkline(this, SPARKDAYS+1, name); + sparkline = new Sparkline(this, name); rperating = new RPErating(this, name); } @@ -112,40 +113,40 @@ RouteOverviewItem::~RouteOverviewItem() } static const QStringList timeInZones = QStringList() - << "percent_in_zone_L1" - << "percent_in_zone_L2" - << "percent_in_zone_L3" - << "percent_in_zone_L4" - << "percent_in_zone_L5" - << "percent_in_zone_L6" - << "percent_in_zone_L7" - << "percent_in_zone_L8" - << "percent_in_zone_L9" - << "percent_in_zone_L10"; + << "time_in_zone_L1" + << "time_in_zone_L2" + << "time_in_zone_L3" + << "time_in_zone_L4" + << "time_in_zone_L5" + << "time_in_zone_L6" + << "time_in_zone_L7" + << "time_in_zone_L8" + << "time_in_zone_L9" + << "time_in_zone_L10"; static const QStringList paceTimeInZones = QStringList() - << "percent_in_zone_P1" - << "percent_in_zone_P2" - << "percent_in_zone_P3" - << "percent_in_zone_P4" - << "percent_in_zone_P5" - << "percent_in_zone_P6" - << "percent_in_zone_P7" - << "percent_in_zone_P8" - << "percent_in_zone_P9" - << "percent_in_zone_P10"; + << "time_in_zone_P1" + << "time_in_zone_P2" + << "time_in_zone_P3" + << "time_in_zone_P4" + << "time_in_zone_P5" + << "time_in_zone_P6" + << "time_in_zone_P7" + << "time_in_zone_P8" + << "time_in_zone_P9" + << "time_in_zone_P10"; static const QStringList timeInZonesHR = QStringList() - << "percent_in_zone_H1" - << "percent_in_zone_H2" - << "percent_in_zone_H3" - << "percent_in_zone_H4" - << "percent_in_zone_H5" - << "percent_in_zone_H6" - << "percent_in_zone_H7" - << "percent_in_zone_H8" - << "percent_in_zone_H9" - << "percent_in_zone_H10"; + << "time_in_zone_H1" + << "time_in_zone_H2" + << "time_in_zone_H3" + << "time_in_zone_H4" + << "time_in_zone_H5" + << "time_in_zone_H6" + << "time_in_zone_H7" + << "time_in_zone_H8" + << "time_in_zone_H9" + << "time_in_zone_H10"; static const QStringList timeInZonesWBAL = QStringList() << "wtime_in_zone_L1" @@ -279,7 +280,8 @@ MetricOverviewItem::MetricOverviewItem(ChartSpace *parent, QString name, QString if (metric) units = metric->units(parent->context->athlete->useMetricUnits); // we may plot the metric sparkline if the tile is big enough - sparkline = new Sparkline(this, SPARKDAYS+1, name); + bool bigdot = parent->scope == ANALYSIS ? true : false; + sparkline = new Sparkline(this, name, bigdot); } @@ -288,6 +290,25 @@ MetricOverviewItem::~MetricOverviewItem() delete sparkline; } +TopNOverviewItem::TopNOverviewItem(ChartSpace *parent, QString name, QString symbol) : ChartSpaceItem(parent, name) +{ + // metric + this->type = OverviewItemType::TOPN; + this->symbol = symbol; + + RideMetricFactory &factory = RideMetricFactory::instance(); + this->metric = const_cast(factory.rideMetric(symbol)); + if (metric) units = metric->units(parent->context->athlete->useMetricUnits); + + animator=new QPropertyAnimation(this, "transition"); +} + +TopNOverviewItem::~TopNOverviewItem() +{ + animator->stop(); + delete animator; +} + PMCOverviewItem::PMCOverviewItem(ChartSpace *parent, QString symbol) : ChartSpaceItem(parent, "") { // PMC doesn't have a title as we show multiple things @@ -319,7 +340,7 @@ MetaOverviewItem::MetaOverviewItem(ChartSpace *parent, QString name, QString sym // sparkline if are we numeric? if (fieldtype == FIELD_INTEGER || fieldtype == FIELD_DOUBLE) { - sparkline = new Sparkline(this, SPARKDAYS+1, name); + sparkline = new Sparkline(this, name); } else { sparkline = NULL; } @@ -372,6 +393,25 @@ KPIOverviewItem::setData(RideItem *item) itemGeometryChanged(); } +void +KPIOverviewItem::setDateRange(DateRange dr) +{ + // calculate the value... + DataFilter parser(this, parent->context, program); + Result res = parser.evaluate(dr); + + // set to zero for daft values + value = QString("%1").arg(res.number); + if (value == "nan") value =""; + value=Utils::removeDP(value); + + // now set the progressbar + progressbar->setValue(start, stop, value.toDouble()); + + // show/hide widgets on the basis of geometry + itemGeometryChanged(); +} + void RPEOverviewItem::setData(RideItem *item) { @@ -542,6 +582,169 @@ MetricOverviewItem::setData(RideItem *item) } } +void +MetricOverviewItem::setDateRange(DateRange dr) +{ + Specification spec; + spec.setDateRange(dr); + + // aggregate sum and count etc + double v=0; // value + double c=0; // count + bool first=true; + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + // get value and count + double value = item->getForSymbol(symbol, parent->context->athlete->useMetricUnits); + double count = item->getCountForSymbol(symbol); + if (count <= 0) count = 1; + + // ignore zeroes when aggregating? + if (metric->aggregateZero() == false && value == 0) continue; + + // what we gonna do with this? + switch(metric->type()) { + case RideMetric::StdDev: + case RideMetric::MeanSquareRoot: + case RideMetric::Average: + v += value*count; + c += count; + break; + case RideMetric::Total: + case RideMetric::RunningTotal: + v += value; + break; + case RideMetric::Peak: + if (first || value > v) v = value; + break; + case RideMetric::Low: + if (first || value < v) v = value; + break; + break; + } + first = false; + } + + // now apply averaging etc + switch(metric->type()) { + case RideMetric::StdDev: + case RideMetric::MeanSquareRoot: + case RideMetric::Average: + if (c) v = v / c; + else v = 0; + break; + default: break; + } + + // get the metric value + value = Utils::removeDP(QString("%1").arg(v,0,'f',metric->precision())); + if (value == "nan") value =""; + if (std::isinf(v) || std::isnan(v)) v=0; + + + // metric history + QList points; + + // how many days + QDate earliest(1900,01,01); + sparkline->setDays(earliest.daysTo(dr.to) - earliest.daysTo(dr.from)); + + double min=0, max=0; + double sum=0; + first=true; + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + double value = item->getForSymbol(symbol, parent->context->athlete->useMetricUnits); + + // no zero values + if (value == 0) continue; + + // cum sum for Total and RunningTotals + if (metric->type() == RideMetric::Total || metric->type() == RideMetric::RunningTotal) { + sum += value; + value = sum; + } + + points << QPointF(earliest.daysTo(item->dateTime.date()) - earliest.daysTo(dr.from), value); + + if (value < min) min=value; + if (first || value > max) max=value; + first = false; + } + + // update the sparkline + sparkline->setPoints(points); + + // set range + sparkline->setRange(min*1.1,max*1.1); // add 10% to each direction + +} + +static bool entrylessthan(struct topnentry &a, const topnentry &b) +{ + return a.v < b.v; +} + +void +TopNOverviewItem::setDateRange(DateRange dr) +{ + // clear out the old values + ranked.clear(); + + // filtering + Specification spec; + spec.setDateRange(dr); + + // pmc data + PMCData stressdata(parent->context, spec, "coggan_tss"); + maxvalue=""; + maxv=0; // must never have -ve max + minv=0; // always zero minimum + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + // get value and count + double v = item->getForSymbol(symbol, parent->context->athlete->useMetricUnits); + QString value = item->getStringForSymbol(symbol, parent->context->athlete->useMetricUnits); + int index = stressdata.indexOf(item->dateTime.date()); + double tsb = 0; + if (index >= 0 && index < stressdata.sb().count()) tsb = stressdata.sb()[index]; + + // add to the list + QColor color = (item->color.red() == 1 && item->color.green() == 1 && item->color.blue() == 1) ? GColor(CPLOTMARKER) : item->color; + ranked << topnentry(item->dateTime.date(), v, value, color, tsb); + + // biggest value? + if (v > maxv) { + maxvalue=value; + maxv = v; + } + + // minv should be 0 unless it goes negative + if (v < minv) minv=v; + } + + // sort the list + if (metric->type() == RideMetric::Low) qSort(ranked.begin(), ranked.end(), entrylessthan); + else qSort(ranked); + + // change painting details + itemGeometryChanged(); + + // animate the transition + animator->stop(); + animator->setStartValue(0); + animator->setEndValue(100); + animator->setEasingCurve(QEasingCurve::OutQuad); + animator->setDuration(400); + animator->start(); +} + void MetaOverviewItem::setData(RideItem *item) { @@ -649,6 +852,112 @@ PMCOverviewItem::setData(RideItem *item) } +void +ZoneOverviewItem::setDateRange(DateRange dr) +{ + QVector vals(10); // max 10 seems ok + vals.fill(0); + + // stop any animation before starting, just in case- stops a crash + // when we update a chart in the middle of its animation + if (chart) chart->setAnimationOptions(QChart::NoAnimation);; + + // enable animation when setting values (disabled at all other times) + if (chart) chart->setAnimationOptions(QChart::SeriesAnimations); + + Specification spec; + spec.setDateRange(dr); + + // aggregate sum and count etc + foreach(RideItem *item, parent->context->athlete->rideCache->rides()) { + + if (!spec.pass(item)) continue; + + switch(series) { + + // + // HEARTRATE + // + case RideFile::hr: + { + if (parent->context->athlete->hrZones(item->isRun)) { + + int numhrzones; + int hrrange = parent->context->athlete->hrZones(item->isRun)->whichRange(item->dateTime.date()); + + if (hrrange > -1) { + + numhrzones = parent->context->athlete->hrZones(item->isRun)->numZones(hrrange); + for(int i=0; igetForSymbol(timeInZonesHR[i]); + } + } + } + } + break; + + // + // POWER + // + default: + case RideFile::watts: + { + if (parent->context->athlete->zones(item->isRun)) { + + int numzones; + int range = parent->context->athlete->zones(item->isRun)->whichRange(item->dateTime.date()); + + if (range > -1) { + + numzones = parent->context->athlete->zones(item->isRun)->numZones(range); + for(int i=0; igetForSymbol(timeInZones[i]); + } + } + } + } + break; + + // + // PACE + // + case RideFile::kph: + { + if ((item->isRun || item->isSwim) && parent->context->athlete->paceZones(item->isSwim)) { + + int numzones; + int range = parent->context->athlete->paceZones(item->isSwim)->whichRange(item->dateTime.date()); + + if (range > -1) { + + numzones = parent->context->athlete->paceZones(item->isSwim)->numZones(range); + for(int i=0; igetForSymbol(paceTimeInZones[i]); + } + } + } + } + break; + + case RideFile::wbal: + { + for(int i=0; i<4; i++) { + vals[i] += item->getForSymbol(timeInZonesWBAL[i]); + } + } + break; + } + } + + // now update the barset converting to percentages + double sum=0; + for(int i=0; ireplace(i, round(vals[i]/sum * 100)); + else barset->replace(i, round(vals[i]/sum * 100)); + } +} + void ZoneOverviewItem::setData(RideItem *item) { @@ -676,9 +985,17 @@ ZoneOverviewItem::setData(RideItem *item) if (hrrange > -1) { + double sum=0; numhrzones = parent->context->athlete->hrZones(item->isRun)->numZones(hrrange); for(int i=0; ireplace(i, round(item->getForSymbol(timeInZonesHR[i]))); + sum += item->getForSymbol(timeInZonesHR[i]); + } + + // update as percent of total + for(int i=0; i<4; i++) { + double time =round(item->getForSymbol(timeInZonesHR[i])); + if (time > 0 && sum > 0) barset->replace(i, round((time/sum) * 100)); + else barset->replace(i, 0); } } else { @@ -706,9 +1023,17 @@ ZoneOverviewItem::setData(RideItem *item) if (range > -1) { + double sum=0; numzones = parent->context->athlete->zones(item->isRun)->numZones(range); for(int i=0; ireplace(i, round(item->getForSymbol(timeInZones[i]))); + sum += item->getForSymbol(timeInZones[i]); + } + + // update as percent of total + for(int i=0; i<4; i++) { + double time =round(item->getForSymbol(timeInZones[i])); + if (time > 0 && sum > 0) barset->replace(i, round((time/sum) * 100)); + else barset->replace(i, 0); } } else { @@ -735,9 +1060,17 @@ ZoneOverviewItem::setData(RideItem *item) if (range > -1) { + double sum=0; numzones = parent->context->athlete->paceZones(item->isSwim)->numZones(range); for(int i=0; ireplace(i, round(item->getForSymbol(paceTimeInZones[i]))); + sum += item->getForSymbol(paceTimeInZones[i]); + } + + // update as percent of total + for(int i=0; i<4; i++) { + double time =round(item->getForSymbol(paceTimeInZones[i])); + if (time > 0 && sum > 0) barset->replace(i, round((time/sum) * 100)); + else barset->replace(i, 0); } } else { @@ -899,6 +1232,9 @@ MetricOverviewItem::itemGeometryChanged() { } } +// painter truncates the list depending upon the size of the widget +void TopNOverviewItem::itemGeometryChanged() { } + void MetaOverviewItem::itemGeometryChanged() { @@ -1160,18 +1496,105 @@ MetricOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem QRectF trirect(bl.x() + trect.width() + ROWHEIGHT, bl.y() - trect.height(), trect.height()*0.66f, trect.height()); - // trend triangle - QPainterPath triangle; - painter->setBrush(QBrush(QColor(up ? Qt::darkGreen : Qt::darkRed))); - painter->setPen(Qt::NoPen); + // activity show if current one is up or down on trend for last 30 days.. + if (parent->scope == ANALYSIS) { - triangle.moveTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); - triangle.lineTo((trirect.left() + trirect.right()) / 2.0f, up ? trirect.top() : trirect.bottom()); - triangle.lineTo(trirect.right(), (trirect.top()+trirect.bottom())/2.0f); - triangle.lineTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); + // trend triangle + QPainterPath triangle; + painter->setBrush(QBrush(QColor(up ? Qt::darkGreen : Qt::darkRed))); + painter->setPen(Qt::NoPen); - painter->drawPath(triangle); + triangle.moveTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); + triangle.lineTo((trirect.left() + trirect.right()) / 2.0f, up ? trirect.top() : trirect.bottom()); + triangle.lineTo(trirect.right(), (trirect.top()+trirect.bottom())/2.0f); + triangle.lineTo(trirect.left(), (trirect.top()+trirect.bottom())/2.0f); + painter->drawPath(triangle); + } + +} + +void +TopNOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + + // CALC GEOM / OFFSETS + QFontMetrics fm(parent->smallfont); + painter->setFont(parent->smallfont); + + // we paint the table, so lets work out the geometry and count etc + QRectF paintarea = QRectF(20,ROWHEIGHT*2, geometry().width()-40, geometry().height()-20-(ROWHEIGHT*2)); + + // max rows + double margins = 30; + double rowheight = fm.ascent() + margins; + int maxrows = (paintarea.height()-margins) / rowheight; + + // min and max values for what is being painted + minv=0; + maxv=0; + for (int i=0; i maxv) maxv = v; + + // strings for rect sizing (remember neg values are longer strings) + if (ranked[i].value.length() > maxvalue.length()) maxvalue = ranked[i].value; + + // minv should be 0 unless it goes negative + if (v < minv) minv=v; + } + + // number rect + QRectF numrect = QRectF(0,0, fm.boundingRect(QString(" %1.").arg(maxrows)).width(), fm.boundingRect(QString(" %1.").arg(maxrows)).height()); + + // date rect + QRectF daterect = QRectF(0,0, fm.boundingRect("31 May yy").width(), fm.boundingRect("31 May yy").height()); + + // value rect + QRectF valuerect = QRectF(0,0, fm.boundingRect(maxvalue).width(), fm.boundingRect(maxvalue).height()); + + // bar rect + int width = paintarea.width() - (numrect.width() + daterect.width() + valuerect.width() + (margins * 6)); + QRectF barrect = QRectF(0,10, width, 30); + + + // PAINT + for (int i=0; isetPen(QColor(100,100,100)); + painter->drawText(paintarea.topLeft()+QPointF(margins, margins+(i*rowheight)+fm.ascent()), QString("%1.").arg(i+1)); + + // date + QString datestring = ranked[i].date.toString("d MMM yy"); + painter->drawText(paintarea.topLeft()+numrect.topRight()+QPointF(margins*2, margins+(i*rowheight)+fm.ascent()), datestring); + + // bar width bearing in mind range might start at -ve value + double width = (ranked[i].v / (maxv-minv)) * barrect.width() * (double(transition)/100.00); + + // 0 width is invisible, always at least 5 + if (width == 0) width = 5; + + // push rect across to account for 0 being in middle of bar when -ve values + // NOTE: minv must NEV£R be > 0 ! + double offset = (barrect.width() / (maxv-minv)) * fabs(minv); + + // rectangles for full and this value + QRectF fullbar(paintarea.left()+numrect.width()+daterect.width()+(margins*3), paintarea.top()+margins+(i*rowheight)+fm.ascent()-35, barrect.width(), barrect.height()); + QRectF bar(offset+paintarea.left()+numrect.width()+daterect.width()+(margins*3), paintarea.top()+margins+(i*rowheight)+fm.ascent()-35, width, barrect.height()); + + // draw rects + QBrush brush(QColor(100,100,100,100)); + painter->fillRect(fullbar, brush); + QBrush markerbrush(ranked[i].color); + painter->fillRect(bar, markerbrush); + + // value + painter->setPen(QColor(100,100,100)); + painter->drawText(paintarea.topLeft()+QPointF(numrect.width()+daterect.width()+fullbar.width()+(margins*4),0)+QPointF(margins, margins+(i*rowheight)+fm.ascent()), ranked[i].value); + } } void @@ -1402,7 +1825,7 @@ OverviewItemConfig::OverviewItemConfig(ChartSpaceItem *item) : QWidget(item->par } // single metric names - if (item->type == OverviewItemType::METRIC || item->type == OverviewItemType::PMC) { + if (item->type == OverviewItemType::TOPN || item->type == OverviewItemType::METRIC || item->type == OverviewItemType::PMC) { metric1 = new MetricSelect(this, item->parent->context, MetricSelect::Metric); layout->addRow(tr("Metric"), metric1); connect(metric1, SIGNAL(textChanged(QString)), this, SLOT(dataChanged())); @@ -1577,6 +2000,14 @@ OverviewItemConfig::setWidgets() } break; + case OverviewItemType::TOPN: + { + TopNOverviewItem *mi = reinterpret_cast(item); + name->setText(mi->name); + metric1->setSymbol(mi->symbol); + } + break; + case OverviewItemType::META: { MetaOverviewItem *mi = reinterpret_cast(item); @@ -1659,6 +2090,17 @@ OverviewItemConfig::dataChanged() } break; + case OverviewItemType::TOPN: + { + TopNOverviewItem *mi = reinterpret_cast(item); + mi->name = name->text(); + if (metric1->isValid()) { + mi->symbol = metric1->rideMetric()->symbol(); + mi->units = metric1->rideMetric()->units(mi->parent->context->athlete->useMetricUnits); + } + } + break; + case OverviewItemType::META: { MetaOverviewItem *mi = reinterpret_cast(item); @@ -1852,7 +2294,7 @@ void RPErating::applyEdit() { // update the item - if we have one - RideItem *item = parent->parent->current; + RideItem *item = parent->parent->currentRideItem; // did it change? if (item && item->ride() && item->getText("RPE","") != value) { @@ -2316,11 +2758,9 @@ BubbleViz::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) painter->restore(); } -Sparkline::Sparkline(QGraphicsWidget *parent, int count, QString name) - : QGraphicsItem(NULL), parent(parent), name(name) +Sparkline::Sparkline(QGraphicsWidget *parent, QString name, bool bigdot) + : QGraphicsItem(NULL), parent(parent), name(name), sparkdays(SPARKDAYS), bigdot(bigdot) { - Q_UNUSED(count) - min = max = 0.0f; setGeometry(20,20,100,100); setZValue(11); @@ -2364,7 +2804,7 @@ Sparkline::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) if (points.isEmpty() || (max-min)==0) return; // so draw a line connecting the points - double xfactor = (geom.width() - (ROWHEIGHT*6)) / SPARKDAYS; + double xfactor = (geom.width() - (ROWHEIGHT*6)) / sparkdays; double xoffset = boundingRect().left()+(ROWHEIGHT*2); double yfactor = (geom.height()-(ROWHEIGHT)) / (max-min); double bottom = boundingRect().bottom()-ROWHEIGHT/2; @@ -2385,13 +2825,15 @@ Sparkline::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*) painter->setPen(pen); painter->drawPath(path); - // and the last one is a dot for this value - double x = (points.first().x()*xfactor)+xoffset-25; - double y = bottom-((points.first().y()-min)*yfactor)-25; - if (std::isfinite(x) && std::isfinite(y)) { - painter->setBrush(QBrush(GColor(CPLOTMARKER).darker(150))); - painter->setPen(Qt::NoPen); - painter->drawEllipse(QRectF(x, y, 50, 50)); + if (bigdot) { + // and the last one is a dot for this value + double x = (points.first().x()*xfactor)+xoffset-25; + double y = bottom-((points.first().y()-min)*yfactor)-25; + if (std::isfinite(x) && std::isfinite(y)) { + painter->setBrush(QBrush(GColor(CPLOTMARKER).darker(150))); + painter->setPen(Qt::NoPen); + painter->drawEllipse(QRectF(x, y, 50, 50)); + } } } } diff --git a/src/Charts/OverviewItems.h b/src/Charts/OverviewItems.h index 88974b7fd..cd29e5f1d 100644 --- a/src/Charts/OverviewItems.h +++ b/src/Charts/OverviewItems.h @@ -46,7 +46,7 @@ class ProgressBar; #define ROUTEPOINTS 250 // types we use start from 100 to avoid clashing with main chart types -enum OverviewItemType { RPE=100, METRIC, META, ZONE, INTERVAL, PMC, ROUTE, KPI }; +enum OverviewItemType { RPE=100, METRIC, META, ZONE, INTERVAL, PMC, ROUTE, KPI, TOPN }; // // Configuration widget for ALL Overview Items @@ -103,6 +103,7 @@ class KPIOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange); QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -131,6 +132,7 @@ class RPEOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -157,6 +159,7 @@ class MetricOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange); QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -172,6 +175,61 @@ class MetricOverviewItem : public ChartSpaceItem Sparkline *sparkline; }; +// top N uses this to hold details for date range +struct topnentry { + +public: + + topnentry(QDate date, double v, QString value, QColor color, int tsb) : date(date), v(v), value(value), color(color), tsb(tsb) {} + inline bool operator<(const topnentry &other) const { return (v > other.v); } + inline bool operator>(const topnentry &other) const { return (v < other.v); } + QDate date; + double v; // for sorting + QString value; // as should be shown + QColor color; // ride color + int tsb; // on the day +}; + +class TopNOverviewItem : public ChartSpaceItem +{ + Q_OBJECT + + // want a meta property for property animation + Q_PROPERTY(int transition READ getTransition WRITE setTransition) + + public: + + TopNOverviewItem(ChartSpace *parent, QString name, QString symbol); + ~TopNOverviewItem(); + + // transition animation 0-100 + int getTransition() const {return transition;} + void setTransition(int x) { if (transition !=x) {transition=x; update();}} + + void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); + void itemGeometryChanged(); + void setData(RideItem *) {} // doesn't support analysis view + void setDateRange(DateRange); + QWidget *config() { return new OverviewItemConfig(this); } + + // create and config + static ChartSpaceItem *create(ChartSpace *parent) { return new TopNOverviewItem(parent, "PowerIndex", "power_index"); } + + QString symbol; + RideMetric *metric; + QString units; + + QList ranked; + + // maximums to index from + QString maxvalue; + double maxv,minv; + + // animation + int transition; + QPropertyAnimation *animator; +}; + class MetaOverviewItem : public ChartSpaceItem { Q_OBJECT @@ -184,6 +242,7 @@ class MetaOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -211,6 +270,7 @@ class PMCOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -233,6 +293,7 @@ class ZoneOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange); void dragChanged(bool x); QWidget *config() { return new OverviewItemConfig(this); } @@ -260,6 +321,7 @@ class RouteOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -280,6 +342,7 @@ class IntervalOverviewItem : public ChartSpaceItem void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *); void itemGeometryChanged(); void setData(RideItem *item); + void setDateRange(DateRange) {} // doesn't support trends view QWidget *config() { return new OverviewItemConfig(this); } // create and config @@ -422,12 +485,13 @@ class RPErating : public QGraphicsItem class Sparkline : public QGraphicsItem { public: - Sparkline(QGraphicsWidget *parent, int count,QString name=""); // create and say how many days + Sparkline(QGraphicsWidget *parent, QString name="", bool bigdot=true); // create and say how many days // we monkey around with this *A LOT* void setGeometry(double x, double y, double width, double height); QRectF geometry() { return geom; } + void setDays(int n) { sparkdays=n; } // defaults to SPARKDAYS void setPoints(QList); void setRange(double min, double max); // upper lower @@ -444,6 +508,8 @@ class Sparkline : public QGraphicsItem QRectF geom; QString name; double min, max; + int sparkdays; + bool bigdot; QList points; }; diff --git a/src/Core/DataFilter.cpp b/src/Core/DataFilter.cpp index 87ef32d59..872bbc9bb 100644 --- a/src/Core/DataFilter.cpp +++ b/src/Core/DataFilter.cpp @@ -173,7 +173,7 @@ static struct { { "argsort", 2 }, // argsort(ascend|descend, list) - return a sorting index (ala numpy.argsort). - { "sort", 0 }, // sort(ascend|descend, list1 [, list2, listn]) - sorts each list together, based upon list1, no limit to the + { "multisort", 0 }, // multisort(ascend|descend, list1 [, list2, listn]) - sorts each list together, based upon list1, no limit to the // number of lists but they must have the same length. the first list contains the values that define // the sort order. since the sort is 'in-situ' the lists must all be user symbols. returns number of items // sorted. this is impure from a functional programming perspective, but allows us to avoid using dataframes @@ -297,6 +297,8 @@ static struct { { "rank", 2 }, // rank(ascend|descend, list) - returns ranks for the list + { "sort", 2 }, // sort(ascend|descend, list) - returns sorted list + // add new ones above this line { "", -1 } }; @@ -401,8 +403,8 @@ DataFilter::builtins(Context *context) } else if (i == 55) { - // argsort - returning << "sort(ascend|descend, list [, list2 .. ,listn])"; + // multisort + returning << "multisort(ascend|descend, list [, list2 .. ,listn])"; } else if (i == 56) { @@ -521,6 +523,11 @@ DataFilter::builtins(Context *context) // rank returning << "rank(ascend|descend, list)"; + } else if (i == 97) { + + // sort + returning << "sort(ascend|descend, list)"; + } else { QString function; @@ -1928,36 +1935,51 @@ void Leaf::validateFilter(Context *context, DataFilterRuntime *df, Leaf *leaf) validateFilter(context, df, leaf->fparms[1]); } - } else if (leaf->function == "sort") { + } else if (leaf->function == "multisort") { if (leaf->fparms.count() < 2) { leaf->inerror = true; - DataFiltererrors << QString(tr("sort(ascend|descend, list [, .. list n])")); + DataFiltererrors << QString(tr("multisort(ascend|descend, list [, .. list n])")); } // need ascend|descend then a list if (leaf->fparms.count() > 0 && leaf->fparms[0]->type != Leaf::Symbol) { leaf->inerror = true; - DataFiltererrors << QString(tr("sort(ascend|descend, list [, .. list n]), need to specify ascend or descend")); + DataFiltererrors << QString(tr("multisort(ascend|descend, list [, .. list n]), need to specify ascend or descend")); } // need all remaining parameters to be symbols - for(int i=1; ifparms.count(); i++) { + + // make sure the parameter makes sense + validateFilter(context, df, leaf->fparms[i]); // check parameter is actually a symbol if (leaf->fparms[i]->type != Leaf::Symbol) { leaf->inerror = true; - DataFiltererrors << QString(tr("sort: list arguments must be a symbol")); + DataFiltererrors << QString(tr("multisort: list arguments must be a symbol")); } else { QString symbol = *(leaf->fparms[i]->lvalue.n); if (!df->symbols.contains(symbol)) { DataFiltererrors << QString(tr("'%1' is not a user symbol").arg(symbol)); leaf->inerror = true; } - } } + } else if (leaf->function == "sort") { + + // need ascend|descend then a list + if (leaf->fparms.count() != 2 || leaf->fparms[0]->type != Leaf::Symbol) { + + leaf->inerror = true; + DataFiltererrors << QString(tr("sort(ascend|descend, list), need to specify ascend or descend")); + + } else { + + validateFilter(context, df, leaf->fparms[1]); + } + } else if (leaf->function == "rank") { // need ascend|descend then a list @@ -2710,6 +2732,34 @@ Result DataFilter::evaluate(RideItem *item, RideFilePoint *p) return res; } +Result DataFilter::evaluate(DateRange dr) +{ + // if there is no current ride item then there is no data + // so it really is ok to baulk at no current ride item here + // we must always have a ride since context is used + if (context->currentRideItem() == NULL || !treeRoot || DataFiltererrors.count()) return Result(0); + + // reset stack + rt.stack = 0; + + Result res(0); + + // if we are a set of functions.. + if (rt.functions.count()) { + + // ... start at main + if (rt.functions.contains("main")) + res = treeRoot->eval(&rt, rt.functions.value("main"), 0, 0, const_cast(context->currentRideItem()), NULL, NULL, Specification(), dr); + + } else { + + // otherwise just evaluate the entire tree + res = treeRoot->eval(&rt, treeRoot, 0, 0, const_cast(context->currentRideItem()), NULL, NULL, Specification(), dr); + } + + return res; +} + QStringList DataFilter::check(QString query) { // since we may use it afterwards @@ -2902,6 +2952,9 @@ Result::vectorize(int count) // used by lowerbound struct comparedouble { bool operator()(const double p1, const double p2) { return p1 < p2; } }; +// qsort descend +static bool doubledescend(const double &s1, const double &s2) { return s1 > s2; } + // date arithmetic, a bit of a brute force, but need to rely upon // QDate arithmetic for handling months (so we don't have to) static int monthsTo(QDate from, QDate to) @@ -4138,6 +4191,29 @@ Result Leaf::eval(DataFilterRuntime *df, Leaf *leaf, float x, long it, RideItem return returning; } + // sort + if (leaf->function == "sort") { + Result returning(0); + + // ascending or descending? + QString symbol = *(leaf->fparms[0]->lvalue.n); + bool ascending= (symbol=="ascend") ? true : false; + + // use the utils function to actually do it + Result v = eval(df, leaf->fparms[1],x, it, m, p, c, s, d); + + if (ascending) qSort(v.vector); + else qSort(v.vector.begin(), v.vector.end(), doubledescend); + + // put the index into the result we are returning. + foreach(double x, v.vector) { + returning.number += x; + returning.vector << x; + } + + return returning; + } + // arguniq if (leaf->function == "arguniq") { Result returning(0); @@ -4282,7 +4358,7 @@ Result Leaf::eval(DataFilterRuntime *df, Leaf *leaf, float x, long it, RideItem } // sort - if (leaf->function == "sort") { + if (leaf->function == "multisort") { // ascend/descend? QString symbol = *(leaf->fparms[0]->lvalue.n); @@ -4306,7 +4382,7 @@ Result Leaf::eval(DataFilterRuntime *df, Leaf *leaf, float x, long it, RideItem // diff length? if (current.vector.count() != len) { - fprintf(stderr, "sort list '%s': not the same length, ignored\n", symbol.toStdString().c_str()); fflush(stderr); + fprintf(stderr, "multisort list '%s': not the same length, ignored\n", symbol.toStdString().c_str()); fflush(stderr); continue; } diff --git a/src/Core/DataFilter.h b/src/Core/DataFilter.h index 243aadc6a..d99b6b97b 100644 --- a/src/Core/DataFilter.h +++ b/src/Core/DataFilter.h @@ -187,6 +187,7 @@ class DataFilter : public QObject // RideItem always available and supplies th context Result evaluate(RideItem *rideItem, RideFilePoint *p); + Result evaluate(DateRange dr); QStringList getErrors() { return errors; }; void colorSyntax(QTextDocument *content, int pos); diff --git a/src/Gui/AddChartWizard.cpp b/src/Gui/AddChartWizard.cpp index e7326b948..e614b0ebb 100644 --- a/src/Gui/AddChartWizard.cpp +++ b/src/Gui/AddChartWizard.cpp @@ -33,7 +33,7 @@ // // Main wizard - if passed a service name we are in edit mode, not add mode. -AddChartWizard::AddChartWizard(Context *context, ChartSpace *space) : QWizard(context->mainWindow), context(context), space(space) +AddChartWizard::AddChartWizard(Context *context, ChartSpace *space, int scope) : QWizard(context->mainWindow), context(context), scope(scope), space(space) { #ifdef Q_OS_MAC setWizardStyle(QWizard::ModernStyle); @@ -100,6 +100,9 @@ AddChartType::initializePage() // iterate over names, as they are sorted alphabetically foreach(ChartSpaceItemDetail detail, registry.items()) { + // limit the type of chart to add + if ((detail.scope&wizard->scope) == 0) continue; + // get the service QCommandLinkButton *p = new QCommandLinkButton(detail.quick, detail.description, this); p->setStyleSheet(QString("font-size: %1px;").arg(font.pointSizeF() * dpiXFactor)); diff --git a/src/Gui/AddChartWizard.h b/src/Gui/AddChartWizard.h index b489eac25..f4b58919e 100644 --- a/src/Gui/AddChartWizard.h +++ b/src/Gui/AddChartWizard.h @@ -31,7 +31,7 @@ class AddChartWizard : public QWizard public: - AddChartWizard(Context *context, ChartSpace *space); + AddChartWizard(Context *context, ChartSpace *space, int scope); QSize sizeHint() const { return QSize(600,650); } Context *context; @@ -39,6 +39,7 @@ public: // what type of chart int type; + int scope; // add here ChartSpace *space; diff --git a/src/Gui/ChartSpace.cpp b/src/Gui/ChartSpace.cpp index 6ac74fe76..16e18ef79 100644 --- a/src/Gui/ChartSpace.cpp +++ b/src/Gui/ChartSpace.cpp @@ -34,8 +34,8 @@ static QIcon grayConfig, whiteConfig, accentConfig; ChartSpaceItemRegistry *ChartSpaceItemRegistry::_instance; -ChartSpace::ChartSpace(Context *context) : - state(NONE), context(context), group(NULL), _viewY(0), +ChartSpace::ChartSpace(Context *context, int scope) : + state(NONE), context(context), scope(scope), group(NULL), _viewY(0), yresizecursor(false), xresizecursor(false), block(false), scrolling(false), setscrollbar(false), lasty(-1) { @@ -92,7 +92,7 @@ ChartSpace::ChartSpace(Context *context) : // we're ready to plot, but not configured configured=false; stale=true; - current=NULL; + currentRideItem=NULL; } // add the item @@ -103,7 +103,8 @@ ChartSpace::addItem(int order, int column, int deep, ChartSpaceItem *item) item->column = column; item->deep = deep; items.append(item); - if (current) item->setData(current); + if (scope&ANALYSIS && currentRideItem) item->setData(currentRideItem); + if (scope&TRENDS) item->setDateRange(currentDateRange); } void @@ -125,31 +126,46 @@ void ChartSpace::rideSelected(RideItem *item) { // don't plot when we're not visible, unless we have nothing plotted yet - if (!isVisible() && current != NULL && item != NULL) { + if (!isVisible() && currentRideItem != NULL && item != NULL) { stale=true; return; } // don't replot .. we already did this one - if (current == item && stale == false) { + if (currentRideItem == item && stale == false) { return; } -// profiling the code -//QTime timer; -//timer.start(); - // ride item changed foreach(ChartSpaceItem *ChartSpaceItem, items) ChartSpaceItem->setData(item); -// profiling the code -//qDebug()<<"took:"<setDateRange(dr); + + // update + updateView(); + + // ok, remember we did this one stale=false; } diff --git a/src/Gui/ChartSpace.h b/src/Gui/ChartSpace.h index 701211e50..7d0512855 100644 --- a/src/Gui/ChartSpace.h +++ b/src/Gui/ChartSpace.h @@ -47,6 +47,9 @@ class ChartSpace; class ChartSpaceItemFactory; +// we need a scope for a chart space, one or more of +enum OverviewScope { ANALYSIS=0x01, TRENDS=0x02 }; + // must be subclassed to add items to a ChartSpace class ChartSpaceItem : public QGraphicsWidget { @@ -58,6 +61,7 @@ class ChartSpaceItem : public QGraphicsWidget virtual void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) =0; virtual void itemGeometryChanged() =0; virtual void setData(RideItem *item)=0; + virtual void setDateRange(DateRange )=0; virtual QWidget *config()=0; // must supply a widget to configure // what type am I- managed by user @@ -127,18 +131,20 @@ class ChartSpace : public QWidget public: - ChartSpace(Context *context); + ChartSpace(Context *context, int scope); // current state for event processing enum { NONE, DRAG, XRESIZE, YRESIZE } state; // used by children Context *context; + int scope; QGraphicsView *view; QFont titlefont, bigfont, midfont, smallfont; // the item we are currently showing - RideItem *current; + RideItem *currentRideItem; + DateRange currentDateRange; // to get paint device QGraphicsView *device() { return view; } @@ -150,8 +156,9 @@ class ChartSpace : public QWidget public slots: - // ride item changed + // user selection void rideSelected(RideItem *item); + void dateRangeChanged(DateRange); // for smooth scrolling void setViewY(int x) { if (_viewY != x) {_viewY =x; updateView();} } diff --git a/src/Gui/GcWindowRegistry.cpp b/src/Gui/GcWindowRegistry.cpp index aeff4ace5..37784c101 100644 --- a/src/Gui/GcWindowRegistry.cpp +++ b/src/Gui/GcWindowRegistry.cpp @@ -76,8 +76,9 @@ GcWindowRegistry* GcWindows; void GcWindowRegistry::initialize() { - static GcWindowRegistry GcWindowsInit[35] = { + static GcWindowRegistry GcWindowsInit[36] = { // name GcWinID + { VIEW_HOME|VIEW_DIARY, tr("Overview "),GcWindowTypes::OverviewTrends }, { VIEW_HOME|VIEW_DIARY, tr("User Chart"),GcWindowTypes::UserTrends }, { VIEW_HOME|VIEW_DIARY, tr("Trends"),GcWindowTypes::LTM }, { VIEW_HOME|VIEW_DIARY, tr("TreeMap"),GcWindowTypes::TreeMap }, @@ -243,8 +244,10 @@ GcWindowRegistry::newGcWindow(GcWinID id, Context *context) case GcWindowTypes::RouteSegment: returning = new GcChartWindow(context); break; #endif #if GC_HAVE_OVERVIEW - case GcWindowTypes::Overview: returning = new OverviewWindow(context); break; + case GcWindowTypes::Overview: returning = new OverviewWindow(context, ANALYSIS); break; + case GcWindowTypes::OverviewTrends: returning = new OverviewWindow(context, TRENDS); break; #else + case GcWindowTypes::OverviewTrends: case GcWindowTypes::Overview: returning = new GcChartWindow(context); break; #endif case GcWindowTypes::SeasonPlan: returning = new PlanningWindow(context); break; diff --git a/src/Gui/GcWindowRegistry.h b/src/Gui/GcWindowRegistry.h index 56f8a384d..a54cb618e 100644 --- a/src/Gui/GcWindowRegistry.h +++ b/src/Gui/GcWindowRegistry.h @@ -72,8 +72,8 @@ enum gcwinid { Python = 43, PythonSeason = 44, UserTrends=45, - UserAnalysis=46 - + UserAnalysis=46, + OverviewTrends=47 }; }; typedef enum GcWindowTypes::gcwinid GcWinID; diff --git a/src/Metrics/BasicRideMetrics.cpp b/src/Metrics/BasicRideMetrics.cpp index 8a36e2082..36450d32d 100644 --- a/src/Metrics/BasicRideMetrics.cpp +++ b/src/Metrics/BasicRideMetrics.cpp @@ -929,6 +929,7 @@ class TotalWork : public RideMetric { void initialize() { setName(tr("Work")); + setType(RideMetric::Total); setMetricUnits(tr("kJ")); setImperialUnits(tr("kJ")); setDescription(tr("Total Work in kJ computed from power data")); diff --git a/src/Resources/application.qrc b/src/Resources/application.qrc index 79a7b47d9..468f6a68d 100644 --- a/src/Resources/application.qrc +++ b/src/Resources/application.qrc @@ -75,6 +75,7 @@ images/twitter.png images/cyclist.png images/imetrics.png + images/medal.png images/power.png images/arduino.png images/query.png diff --git a/src/Resources/images/medal.png b/src/Resources/images/medal.png new file mode 100644 index 0000000000000000000000000000000000000000..220d49df700076806089f1ae9466fa907b8d5273 GIT binary patch literal 28092 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