Developer Flexicharts documentation

Flexicharts provide a powerful and flexible way to make custom charts.

Each chart starts out with a chart(), which generates an object containing ride data which can be manipulated into something to chart by a variety of functions. For example, the following charts the total training load for each month:

chart().load().group_by(month).aggregate(sum)

Functions

total_time([axis])
duration([axis])
moving_time([axis])
distance([axis])
climbing([axis])
work([axis])
epower([axis])
intensity([axis])
variability([axis])
load([axis])
trimp([axis])
avg_cadence([axis])
avg_speed([axis])
avg_heartrate([axis])
avg_power([axis])
avg_temperature([axis])
max_cadence([axis])
max_speed([axis])
max_heartrate([axis])
max_power([axis])
max_temperature([axis])
min_temperature([axis])
lrbalance([axis])
date([axis])
date_year([axis])
day_lts([axis])
day_sts([axis])
day_sb([axis])

A group of related functions that choose what information to display about each ride.

Notes

  • One of these will generally be the first function called.
  • By default, the x values are set to the date, which is equivalent to calling date('x'). Therefore, for many charts, only the y values need to be set.
  • The difference between total_time, duration and moving_time is that total_time is the time from the start to the end of the ride, duration is the time of the ride with all gaps longer than three minutes reduced to three minutes, and moving_time is the time when the speed was greater than 3km/h (to account for GPS errors).
  • Where applicable, all units are metric. Times are in seconds.
  • See the API documentation for details about each value.
  • lrbalance filters out rides that don’t have left/right power balance data.
  • date_year is the year of the ride, which can be useful for color coding points.
  • day_lts, day_sts and day_sb provide the long term stress, short term stress and stress balance for the day on which the ride was done.

Options

axis is the name of the axis to set data for. When creating charts, it can be either 'x', 'y' or 'color' (it must be quoted, as it is a string). It defaults to 'y' if not provided. Use this to make x-y scatter plots:

chart().load('x').epower('y')

When creating tables, axis can be any string, and a column with that name will be shown.

See the technical details, below, for more information.

Technical details

Under the hood, chart() creates an object that contains an array of ride data. Each element of this array is in the format that the API returns with that addition of an x, y and, if set, color property, and the day’s LTS, STS, and SB. x is set to the date by default. When the chart is created, the x and y value of each ride in the array is retrieved to display on the chart. All of the functions here exist to either manipulate the x and y values, or manipulate how they are displayed.

Note that although the “array of ride data” starts off as that, the data in it may be transformed into something else so that each element is no longer for a specific ride. However, this documention generally refers to this as “ride data” and each element in the array as a “ride”.

When axis is not provided or 'y', this group of functions sets y to the appropriate value. Therefore, it is unnecessary to use one of these if y is going to be set in another way, such as if map is being used. This group of functions also modifies the way the charts are displayed by setting the appropriate name in the legend and causing the y-axis to show the time when appropriate.

When axis is 'x', the x value is set instead of the y value. There is no y value set by default, so that will need to be set separately. This is intended to be used to create x-y scatter plots with x values apart from the date.

Multiple charts can be created simultaneously by separating the commands with a semicolon.

Examples

Show training load for all rides, with x set to date (by default) and y set to training load:

chart().load()

Show effective power for all rides and duration for all rides in two charts:

chart().epower(); chart().duration()

Create an x-y scatter plot of duration and training load:

chart().load('x').duration('y')

As axis defaults to 'y', this is equivalent to:

chart().load('x').duration()

Create an x-y scatter plot of effective power and duration, with the points color coded by year:

chart().epower('x').duration('y').date_year('color')

power_curve(seconds, [axis])
epower_curve(seconds, [axis])

Show the highest average power or effective power of a given duration for each ride.

Rides that don’t have power data of the given duration are removed.

Options

seconds is the duration of which the highest average power or effective power is displayed. It must be provided and it must be a number.

axis is the same as for the above group of functions, and can be 'x', 'y' or 'color', or any string for tables, and defaults to 'y' if not provided.

Examples

Show the best 5 second power for all rides:

chart().power_curve(5)

Show the best 20 minute effective power for all rides:

chart().epower_curve(1200)

pwc130([r2, [axis]])
pwc150([r2, [axis]])
pwc170([r2, [axis]])

Show the PWC130, PWC150 or PWC170 for each ride.

PWC130, PWC150 and PWC170 are the predicted power output with a heart rate at 130BPM, 150BPM and 170BPM based on a statistical correlation between heart rate and power for each ride. The quality of that relationship is indicated by the r2 value — the higher the r2 value, the more likely it is that the PWC values are meaningful.

Rides with an r2 value below a given value can be filtered out by using the r2 parameter. By default, PWC values are only shown for rides with an r2 value greater than 0.5. Rides without power data are also removed.

Technical details

pwc150 and pwc170 are stored in the ride summary. The PWC concept as implemented uses a linear fit, so pwc130 is calculated from the other two values.

Options

r2 can be provided to filter out rides with an r2 value below the given value. If this is not given, it defaults to 0.5.

axis is the name of the axis to set data for. It can be 'x', 'y' or 'color', or any string for tables, and defaults to 'y' if not provided.

Examples

Show the PWC150 of all rides (with an r2 value greater than 0.5):

chart().pwc150()

Show the PWC170 of all rides with an r2 value greater than 0.8:

chart().pwc170(0.8)

power_zone(index, [axis])
hr_zone(index, [axis])

Show the time in a power or heart rate zone for each ride.

Options

index is the index of the zone to display information for. Zones are 1-indexed, so if you have five heart rate zones, index can be 1, 2, 3, 4, or 5.

axis is the name of the axis to set data for. It can be 'x', 'y' or 'color', or any string for tables, and defaults to 'y' if not provided.

Examples

Show the how long you were in power zone 3 for all rides:

chart().power_zone(3)

Show how much time you were in heart rate zone 2 in the first half of 2014:

chart().hr_zone(2).filter(date_filter(2014, 1, 2014, 6)).group_by(all_time).aggregate(sum).table()

segment(name)

Choose a segment.

This makes the chart work on a per-segment basis, rather than a per-ride basis, and allows the use of the per-segment data sources listed below. Ride summary data for the ride that each segment is part of is still accessible.

Options

name is the segment name as a string.

Technical details

This replaces the data to chart in the chart() object. Each element of the new array contains the summary data of a segment (in a segment property) and also contains the ride summary data for the ride that it was in. One element is created for each segment, so there are multiple elements created for a ride if a segment was ridden multiple times in the ride, and no elements created for a ride if the segment wasn’t ridden in the ride.

Existing x, y and color values are preserved, which means x defaults to the date.

Examples

Select all segments named “3:00 interval”. Note that this doesn’t create a chart by itself, as no data source is specified (see below).

chart().segment('3:00 interval')

segment_duration([axis])
segment_distance([axis])
segment_climbing([axis])
segment_speed([axis])
segment_power([axis])
segment_heartrate([axis])
segment_cadence([axis])
segment_epower([axis])
segment_work([axis])
segment_vam([axis])
segment_decoupling([axis])

A group of related functions that choose what information to display about each segment.

Options

axis is the name of the axis to set data for. It can be 'x', 'y' or 'color', or any string for tables, and defaults to 'y' if not provided.

Technical details

This sets the x, y or color property of each element in the array of data to chart.

Examples

Segment power for all segments named “5:00 interval”:

chart().segment('5:00 interval').segment_power()

Segment power versus segment heart rate for all segments named “3:00 interval”, color coded by the LTS of the day:

chart().segment('3:00 interval').segment_power('x').segment_heartrate('y').day_lts('color')

Segment duration for all segments named “1 in 20”:

chart().segment('1 in 20').segment_duration()

lts()
sts()
sb()

Show the long-term stress, short-term stress, or stress balance, as shown on the training load chart.

LTS, STS and SB are calculated according to the same rules (initial values, data source etc.) that the training load chart is calculated with.

Technical details

This replaces the data with a new array of data with an x value for each day and the appropriate y value.

Examples

Show long-term stress:

chart().lts()

group_by(fn)

Groups the data based on a provided function. Generally used with aggregate.

Options

fn is a function that decides which items will be grouped together. Built in functions for grouping include:

  • day — On the same day.
  • week — In the same week.
  • month — In the same month (note that months aren’t always the same length).
  • year — In the same year.
  • all_time — All items.
  • range(n) — The chosen data is in a range with a width of n.

Technical details

This replaces the data to chart in the chart() object. x values are now based on what fn returned, and y values are arrays of the y value of each ride for which fn returned the same value. Any custom fields for tables are likewise turned into arrays. There is also a data array, where each element contains the ride data for the corresponding element in the y array, which can be used by an aggregation function.

A custom JavaScript function can be provided instead of a built-in function. It is given a ride (or whatever is in the array of data that chart() initially created) and must return an object {x: <start of range>, x2: <end of range>}.

Examples

Group the training load for each month. Note that this can’t be charted directly, as an aggregation function must also be used.

chart().load().group_by(month)

Creates a histogram of ride distances, where each bin is 10km wide:

chart().distance().group_by(range(10))

The same as above, but using a custom function:

chart().distance().group_by(function(ride) {
    return {x: Math.floor(ride.summary.distance / 10) * 10, x2: Math.floor(ride.summary.distance / 10) * 10 + 10 }
})

aggregate(fn, [fnMap])

Aggregates the data based on a provided function. Generally used with group_by.

min, max, min(n) and max(n) preserve the rest of the ride data, making it possible to, for example, find the longest rides of each month, and then chart their average power.

Options

fn is a function that aggregates the groups created with group_by in some way. Built in functions include:

  • sum — Adds up the items.
  • average — Calculates the average (arithmetic mean) of the items.
  • max — Finds the maximum value.
  • min — Finds the minimum value.
  • max(n) — Finds the n maximum values.
  • min(n) — Finds the n minimum values.
  • count — The number of items.

fn is also used on all custom fields, unless specified in fnMap.

fnMap is an object that maps custom fields to aggregate functions. It is only used when custom fields are used, and overrides fn for the specified fields.

Technical details

Each y value is replaced with what fn returns, where fn is a function that is given the existing value of y (which is assumed to be an array, which it will be when a group_by operation has just been done).

A custom JavaScript function can be provided instead of a built-in function. In the simplest form, it is given the y values in an array, and must return a value. The function has the signature of:

function(yData, rideData)

Where yData is an array of all of the numbers in the same group generated by the group_by function and rideData is the ride data associated with each each of the y values in yData. This function can return one of:

  • A number (such as the average of all of the values in yData).
  • A ride object (such as the element in rideData with the higest y value).
  • An array of ride objects (such as the five elements in rideData with the highest y values).

The x value set with the group_by function is retained, except when used with max(n) or min(n). When used with max or min, date('x') can be used to set the x value to the date of the ride.

Examples

Charts the total training load for each month:

chart().load().group_by(month).aggregate(sum)

Creates a histogram of ride distances, where each bin is 10km wide:

chart().distance().group_by(range(10)).aggregate(count)

The same as above, but using a custom function:

chart().distance().group_by(range(10)).aggregate(function (data) {
    return data.length;
})

The average of the best five twenty minute effective power efforts for each month:

chart().epower_curve(1200).group_by(month).aggregate(max(5)).group_by(month).aggregate(average).points()

filter(fn)

Removes rides that don’t match the filter.

Options

fn is a function that must return true for each ride you want to include. The following built in functions are available:

  • this_year — Use only rides ridden this year.
  • this_month
  • this_week — The week starts on Monday and goes through to Sunday.
  • last_year
  • last_month
  • last_week
  • date_filter(year, [month, [day]], [year, [month, [day]]]) — If one date is specified, use only rides from that year/month/day (depending on how much of the date was specified). If two dates are specified, use only rides between that date range, where dates are as inclusive as possible. See examples below.
  • on_monday — Use only rides ridden on a Monday.
  • on_tuesday
  • on_wednesday
  • on_thursday
  • on_friday
  • on_saturday
  • on_sunday

When creating custom functions, refer to the API documention for information about the structure of the data that this function is given.

Examples

Create a Flexichart of rides for this year:

chart().filter(this_year).distance()

Use rides from 2012:

chart().filter(date_filter(2012)).distance()

Use rides from the start of 2012 to the end of 2014:

chart().filter(date_filter(2012, 2014)).distance()

Use rides from January 2013:

chart().filter(date_filter(2013, 1)).distance()

Use rides from the first six months of 2013:

chart().filter(date_filter(2013, 1, 2013, 6)).distance()

Use rides that were ridden on a Monday:

chart().filter(on_monday).distance()

Use only rides that have power and heart rate data:

chart().filter(function(ride) {
    return ride.has.power && ride.has.heartrate;
})

map(fn, [axis])

Sets the data using a custom function.

Options

fn is a function that is give the ride data and must return the y value to use.

Alternatively, the function can call emit(y) or emit(x, y) for more control — multiple values can be emitted, or none can. When using emit, fn should not return anything.

axis is optional and allows map to set values other than the y value. Ignored when using emit.

Examples

This is equivalent to chart().load():

chart().map(function(ride) { return ride.summary.load; })

Calculate the ratio between effective power and average heart rate:

chart().filter(function(ride) {
   return ride.has.power && ride.has.heartrate;
}).map(function(ride) {
    return ride.summary.epower / ride.summary.avg_heartrate;
}).group_by(month).aggregate(average);

This is equivalent to the above, but using emit:

chart().map(function(ride) {
    if (ride.has.power && ride.has.heartrate) {
        emit(ride.summary.epower / ride.summary.avg_heartrate);
    }
}).group_by(month).aggregate(average)

rebase_dates(year, [month, [day]])

Shifts dates to be relative to the provided date to allow data from different times to overlap.

Options

Only year is required. If month is not provided it defaults to 1. If day is not provided it defaults to 1.

  • year
  • month — A number between 1 and 12.
  • day — A number between 1 and up to 31 (depending on the month).

Technical details

This relies on the x values of the data being dates. These x values are replaced with the integer offset of the date to the date provided to this function.

The x-axis will show dates. Note that when comparing one year to another, leap years will mean that days don’t always correspond perfectly, as, for example, the 217th day of a year doesn’t always have the same date. To show numbers instead of dates, add x_axis({format: null}).

Examples

Compare long-term stress of two years (it is common to use the same arguments with date_filter and rebase_dates):

chart().lts().filter(date_filter(2014)).rebase_dates(2014).line().name('2014');
chart().lts().filter(date_filter(2015)).rebase_dates(2015).line().name('2015').on(-1);

accumulate()

Shows cumulative data.

Technical details

Each y value is set to itself plus the sum of all previous y values.

When using with filter, it is important to do the filter before the accumulate.

Examples

Compare cumulative distance of two years:

chart().distance().filter(date_filter(2014)).rebase_dates(2014).accumulate().line().name('2014');
chart().distance().filter(date_filter(2015)).rebase_dates(2015).accumulate().line().name('2015').on(-1);

trendline([bandwidth])

Creates a trendline for the data using the Loess method.

The x and y data are used to create the trendline.

Options

bandwidth (also known as the smoothing parameter) determines how much of the data is used to create points. When none is provided, it uses a default value of 0.5. The higher the value, the smoother the trendline. Useful values are often between 0.25 and 0.5.

Examples

Show PWC170 for each ride and a trendline:

chart().pwc170();
chart().pwc170().trendline().on(-1);

name(name)

Sets the name of the series shown in the legend.

Options

name is the string to use.

Examples

Show a scatter plot with a custom name:

chart().pwc170(0.5, 'x').epower_curve(1200, 'y').name('PWC vs. EP')

color(scale_name, [alpha])
color(scale, [alpha])
color(options)
color(color)
color(color, fill_color)

Sets the color of the series.

This is used in different ways depending on what sort of chart is being drawn. If points or a line is being drawn then it just requires one color. If columns are being drawn then the outline color and fill color are required. If color is being used as an axis, the color of points varies, so a color scale can be specified.

Options

scale_name, scale or options can be specified when using color as an axis.

scale_name is one of the built-in color scales:

  • 'yellow-purple' (default)
  • 'rainbow'
  • 'green-red'
  • 'blue-gold'
  • 'white-black'

scale is a custom scale created using chroma.js. For reference, the default yellow-purple scale is created with:

chroma.scale(['#f8dc5e', '#e21236', '#333754']).mode('lch')

A color picker like this is useful for creating gradients.

alpha is how transparent/opaque the colors are. 0.0 is full transparent, and 1.0 is fully opaque. The default is 0.7.

options is an object containing any of:

  • scale — Either a scale or scale name (as above).
  • alpha — As above.
  • reverse — A boolean that reverses the scale, which might be useful if using the ColorBrewer scales built into chroma.js.

color is a valid CSS color string, and applies to the color of points, lines, and the outline of column charts.

fill_color is a valid CSS color string, and applies to the fill color of column charts.

Examples

Use a built-in color scale:

chart().pwc170(0.5, 'x').epower_curve(1200, 'y').day_lts('color').color('rainbow')

Use a custom color scale and set the alpha value:

chart().epower('x').duration('y').date_year('color').color(chroma.scale(['#f8dc5e', '#e21236', '#333754']).mode('lch'), 0.8)

Use a ColorBrewer color scale and reverse it:

chart().epower('x').duration('y').date_year('color').color({scale: chroma.scale('Spectral'), reverse: true})

Set the color for a chart with points:

chart().distance().color('hsla(30, 78%, 55%, 0.3)')

x_axis(min, max)
x_axis(options)
x_axis('fit')
y_axis(min, max)
y_axis(options)
y_axis('fit')
color_axis(min, max)

Sets the minimum and maximum limits of the axes.

This also controls whether multiple series on one chart will share axes or use independent axes. By default, series will share the chart axes, which are the axes of the first series added to the chart. However, if limits are specified (even if set to null) then an independent axis will be used, unless the share option is also used.

When an axis is shared, it’s minimum and maximum are extended to fit the new limits, which are found automatically or provided here.

The x-axis and y-axis are independent with respect to their sharing behaviour, so the x-axis can be shared and the y-axis independent.

Options

min and max are the minimum and maximum values. If set to null, values will be found so that all of the data fits, which is what would have happened anyway, but this also forces the chart to not share the axis with series already added to the chart.

Specifying 'fit' is equivalent to setting the values to null, so limits are found automatically, but the series will use an independent axis.

options is an object that can contain any of the following properties:

  • min
  • max
  • share — Whether or not to use the same axes for multiple series on the same chart. If not provided, it defaults to false if min and max are provided, else true.

Examples

Show a scatter plot with a custom x-axis and y-axis.

chart().pwc170(0.5, 'x').epower_curve(1200, 'y').x_axis(205, 405).y_axis(185, 355)

The best 20:00 effort for each month, and LTS on one chart, with the LTS series using its own y-axis:

chart().epower_curve(1200, 'y').group_by(month).aggregate(max).points()
chart().lts().y_axis('fit').on(-1)

The second line is equivalent to:

chart().lts().y_axis({share: false}).on(-1)

And:

chart().lts().y_axis(null, null).on(-1)

line([options])
points([options])
columns([options])

Change how the data is displayed on the chart.

By default, the chart is drawn with points unless an aggregation is used, in which case it is drawn with columns. This can be controlled by using these functions. options doesn’t need to be given, but can be used to control other aspects of the chart.

If the color axis is being used, points are always drawn.

Options

options is an object that can be optionally provided. The following options can be given:

  • color — Any valid CSS color.
  • fill_color — Only useful for columns and line charts.
  • name — As shown in the legend.
  • lineWidth — The width of the line; defaults to 1.5. (Only for line.)
  • position — When there is a range of dates (i.e., for aggregations), where to draw the point; defaults to "centre", other options are "left" and "right". (Only for line.)
  • radius — The radius of the points; defaults to 4. (Only for points.)
  • xAxis, yAxis — Axis options, an object with the following options (this should only be needed when charting multiple series on one chart).
    • min — Value at the left/bottom edge of the chart; defaults to 0 if only max is provided.
    • max — Value at the right/top edge of the chart.

Technical details

These charts have flexible axis settings. Each chart can have axis settings it uses to render the chart (most importantly, minimum and maximum x and y values), and each series can have its own axis settings. This is useful, for example, for charting speed and power on one chart — the two series can have independent y axes. By default, the new series takes the chart axis settings, which are the settings used for the first series added to the chart.

Examples

Change the color and name:

chart().avg_heartrate().group_by(week).aggregate(average).line({color: 'hsl(270, 70%, 70%)', name: 'Average weekly HR'})

Show the distance for each year and distance for each ride on one chart:

chart().distance().group_by(year).aggregate(sum)
chart().distance().points({yAxis: {max: 200}}).on(-1)

on(chart_number)

Show this series on an existing chart.

Options

chart_number is the negative index of the chart, where the previous chart is -1, the one before that -2 and so on.

Examples

The following four commands will create one chart with the highest five second, one minute, five minute and twenty minute average power output for each month (each command needs to be entered separately):

chart().power_curve(5).group_by(month).aggregate(max).line({color: 'hsla(190, 60%, 70%, 1)'})
chart().power_curve(60).group_by(month).aggregate(max).line({color: 'hsla(260, 60%, 70%, 1)'}).on(-1)
chart().power_curve(300).group_by(month).aggregate(max).line({color: 'hsla(330, 60%, 70%, 1)'}).on(-1)
chart().power_curve(1200).group_by(month).aggregate(max).line({color: 'hsla(40, 60%, 70%, 1)'}).on(-1)

inspect()

Logs the data object to the browser’s developer tools console.

table([format_map])

Show the data as a table rather than a chart.

If the data is for individual rides, a link to the ride is also shown. Note that if a group_by function is called, links are no longer shown, even though it would be reasonable to show the link in cases like chart().max_power().group_by(month).aggregate(max).table().

Options

format_map is an optional object that specifies how columns should be formatted. Keys are column names, and values are objects containing one or more of the following:

  • unit — Either 'miles', 'feet', or 'fahrenheit' to convert units from the default SI units.
  • round — Round to the given number of decimal places.
  • fixed — Display this many decimal places (i.e., fixed: 3 will display 5.4 as 5.400).

Examples

Show the distance for each ride (with links to the ride pages):

chart().distance().table()

Show the maximum effective power for one hour for each month:

chart().epower_curve(3600).group_by(month).aggregate(max).table()

max([count])
min([count])

Shows a table with the highest or lowest values.

Options

count is the number of items to show. If this is not provided, only one is shown.

Technical details

This sorts the data based on y values and then takes the first count items.

Examples

Show the five longest rides:

chart().distance().max(5)

count()

Shows how many rides there are (in a table).

This is probably best used in conjunction with filter.

Examples

Shows how many rides there are that are longer than four hours:

chart().filter(function(ride) { return ride.summary.duration > 14400; }).count()

sort(axis, [ascending])

Sorts data.

Options

ascending — Optional, defaults to false.

reverse()

Reverses the data.

limit([count])

Shows only the first count elements of data.

Options

count — The number of elements of data to keep. Defaults to 1.