@@ -7,8 +7,8 @@ use plotly::{
77 common:: { Anchor , ColorScalePalette , Font , Mode , Pad , Title , Visible } ,
88 layout:: {
99 update_menu:: { ButtonBuilder , UpdateMenu , UpdateMenuDirection , UpdateMenuType } ,
10- Axis , BarMode , Layout , Slider , SliderCurrentValue , SliderCurrentValueXAnchor , SliderStep ,
11- SliderStepBuilder ,
10+ AnimationOptions , Axis , BarMode , Layout , Slider , SliderCurrentValue ,
11+ SliderCurrentValueXAnchor , SliderStep , SliderStepBuilder ,
1212 } ,
1313 Bar , HeatMap , Plot , Scatter ,
1414} ;
@@ -388,7 +388,7 @@ fn gdp_life_expectancy_slider_example(show: bool, file_name: &str) {
388388 visible[ start..end] . fill ( Visible :: True ) ;
389389
390390 SliderStepBuilder :: new ( )
391- . label ( format ! ( " year = {year}" ) )
391+ . label ( year. to_string ( ) )
392392 . value ( year)
393393 . push_restyle ( Scatter :: < f64 , f64 > :: modify_visible ( visible) )
394394 . push_relayout ( Layout :: modify_title ( format ! (
@@ -409,8 +409,18 @@ fn gdp_life_expectancy_slider_example(show: bool, file_name: &str) {
409409 . title ( Title :: with_text ( "gdpPercap" ) )
410410 . type_ ( plotly:: layout:: AxisType :: Log ) ,
411411 )
412- . y_axis ( Axis :: new ( ) . title ( Title :: with_text ( "lifeExp" ) ) )
413- . sliders ( vec ! [ Slider :: new( ) . active( 0 ) . steps( steps) ] ) ;
412+ . y_axis (
413+ Axis :: new ( )
414+ . title ( Title :: with_text ( "lifeExp" ) )
415+ . range ( vec ! [ 30.0 , 85.0 ] ) , // Fixed range for Life Expectancy
416+ )
417+ . sliders ( vec ! [ Slider :: new( ) . active( 0 ) . steps( steps) . current_value(
418+ SliderCurrentValue :: new( )
419+ . visible( true )
420+ . prefix( "Year: " )
421+ . x_anchor( SliderCurrentValueXAnchor :: Right )
422+ . font( Font :: new( ) . size( 20 ) . color( "rgb(102, 102, 102)" ) ) ,
423+ ) ] ) ;
414424 plot. set_layout ( layout) ;
415425 let path = write_example_to_html ( & plot, file_name) ;
416426 if show {
@@ -419,6 +429,291 @@ fn gdp_life_expectancy_slider_example(show: bool, file_name: &str) {
419429}
420430// ANCHOR_END: gdp_life_expectancy_slider_example
421431
432+ // ANCHOR: gdp_life_expectancy_animation_example
433+ // GDP per Capita/Life Expectancy Animation (animated version of the slider
434+ // example)
435+ fn gdp_life_expectancy_animation_example ( show : bool , file_name : & str ) {
436+ use plotly:: {
437+ common:: Font ,
438+ common:: Pad ,
439+ common:: Title ,
440+ layout:: Axis ,
441+ layout:: {
442+ update_menu:: { ButtonBuilder , UpdateMenu , UpdateMenuDirection , UpdateMenuType } ,
443+ Animation , AnimationMode , Frame , FrameSettings , Slider , SliderCurrentValue ,
444+ SliderCurrentValueXAnchor , SliderStepBuilder , TransitionSettings ,
445+ } ,
446+ Layout , Plot , Scatter ,
447+ } ;
448+
449+ let data = load_gapminder_data ( ) ;
450+
451+ // Get unique years and sort them
452+ let years: Vec < i32 > = data
453+ . iter ( )
454+ . map ( |d| d. year )
455+ . collect :: < std:: collections:: HashSet < _ > > ( )
456+ . into_iter ( )
457+ . sorted ( )
458+ . collect ( ) ;
459+
460+ // Create color mapping for continents to match the Python plotly example
461+ let continent_colors = HashMap :: from ( [
462+ ( "Asia" . to_string ( ) , "rgb(99, 110, 250)" ) ,
463+ ( "Europe" . to_string ( ) , "rgb(239, 85, 59)" ) ,
464+ ( "Africa" . to_string ( ) , "rgb(0, 204, 150)" ) ,
465+ ( "Americas" . to_string ( ) , "rgb(171, 99, 250)" ) ,
466+ ( "Oceania" . to_string ( ) , "rgb(255, 161, 90)" ) ,
467+ ] ) ;
468+ let continents: Vec < String > = continent_colors. keys ( ) . cloned ( ) . sorted ( ) . collect ( ) ;
469+
470+ let mut plot = Plot :: new ( ) ;
471+ let mut initial_traces = Vec :: new ( ) ;
472+
473+ for ( frame_index, & year) in years. iter ( ) . enumerate ( ) {
474+ let mut frame_traces = plotly:: Traces :: new ( ) ;
475+
476+ for continent in & continents {
477+ let records: Vec < & GapminderData > = data
478+ . iter ( )
479+ . filter ( |d| d. continent == * continent && d. year == year)
480+ . collect ( ) ;
481+
482+ if !records. is_empty ( ) {
483+ let x: Vec < f64 > = records. iter ( ) . map ( |r| r. gdp_per_cap ) . collect ( ) ;
484+ let y: Vec < f64 > = records. iter ( ) . map ( |r| r. life_exp ) . collect ( ) ;
485+ let size: Vec < f64 > = records. iter ( ) . map ( |r| r. pop ) . collect ( ) ;
486+ let hover: Vec < String > = records. iter ( ) . map ( |r| r. country . clone ( ) ) . collect ( ) ;
487+
488+ let trace = Scatter :: new ( x, y)
489+ . name ( continent)
490+ . mode ( Mode :: Markers )
491+ . hover_text_array ( hover)
492+ . marker (
493+ plotly:: common:: Marker :: new ( )
494+ . color ( * continent_colors. get ( continent) . unwrap ( ) )
495+ . size_array ( size. into_iter ( ) . map ( |s| s as usize ) . collect ( ) )
496+ . size_mode ( plotly:: common:: SizeMode :: Area )
497+ . size_ref ( 200000 )
498+ . size_min ( 4 ) ,
499+ ) ;
500+
501+ frame_traces. push ( trace. clone ( ) ) ;
502+
503+ // Store traces from first year for initial plot
504+ if frame_index == 0 {
505+ initial_traces. push ( trace) ;
506+ }
507+ }
508+ }
509+
510+ // Create layout for this frame
511+ let frame_layout = Layout :: new ( )
512+ . title ( Title :: with_text ( format ! (
513+ "GDP vs. Life Expectancy ({year})"
514+ ) ) )
515+ . x_axis (
516+ Axis :: new ( )
517+ . title ( Title :: with_text ( "gdpPercap" ) )
518+ . type_ ( plotly:: layout:: AxisType :: Log ) ,
519+ )
520+ . y_axis (
521+ Axis :: new ( )
522+ . title ( Title :: with_text ( "lifeExp" ) )
523+ . range ( vec ! [ 30.0 , 85.0 ] ) , // Fixed range for Life Expectancy
524+ ) ;
525+
526+ // Add frame with all traces for this year
527+ plot. add_frame (
528+ Frame :: new ( )
529+ . name ( format ! ( "frame{frame_index}" ) )
530+ . data ( frame_traces)
531+ . layout ( frame_layout) ,
532+ ) ;
533+ }
534+
535+ // Add initial traces to the plot (all traces from first year)
536+ for trace in initial_traces {
537+ plot. add_trace ( trace) ;
538+ }
539+
540+ // Create animation configuration for playing all frames
541+ let play_animation = Animation :: all_frames ( ) . options (
542+ AnimationOptions :: new ( )
543+ . mode ( AnimationMode :: Immediate )
544+ . frame ( FrameSettings :: new ( ) . duration ( 500 ) . redraw ( false ) )
545+ . transition ( TransitionSettings :: new ( ) . duration ( 300 ) )
546+ . fromcurrent ( true ) ,
547+ ) ;
548+
549+ let play_button = ButtonBuilder :: new ( )
550+ . label ( "Play" )
551+ . animation ( play_animation)
552+ . build ( )
553+ . unwrap ( ) ;
554+
555+ let pause_animation = Animation :: pause ( ) ;
556+
557+ let pause_button = ButtonBuilder :: new ( )
558+ . label ( "Pause" )
559+ . animation ( pause_animation)
560+ . build ( )
561+ . unwrap ( ) ;
562+
563+ let updatemenu = UpdateMenu :: new ( )
564+ . ty ( UpdateMenuType :: Buttons )
565+ . direction ( UpdateMenuDirection :: Right )
566+ . buttons ( vec ! [ play_button, pause_button] )
567+ . x ( 0.1 )
568+ . y ( 1.15 )
569+ . show_active ( true )
570+ . visible ( true ) ;
571+
572+ // Create slider steps for each year
573+ let mut slider_steps = Vec :: new ( ) ;
574+ for ( i, & year) in years. iter ( ) . enumerate ( ) {
575+ let frame_animation = Animation :: frames ( vec ! [ format!( "frame{}" , i) ] ) . options (
576+ AnimationOptions :: new ( )
577+ . mode ( AnimationMode :: Immediate )
578+ . frame ( FrameSettings :: new ( ) . duration ( 300 ) . redraw ( false ) )
579+ . transition ( TransitionSettings :: new ( ) . duration ( 300 ) ) ,
580+ ) ;
581+ let step = SliderStepBuilder :: new ( )
582+ . label ( year. to_string ( ) )
583+ . value ( year)
584+ . animation ( frame_animation)
585+ . build ( )
586+ . unwrap ( ) ;
587+ slider_steps. push ( step) ;
588+ }
589+
590+ let slider = Slider :: new ( )
591+ . pad ( Pad :: new ( 55 , 0 , 130 ) )
592+ . current_value (
593+ SliderCurrentValue :: new ( )
594+ . visible ( true )
595+ . prefix ( "Year: " )
596+ . x_anchor ( SliderCurrentValueXAnchor :: Right )
597+ . font ( Font :: new ( ) . size ( 20 ) . color ( "rgb(102, 102, 102)" ) ) ,
598+ )
599+ . steps ( slider_steps) ;
600+
601+ // Set the layout with initial title, buttons, and slider
602+ let layout = Layout :: new ( )
603+ . title ( Title :: with_text ( format ! (
604+ "GDP vs. Life Expectancy ({}) - Click 'Play' to animate" ,
605+ years[ 0 ]
606+ ) ) )
607+ . x_axis (
608+ Axis :: new ( )
609+ . title ( Title :: with_text ( "gdpPercap" ) )
610+ . type_ ( plotly:: layout:: AxisType :: Log ) ,
611+ )
612+ . y_axis (
613+ Axis :: new ( )
614+ . title ( Title :: with_text ( "lifeExp" ) )
615+ . range ( vec ! [ 30.0 , 85.0 ] ) , // Fixed range for Life Expectancy
616+ )
617+ . update_menus ( vec ! [ updatemenu] )
618+ . sliders ( vec ! [ slider] ) ;
619+
620+ plot. set_layout ( layout) ;
621+
622+ let path = write_example_to_html ( & plot, file_name) ;
623+ if show {
624+ plot. show_html ( path) ;
625+ }
626+ }
627+ // ANCHOR_END: gdp_life_expectancy_animation_example
628+
629+ // ANCHOR: animation_randomize_example
630+ /// Animation example based on the Plotly.js "Randomize" animation.
631+ /// This demonstrates the new builder API for animation configuration.
632+ fn animation_randomize_example ( show : bool , file_name : & str ) {
633+ use plotly:: {
634+ layout:: update_menu:: { ButtonBuilder , UpdateMenu , UpdateMenuDirection , UpdateMenuType } ,
635+ layout:: {
636+ Animation , AnimationEasing , AnimationMode , Frame , FrameSettings , TransitionSettings ,
637+ } ,
638+ Plot , Scatter ,
639+ } ;
640+
641+ // Initial data
642+ let x = vec ! [ 1 , 2 , 3 ] ;
643+ let y0 = vec ! [ 0.0 , 0.5 , 1.0 ] ;
644+ let y1 = vec ! [ 0.2 , 0.8 , 0.3 ] ;
645+ let y2 = vec ! [ 0.9 , 0.1 , 0.7 ] ;
646+
647+ let mut plot = Plot :: new ( ) ;
648+ let base =
649+ Scatter :: new ( x. clone ( ) , y0. clone ( ) ) . line ( plotly:: common:: Line :: new ( ) . simplify ( false ) ) ;
650+ plot. add_trace ( base. clone ( ) ) ;
651+
652+ // Add frames with different y-values and auto-adjusting layouts
653+ let mut trace0 = plotly:: Traces :: new ( ) ;
654+ trace0. push ( base) ;
655+
656+ let mut trace1 = plotly:: Traces :: new ( ) ;
657+ trace1. push ( Scatter :: new ( x. clone ( ) , y1. clone ( ) ) ) ;
658+
659+ let mut trace2 = plotly:: Traces :: new ( ) ;
660+ trace2. push ( Scatter :: new ( x. clone ( ) , y2. clone ( ) ) ) ;
661+
662+ let animation = Animation :: new ( ) . options (
663+ AnimationOptions :: new ( )
664+ . mode ( AnimationMode :: Immediate )
665+ . frame ( FrameSettings :: new ( ) . duration ( 500 ) )
666+ . transition (
667+ TransitionSettings :: new ( )
668+ . duration ( 500 )
669+ . easing ( AnimationEasing :: CubicInOut ) ,
670+ ) ,
671+ ) ;
672+
673+ let layout0 = plotly:: Layout :: new ( )
674+ . title ( Title :: with_text ( "First frame" ) )
675+ . y_axis ( plotly:: layout:: Axis :: new ( ) . range ( vec ! [ 0.0 , 1.0 ] ) ) ;
676+ let layout1 = plotly:: Layout :: new ( )
677+ . title ( Title :: with_text ( "Second frame" ) )
678+ . y_axis ( plotly:: layout:: Axis :: new ( ) . range ( vec ! [ 0.0 , 1.0 ] ) ) ;
679+ let layout2 = plotly:: Layout :: new ( )
680+ . title ( Title :: with_text ( "Third frame" ) )
681+ . y_axis ( plotly:: layout:: Axis :: new ( ) . range ( vec ! [ 0.0 , 1.0 ] ) ) ;
682+
683+ // Add frames using the new API
684+ plot. add_frame ( Frame :: new ( ) . name ( "frame0" ) . data ( trace0) . layout ( layout0) )
685+ . add_frame ( Frame :: new ( ) . name ( "frame1" ) . data ( trace1) . layout ( layout1) )
686+ . add_frame ( Frame :: new ( ) . name ( "frame2" ) . data ( trace2) . layout ( layout2) ) ;
687+
688+ let randomize_button = ButtonBuilder :: new ( )
689+ . label ( "Animate" )
690+ . animation ( animation)
691+ . build ( )
692+ . unwrap ( ) ;
693+
694+ let updatemenu = UpdateMenu :: new ( )
695+ . ty ( UpdateMenuType :: Buttons )
696+ . direction ( UpdateMenuDirection :: Right )
697+ . buttons ( vec ! [ randomize_button] )
698+ . x ( 0.1 )
699+ . y ( 1.15 )
700+ . show_active ( true )
701+ . visible ( true ) ;
702+
703+ plot. set_layout (
704+ Layout :: new ( )
705+ . title ( "Animation Example - Click 'Animate'" )
706+ . y_axis ( Axis :: new ( ) . title ( "Y Axis" ) . range ( vec ! [ 0.0 , 1.0 ] ) )
707+ . update_menus ( vec ! [ updatemenu] ) ,
708+ ) ;
709+
710+ let path = plotly_utils:: write_example_to_html ( & plot, file_name) ;
711+ if show {
712+ plot. show_html ( path) ;
713+ }
714+ }
715+ // ANCHOR_END: animation_randomize_example
716+
422717fn main ( ) {
423718 // Change false to true on any of these lines to display the example.
424719 bar_plot_with_dropdown_for_different_data ( false , "bar_plot" ) ;
@@ -429,4 +724,7 @@ fn main() {
429724 bar_chart_with_slider_customization ( false , "bar_chart_with_slider_customization" ) ;
430725 sinusoidal_slider_example ( false , "sinusoidal_slider_example" ) ;
431726 gdp_life_expectancy_slider_example ( false , "gdp_life_expectancy_slider_example" ) ;
727+ // Animation examples
728+ animation_randomize_example ( false , "animation_randomize_example" ) ;
729+ gdp_life_expectancy_animation_example ( false , "gdp_life_expectancy_animation_example" ) ;
432730}
0 commit comments