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)
total_time([axis])
duration([axis])
moving_time([axis])
distance([axis])
climbing([axis])
work([axis])
epower([axis])
intensity([axis])
variability([axis])
load([axis])
trimp([axis])
load_power([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.
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.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).lrbalance
filters out rides that don’t have left/right power balance data.load
uses power and heart rate data, like the training load chart. load_power
only uses power data and trimp
uses heart rate 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.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
name
is the segment name as a string.
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.
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.
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.
This sets the x
, y
or color
property of each element in the array of data to chart.
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.
This replaces the data with a new array of data with an x
value for each day and the appropriate y
value.
Show long-term stress:
chart().lts()
type(type)
Removes all activities that aren’t of the specified type.
type
is a string such as 'cycling'
, 'running'
, 'gym'
or 'swimming'
. The type for each activity can be shown on the rides table.
Show the time spent running each week:
chart().type('running').duration().group_by(week).aggregate(sum)
group_by(fn)
Groups the data based on a provided function. Generally used with aggregate
.
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
.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>}
.
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.
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.
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:
yData
).rideData
with the higest y
value).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.
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.
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
last_n_months(n)
last_n_weeks(n)
last_n_days(n)
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.
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.
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.
Alternatively, the function can call set_value(axis, value)
. This can be called multiple times and allows the mapping function to set multiple values, which is especially useful for creating tables.
axis
is optional and allows map
to set values other than the y
value. Ignored when using emit
or set_value
.
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)
This creates a table showing PWC130, PWC150, PWC170 and PWC190 values for each ride.
chart().map(function(ride) {
var pwc150 = ride.summary.pwc150;
var pwc170 = ride.summary.pwc170;
var diff = pwc170 - pwc150;
set_value('pwc r2', Math.round(ride.summary.pwc_r2 * 100));
set_value('pwc130', pwc150 - diff);
set_value('pwc150', pwc150);
set_value('pwc170', pwc170);
set_value('pwc190', pwc170 + diff);
}).reverse().table()
rebase_dates(year, [month, [day]])
Shifts dates to be relative to the provided date to allow data from different times to overlap.
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).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})
.
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.
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
.
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.
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.
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.
name
is the string to use.
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.
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.
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.
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
.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
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.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.
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.
chart_number
is the negative index of the chart, where the previous chart is -1
, the one before that -2
and so on.
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()
.
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
).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.
count
is the number of items to show. If this is not provided, only one is shown.
This sorts the data based on y
values and then takes the first count
items.
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
.
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.
ascending
— Optional, defaults to false
.
reverse()
Reverses the data.
limit([count])
Shows only the first count
elements of data.
count
— The number of elements of data to keep. Defaults to 1
.