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); } } }