use plotly_derive::FieldSetter;
use serde::ser::{SerializeSeq, Serializer};
use serde::Serialize;
use crate::{Layout, Traces};
#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, FieldSetter)]
pub struct Frame {
group: Option<String>,
name: Option<String>,
traces: Option<Vec<usize>>,
baseframe: Option<String>,
data: Option<Traces>,
layout: Option<Layout>,
}
impl Frame {
pub fn new() -> Self {
Default::default()
}
}
#[derive(Clone, Debug)]
pub struct Animation {
frames: FrameListMode,
options: AnimationOptions,
}
impl Default for Animation {
fn default() -> Self {
Self {
frames: FrameListMode::All,
options: AnimationOptions::default(),
}
}
}
impl Animation {
pub fn new() -> Self {
Self::default()
}
pub fn all_frames() -> Self {
Self::new()
}
pub fn pause() -> Self {
Self {
frames: FrameListMode::Pause,
options: AnimationOptions::new()
.mode(AnimationMode::Immediate)
.frame(FrameSettings::new().duration(0).redraw(false))
.transition(TransitionSettings::new().duration(0)),
}
}
pub fn frames(frames: Vec<String>) -> Self {
Self {
frames: FrameListMode::Frames(frames),
..Default::default()
}
}
pub fn options(mut self, options: AnimationOptions) -> Self {
self.options = options;
self
}
}
impl Serialize for Animation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element(&self.frames)?;
seq.serialize_element(&self.options)?;
seq.end()
}
}
#[derive(Clone, Debug)]
pub enum FrameListMode {
All,
Frames(Vec<String>),
Pause,
}
impl Serialize for FrameListMode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
FrameListMode::All => serializer.serialize_unit(),
FrameListMode::Pause => {
let arr = vec![serde_json::Value::Null];
arr.serialize(serializer)
}
FrameListMode::Frames(frames) => frames.serialize(serializer),
}
}
}
#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, Debug, FieldSetter)]
pub struct AnimationOptions {
frame: Option<FrameSettings>,
transition: Option<TransitionSettings>,
mode: Option<AnimationMode>,
direction: Option<AnimationDirection>,
fromcurrent: Option<bool>,
}
impl AnimationOptions {
pub fn new() -> Self {
Default::default()
}
}
#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, Debug, FieldSetter)]
pub struct FrameSettings {
duration: Option<usize>,
redraw: Option<bool>,
}
impl FrameSettings {
pub fn new() -> Self {
Default::default()
}
}
#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, Debug, FieldSetter)]
pub struct TransitionSettings {
duration: Option<usize>,
easing: Option<AnimationEasing>,
ordering: Option<TransitionOrdering>,
}
impl TransitionSettings {
pub fn new() -> Self {
Default::default()
}
}
#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AnimationMode {
Immediate,
Next,
AfterAll,
}
#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AnimationDirection {
Forward,
Reverse,
}
#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TransitionOrdering {
#[serde(rename = "layout first")]
LayoutFirst,
#[serde(rename = "traces first")]
TracesFirst,
}
#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AnimationEasing {
Linear,
Quad,
Cubic,
Sin,
Exp,
Circle,
Elastic,
Back,
Bounce,
#[serde(rename = "linear-in")]
LinearIn,
#[serde(rename = "quad-in")]
QuadIn,
#[serde(rename = "cubic-in")]
CubicIn,
#[serde(rename = "sin-in")]
SinIn,
#[serde(rename = "exp-in")]
ExpIn,
#[serde(rename = "circle-in")]
CircleIn,
#[serde(rename = "elastic-in")]
ElasticIn,
#[serde(rename = "back-in")]
BackIn,
#[serde(rename = "bounce-in")]
BounceIn,
#[serde(rename = "linear-out")]
LinearOut,
#[serde(rename = "quad-out")]
QuadOut,
#[serde(rename = "cubic-out")]
CubicOut,
#[serde(rename = "sin-out")]
SinOut,
#[serde(rename = "exp-out")]
ExpOut,
#[serde(rename = "circle-out")]
CircleOut,
#[serde(rename = "elastic-out")]
ElasticOut,
#[serde(rename = "back-out")]
BackOut,
#[serde(rename = "bounce-out")]
BounceOut,
#[serde(rename = "linear-in-out")]
LinearInOut,
#[serde(rename = "quad-in-out")]
QuadInOut,
#[serde(rename = "cubic-in-out")]
CubicInOut,
#[serde(rename = "sin-in-out")]
SinInOut,
#[serde(rename = "exp-in-out")]
ExpInOut,
#[serde(rename = "circle-in-out")]
CircleInOut,
#[serde(rename = "elastic-in-out")]
ElasticInOut,
#[serde(rename = "back-in-out")]
BackInOut,
#[serde(rename = "bounce-in-out")]
BounceInOut,
}
#[cfg(test)]
mod tests {
use serde_json::{json, to_value};
use super::*;
use crate::Scatter;
#[test]
fn serialize_animation_easing() {
let test_cases = [
(AnimationEasing::Linear, "linear"),
(AnimationEasing::Cubic, "cubic"),
(AnimationEasing::CubicInOut, "cubic-in-out"),
(AnimationEasing::ElasticInOut, "elastic-in-out"),
];
for (easing, expected) in test_cases {
assert_eq!(
to_value(easing).unwrap(),
json!(expected),
"Failed for {:?}",
easing
);
}
}
#[test]
fn serialize_animation_mode() {
let test_cases = [
(AnimationMode::Immediate, "immediate"),
(AnimationMode::Next, "next"),
(AnimationMode::AfterAll, "afterall"),
];
for (mode, expected) in test_cases {
assert_eq!(
to_value(mode).unwrap(),
json!(expected),
"Failed for {:?}",
mode
);
}
}
#[test]
fn serialize_animation_direction() {
let test_cases = [
(AnimationDirection::Forward, "forward"),
(AnimationDirection::Reverse, "reverse"),
];
for (direction, expected) in test_cases {
assert_eq!(
to_value(direction).unwrap(),
json!(expected),
"Failed for {:?}",
direction
);
}
}
#[test]
fn serialize_transition_ordering() {
let test_cases = [
(TransitionOrdering::LayoutFirst, "layout first"),
(TransitionOrdering::TracesFirst, "traces first"),
];
for (ordering, expected) in test_cases {
assert_eq!(
to_value(ordering).unwrap(),
json!(expected),
"Failed for {:?}",
ordering
);
}
}
#[test]
fn serialize_frame() {
let frame = Frame::new()
.name("test_frame")
.group("test_group")
.baseframe("base_frame");
let expected = json!({
"name": "test_frame",
"group": "test_group",
"baseframe": "base_frame"
});
assert_eq!(to_value(frame).unwrap(), expected);
}
#[test]
fn serialize_frame_with_data() {
let trace = Scatter::new(vec![1, 2, 3], vec![1, 2, 3]);
let mut traces = Traces::new();
traces.push(trace);
let frame = Frame::new().name("frame_with_data").data(traces);
let expected = json!({
"name": "frame_with_data",
"data": [
{
"type": "scatter",
"x": [1, 2, 3],
"y": [1, 2, 3]
}
]
});
assert_eq!(to_value(frame).unwrap(), expected);
}
#[test]
fn serialize_animation() {
let test_cases = [
(
Animation::all_frames(),
json!(null),
"all frames should serialize to null",
),
(
Animation::pause(),
json!([null]),
"pause should serialize to [null]",
),
(
Animation::frames(vec!["frame1".to_string(), "frame2".to_string()]),
json!(["frame1", "frame2"]),
"specific frames should serialize to frame names array",
),
];
for (animation, expected_frames, description) in test_cases {
let json = to_value(animation).unwrap();
assert_eq!(json[0], expected_frames, "{}", description);
assert!(json[1].is_object(), "Second element should be an object");
}
}
#[test]
fn serialize_animation_options_defaults() {
let options = AnimationOptions::new();
assert_eq!(to_value(options).unwrap(), json!({}));
}
#[test]
fn serialize_animation_options() {
let options = AnimationOptions::new()
.mode(AnimationMode::Immediate)
.direction(AnimationDirection::Forward)
.fromcurrent(false)
.frame(FrameSettings::new().duration(500).redraw(true))
.transition(
TransitionSettings::new()
.duration(300)
.easing(AnimationEasing::CubicInOut)
.ordering(TransitionOrdering::LayoutFirst),
);
let expected = json!({
"mode": "immediate",
"direction": "forward",
"fromcurrent": false,
"frame": {
"duration": 500,
"redraw": true
},
"transition": {
"duration": 300,
"easing": "cubic-in-out",
"ordering": "layout first"
}
});
assert_eq!(to_value(options).unwrap(), expected);
}
}