Introduction
Plotly.js is a powerful JavaScript library for creating interactive data visualizations. In this blog, we will demystify the process of creating interactive animations using Plotly.js. We will learn how to make the animation below in which a marker traces the unit circle as the angle increases from 0 to $2\pi$ and simultaneously traces the cosine and sine waves the represent the x and y coordinates of the marker, respectively.
See the Pen Untitled by Riksi (@Riksi) on CodePen.
While the Plotly website provides numerous example codes, understanding the intricacies of animating visualizations can sometimes be challenging. Additionally, there is limited information available on animating a pair of subplots. This blog aims to address these issues by providing a step-by-step guide to animating a sequence of frames comprising of a pair of subplots.
HTML and JavaScript Setup
To embed Plotly.js animations in a web page, you’ll need to set up the HTML structure and include the Plotly.js library. Here’s a basic HTML template to get you started:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Plotly.js Animation</title>
<!-- Include the Plotly.js library -->
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<!-- Create a container div for the plot -->
<div id="plotContainer"></div>
<!-- Add additional HTML elements for buttons, sliders, or other UI components -->
<script>
// Your JavaScript code goes here or alternatively you can link to an external .js file
</script>
</body>
</html>
You can open this file in a web browser. At this point, the page will be blank since we haven’t added any content yet. Let’s go ahead and do that now.
Making Line Plots
To begin, let’s create a basic line plot using Plotly.js. Add this code to plot a cosine and sine wave between 0 and $2\pi$:
// Line plot data
const numPoints = 100;
const diff = 2 * Math.PI / (numPoints - 1);
const theta = Plotly.d3.range(0, 2 * Math.PI + diff, diff);
const y1 = theta.map(t => Math.cos(t));
const y2 = theta.map(t => Math.sin(t));
const trigData = [
{ x: theta, y: y1, name: 'Cosine / x-coordinate', mode: 'lines', line: { color: 'blue' } },
{ x: theta, y: y2, name: 'Sine / y-coordinate', mode: 'lines', line: { color: 'red' } },
];
Here’s a breakdown of the parameters used in trigData:
x: thetaindicates that thethetaarray is used as the x-axis values for both the cosine and sine data.y: y1andy: y2specify the y-axis values for the cosine and sine data, respectively, based on the correspondingy1andy2arrays.name: 'Cosine / x-coordinate'andname: 'Sine / y-coordinate'provide labels for identifying the traces in the plot.mode: 'lines'indicates that the data should be displayed as lines.line: { color: 'blue' }andline: { color: 'red' }set the colors for the lines representing the cosine and sine waves.
Now let’s move on to the code for generating data for the unit circle plot:
const r = theta.map(t => 1);
// Circle data
const circData = [
{
x: y1,
y: y2,
mode: 'lines',
name: 'Unit Circle',
xaxis: 'x2',
yaxis: 'y2',
line: {color: 'green' },
},
{
x: [0, 1],
y: [0, 0],
mode: 'lines',
name: 'Radius',
xaxis: 'x2',
yaxis: 'y2',
line: {color: 'black' },
},
];
The parameters used in circData are similar to those used in trigData except that
we have also set the xaxis and yaxis values to 'x2' and 'y2' to indicate that the data should be plotted in the second subplot.
Finally, let’s set up the layout and add the plot to the 'plotContainer' div:
// Create the layout with subplots
const layout = {
grid: { rows: 1, columns: 2, pattern: 'independent' },
yaxis2: {scaleanchor: "x2"}
};
const data = [...trigData, ...circData];
Plotly.newPlot('plotContainer', data, layout);
Here, we create the layout object specifying the grid configuration for the subplots. The parameters used are:
rows: 1andcolumns: 2indicate that we want one row and two columns for our subplot grid.pattern: 'independent'ensures that the subplots have independent scales.yaxis2: { scaleanchor: 'x2' }ensures that the y-axis of the second subplot shares the same scale as the x-axis of the subplot. This ensures that the circle appears as a circle and not an ellipse.
Finally, we combine the trigData and circData arrays into a single data array and use Plotly.newPlot to create the plot in the 'plotContainer' div using the specified data and layout.
Adding Markers for Animation:
In the animation, we’ll have markers that move along the cosine and sine waves and the circumference of the circle.
const markerShared = {marker:{color: 'black', size: 10}, showlegend: false, mode: 'markers'};
const markerData = [
{ x: [0], y: [Math.cos(0)], ...markerShared},
{ x: [0], y: [Math.sin(0)], ...markerShared},
{ x: [1], y: [0], xaxis: 'x2', yaxis: 'y2', ...markerShared}
];
Plotly.addTraces('plotContainer', markerData);
In this code snippet, we add markers to the line plot and polar plot for animation. Here’s an explanation of the parameters used in markerData and the Plotly.addTraces function:
markerShared: This variable defines an object that contains shared properties for the markers:- The marker appearance is set using
color: 'black'andsize: 10. - We set
showlegend: falseto avoid displaying distracting legend entries - Setting
mode: 'markers'indicates that the data should be displayed as markers.
- The marker appearance is set using
markerData: This array contains three objects representing the initial markers- The markers on the cosine and sine curves are initially placed at
theta = 0, while the marker on the unit circle is initially placed attheta = 0andr = 1. - The
xaxis: 'x2'andyaxis: 'y2'properties are used to assign the marker on the unit circle to the second subplot. ...markerSharedspreads the properties from themarkerSharedobject to inherit the shared marker properties.
- The markers on the cosine and sine curves are initially placed at
- The
Plotly.addTracesfunction adds themarkerDatato the'plotContainer'plot, introducing the markers to the existing subplots.
These markers will be used in the subsequent animation frames to show the movement along the waveforms and the circumference of the circle.
Creating Animation Frames:
Now, let’s create animation frames that move the markers as theta goes from 0 to $2\pi$.
// Create animation frames
const frames = theta.map(
(i, idx) => {
let cosX = Math.cos(i);
let sinX = Math.sin(i);
return {
name: idx,
data: [
{ x: [0, cosX], y: [0, sinX] },
{ x: [i], y: [cosX] },
{ x: [i], y: [sinX] },
{ x: [cosX], y: [sinX] },
],
traces: [3, 4, 5, 6],
}
}
)
Plotly.addFrames('plotContainer', frames);
The frame variable holds an array of frame objects that define the animation frames. Each frame object represents a specific theta value in the theta array and has the following properties:
-
name: idxassigns a name to each frame, using the indexidxto identify the frame. -
datacontains an array of objects representing the updated data for each trace in the plot for a given frame. The objects specify the new x and y coordinates for each trace via thexandyproperties using the current value of theta (i). -
traces: [3, 4, 5, 6]specifies the traces (marker traces) that need to be updated in the plot. The trace indices correspond to the cosine marker, cosine marker x-axis, sine marker y-axis, and circle marker, respectively. By contrast, the circle, cosine and sine wave traces remain unchanged.
Plotly.addFrames adds the frames to the plot identified by the ‘plotContainer’ div, allowing the animation to take place. The frames array is passed as the second parameter to incorporate the animation frames.
We have now defined the animation sequence for the markers’ movement along the curves. Now let’s add some buttons to control the animation.
Adding Buttons for Control
To provide control over the animation, let’s add buttons to start, pause, and reset the animation. Here’s an example code snippet to add these buttons:
// Code snippet to add buttons for control
// Add buttons for control
const updatemenus = [
{
buttons: [{
method: 'animate',
args: [null, {
mode: 'immediate',
fromcurrent: true,
transition: {duration: 0},
frame: {duration: 100, redraw: false}
}],
label: 'Play'
}, {
method: 'animate',
args: [[null], {
mode: 'immediate',
transition: {duration: 0},
frame: {duration: 0, redraw: false}
}],
label: 'Pause'
},{
method: 'animate',
args: [[frames[0].name], {
mode: 'immediate',
transition: {duration: 0},
frame: {duration: 0, redraw: false}
}],
label: 'Reset'
}],
type: 'buttons',
pad: {t: -50, r: 100},
x: 0.1,
y: 1.1,
},
]
Plotly.update('plotContainer', {}, { updatemenus: updatemenus });
Here’s an explanation of how the buttons are created and the layout is updated:
-
updatemenus: This variable holds an array of objects representing the update menus. Each object represents a set of buttons for a specific update menu.buttons: This property within each update menu object holds an array of button objects. Each button object represents a button with its associated properties.type: 'buttons'specifies that the update menu contains buttons.pad: { t: -50, r: 100 }adjusts the padding of the update menu, pushing it up and to the right.x: 0.1andy: 1.1set the position of the update menu within the plot.
Let us now look at how the buttons are configured:
- The “Play” button:
method: 'animate'specifies that the button triggers an animation by calling theanimatemethod.args: [null, {...}]defines the arguments passed to theanimatemethod. The first argument is set tonull, indicating that all frames should be played. The second argument is an object that specifies the animation configuration:mode: 'immediate'ensures that the animation starts immediately.transition: { duration: 0 }sets the transition duration to 0 milliseconds, resulting in an immediate transition between frames.frameis an object that specifies the frame configuration:- The
redrawattribute indicates whether the plot should be redrawn between frames. By setting it tofalse, the plot is not redrawn, resulting in smoother animation without unnecessary flickering or delays. - The
durationattribute sets the duration of each frame to 100 milliseconds.
- The
- The settings for the “Pause” button are very similar to the “Play” button, with the exception of the following differences:
- The first element of the
argsis[null]instead ofnull, indicating that we wish to interrupt any currently running animations with a new list of frames. Here the new list of frames is empty, so the animation is halted. - The frame duration is set to
0milliseconds, meaning that there is no delay between frames, resulting in an immediate pause.
- The first element of the
- The settings for the “Reset” button are similar to the “Pause” button except that the first argument is
[[frames[0].name]]instead of[[null]], specifying that the animation should reset to the initial frame.
After the buttons have been configured the Plotly.update function updates the layout of the 'plotContainer' plot by adding the specified updatemenus. The empty object {} is passed as the second parameter to Plotly.update to indicate that no data update is required.
Incorporating a Slider:
To enhance the interactive nature of our animation, let’s add a slider that allows users to control the animation frame. Here’s an example code snippet to add a slider:
// Code snippet to add a slider
const sliderSteps = [];
for (let i = 0; i <= 2 * Math.PI; i += 0.1) {
sliderSteps.push({ method: 'animate', args: [['linePlot'], { frame: { duration: 50, redraw: false }, transition: { duration: 0 } }] });
}
const slider = [
{
pad: { t: 30 },
steps: sliderSteps,
},
];
Plotly.update('linePlot', { sliders: slider });
Breakdown of the code:
-
sliderSteps: This variable holds an array of steps that define the configuration for each step of the slider. Each step corresponds to a specific frame in the animation.-
frames.map((frame, idx) => {...}): Themapfunction is used to iterate over each frame in theframesarray and create a corresponding step object for the slider.method: 'animate'specifies that the step triggers an animation.label: (theta[idx] * 180 / Math.PI).toFixed() + '°'sets the label for the step, displaying the angle in degrees corresponding to the current frame’s theta value.args: [[frame.name], {...}]defines the arguments passed to theanimatemethod. The first argument is an array containing the name of the current frame, indicating which frame to display. The second argument is an object that specifies the animation configuration.- In the
frameattribute, the duration of each frame to 50 milliseconds and as before we setredrawtofalse. - As for the button, in the
transitionattribute, the transition duration is set to 0 milliseconds, resulting in an immediate transition between frames.
- In the
-
-
A
sliderslide object is creater with attributesstepwhich are the steps defined above andpadwhich adjusts the padding of the slider, in this case pushing it down from the top.
Finally we call Plotly.update once more, again with no data, and with the slider object passed in as an update to the layout.
Conclusion
In this tutorial we have seen how to create an animated plot using Plotly.js. We have seen how to create a plot with multiple traces and subplots, how to create a frame for each trace, and how to define the animation sequence. We have also seen how to add buttons and a slider to control the animation. You can now build on this example to create your own animated plots. To learn more about creating animations with Plotly.js, check out the Plotly.js documentation.