Skip to content

Plotly Template

Plotly is a powerful graphing library that makes interactive, publication-quality graphs online. It is well-suited for both static and animated plots, making it a great choice for web applications.

Simplest plotly plot

The following example demonstrates how to create a simple static plot using Plotly within a Shiny app

Application

Code
app.py
import plotly.express as px
from palmerpenguins import load_penguins
from shiny.express import input, ui
from shinywidgets import render_widget

penguins = load_penguins()

ui.input_slider("n", "Number of bins", 1, 100, 20)
ui.input_dark_mode(id="dark_mode")

@render_widget
def plot():
    if input.dark_mode() == "dark":
        template = "plotly_dark"
    else:
        template = "plotly_white"

    scatterplot = px.histogram(
        data_frame=penguins,
        x="body_mass_g",
        nbins=input.n(),
    ).update_layout(
        template=template,
        title={"text": "Penguin Mass", "x": 0.5},
        yaxis_title="Count",
        xaxis_title="Body Mass (g)",
    )

    return scatterplot

Animations using plotly (python)

Plotly makes it easy to create animated plots with minimal setup. The following example demonstrates how to create an animated plot using Plotly within a Shiny app.

Application

Code
app.py
import numpy as np
import plotly.graph_objects as go
from shiny import App, Inputs, Outputs, Session, render, ui


app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_slider('amplitude', 'Amplitude', min=0.1, max=2, value=1, step=0.1),
        ui.input_dark_mode(id="dark_mode")
    ),
    ui.output_ui("plot", height='100%')
)

def server(input: Inputs, output: Outputs, session: Session):
    x = np.linspace(0, 2 * np.pi, 100)

    @render.ui
    def plot():
        # Set template based on dark mode
        template = "plotly_dark" if input.dark_mode() == "dark" else "plotly_white"

        amp = input.amplitude()
        num_frames = 100

        fig = go.Figure(
            data=[go.Scatter(x=x, y=amp * np.sin(x))],
            frames=[
                go.Frame(data=[go.Scatter(y=amp * np.sin(x + i / num_frames * 2 * np.pi))])
                for i in range(num_frames)
            ]
        )

        fig.update_layout(
            template=template,
            margin=dict(l=20, r=20, t=20, b=20),
            showlegend=False
        )

        # Generate Plotly HTML without full document wrapper
        plot_html = fig.to_html(
            auto_play=False, # Disable animation here, because we will control it via JavaScript injection below
            include_plotlyjs=True,#"cdn",
            full_html=False,
            div_id="animated_plot"
        )

        # Inject JavaScript to force infinite looping
        loop_script = """
        <script>
        (function () {
            function startAnimation() {
                const gd = document.getElementById("animated_plot");
                if (!gd || gd.__loopStarted) return;
                gd.__loopStarted = true;

                function loop() {
                    Plotly.animate(gd, null, {
                        frame: { duration: 10, redraw: false },
                        transition: { duration: 0 }
                    }).then(loop);
                }

                loop();
            }
            startAnimation();
        })();
        </script>
        """
        return ui.HTML(plot_html + loop_script)

app = App(app_ui, server)

Note that this approach requires animations that loop smoothly. The entire frame stack must be preloaded when the plot is first rendered, which may lead to longer initial load times for complex animations. Additionally, this precludes a true "live" animation that updates based on real-time or on-the-fly generated data.

Animations using plotly (injecting JavaScript)

For more advanced use cases, you can inject custom JavaScript code to control the animation behavior in Plotly directly. This allows for greater flexibility and smoother animations. The following example (from plotly.js) demonstrates how to create an animated plot using Plotly with custom JavaScript within a Shiny app, allowing for on-the-fly generation of new frames.

Application

Code
app.py
import plotly.graph_objects as go
from shiny import App, Inputs, Outputs, Session, render, ui

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_slider("sigma", "Sigma (s)", min=0, max=20, value=10, step=0.1),
        ui.input_slider("beta", "Beta (b)", min=0, max=5, value=8/3, step=0.01),
        ui.input_slider("rho", "Rho (r)", min=0, max=50, value=28, step=0.5),
        ui.input_slider("n", "Number of points", min=10, max=500, value=100, step=10),
        ui.input_slider("dt", "Time step (dt)", min=0.001, max=0.05, value=0.015, step=0.001),
    ),
    ui.output_ui("plot", height="100%")
)


def build_plot_from_js(javascript_text, **kwargs):
    fig = go.Figure(
    )

    plot_html = fig.to_html(
        auto_play=False,
        include_plotlyjs=True,
        full_html=False,
        div_id="animated_plot"
    )

    for k, v in kwargs.items():
        javascript_text = javascript_text.replace(k, str(v))

    loop_script = f"""
    <script>
    {javascript_text}
    </script>
    """

    return ui.HTML(plot_html + loop_script)

def server(input: Inputs, output: Outputs, session: Session):
    @render.ui
    def plot():
        s = input.sigma()
        b = input.beta()
        r = input.rho()
        dt = input.dt()

        n = input.n()

        loop_script="""
        (function () {
            // ----- DESTROY PREVIOUS INSTANCE -----
            if (window.lorenzAnimation) {
                cancelAnimationFrame(window.lorenzAnimation.frameId);
                Plotly.purge('animated_plot');
                window.lorenzAnimation = null;
            }

            // ----- CREATE NEW INSTANCE -----
            var controller = {
                frameId: null
            };
            window.lorenzAnimation = controller;

            var n = VAR_n;
            var x = [], y = [], z = [];
            var dt = VAR_dt;

            var s = VAR_s;
            var b = VAR_b;
            var r = VAR_r;

            for (var i = 0; i < n; i++) {
                x[i] = Math.random() * 2 - 1;
                y[i] = Math.random() * 2 - 1;
                z[i] = 30 + Math.random() * 10;
            }

            Plotly.react('animated_plot', [{
                x: x,
                y: z,
                mode: 'markers',
            }], {
                xaxis: {
                    range: [-40, 40],
                },
                yaxis: {
                    range: [0, 60],
                }
            });


            function compute () {
                for (var i = 0; i < n; i++) {
                    var dx = s * (y[i] - x[i]);
                    var dy = x[i] * (r - z[i]) - y[i];
                    var dz = x[i] * y[i] - b * z[i];

                    x[i] += dx * dt;
                    y[i] += dy * dt;
                    z[i] += dz * dt;
                }
            }

            function update () {
                compute();

                Plotly.animate('animated_plot', {
                    data: [{ x: x, y: z }]
                }, {
                    transition: { duration: 0 },
                    frame: { duration: 0, redraw: false }
                });

                controller.frameId = requestAnimationFrame(update);
            }

            controller.frameId = requestAnimationFrame(update);
        })();
        """

        return build_plot_from_js(loop_script, VAR_n=n, VAR_dt=dt, VAR_s=s, VAR_b=b, VAR_r=r)


app = App(app_ui, server)