Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Animations

Animations in Plotly.rs allow you to create dynamic, interactive visualizations that can play through different data states over time.

GDP vs. Life Expectancy Animation

This example demonstrates an animation based on the Gapminder dataset, showing the relationship between GDP per capita and life expectancy across different continents over several decades. The animation is based on the JavaScript example https://plotly.com/javascript/gapminder-example/ and shows how to create buttons and sliders that interact with the animation mechanism.

#![allow(unused)]
fn main() {
// GDP per Capita/Life Expectancy Animation (animated version of the slider
// example)
fn gdp_life_expectancy_animation_example(show: bool, file_name: &str) {
    use plotly::{
        common::Font,
        common::Pad,
        common::Title,
        layout::Axis,
        layout::{
            update_menu::{ButtonBuilder, UpdateMenu, UpdateMenuDirection, UpdateMenuType},
            Animation, AnimationMode, Frame, FrameSettings, Slider, SliderCurrentValue,
            SliderCurrentValueXAnchor, SliderStepBuilder, TransitionSettings,
        },
        Layout, Plot, Scatter,
    };

    let data = load_gapminder_data();

    // Get unique years and sort them
    let years: Vec<i32> = data
        .iter()
        .map(|d| d.year)
        .collect::<std::collections::HashSet<_>>()
        .into_iter()
        .sorted()
        .collect();

    // Create color mapping for continents to match the Python plotly example
    let continent_colors = HashMap::from([
        ("Asia".to_string(), "rgb(99, 110, 250)"),
        ("Europe".to_string(), "rgb(239, 85, 59)"),
        ("Africa".to_string(), "rgb(0, 204, 150)"),
        ("Americas".to_string(), "rgb(171, 99, 250)"),
        ("Oceania".to_string(), "rgb(255, 161, 90)"),
    ]);
    let continents: Vec<String> = continent_colors.keys().cloned().sorted().collect();

    let mut plot = Plot::new();
    let mut initial_traces = Vec::new();

    for (frame_index, &year) in years.iter().enumerate() {
        let mut frame_traces = plotly::Traces::new();

        for continent in &continents {
            let records: Vec<&GapminderData> = data
                .iter()
                .filter(|d| d.continent == *continent && d.year == year)
                .collect();

            if !records.is_empty() {
                let x: Vec<f64> = records.iter().map(|r| r.gdp_per_cap).collect();
                let y: Vec<f64> = records.iter().map(|r| r.life_exp).collect();
                let size: Vec<f64> = records.iter().map(|r| r.pop).collect();
                let hover: Vec<String> = records.iter().map(|r| r.country.clone()).collect();

                let trace = Scatter::new(x, y)
                    .name(continent)
                    .mode(Mode::Markers)
                    .hover_text_array(hover)
                    .marker(
                        plotly::common::Marker::new()
                            .color(*continent_colors.get(continent).unwrap())
                            .size_array(size.into_iter().map(|s| s as usize).collect())
                            .size_mode(plotly::common::SizeMode::Area)
                            .size_ref(200000)
                            .size_min(4),
                    );

                frame_traces.push(trace.clone());

                // Store traces from first year for initial plot
                if frame_index == 0 {
                    initial_traces.push(trace);
                }
            }
        }

        // Create layout for this frame
        let frame_layout = Layout::new()
            .title(Title::with_text(format!(
                "GDP vs. Life Expectancy ({year})"
            )))
            .x_axis(
                Axis::new()
                    .title(Title::with_text("gdpPercap"))
                    .type_(plotly::layout::AxisType::Log),
            )
            .y_axis(
                Axis::new()
                    .title(Title::with_text("lifeExp"))
                    .range(vec![30.0, 85.0]), // Fixed range for Life Expectancy
            );

        // Add frame with all traces for this year
        plot.add_frame(
            Frame::new()
                .name(format!("frame{frame_index}"))
                .data(frame_traces)
                .layout(frame_layout),
        );
    }

    // Add initial traces to the plot (all traces from first year)
    for trace in initial_traces {
        plot.add_trace(trace);
    }

    // Create animation configuration for playing all frames
    let play_animation = Animation::all_frames().options(
        AnimationOptions::new()
            .mode(AnimationMode::Immediate)
            .frame(FrameSettings::new().duration(500).redraw(false))
            .transition(TransitionSettings::new().duration(300))
            .fromcurrent(true),
    );

    let play_button = ButtonBuilder::new()
        .label("Play")
        .animation(play_animation)
        .build()
        .unwrap();

    let pause_animation = Animation::pause();

    let pause_button = ButtonBuilder::new()
        .label("Pause")
        .animation(pause_animation)
        .build()
        .unwrap();

    let updatemenu = UpdateMenu::new()
        .ty(UpdateMenuType::Buttons)
        .direction(UpdateMenuDirection::Right)
        .buttons(vec![play_button, pause_button])
        .x(0.1)
        .y(1.15)
        .show_active(true)
        .visible(true);

    // Create slider steps for each year
    let mut slider_steps = Vec::new();
    for (i, &year) in years.iter().enumerate() {
        let frame_animation = Animation::frames(vec![format!("frame{}", i)]).options(
            AnimationOptions::new()
                .mode(AnimationMode::Immediate)
                .frame(FrameSettings::new().duration(300).redraw(false))
                .transition(TransitionSettings::new().duration(300)),
        );
        let step = SliderStepBuilder::new()
            .label(year.to_string())
            .value(year)
            .animation(frame_animation)
            .build()
            .unwrap();
        slider_steps.push(step);
    }

    let slider = Slider::new()
        .pad(Pad::new(55, 0, 130))
        .current_value(
            SliderCurrentValue::new()
                .visible(true)
                .prefix("Year: ")
                .x_anchor(SliderCurrentValueXAnchor::Right)
                .font(Font::new().size(20).color("rgb(102, 102, 102)")),
        )
        .steps(slider_steps);

    // Set the layout with initial title, buttons, and slider
    let layout = Layout::new()
        .title(Title::with_text(format!(
            "GDP vs. Life Expectancy ({}) - Click 'Play' to animate",
            years[0]
        )))
        .x_axis(
            Axis::new()
                .title(Title::with_text("gdpPercap"))
                .type_(plotly::layout::AxisType::Log),
        )
        .y_axis(
            Axis::new()
                .title(Title::with_text("lifeExp"))
                .range(vec![30.0, 85.0]), // Fixed range for Life Expectancy
        )
        .update_menus(vec![updatemenu])
        .sliders(vec![slider]);

    plot.set_layout(layout);

    let path = write_example_to_html(&plot, file_name);
    if show {
        plot.show_html(path);
    }
}
}