From 6f1c4c36a59a1ef366edfe2f517da52a088af089 Mon Sep 17 00:00:00 2001 From: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> Date: Sat, 12 Jul 2025 11:21:56 +0200 Subject: [PATCH 1/3] fix plotly_static readme link Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> --- plotly/Cargo.toml | 2 +- plotly_static/Cargo.toml | 4 ++-- plotly_static/README.md | 4 ++-- plotly_static/src/lib.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plotly/Cargo.toml b/plotly/Cargo.toml index 12ce6aea..8fe5773a 100644 --- a/plotly/Cargo.toml +++ b/plotly/Cargo.toml @@ -51,7 +51,7 @@ dyn-clone = "1" erased-serde = "0.4" image = { version = "0.25", optional = true } plotly_derive = { version = "0.13", path = "../plotly_derive" } -plotly_static = { version = "0.0.2", path = "../plotly_static", optional = true } +plotly_static = { version = "0.0.3", path = "../plotly_static", optional = true } plotly_kaleido = { version = "0.13", path = "../plotly_kaleido", optional = true } ndarray = { version = "0.16", optional = true } once_cell = "1" diff --git a/plotly_static/Cargo.toml b/plotly_static/Cargo.toml index a2f64c4f..efbd71a1 100644 --- a/plotly_static/Cargo.toml +++ b/plotly_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plotly_static" -version = "0.0.2" +version = "0.0.3" description = "Export Plotly graphs to static images using WebDriver" authors = ["Andrei Gherghescu andrei-ng@protonmail.com"] license = "MIT" @@ -46,4 +46,4 @@ webdriver-downloader = "0.16" # Needed for docs.rs to build the documentation [package.metadata.docs.rs] -features = ["chromedriver"] \ No newline at end of file +features = ["chromedriver"] diff --git a/plotly_static/README.md b/plotly_static/README.md index 30240329..a0e653e9 100644 --- a/plotly_static/README.md +++ b/plotly_static/README.md @@ -56,7 +56,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -plotly_static = { version = "0.0.2", features = ["chromedriver", "webdriver_download"] } +plotly_static = { version = "0.0.3", features = ["chromedriver", "webdriver_download"] } serde_json = "1.0" ``` @@ -151,7 +151,7 @@ Similar examples are available in the [Plotly.rs package](https://github.com/plo ## Documentation - [API Documentation](https://docs.rs/plotly_static/) -- [Static Image Export Guide](../../docs/book/src/fundamentals/static_image_export.md) +- [Static Image Export Guide](https://github.com/plotly/plotly.rs/tree/main/docs/book/src/fundamentals/static_image_export.md) ## License diff --git a/plotly_static/src/lib.rs b/plotly_static/src/lib.rs index 38cded1f..3f6dbbf1 100644 --- a/plotly_static/src/lib.rs +++ b/plotly_static/src/lib.rs @@ -74,7 +74,7 @@ //! //! ```toml //! [dependencies] -//! plotly_static = { version = "0.0.2", features = ["chromedriver", "webdriver_download"] } +//! plotly_static = { version = "0.0.3", features = ["chromedriver", "webdriver_download"] } //! ``` //! //! ## Advanced Usage From 5347e0343952405140d7f5088bf3696165db6b00 Mon Sep 17 00:00:00 2001 From: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> Date: Fri, 11 Jul 2025 23:24:09 +0200 Subject: [PATCH 2/3] add animation feature Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> --- docs/book/src/SUMMARY.md | 1 + .../src/fundamentals/static_image_export.md | 5 - docs/book/src/recipes/custom_controls.md | 1 + .../src/recipes/custom_controls/animations.md | 12 + docs/book/src/recipes/img/animation.png | Bin 0 -> 72895 bytes examples/custom_controls/src/main.rs | 308 +++++++++++- plotly/src/layout/animation.rs | 474 ++++++++++++++++++ plotly/src/layout/mod.rs | 9 + plotly/src/layout/slider.rs | 184 ++++--- plotly/src/layout/update_menu.rs | 81 +-- plotly/src/lib.rs | 2 +- plotly/src/plot.rs | 43 +- 12 files changed, 1010 insertions(+), 110 deletions(-) create mode 100644 docs/book/src/recipes/custom_controls/animations.md create mode 100644 docs/book/src/recipes/img/animation.png create mode 100644 plotly/src/layout/animation.rs diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index f53ab8c5..4b70fa75 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -35,3 +35,4 @@ - [Custom Controls](./recipes/custom_controls.md) - [Dropdowns](./recipes/custom_controls/dropdowns.md) - [Sliders](./recipes/custom_controls/sliders.md) + - [Animations](./recipes/custom_controls/animations.md) diff --git a/docs/book/src/fundamentals/static_image_export.md b/docs/book/src/fundamentals/static_image_export.md index 3eec1438..29ec2cbd 100644 --- a/docs/book/src/fundamentals/static_image_export.md +++ b/docs/book/src/fundamentals/static_image_export.md @@ -198,11 +198,6 @@ let mut exporter = StaticExporterBuilder::default() - **Exporter Reuse**: Create a single `StaticExporter` and reuse it for multiple plots - **Parallel Usage**: Use unique ports for parallel operations (tests, etc.) - **Resource Management**: The exporter automatically manages WebDriver lifecycle -- **Format Selection**: Choose appropriate formats for your use case: - - PNG: Good quality, lossless - - JPEG: Smaller files, lossy - - SVG: Scalable, good for web - - PDF: Good for printing ## Complete Example diff --git a/docs/book/src/recipes/custom_controls.md b/docs/book/src/recipes/custom_controls.md index 0527e6f4..f6cb7ab4 100644 --- a/docs/book/src/recipes/custom_controls.md +++ b/docs/book/src/recipes/custom_controls.md @@ -6,3 +6,4 @@ This section covers interactive controls that can be added to plots to modify da |--------------|---------| | [Dropdown Menus and Buttons](./custom_controls/dropdowns.md) | ![Dropdown Example](./img/dropdown.png) | | [Sliders](./custom_controls/sliders.md) | ![Slider Example](./img/sliders.png) | +| [Animations](./custom_controls/animations.md) | ![Animation Example](./img/animation.png) | diff --git a/docs/book/src/recipes/custom_controls/animations.md b/docs/book/src/recipes/custom_controls/animations.md new file mode 100644 index 00000000..1914a70b --- /dev/null +++ b/docs/book/src/recipes/custom_controls/animations.md @@ -0,0 +1,12 @@ +# 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. + +```rust,no_run +{{#include ../../../../../examples/custom_controls/src/main.rs:gdp_life_expectancy_animation_example}} +``` +{{#include ../../../../../examples/custom_controls/output/inline_gdp_life_expectancy_animation_example.html}} diff --git a/docs/book/src/recipes/img/animation.png b/docs/book/src/recipes/img/animation.png new file mode 100644 index 0000000000000000000000000000000000000000..7c3b8dac490ebfe79c80ed4dfb30d2971649323c GIT binary patch literal 72895 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 WAbUx<-I?hOK;Y@>=d#Wzp$Pzb?2#G( literal 0 HcmV?d00001 diff --git a/examples/custom_controls/src/main.rs b/examples/custom_controls/src/main.rs index a82d2da7..1643b69d 100644 --- a/examples/custom_controls/src/main.rs +++ b/examples/custom_controls/src/main.rs @@ -7,8 +7,8 @@ use plotly::{ common::{Anchor, ColorScalePalette, Font, Mode, Pad, Title, Visible}, layout::{ update_menu::{ButtonBuilder, UpdateMenu, UpdateMenuDirection, UpdateMenuType}, - Axis, BarMode, Layout, Slider, SliderCurrentValue, SliderCurrentValueXAnchor, SliderStep, - SliderStepBuilder, + AnimationOptions, Axis, BarMode, Layout, Slider, SliderCurrentValue, + SliderCurrentValueXAnchor, SliderStep, SliderStepBuilder, }, Bar, HeatMap, Plot, Scatter, }; @@ -388,7 +388,7 @@ fn gdp_life_expectancy_slider_example(show: bool, file_name: &str) { visible[start..end].fill(Visible::True); SliderStepBuilder::new() - .label(format!("year = {year}")) + .label(year.to_string()) .value(year) .push_restyle(Scatter::::modify_visible(visible)) .push_relayout(Layout::modify_title(format!( @@ -409,8 +409,18 @@ fn gdp_life_expectancy_slider_example(show: bool, file_name: &str) { .title(Title::with_text("gdpPercap")) .type_(plotly::layout::AxisType::Log), ) - .y_axis(Axis::new().title(Title::with_text("lifeExp"))) - .sliders(vec![Slider::new().active(0).steps(steps)]); + .y_axis( + Axis::new() + .title(Title::with_text("lifeExp")) + .range(vec![30.0, 85.0]), // Fixed range for Life Expectancy + ) + .sliders(vec![Slider::new().active(0).steps(steps).current_value( + SliderCurrentValue::new() + .visible(true) + .prefix("Year: ") + .x_anchor(SliderCurrentValueXAnchor::Right) + .font(Font::new().size(20).color("rgb(102, 102, 102)")), + )]); plot.set_layout(layout); let path = write_example_to_html(&plot, file_name); if show { @@ -419,6 +429,291 @@ fn gdp_life_expectancy_slider_example(show: bool, file_name: &str) { } // ANCHOR_END: gdp_life_expectancy_slider_example +// ANCHOR: gdp_life_expectancy_animation_example +// 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 = data + .iter() + .map(|d| d.year) + .collect::>() + .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 = 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 = records.iter().map(|r| r.gdp_per_cap).collect(); + let y: Vec = records.iter().map(|r| r.life_exp).collect(); + let size: Vec = records.iter().map(|r| r.pop).collect(); + let hover: Vec = 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); + } +} +// ANCHOR_END: gdp_life_expectancy_animation_example + +// ANCHOR: animation_randomize_example +/// Animation example based on the Plotly.js "Randomize" animation. +/// This demonstrates the new builder API for animation configuration. +fn animation_randomize_example(show: bool, file_name: &str) { + use plotly::{ + layout::update_menu::{ButtonBuilder, UpdateMenu, UpdateMenuDirection, UpdateMenuType}, + layout::{ + Animation, AnimationEasing, AnimationMode, Frame, FrameSettings, TransitionSettings, + }, + Plot, Scatter, + }; + + // Initial data + let x = vec![1, 2, 3]; + let y0 = vec![0.0, 0.5, 1.0]; + let y1 = vec![0.2, 0.8, 0.3]; + let y2 = vec![0.9, 0.1, 0.7]; + + let mut plot = Plot::new(); + let base = + Scatter::new(x.clone(), y0.clone()).line(plotly::common::Line::new().simplify(false)); + plot.add_trace(base.clone()); + + // Add frames with different y-values and auto-adjusting layouts + let mut trace0 = plotly::Traces::new(); + trace0.push(base); + + let mut trace1 = plotly::Traces::new(); + trace1.push(Scatter::new(x.clone(), y1.clone())); + + let mut trace2 = plotly::Traces::new(); + trace2.push(Scatter::new(x.clone(), y2.clone())); + + let animation = Animation::new().options( + AnimationOptions::new() + .mode(AnimationMode::Immediate) + .frame(FrameSettings::new().duration(500)) + .transition( + TransitionSettings::new() + .duration(500) + .easing(AnimationEasing::CubicInOut), + ), + ); + + let layout0 = plotly::Layout::new() + .title(Title::with_text("First frame")) + .y_axis(plotly::layout::Axis::new().range(vec![0.0, 1.0])); + let layout1 = plotly::Layout::new() + .title(Title::with_text("Second frame")) + .y_axis(plotly::layout::Axis::new().range(vec![0.0, 1.0])); + let layout2 = plotly::Layout::new() + .title(Title::with_text("Third frame")) + .y_axis(plotly::layout::Axis::new().range(vec![0.0, 1.0])); + + // Add frames using the new API + plot.add_frame(Frame::new().name("frame0").data(trace0).layout(layout0)) + .add_frame(Frame::new().name("frame1").data(trace1).layout(layout1)) + .add_frame(Frame::new().name("frame2").data(trace2).layout(layout2)); + + let randomize_button = ButtonBuilder::new() + .label("Animate") + .animation(animation) + .build() + .unwrap(); + + let updatemenu = UpdateMenu::new() + .ty(UpdateMenuType::Buttons) + .direction(UpdateMenuDirection::Right) + .buttons(vec![randomize_button]) + .x(0.1) + .y(1.15) + .show_active(true) + .visible(true); + + plot.set_layout( + Layout::new() + .title("Animation Example - Click 'Animate'") + .y_axis(Axis::new().title("Y Axis").range(vec![0.0, 1.0])) + .update_menus(vec![updatemenu]), + ); + + let path = plotly_utils::write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: animation_randomize_example + fn main() { // Change false to true on any of these lines to display the example. bar_plot_with_dropdown_for_different_data(false, "bar_plot"); @@ -429,4 +724,7 @@ fn main() { bar_chart_with_slider_customization(false, "bar_chart_with_slider_customization"); sinusoidal_slider_example(false, "sinusoidal_slider_example"); gdp_life_expectancy_slider_example(false, "gdp_life_expectancy_slider_example"); + // Animation examples + animation_randomize_example(false, "animation_randomize_example"); + gdp_life_expectancy_animation_example(false, "gdp_life_expectancy_animation_example"); } diff --git a/plotly/src/layout/animation.rs b/plotly/src/layout/animation.rs new file mode 100644 index 00000000..06fa666d --- /dev/null +++ b/plotly/src/layout/animation.rs @@ -0,0 +1,474 @@ +//! Animation support for Plotly.rs +//! +//! This module provides animation configuration for Plotly.js updatemenu +//! buttons and slider steps, following the Plotly.js animation API +//! specification. + +use plotly_derive::FieldSetter; +use serde::ser::{SerializeSeq, Serializer}; +use serde::Serialize; + +use crate::{Layout, Traces}; + +/// A frame represents a single state in an animation sequence. +/// Based on Plotly.js frame_attributes.js specification +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, FieldSetter)] +pub struct Frame { + /// An identifier that specifies the group to which the frame belongs, + /// used by animate to select a subset of frames + group: Option, + /// A label by which to identify the frame + name: Option, + /// A list of trace indices that identify the respective traces in the data + /// attribute + traces: Option>, + /// The name of the frame into which this frame's properties are merged + /// before applying. This is used to unify properties and avoid needing + /// to specify the same values for the same properties in multiple + /// frames. + baseframe: Option, + /// A list of traces this frame modifies. The format is identical to the + /// normal trace definition. + data: Option, + /// Layout properties which this frame modifies. The format is identical to + /// the normal layout definition. + layout: Option, +} + +impl Frame { + pub fn new() -> Self { + Default::default() + } +} + +/// Represents the animation arguments array for Plotly.js +/// Format: [frameNamesOrNull, animationOptions] +#[derive(Clone, Debug)] +pub struct Animation { + /// Frames sequence: null, [null], or array of frame names + frames: FrameListMode, + /// Animation options/configuration + options: AnimationOptions, +} + +impl Default for Animation { + fn default() -> Self { + Self { + frames: FrameListMode::All, + options: AnimationOptions::default(), + } + } +} + +impl Animation { + /// Create a new animation args with default values for options and + /// FramesMode set to all frames + pub fn new() -> Self { + Self::default() + } + + /// Create a animation for playing all frames (default) + pub fn all_frames() -> Self { + Self::new() + } + + /// Create a animation setup specifically for pausing a running animation + 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)), + } + } + + /// Create animation args for specific frames + pub fn frames(frames: Vec) -> Self { + Self { + frames: FrameListMode::Frames(frames), + ..Default::default() + } + } + + /// Set the animation options + pub fn options(mut self, options: AnimationOptions) -> Self { + self.options = options; + self + } +} + +impl Serialize for Animation { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.frames)?; + seq.serialize_element(&self.options)?; + seq.end() + } +} + +/// First argument in animation args - can be null, [null], or frame names +#[derive(Clone, Debug)] +pub enum FrameListMode { + /// null - animate all frames + All, + /// Array of frame names to animate + Frames(Vec), + /// special mode, [null], for pausing an animation + Pause, +} + +impl Serialize for FrameListMode { + fn serialize(&self, serializer: S) -> Result + 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), + } + } +} + +/// Animation configuration options +/// Based on actual Plotly.js animation API from animation_attributes.js +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct AnimationOptions { + /// Frame animation settings + frame: Option, + /// Transition animation settings + transition: Option, + /// Animation mode + mode: Option, + /// Animation direction + direction: Option, + /// Play frames starting at the current frame instead of the beginning + fromcurrent: Option, +} + +impl AnimationOptions { + pub fn new() -> Self { + Default::default() + } +} + +/// Frame animation settings +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct FrameSettings { + /// The duration in milliseconds of each frame + duration: Option, + /// Redraw the plot at completion of the transition + redraw: Option, +} + +impl FrameSettings { + pub fn new() -> Self { + Default::default() + } +} + +/// Transition animation settings +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct TransitionSettings { + /// The duration of the transition, in milliseconds + duration: Option, + /// The easing function used for the transition + easing: Option, + /// Determines whether the figure's layout or traces smoothly transitions + ordering: Option, +} + +impl TransitionSettings { + pub fn new() -> Self { + Default::default() + } +} + +/// Animation modes +#[derive(Serialize, Debug, Clone, Copy, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum AnimationMode { + Immediate, + Next, + AfterAll, +} + +/// Animation directions +#[derive(Serialize, Debug, Clone, Copy, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum AnimationDirection { + Forward, + Reverse, +} + +/// Transition ordering options +#[derive(Serialize, Debug, Clone, Copy, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum TransitionOrdering { + #[serde(rename = "layout first")] + LayoutFirst, + #[serde(rename = "traces first")] + TracesFirst, +} + +/// Easing functions for animation transitions +#[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); + } +} diff --git a/plotly/src/layout/mod.rs b/plotly/src/layout/mod.rs index a35d9626..0424b593 100644 --- a/plotly/src/layout/mod.rs +++ b/plotly/src/layout/mod.rs @@ -11,6 +11,7 @@ use crate::common::{Calendar, ColorScale, Font, Label, Orientation, Title}; pub mod themes; pub mod update_menu; +mod animation; mod annotation; mod axis; mod geo; @@ -24,6 +25,10 @@ mod shape; mod slider; // Re-export layout sub-module types +pub use self::animation::{ + Animation, AnimationDirection, AnimationEasing, AnimationMode, AnimationOptions, Frame, + FrameSettings, TransitionOrdering, TransitionSettings, +}; pub use self::annotation::{Annotation, ArrowSide, ClickToShow}; pub use self::axis::{ ArrayShow, Axis, AxisConstrain, AxisRange, AxisType, CategoryOrder, ColorAxis, @@ -59,6 +64,7 @@ pub enum ControlBuilderError { ValueSerializationError(String), InvalidRestyleObject(String), InvalidRelayoutObject(String), + AnimationSerializationError(String), } impl std::fmt::Display for ControlBuilderError { @@ -79,6 +85,9 @@ impl std::fmt::Display for ControlBuilderError { ControlBuilderError::InvalidRelayoutObject(s) => { write!(f, "Invalid relayout object: expected object but got {s}") } + ControlBuilderError::AnimationSerializationError(e) => { + write!(f, "Failed to serialize animation: {e}") + } } } } diff --git a/plotly/src/layout/slider.rs b/plotly/src/layout/slider.rs index bb5d3ef6..4ffd6af4 100644 --- a/plotly/src/layout/slider.rs +++ b/plotly/src/layout/slider.rs @@ -6,7 +6,7 @@ use serde_json::{Map, Value}; use crate::{ color::Color, common::{Anchor, Font, Pad}, - layout::ControlBuilderError, + layout::{Animation, ControlBuilderError}, private::NumOrString, Relayout, Restyle, }; @@ -90,11 +90,8 @@ impl SliderStep { /// Builder struct to create slider steps which can do restyles and/or relayouts #[derive(FieldSetter)] pub struct SliderStepBuilder { - #[field_setter(skip)] label: Option, - #[field_setter(skip)] name: Option, - #[field_setter(skip)] template_item_name: Option, visible: Option, #[field_setter(skip)] @@ -105,6 +102,9 @@ pub struct SliderStepBuilder { relayouts: Map, #[field_setter(skip)] error: Option, + /// Animation configuration + #[field_setter(skip)] + animation: Option, } impl SliderStepBuilder { @@ -112,21 +112,6 @@ impl SliderStepBuilder { Default::default() } - pub fn label>(mut self, label: S) -> Self { - self.label = Some(label.as_ref().to_string()); - self - } - - pub fn name>(mut self, name: S) -> Self { - self.name = Some(name.as_ref().to_string()); - self - } - - pub fn template_item_name>(mut self, template_item_name: S) -> Self { - self.template_item_name = Some(template_item_name.as_ref().to_string()); - self - } - pub fn push_restyle(mut self, restyle: impl Restyle) -> Self { if self.error.is_none() { if let Err(e) = self.try_push_restyle(restyle) { @@ -196,24 +181,37 @@ impl SliderStepBuilder { Ok(()) } - fn method_and_args( - restyles: Map, - relayouts: Map, - ) -> (SliderMethod, Value) { - match (restyles.is_empty(), relayouts.is_empty()) { - (true, true) => (SliderMethod::Skip, Value::Null), - (false, true) => (SliderMethod::Restyle, vec![restyles].into()), - (true, false) => (SliderMethod::Relayout, vec![relayouts].into()), - (false, false) => (SliderMethod::Update, vec![restyles, relayouts].into()), - } + /// Set the animation configuration for this slider step + pub fn animation(mut self, animation: Animation) -> Self { + self.animation = Some(animation); + self } - pub fn build(self) -> Result { if let Some(error) = self.error { return Err(error); } - let (method, args) = Self::method_and_args(self.restyles, self.relayouts); + let (method, args) = match ( + self.animation, + self.restyles.is_empty(), + self.relayouts.is_empty(), + ) { + // Animation takes precedence + (Some(animation), _, _) => ( + SliderMethod::Animate, + serde_json::to_value(animation) + .map_err(|e| ControlBuilderError::AnimationSerializationError(e.to_string()))?, + ), + // Regular restyle/relayout combinations + (None, true, true) => (SliderMethod::Skip, Value::Null), + (None, false, true) => (SliderMethod::Restyle, vec![self.restyles].into()), + (None, true, false) => (SliderMethod::Relayout, vec![self.relayouts].into()), + (None, false, false) => ( + SliderMethod::Update, + vec![self.restyles, self.relayouts].into(), + ), + }; + Ok(SliderStep { label: self.label, args: Some(args), @@ -420,16 +418,27 @@ mod tests { use serde_json::{json, to_value}; use super::*; - use crate::common::Anchor; - use crate::common::Visible; + use crate::common::{Anchor, Visible}; + use crate::layout::{Animation, AnimationMode, FrameSettings, TransitionSettings}; #[test] fn serialize_slider_method() { - assert_eq!(to_value(SliderMethod::Restyle).unwrap(), json!("restyle")); - assert_eq!(to_value(SliderMethod::Relayout).unwrap(), json!("relayout")); - assert_eq!(to_value(SliderMethod::Animate).unwrap(), json!("animate")); - assert_eq!(to_value(SliderMethod::Update).unwrap(), json!("update")); - assert_eq!(to_value(SliderMethod::Skip).unwrap(), json!("skip")); + let test_cases = [ + (SliderMethod::Restyle, "restyle"), + (SliderMethod::Relayout, "relayout"), + (SliderMethod::Animate, "animate"), + (SliderMethod::Update, "update"), + (SliderMethod::Skip, "skip"), + ]; + + for (method, expected) in test_cases { + assert_eq!( + to_value(method).unwrap(), + json!(expected), + "Failed for {:?}", + method + ); + } } #[test] @@ -544,34 +553,38 @@ mod tests { #[test] fn serialize_slider_current_value_x_anchor() { - assert_eq!( - to_value(SliderCurrentValueXAnchor::Left).unwrap(), - json!("left") - ); - assert_eq!( - to_value(SliderCurrentValueXAnchor::Center).unwrap(), - json!("center") - ); - assert_eq!( - to_value(SliderCurrentValueXAnchor::Right).unwrap(), - json!("right") - ); + let test_cases = [ + (SliderCurrentValueXAnchor::Left, "left"), + (SliderCurrentValueXAnchor::Center, "center"), + (SliderCurrentValueXAnchor::Right, "right"), + ]; + + for (anchor, expected) in test_cases { + assert_eq!( + to_value(&anchor).unwrap(), + json!(expected), + "Failed for {:?}", + anchor + ); + } } #[test] fn serialize_slider_transition_easing() { - assert_eq!( - to_value(SliderTransitionEasing::Linear).unwrap(), - json!("linear") - ); - assert_eq!( - to_value(SliderTransitionEasing::CubicInOut).unwrap(), - json!("cubic-in-out") - ); - assert_eq!( - to_value(SliderTransitionEasing::BounceIn).unwrap(), - json!("bounce-in") - ); + let test_cases = [ + (SliderTransitionEasing::Linear, "linear"), + (SliderTransitionEasing::CubicInOut, "cubic-in-out"), + (SliderTransitionEasing::BounceIn, "bounce-in"), + ]; + + for (easing, expected) in test_cases { + assert_eq!( + to_value(&easing).unwrap(), + json!(expected), + "Failed for {:?}", + easing + ); + } } #[test] @@ -665,13 +678,48 @@ mod tests { struct InvalidJsonObject; impl Relayout for InvalidJsonObject {} - let builder = SliderStepBuilder::new().push_relayout(InvalidJsonObject); - let err = builder.build().unwrap_err(); - match err { - ControlBuilderError::InvalidRelayoutObject(s) => { - assert!(s.contains("null")); - } + let result = SliderStepBuilder::new() + .label("Test") + .push_relayout(InvalidJsonObject) + .build(); + + assert!(result.is_err()); + match result.unwrap_err() { + ControlBuilderError::InvalidRelayoutObject(_) => {} _ => panic!("Expected InvalidRelayoutObject error"), } } + + #[test] + fn serialize_slider_step_builder_with_animation() { + let animation = Animation::frames(vec!["frame1".to_string()]).options( + crate::layout::AnimationOptions::new() + .mode(AnimationMode::Immediate) + .frame(FrameSettings::new().duration(300).redraw(false)) + .transition(TransitionSettings::new().duration(300)), + ); + + let slider_step = SliderStepBuilder::new() + .label("Animate to frame1") + .value("frame1") + .animation(animation) + .build() + .unwrap(); + + let expected = json!({ + "args": [ + ["frame1"], + { + "mode": "immediate", + "transition": {"duration": 300}, + "frame": {"duration": 300, "redraw": false} + } + ], + "method": "animate", + "label": "Animate to frame1", + "value": "frame1" + }); + + assert_eq!(to_value(slider_step).unwrap(), expected); + } } diff --git a/plotly/src/layout/update_menu.rs b/plotly/src/layout/update_menu.rs index 5fb9be08..5b794e7a 100644 --- a/plotly/src/layout/update_menu.rs +++ b/plotly/src/layout/update_menu.rs @@ -7,7 +7,7 @@ use serde_json::{Map, Value}; use crate::{ color::Color, common::{Anchor, Font, Pad}, - layout::ControlBuilderError, + layout::{Animation, ControlBuilderError}, Relayout, Restyle, }; @@ -92,11 +92,8 @@ impl Button { /// Builder struct to create buttons which can do restyles and/or relayouts #[derive(FieldSetter)] pub struct ButtonBuilder { - #[field_setter(skip)] label: Option, - #[field_setter(skip)] name: Option, - #[field_setter(skip)] template_item_name: Option, visible: Option, #[field_setter(default = "Map::new()")] @@ -105,6 +102,9 @@ pub struct ButtonBuilder { relayouts: Map, #[field_setter(skip)] error: Option, + // Animation configuration + #[field_setter(skip)] + animation: Option, } impl ButtonBuilder { @@ -112,21 +112,6 @@ impl ButtonBuilder { Default::default() } - pub fn label>(mut self, label: S) -> Self { - self.label = Some(label.as_ref().to_string()); - self - } - - pub fn name>(mut self, name: S) -> Self { - self.name = Some(name.as_ref().to_string()); - self - } - - pub fn template_item_name>(mut self, template_item_name: S) -> Self { - self.template_item_name = Some(template_item_name.as_ref().to_string()); - self - } - pub fn push_restyle(mut self, restyle: impl Restyle) -> Self { if self.error.is_none() { if let Err(e) = self.try_push_restyle(restyle) { @@ -172,16 +157,10 @@ impl ButtonBuilder { Ok(()) } - fn method_and_args( - restyles: Map, - relayouts: Map, - ) -> (ButtonMethod, Value) { - match (restyles.is_empty(), relayouts.is_empty()) { - (true, true) => (ButtonMethod::Skip, Value::Null), - (false, true) => (ButtonMethod::Restyle, vec![restyles].into()), - (true, false) => (ButtonMethod::Relayout, vec![relayouts].into()), - (false, false) => (ButtonMethod::Update, vec![restyles, relayouts].into()), - } + /// Sets the animation configuration for the button + pub fn animation(mut self, animation: Animation) -> Self { + self.animation = Some(animation); + self } pub fn build(self) -> Result { @@ -189,7 +168,27 @@ impl ButtonBuilder { return Err(error); } - let (method, args) = Self::method_and_args(self.restyles, self.relayouts); + let (method, args) = match ( + self.animation, + self.restyles.is_empty(), + self.relayouts.is_empty(), + ) { + // Animation takes precedence + (Some(animation), _, _) => { + let animation_args = serde_json::to_value(animation) + .map_err(|e| ControlBuilderError::AnimationSerializationError(e.to_string()))?; + (ButtonMethod::Animate, animation_args) + } + // Regular restyle/relayout combinations + (None, true, true) => (ButtonMethod::Skip, Value::Null), + (None, false, true) => (ButtonMethod::Restyle, vec![self.restyles].into()), + (None, true, false) => (ButtonMethod::Relayout, vec![self.relayouts].into()), + (None, false, false) => ( + ButtonMethod::Update, + vec![self.restyles, self.relayouts].into(), + ), + }; + Ok(Button { label: self.label, args: Some(args), @@ -454,4 +453,26 @@ mod tests { _ => panic!("Expected InvalidRelayoutObject error"), } } + + #[test] + fn serialize_animation_button_args() { + let animation = Animation::all_frames(); + + let button = ButtonBuilder::new() + .label("Animate") + .animation(animation) + .build() + .unwrap(); + + let args = button.args.unwrap(); + assert!(args.is_array(), "Animation button args must be an array"); + + // Verify the structure: [frameArg, options] + assert_eq!(args[0], json!(null)); // Should be null for all_frames + assert!( + args[1].is_object(), + "Second arg should be animation options object" + ); + assert_eq!(to_value(button.method.unwrap()).unwrap(), json!("animate")); + } } diff --git a/plotly/src/lib.rs b/plotly/src/lib.rs index 23290350..4c75c15f 100644 --- a/plotly/src/lib.rs +++ b/plotly/src/lib.rs @@ -56,7 +56,7 @@ pub mod traces; pub use common::color; pub use configuration::Configuration; pub use layout::Layout; -pub use plot::{Plot, Trace}; +pub use plot::{Plot, Trace, Traces}; #[cfg(feature = "kaleido")] pub use plotly_kaleido::ImageFormat; #[cfg(feature = "plotly_static")] diff --git a/plotly/src/plot.rs b/plotly/src/plot.rs index 9a45f43e..4e18fffc 100644 --- a/plotly/src/plot.rs +++ b/plotly/src/plot.rs @@ -13,7 +13,7 @@ use rand::{ }; use serde::Serialize; -use crate::{Configuration, Layout}; +use crate::{layout::Frame, Configuration, Layout}; #[derive(Template)] #[template(path = "plot.html", escape = "none")] @@ -158,6 +158,8 @@ pub struct Plot { layout: Layout, #[serde(rename = "config")] configuration: Configuration, + /// Animation frames + frames: Option>, #[serde(skip)] js_scripts: String, } @@ -219,6 +221,43 @@ impl Plot { &self.configuration } + /// Add a single frame to the animation sequence. + pub fn add_frame(&mut self, frame: Frame) -> &mut Self { + if self.frames.is_none() { + self.frames = Some(Vec::new()); + } + self.frames.as_mut().unwrap().push(frame); + self + } + + /// Add multiple frames to the animation sequence. + pub fn add_frames(&mut self, frames: &[Frame]) -> &mut Self { + if self.frames.is_none() { + self.frames = Some(frames.to_vec()); + } + self.frames.as_mut().unwrap().extend(frames.iter().cloned()); + self + } + + pub fn clear_frames(&mut self) -> &mut Self { + self.frames = None; + self + } + + pub fn frame_count(&self) -> usize { + self.frames.as_ref().map(|f| f.len()).unwrap_or(0) + } + + /// Get the animation frames as mutable reference + pub fn frames_mut(&mut self) -> Option<&mut Vec> { + self.frames.as_mut() + } + + /// Get the animation frames. + pub fn frames(&self) -> Option<&[Frame]> { + self.frames.as_deref() + } + /// Display the fully rendered HTML `Plot` in the default system browser. /// /// The HTML file is saved in a temp file, from which it is read and @@ -901,6 +940,7 @@ mod tests { ], "layout": {}, "config": {}, + "frames": null, }); assert_eq!(to_value(plot).unwrap(), expected); @@ -927,6 +967,7 @@ mod tests { } }, "config": {}, + "frames": null, }); assert_eq!(to_value(plot).unwrap(), expected); From f8de4496f0ffa891d931c2a8eb3e6803fcd597b3 Mon Sep 17 00:00:00 2001 From: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> Date: Sat, 12 Jul 2025 11:52:26 +0200 Subject: [PATCH 3/3] bump versions Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ plotly/Cargo.toml | 2 +- plotly_derive/Cargo.toml | 2 +- plotly_kaleido/Cargo.toml | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b09fe5b..c8c872e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [0.13.3] - 2025-xx-xx ### Fixed + +### Changed + +## [0.13.3] - 2025-07-12 + ### Changed +- [[#335](https://github.com/plotly/plotly.rs/pull/335)] Add minimal animation support ## [0.13.2] - 2025-07-12 diff --git a/plotly/Cargo.toml b/plotly/Cargo.toml index 8fe5773a..45fd011c 100644 --- a/plotly/Cargo.toml +++ b/plotly/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plotly" -version = "0.13.2" +version = "0.13.3" description = "A plotting library powered by Plotly.js" authors = [ "Ioannis Giagkiozis ", diff --git a/plotly_derive/Cargo.toml b/plotly_derive/Cargo.toml index 3951417e..86771720 100644 --- a/plotly_derive/Cargo.toml +++ b/plotly_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plotly_derive" -version = "0.13.2" +version = "0.13.3" description = "Internal proc macro crate for Plotly-rs." authors = ["Ioannis Giagkiozis "] license = "MIT" diff --git a/plotly_kaleido/Cargo.toml b/plotly_kaleido/Cargo.toml index 244de21e..64dfd642 100644 --- a/plotly_kaleido/Cargo.toml +++ b/plotly_kaleido/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plotly_kaleido" -version = "0.13.2" +version = "0.13.3" description = "Additional output format support for plotly using Kaleido" authors = [ "Ioannis Giagkiozis ",