Exploring time series with gramm
In this example script, we will explore gramm's capabilities for plotting time series data (or any kind of continous data)
To benefit from interactive elements, you should open it in MATLAB's editor with
We will load a partial dataset from a human movement science experiment
websave('example_movement','https://github.com/piermorel/gramm/raw/master/sample_data/example_movement.mat'); %Download data from repository
load example_movement.mat
T
T = 3170×13 table
| | subject | session | trial_index | reference_direction | hit | m_reaction_time | valid_perc | valid_perc_session | px | py | t | tperc |
|---|
| 1 | 2 | 3 |
|---|
| 1 | IHTA | 1 | 2 | 105 | 0 | 616.7897 | -20.7055 | 37.2741 | 0 | 0.1634 | 0.1634 | 1×362 double | 1×362 double | 1×362 double | 1×362 double |
|---|
| 2 | IHTA | 1 | 3 | 60 | 1 | 404.2587 | 40 | 29.2820 | 0 | 0.3268 | 0.3268 | 1×317 double | 1×317 double | 1×317 double | 1×317 double |
|---|
| 3 | IHTA | 1 | 4 | 330 | 0 | 341.6924 | 69.2820 | -80 | 0 | 0.4902 | 0.4902 | 1×362 double | 1×362 double | 1×362 double | 1×362 double |
|---|
| 4 | IHTA | 1 | 7 | 240 | 1 | 303.2130 | -40 | -109.2820 | 0 | 0.8170 | 0.8170 | 1×226 double | 1×226 double | 1×226 double | 1×226 double |
|---|
| 5 | IHTA | 1 | 9 | 15 | 1 | 283.2674 | 77.2741 | -19.2945 | 0 | 1.1438 | 1.1438 | 1×349 double | 1×349 double | 1×349 double | 1×349 double |
|---|
| 6 | IHTA | 1 | 14 | 150 | 0 | 306.7775 | -69.2820 | -7.1054e-15 | 0 | 1.7974 | 1.7974 | 1×362 double | 1×362 double | 1×362 double | 1×362 double |
|---|
| 7 | IHTA | 1 | 16 | 60 | 1 | 294.8469 | 40 | 29.2820 | 0 | 1.9608 | 1.9608 | 1×129 double | 1×129 double | 1×129 double | 1×129 double |
|---|
| 8 | IHTA | 1 | 18 | 240 | 1 | 320.3605 | -40 | -109.2820 | 0 | 2.2876 | 2.2876 | 1×159 double | 1×159 double | 1×159 double | 1×159 double |
|---|
| 9 | IHTA | 1 | 19 | 150 | 0 | 367.6910 | -69.2820 | -7.1054e-15 | 0 | 2.4510 | 2.4510 | 1×362 double | 1×362 double | 1×362 double | 1×362 double |
|---|
| 10 | IHTA | 1 | 27 | 195 | 1 | 309.4387 | -77.2741 | -60.7055 | 0 | 2.9412 | 2.9412 | 1×283 double | 1×283 double | 1×283 double | 1×283 double |
|---|
| 11 | IHTA | 1 | 29 | 15 | 1 | 332.9378 | 77.2741 | -19.2945 | 0 | 3.1046 | 3.1046 | 1×167 double | 1×167 double | 1×167 double | 1×167 double |
|---|
| 12 | IHTA | 1 | 34 | 330 | 0 | 369.4775 | 69.2820 | -80 | 0 | 3.4314 | 3.4314 | 1×295 double | 1×295 double | 1×295 double | 1×295 double |
|---|
| 13 | IHTA | 1 | 37 | 105 | 0 | 337.2823 | -20.7055 | 37.2741 | 0 | 3.7582 | 3.7582 | 1×362 double | 1×362 double | 1×362 double | 1×362 double |
|---|
| 14 | IHTA | 1 | 40 | 195 | 0 | 423.8782 | -77.2741 | -60.7055 | 0 | 3.9216 | 3.9216 | 1×362 double | 1×362 double | 1×362 double | 1×362 double |
|---|
| ⋮ |
|---|
In this dataset, we have four different subjects (subject), each coming for two sessions (session) on consecutive days at the lab. During each of these sessions they learn to control the displacement of a cursor on a screen, and their task is to reach targets with the cursor. The targets are arranged at discrete angles (reference_direction) in a circle around a starting point. Each line corresponds to a trial (trial_index), and we transformed the index in percentage of trials performed within session (valid_perc, goes from 0 to 100% in each session) or across sessions (valid_perc_session, goes from 0 to 200% across both sessions).
We record for each movement the cursor trajectories in the horizontal (px) and vertical (py) dimensions at a sampling rate of 120Hz. Time values in ms from movement onset (t) and in percentage of movement completion (tperc) are stored. Note here that these time series columns are cells of arrays, in order to store different movement durations for each trial.
Simple time series
gramm can of course represent simple time series data such as the cursor's horizontal position for a given trial. For these uses it is not much more advantageous compared to standard plotting functions.
g=gramm('x',T.t{trial},'y',T.px{trial});
g.set_names('x','Time (ms)','y','x cursor position (mm)');
Plotting repeated time series
A big advantage of gramm over equivalent libraries is the ability of gramm to interpret datasets with repeated time series. We can plot all horizontal cursor trajectories across the different movement directions with a standard gramm call, despite the movements being of different durations and this having a varying number of samples. gramm can use cells of arrays or 2D arrays for this type of data.
To note:
- We will plot the various directions in different subplots. Since there are many directions, instead of using facet_grid(), we use facet_wrap(), which allows a single variable to determine subplot columns, but with wrapping across several rows. This can be deactivated with the button below
- Here we will restrict the display to fast movements of a single subject from the second session, using 'subset'
figure('Position',[100 100 800 400])
selection = T.subject=="IIFG" & T.session==2 & T.hit==1 & cellfun(@max,T.t)<800;
g=gramm('x',T.t,'y',T.px,'color',T.reference_direction,'subset',selection);
g.facet_wrap(T.reference_direction);
g.set_names('x','Time (%of movement)','y','x cursor position (mm)','column','Direction','color','Direction');
Continuous color scales
As seen above, time series data can be colored using a discrete variable, like the movement direction. When the color aesthetic is mapped to a continuous variable, gramm switches to using a continuous color scale. Here we demonstrate it by coloring movements depending on the progression over both sessions from dark blue to yellow (100% denotes the end of session 1 and 200% the end of session 2). This figure clearly shows the progress of the subject, with shorter and more direct trajectories with learning.
To note: the color aesthetic could also be mapped to a variable with the same format as x or y, in which case the color would change within a line.
figure('Position',[100 100 800 400])
g=gramm('x',T.t,'y',T.px,'color',T.reference_direction, 'color',T.valid_perc_session,...
'subset',T.subject=="IIFG" & T.reference_direction==15);
g.set_names('x','Time (ms)','y','x cursor position (mm)','Color','Task progression');
g.export('file_name','timeseries_export','file_type','png');
Averaging repeated time series
In order to summarize this data, gramm can also average these trajectories, with stat_summary(). Here instead of using the column t for the x-axis, we use tperc which corresponds to a percentage of completion for a given movement.
Interactive parameters: Since our timepoints are not aligned, we use linear interpolation on each trajectory with the 'interp_in' option that will compute the mean and confidence interval at the given number of points along the abcissa.
g=gramm('x',T.tperc,'y',T.px,'color',T.reference_direction,'subset',selection);
g.stat_summary('interp_in',40);
g.set_names('x','Time (%of movement)','y','x cursor position (mm)','column','Direction','color','Direction');
Smoothing time series
If we extract the horizontal velocity of the performed movements by simple differentiation of the raw data
T.vx = cellfun(@(t,x)[NaN diff(x)./diff(t)],T.t,T.px,'UniformOutput',false);
When displaying the velocities with geom_line() we obtain noisy velocity values. If we want to plot smoothed lines we can use stat_smooth() instead, which works on repeated trajectories. This is toggled by clicking the button in the code below.
g=gramm('x',T.t,'y',T.vx, ...
'subset',selection & T.reference_direction==15);
g.set_names('x','Time (ms)','y','x cursor speed (m/s)');
2D trajectories
Plotting of time series data is not limited to representing one variable against time. We can for example plot the 2D trajectory of the cursor in the plane across all subjects and both sessions. This figure allows us to understand the different movement strategies across subjects, and confirms the vast improvement between experimental sessions.
To note:
- Since there are many trajectories in the dataset, we will only plot a fifth of them using 'subset'
- We need to display trajectories with the correct aspect ratio. Usually this would be done with axis equal, which sets the axes property DataAspectRatio to [1 1 1]. Since this change needs to be applied to all gramm subplots, we can use the gramm method axe_property() to do it all at once.
- We could also provide a 'z' aesthetic to create 3D plots without further changes
figure('Position',[100 100 700 1000])
g=gramm('x',T.px,'y',T.py,'color',T.reference_direction,'subset',mod(T.trial_index,5)==0);
g.facet_grid(T.subject,T.session);
g.set_names('x','x cursor position (mm)','y','y cursor position (mm)','column','Session','color','Direction','row','Subject');
g.axe_property('DataAspectRatio',[1 1 1]);