Skip to main content

plotly/layout/
animation.rs

1//! Animation support for Plotly.rs
2//!
3//! This module provides animation configuration for Plotly.js updatemenu
4//! buttons and slider steps, following the Plotly.js animation API
5//! specification.
6
7use plotly_derive::FieldSetter;
8use serde::ser::{SerializeSeq, Serializer};
9use serde::Serialize;
10
11use crate::{Layout, Traces};
12
13/// A frame represents a single state in an animation sequence.
14/// Based on Plotly.js frame_attributes.js specification
15#[serde_with::skip_serializing_none]
16#[derive(Serialize, Clone, FieldSetter)]
17pub struct Frame {
18    /// An identifier that specifies the group to which the frame belongs,
19    /// used by animate to select a subset of frames
20    group: Option<String>,
21    /// A label by which to identify the frame
22    name: Option<String>,
23    /// A list of trace indices that identify the respective traces in the data
24    /// attribute
25    traces: Option<Vec<usize>>,
26    /// The name of the frame into which this frame's properties are merged
27    /// before applying. This is used to unify properties and avoid needing
28    /// to specify the same values for the same properties in multiple
29    /// frames.
30    baseframe: Option<String>,
31    /// A list of traces this frame modifies. The format is identical to the
32    /// normal trace definition.
33    data: Option<Traces>,
34    /// Layout properties which this frame modifies. The format is identical to
35    /// the normal layout definition.
36    layout: Option<Layout>,
37}
38
39impl Frame {
40    pub fn new() -> Self {
41        Default::default()
42    }
43}
44
45/// Represents the animation arguments array for Plotly.js
46/// Format: [frameNamesOrNull, animationOptions]
47#[derive(Clone, Debug)]
48pub struct Animation {
49    /// Frames sequence: null, [null], or array of frame names
50    frames: FrameListMode,
51    /// Animation options/configuration
52    options: AnimationOptions,
53}
54
55impl Default for Animation {
56    fn default() -> Self {
57        Self {
58            frames: FrameListMode::All,
59            options: AnimationOptions::default(),
60        }
61    }
62}
63
64impl Animation {
65    /// Create a new animation args with default values for options and
66    /// FramesMode set to all frames
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Create a animation for playing all frames (default)    
72    pub fn all_frames() -> Self {
73        Self::new()
74    }
75
76    /// Create a animation setup specifically for pausing a running animation
77    pub fn pause() -> Self {
78        Self {
79            frames: FrameListMode::Pause,
80            options: AnimationOptions::new()
81                .mode(AnimationMode::Immediate)
82                .frame(FrameSettings::new().duration(0).redraw(false))
83                .transition(TransitionSettings::new().duration(0)),
84        }
85    }
86
87    /// Create animation args for specific frames
88    pub fn frames(frames: Vec<String>) -> Self {
89        Self {
90            frames: FrameListMode::Frames(frames),
91            ..Default::default()
92        }
93    }
94
95    /// Set the animation options
96    pub fn options(mut self, options: AnimationOptions) -> Self {
97        self.options = options;
98        self
99    }
100}
101
102impl Serialize for Animation {
103    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
104    where
105        S: Serializer,
106    {
107        let mut seq = serializer.serialize_seq(Some(2))?;
108        seq.serialize_element(&self.frames)?;
109        seq.serialize_element(&self.options)?;
110        seq.end()
111    }
112}
113
114/// First argument in animation args - can be null, [null], or frame names
115#[derive(Clone, Debug)]
116pub enum FrameListMode {
117    /// null - animate all frames
118    All,
119    /// Array of frame names to animate
120    Frames(Vec<String>),
121    /// special mode, [null], for pausing an animation
122    Pause,
123}
124
125impl Serialize for FrameListMode {
126    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127    where
128        S: serde::Serializer,
129    {
130        match self {
131            FrameListMode::All => serializer.serialize_unit(),
132            FrameListMode::Pause => {
133                let arr = vec![serde_json::Value::Null];
134                arr.serialize(serializer)
135            }
136            FrameListMode::Frames(frames) => frames.serialize(serializer),
137        }
138    }
139}
140
141/// Animation configuration options
142/// Based on actual Plotly.js animation API from animation_attributes.js
143#[serde_with::skip_serializing_none]
144#[derive(Serialize, Clone, Debug, FieldSetter)]
145pub struct AnimationOptions {
146    /// Frame animation settings
147    frame: Option<FrameSettings>,
148    /// Transition animation settings
149    transition: Option<TransitionSettings>,
150    /// Animation mode
151    mode: Option<AnimationMode>,
152    /// Animation direction
153    direction: Option<AnimationDirection>,
154    /// Play frames starting at the current frame instead of the beginning
155    fromcurrent: Option<bool>,
156}
157
158impl AnimationOptions {
159    pub fn new() -> Self {
160        Default::default()
161    }
162}
163
164/// Frame animation settings
165#[serde_with::skip_serializing_none]
166#[derive(Serialize, Clone, Debug, FieldSetter)]
167pub struct FrameSettings {
168    /// The duration in milliseconds of each frame
169    duration: Option<usize>,
170    /// Redraw the plot at completion of the transition
171    redraw: Option<bool>,
172}
173
174impl FrameSettings {
175    pub fn new() -> Self {
176        Default::default()
177    }
178}
179
180/// Transition animation settings
181#[serde_with::skip_serializing_none]
182#[derive(Serialize, Clone, Debug, FieldSetter)]
183pub struct TransitionSettings {
184    /// The duration of the transition, in milliseconds
185    duration: Option<usize>,
186    /// The easing function used for the transition
187    easing: Option<AnimationEasing>,
188    /// Determines whether the figure's layout or traces smoothly transitions
189    ordering: Option<TransitionOrdering>,
190}
191
192impl TransitionSettings {
193    pub fn new() -> Self {
194        Default::default()
195    }
196}
197
198/// Animation modes
199#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
200#[serde(rename_all = "lowercase")]
201pub enum AnimationMode {
202    Immediate,
203    Next,
204    AfterAll,
205}
206
207/// Animation directions
208#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
209#[serde(rename_all = "lowercase")]
210pub enum AnimationDirection {
211    Forward,
212    Reverse,
213}
214
215/// Transition ordering options
216#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
217#[serde(rename_all = "lowercase")]
218pub enum TransitionOrdering {
219    #[serde(rename = "layout first")]
220    LayoutFirst,
221    #[serde(rename = "traces first")]
222    TracesFirst,
223}
224
225/// Easing functions for animation transitions
226#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
227#[serde(rename_all = "lowercase")]
228pub enum AnimationEasing {
229    Linear,
230    Quad,
231    Cubic,
232    Sin,
233    Exp,
234    Circle,
235    Elastic,
236    Back,
237    Bounce,
238    #[serde(rename = "linear-in")]
239    LinearIn,
240    #[serde(rename = "quad-in")]
241    QuadIn,
242    #[serde(rename = "cubic-in")]
243    CubicIn,
244    #[serde(rename = "sin-in")]
245    SinIn,
246    #[serde(rename = "exp-in")]
247    ExpIn,
248    #[serde(rename = "circle-in")]
249    CircleIn,
250    #[serde(rename = "elastic-in")]
251    ElasticIn,
252    #[serde(rename = "back-in")]
253    BackIn,
254    #[serde(rename = "bounce-in")]
255    BounceIn,
256    #[serde(rename = "linear-out")]
257    LinearOut,
258    #[serde(rename = "quad-out")]
259    QuadOut,
260    #[serde(rename = "cubic-out")]
261    CubicOut,
262    #[serde(rename = "sin-out")]
263    SinOut,
264    #[serde(rename = "exp-out")]
265    ExpOut,
266    #[serde(rename = "circle-out")]
267    CircleOut,
268    #[serde(rename = "elastic-out")]
269    ElasticOut,
270    #[serde(rename = "back-out")]
271    BackOut,
272    #[serde(rename = "bounce-out")]
273    BounceOut,
274    #[serde(rename = "linear-in-out")]
275    LinearInOut,
276    #[serde(rename = "quad-in-out")]
277    QuadInOut,
278    #[serde(rename = "cubic-in-out")]
279    CubicInOut,
280    #[serde(rename = "sin-in-out")]
281    SinInOut,
282    #[serde(rename = "exp-in-out")]
283    ExpInOut,
284    #[serde(rename = "circle-in-out")]
285    CircleInOut,
286    #[serde(rename = "elastic-in-out")]
287    ElasticInOut,
288    #[serde(rename = "back-in-out")]
289    BackInOut,
290    #[serde(rename = "bounce-in-out")]
291    BounceInOut,
292}
293
294#[cfg(test)]
295mod tests {
296    use serde_json::{json, to_value};
297
298    use super::*;
299    use crate::Scatter;
300
301    #[test]
302    fn serialize_animation_easing() {
303        let test_cases = [
304            (AnimationEasing::Linear, "linear"),
305            (AnimationEasing::Cubic, "cubic"),
306            (AnimationEasing::CubicInOut, "cubic-in-out"),
307            (AnimationEasing::ElasticInOut, "elastic-in-out"),
308        ];
309
310        for (easing, expected) in test_cases {
311            assert_eq!(
312                to_value(easing).unwrap(),
313                json!(expected),
314                "Failed for {:?}",
315                easing
316            );
317        }
318    }
319
320    #[test]
321    fn serialize_animation_mode() {
322        let test_cases = [
323            (AnimationMode::Immediate, "immediate"),
324            (AnimationMode::Next, "next"),
325            (AnimationMode::AfterAll, "afterall"),
326        ];
327
328        for (mode, expected) in test_cases {
329            assert_eq!(
330                to_value(mode).unwrap(),
331                json!(expected),
332                "Failed for {:?}",
333                mode
334            );
335        }
336    }
337
338    #[test]
339    fn serialize_animation_direction() {
340        let test_cases = [
341            (AnimationDirection::Forward, "forward"),
342            (AnimationDirection::Reverse, "reverse"),
343        ];
344
345        for (direction, expected) in test_cases {
346            assert_eq!(
347                to_value(direction).unwrap(),
348                json!(expected),
349                "Failed for {:?}",
350                direction
351            );
352        }
353    }
354
355    #[test]
356    fn serialize_transition_ordering() {
357        let test_cases = [
358            (TransitionOrdering::LayoutFirst, "layout first"),
359            (TransitionOrdering::TracesFirst, "traces first"),
360        ];
361
362        for (ordering, expected) in test_cases {
363            assert_eq!(
364                to_value(ordering).unwrap(),
365                json!(expected),
366                "Failed for {:?}",
367                ordering
368            );
369        }
370    }
371
372    #[test]
373    fn serialize_frame() {
374        let frame = Frame::new()
375            .name("test_frame")
376            .group("test_group")
377            .baseframe("base_frame");
378
379        let expected = json!({
380            "name": "test_frame",
381            "group": "test_group",
382            "baseframe": "base_frame"
383        });
384
385        assert_eq!(to_value(frame).unwrap(), expected);
386    }
387
388    #[test]
389    fn serialize_frame_with_data() {
390        let trace = Scatter::new(vec![1, 2, 3], vec![1, 2, 3]);
391        let mut traces = Traces::new();
392        traces.push(trace);
393
394        let frame = Frame::new().name("frame_with_data").data(traces);
395
396        let expected = json!({
397            "name": "frame_with_data",
398            "data": [
399                {
400                    "type": "scatter",
401                    "x": [1, 2, 3],
402                    "y": [1, 2, 3]
403                }
404            ]
405        });
406
407        assert_eq!(to_value(frame).unwrap(), expected);
408    }
409
410    #[test]
411    fn serialize_animation() {
412        let test_cases = [
413            (
414                Animation::all_frames(),
415                json!(null),
416                "all frames should serialize to null",
417            ),
418            (
419                Animation::pause(),
420                json!([null]),
421                "pause should serialize to [null]",
422            ),
423            (
424                Animation::frames(vec!["frame1".to_string(), "frame2".to_string()]),
425                json!(["frame1", "frame2"]),
426                "specific frames should serialize to frame names array",
427            ),
428        ];
429
430        for (animation, expected_frames, description) in test_cases {
431            let json = to_value(animation).unwrap();
432            assert_eq!(json[0], expected_frames, "{}", description);
433            assert!(json[1].is_object(), "Second element should be an object");
434        }
435    }
436
437    #[test]
438    fn serialize_animation_options_defaults() {
439        let options = AnimationOptions::new();
440        assert_eq!(to_value(options).unwrap(), json!({}));
441    }
442
443    #[test]
444    fn serialize_animation_options() {
445        let options = AnimationOptions::new()
446            .mode(AnimationMode::Immediate)
447            .direction(AnimationDirection::Forward)
448            .fromcurrent(false)
449            .frame(FrameSettings::new().duration(500).redraw(true))
450            .transition(
451                TransitionSettings::new()
452                    .duration(300)
453                    .easing(AnimationEasing::CubicInOut)
454                    .ordering(TransitionOrdering::LayoutFirst),
455            );
456
457        let expected = json!({
458            "mode": "immediate",
459            "direction": "forward",
460            "fromcurrent": false,
461            "frame": {
462                "duration": 500,
463                "redraw": true
464            },
465            "transition": {
466                "duration": 300,
467                "easing": "cubic-in-out",
468                "ordering": "layout first"
469            }
470        });
471
472        assert_eq!(to_value(options).unwrap(), expected);
473    }
474}