11 min read

Categories

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: theta indicates that the theta array is used as the x-axis values for both the cosine and sine data.
  • y: y1 and y: y2 specify the y-axis values for the cosine and sine data, respectively, based on the corresponding y1 and y2 arrays.
  • name: 'Cosine / x-coordinate' and name: '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' } and line: { 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: 1 and columns: 2 indicate 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' and size: 10.
    • We set showlegend: false to avoid displaying distracting legend entries
    • Setting mode: 'markers' indicates that the data should be displayed as markers.
  • 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 at theta = 0 and r = 1.
    • The xaxis: 'x2' and yaxis: 'y2' properties are used to assign the marker on the unit circle to the second subplot.
    • ...markerShared spreads the properties from the markerShared object to inherit the shared marker properties.
  • The Plotly.addTraces function adds the markerData to 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: idx assigns a name to each frame, using the index idx to identify the frame.

  • data contains 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 the x and y properties 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.1 and y: 1.1 set 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 the animate method.
    • args: [null, {...}] defines the arguments passed to the animate method. The first argument is set to null, 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.
    • frame is an object that specifies the frame configuration:
      • The redraw attribute indicates whether the plot should be redrawn between frames. By setting it to false, the plot is not redrawn, resulting in smoother animation without unnecessary flickering or delays.
      • The duration attribute sets the duration of each frame to 100 milliseconds.
  • 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 args is [null] instead of null, 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 0 milliseconds, meaning that there is no delay between frames, resulting in an immediate pause.
  • 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) => {...}): The map function is used to iterate over each frame in the frames array 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 the animate method. 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 frame attribute, the duration of each frame to 50 milliseconds and as before we set redraw to false.
        • As for the button, in the transition attribute, the transition duration is set to 0 milliseconds, resulting in an immediate transition between frames.
  • A slider slide object is creater with attributes step which are the steps defined above and pad which 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.