diff --git a/.cargo/config.toml b/.cargo/config.toml
deleted file mode 100644
index 2e07606d..00000000
--- a/.cargo/config.toml
+++ /dev/null
@@ -1,2 +0,0 @@
-[target.wasm32-unknown-unknown]
-rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 33c8c1f3..e32ec3a8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,8 +25,9 @@ jobs:
- run: cargo publish --allow-dirty -p plotly_kaleido
- run: sleep 10
- run: cargo publish --allow-dirty -p plotly
- - run: sleep 10
- - run: cargo publish --allow-dirty -p plotly_static --features webdriver_download,chromedriver
+ # plotly_static is not part of the same ecosystem yet so it doesn't use the same TOKEN
+ # - run: sleep 10
+ # - run: cargo publish --allow-dirty -p plotly_static --features webdriver_download,chromedriver
create-gh-release:
name: Deploy to GH Releases
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94785f5d..a88c9c1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,8 +3,53 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+https://github.com/plotly/plotly.rs/pull/350
+
+## [0.13.6] - 2025-xx-xx
+
+### Fixed
+
+- [[#348](https://github.com/plotly/plotly.rs/pull/348)] Fix Pie chart color setting
+
+### Changed
+
+- [[#350](https://github.com/plotly/plotly.rs/pull/350)] Add `plotly_static` `async` API
+
+## [0.13.5] - 2025-07-31
+
+### Fixed
+
+- [[#346](https://github.com/plotly/plotly.rs/pull/346)] Remove usage of `getrandom` for `rand` dependency and for this crate
+- [[#345](https://github.com/plotly/plotly.rs/pull/345)] Re-export `ImageFormat` from `plotly_static` into `plotly`
+
+## [0.13.4] - 2025-07-17
+
+### Fixed
+
+- [[#341](https://github.com/plotly/plotly.rs/pull/341)] Fix documentation related to `wasm` support
+
+### Changed
+- [[#339](https://github.com/plotly/plotly.rs/pull/339)] Replace default Windows app with `explorer`
+
+## [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
+
+### Fixed
+- [[#336](https://github.com/plotly/plotly.rs/pull/336)] Fix `plotly_static` docs.rs build
+- [[#327](https://github.com/plotly/plotly.rs/pull/327)] Fix book broken link
+
+## [0.13.1] - 2025-07-07
+
+### Fixed
+- [[#326](https://github.com/plotly/plotly.rs/pull/326)] Fix book badges
+
+
+## [0.13.0] - 2025-07-07
-## [0.13.0] - 2025-xx-xx
### Changed
- [[#277](https://github.com/plotly/plotly.rs/pull/277)] Removed `wasm` feature flag and put everything behind target specific dependencies. Added `.cargo/config.toml` for configuration flags needed by `getrandom` version 0.3 on `wasm` targets.
- [[#281](https://github.com/plotly/plotly.rs/pull/281)] Update to askama 0.13.0
diff --git a/README.md b/README.md
index 3cdcdb34..6d796956 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Plotly for Rust
-
+
@@ -39,9 +39,10 @@
* [Introduction](#introduction)
* [Basic Usage](#basic-usage)
- * [Exporting an Interactive Plot](#exporting-an-interactive-plot)
- * [Exporting Static Images with Kaleido](#exporting-static-images-with-kaleido)
- * [Usage Within a Wasm Environment](#usage-within-a-wasm-environment)
+ * [Exporting a single Interactive Plot](#exporting-a-single-interactive-plot)
+ * [Exporting Static Images with plotly_static (Recommended)](#exporting-static-images-with-plotly_static-recommended)
+ * [Exporting Static Images with Kaleido (legacy)](#exporting-static-images-with-kaleido-legacy)
+ * [Usage Within a WASM Environment](#usage-within-a-wasm-environment)
* [Crate Feature Flags](#crate-feature-flags)
* [Contributing](#contributing)
* [Code of Conduct](#code-of-conduct)
@@ -95,18 +96,6 @@ If you only want to view the plot in the browser quickly, use the `Plot.show()`
plot.show(); // The default web browser will open, displaying an interactive plot
```
-## Exporting Static Images with Kaleido
-
-To save a plot as a static image, the `kaleido` feature is required as well as installing an **external dependency**.
-
-### Kaleido external dependency
-
-When developing applications for your host, enabling both `kaleido` and `kaleido_download` features will ensure that the `kaleido` binary is downloaded for your system's architecture at compile time. After download, it is unpacked into a specific path, e.g., on Linux this is `/home/USERNAME/.config/kaleido`. With these two features enabled, static images can be exported as described in the next section as long as the application runs on the same machine where it has been compiled on.
-
-When the applications developed with `plotly.rs` are intended for other targets or when the user wants to control where the `kaleido` binary is installed then Kaleido must be manually downloaded and installed. Setting the environment variable `KALEIDO_PATH=/path/installed/kaleido/` will ensure that applications that were built with the `kaleido` feature enabled can locate the `kaleido` executable and use it to generate static images.
-
-Kaleido binaries are available on Github [release page](https://github.com/plotly/Kaleido/releases). It currently supports Linux(`x86_64`), Windows(`x86_64`) and MacOS(`x86_64`/`aarch64`).
-
## Exporting Static Images with plotly_static (Recommended)
The recommended way to export static images is using the `plotly_static` backend, which uses a headless browser via WebDriver (Chrome or Firefox) for rendering. This is available via the `static_export_default` feature:
@@ -119,7 +108,7 @@ plotly = { version = "0.13", features = ["static_export_default"] }
This supports PNG, JPEG, WEBP, SVG, and PDF formats:
```rust
-use plotly::{Plot, Scatter, ImageFormat};
+use plotly::{Plot, Scatter,ImageFormat};
let mut plot = Plot::new();
plot.add_trace(Scatter::new(vec![0, 1, 2], vec![2, 1, 0]));
@@ -130,9 +119,15 @@ let base64_data = plot.to_base64(ImageFormat::PNG, 800, 600, 1.0)?;
let svg_string = plot.to_svg(800, 600, 1.0)?;
```
-**Note:** This feature requires a WebDriver-compatible browser (Chrome or Firefox) as well as a Webdriver (chromedriver/geckodriver) to be available on the system. For advanced usage, see the [`plotly_static` crate documentation](https://docs.rs/plotly_static/).
+**Note:** This feature requires a WebDriver-compatible browser (Chrome or Firefox) as well as a Webdriver (chromedriver/geckodriver) to be available on the system.
+
+The above example uses the legacy API that is backwards compatible with the Kaleido API. However, for more efficient workflows a `StaticExporter` object can be built and reused between calls to `write_image`.
+
+More specificallt, enabling any of the `plotly` features `static_export_chromedriver`, `static_export_geckodriver`, or `static_export_default` gives access to both the synchronous `StaticExporter` and the asynchronous `AsyncStaticExporter` (available via `plotly::plotly_static`). For exporter reuse and up-to-date sync/async usage patterns, please see the dedicated example in `examples/static_export`, which demonstrates both synchronous and asynchronous exporters and how to reuse a single exporter instance across multiple exports.
+
+ For further details see [`plotly_static` crate documentation](https://docs.rs/plotly_static/).
-## Exporting Static Images with Kaleido (to be deprecated)
+## Exporting Static Images with Kaleido (legacy)
Enable the `kaleido` feature and opt in for automatic downloading of the `kaleido` binaries by doing the following
@@ -165,9 +160,19 @@ plot.add_trace(trace);
plot.write_image("out.png", ImageFormat::PNG, 800, 600, 1.0);
```
-## Usage Within a Wasm Environment
+### Kaleido external dependency
+
+When developing applications for your host, enabling both `kaleido` and `kaleido_download` features will ensure that the `kaleido` binary is downloaded for your system's architecture at compile time. After download, it is unpacked into a specific path, e.g., on Linux this is `/home/USERNAME/.config/kaleido`. With these two features enabled, static images can be exported as described in the next section as long as the application runs on the same machine where it has been compiled on.
+
+When the applications developed with `plotly.rs` are intended for other targets or when the user wants to control where the `kaleido` binary is installed then Kaleido must be manually downloaded and installed. Setting the environment variable `KALEIDO_PATH=/path/installed/kaleido/` will ensure that applications that were built with the `kaleido` feature enabled can locate the `kaleido` executable and use it to generate static images.
+
+Kaleido binaries are available on Github [release page](https://github.com/plotly/Kaleido/releases). It currently supports Linux(`x86_64`), Windows(`x86_64`) and MacOS(`x86_64`/`aarch64`).
+
+## Usage Within a WASM Environment
+
+`Plotly.rs` can be used with a WASM-based frontend framework. Note that the `kaleido` and `plotly_static` static export features are not supported in WASM environments and will throw a compilation error if used.
-`Plotly.rs` can be used with a Wasm-based frontend framework. The needed dependencies are automatically enabled on `wasm32` targets. Note that the `kaleido` feature is not supported in Wasm environments and will throw a compilation error if enabled.
+The needed dependencies are automatically enabled for `wasm32` targets at compile time and there is no longer a need for the custom `wasm` flag in this crate.
First, make sure that you have the Plotly JavaScript library in your base HTML template:
@@ -190,7 +195,6 @@ A simple `Plot` component would look as follows, using `Yew` as an example front
use plotly::{Plot, Scatter};
use yew::prelude::*;
-
#[function_component(PlotComponent)]
pub fn plot_component() -> Html {
let p = yew_hooks::use_async::<_, _, ()>({
@@ -218,22 +222,23 @@ pub fn plot_component() -> Html {
}
```
-More detailed standalone examples can be found in the [examples/](https://github.com/plotly/plotly.rs/tree/main/examples) directory.
+More detailed standalone examples can be found in the [examples/wasm-yew](https://github.com/plotly/plotly.rs/tree/main/examples/wasm-yew) directory.
# Crate Feature Flags
The following feature flags are available:
-### `kaleido`
+### `static_export_default`
-Adds plot save functionality to the following formats: `png`, `jpeg`, `webp`, `svg`, `pdf` and `eps`.
+Since version `0.13.0` support for exporting to static images is based on using a new crate called `plotly_static` that uses WebDriver and browser automation for static export functionality.
-Requires `Kaleido` to have been previously installed on the host machine. See the following feature flag and [Kaleido external dependency](#kaleido-external-dependency).
+This feature flag automatically enables the usage of the `plotly_static` dependency as well as the `chromedriver` and `webdriver_download` features of that crate. For more details about these feature flags, refer to the `plotly_static` [documentation](plotly_static/README.md).
-### `kaleido_download`
+The other related features allow controlling other aspects of the `plotly_static` crate
+ - `static_export_chromedriver`
+ - `static_export_geckodriver`
+ - `static_export_wd_download`
-Enable download and install of Kaleido binary at build time from [Kaleido releases](https://github.com/plotly/Kaleido/releases/) on the host machine.
-See [Kaleido external dependency](#kaleido-external-dependency) for more details.
### `plotly_image`
@@ -253,9 +258,16 @@ When the feature is enabled, users can still opt in for the CDN version by using
Note that when using `Plot::to_inline_html()`, it is assumed that the `plotly.js` library is already in scope within the HTML file, so enabling this feature flag will have no effect.
-### `wasm`
+### `kaleido` (legacy)
+
+Adds plot save functionality to the following formats: `png`, `jpeg`, `webp`, `svg`, `pdf` and `eps`.
+
+Requires `Kaleido` to have been previously installed on the host machine. See the following feature flag and [Kaleido external dependency](#kaleido-external-dependency).
+
+### `kaleido_download` (legacy)
-Enables compilation for the `wasm32-unknown-unknown` target and provides access to a `bindings` module containing wrappers around functions exported by the plotly.js library.
+Enable download and install of Kaleido binary at build time from [Kaleido releases](https://github.com/plotly/Kaleido/releases/) on the host machine.
+See [Kaleido external dependency](#kaleido-external-dependency) for more details.
# Contributing
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.md b/docs/book/src/fundamentals.md
index e231b130..07eeb663 100644
--- a/docs/book/src/fundamentals.md
+++ b/docs/book/src/fundamentals.md
@@ -3,7 +3,7 @@
-
+
diff --git a/docs/book/src/fundamentals/static_image_export.md b/docs/book/src/fundamentals/static_image_export.md
index 3eec1438..2c4f6265 100644
--- a/docs/book/src/fundamentals/static_image_export.md
+++ b/docs/book/src/fundamentals/static_image_export.md
@@ -34,11 +34,13 @@ plotly = { version = "0.13", features = ["static_export_chromedriver", "static_e
plotly = { version = "0.13", features = ["static_export_default"] }
```
+> Enabling any of the static export features in `plotly` (`static_export_chromedriver`, `static_export_geckodriver`, or `static_export_default`) enables both APIs from `plotly_static`: the sync `StaticExporter` and the async `AsyncStaticExporter` (reachable as `plotly::plotly_static::AsyncStaticExporter`). Prefer the async API inside async code.
+
## Prerequisites
1. **WebDriver Installation**: You need either chromedriver or geckodriver installed
- - Chrome: Download from https://chromedriver.chromium.org/
- - Firefox: Download from https://github.com/mozilla/geckodriver/releases
+ - Chrome: Download from [https://chromedriver.chromium.org/](https://chromedriver.chromium.org/)
+ - Firefox: Download from [https://github.com/mozilla/geckodriver/releases](https://github.com/mozilla/geckodriver/releases)
- Or use the `static_export_wd_download` feature for automatic download
2. **Browser Installation**: You need Chrome/Chromium or Firefox installed
@@ -57,8 +59,7 @@ plotly = { version = "0.13", features = ["static_export_default"] }
### Simple Export
```rust
-use plotly::{Plot, Scatter};
-use plotly::plotly_static::ImageFormat;
+use plotly::{Plot, Scatter, ImageFormat};
let mut plot = Plot::new();
plot.add_trace(Scatter::new(vec![1, 2, 3], vec![4, 5, 6]));
@@ -75,6 +76,7 @@ For better performance when exporting multiple plots, reuse a single `StaticExpo
```rust
use plotly::{Plot, Scatter};
use plotly::plotly_static::{StaticExporterBuilder, ImageFormat};
+use plotly::prelude::*;
let mut plot1 = Plot::new();
plot1.add_trace(Scatter::new(vec![1, 2, 3], vec![4, 5, 6]));
@@ -88,10 +90,13 @@ let mut exporter = StaticExporterBuilder::default()
.expect("Failed to create StaticExporter");
// Export multiple plots using the same exporter
-plot1.write_image_with_exporter(&mut exporter, "plot1", ImageFormat::PNG, 800, 600, 1.0)
+exporter.write_image(&plot1, "plot1", ImageFormat::PNG, 800, 600, 1.0)
.expect("Failed to export plot1");
-plot2.write_image_with_exporter(&mut exporter, "plot2", ImageFormat::JPEG, 800, 600, 1.0)
+exporter.write_image(&plot2, "plot2", ImageFormat::JPEG, 800, 600, 1.0)
.expect("Failed to export plot2");
+
+// Always close the exporter to ensure proper release of WebDriver resources
+exporter.close();
```
## Supported Formats
@@ -115,6 +120,7 @@ For web applications or APIs, you can export to strings:
```rust
use plotly::{Plot, Scatter};
use plotly::plotly_static::{StaticExporterBuilder, ImageFormat};
+use plotly::prelude::*;
let mut plot = Plot::new();
plot.add_trace(Scatter::new(vec![1, 2, 3], vec![4, 5, 6]));
@@ -124,14 +130,19 @@ let mut exporter = StaticExporterBuilder::default()
.expect("Failed to create StaticExporter");
// Get base64 data (useful for embedding in HTML)
-let base64_data = plot.to_base64_with_exporter(&mut exporter, ImageFormat::PNG, 400, 300, 1.0)
+let base64_data = exporter.to_base64(&plot, ImageFormat::PNG, 400, 300, 1.0)
.expect("Failed to export plot");
// Get SVG data (vector format, scalable)
-let svg_data = plot.to_svg_with_exporter(&mut exporter, 400, 300, 1.0)
+let svg_data = exporter.to_svg(&plot, 400, 300, 1.0)
.expect("Failed to export plot");
+
+// Always close the exporter to ensure proper release of WebDriver resources
+exporter.close();
```
+Always call `close()` on the exporter to ensure proper release of WebDriver resources. Due to the nature of WebDriver implementation, close has to be called as resources cannot be automatically dropped or released.
+
## Advanced Configuration
### Custom WebDriver Configuration
@@ -151,6 +162,10 @@ let mut exporter = StaticExporterBuilder::default()
])
.build()
.expect("Failed to create StaticExporter");
+
+// Always close the exporter to ensure proper release of WebDriver resources
+exporter.close();
+
```
### Parallel Usage
@@ -173,8 +188,19 @@ let mut exporter = StaticExporterBuilder::default()
.webdriver_port(get_unique_port())
.build()
.expect("Failed to build StaticExporter");
+
+// Always close the exporter to ensure proper release of WebDriver resources
+exporter.close();
```
+### Async support
+
+`plotly_static` package offers an `async` API which is exposed in `plotly` via the `write_image_async`, `to_base64_async` and `to_svg_async` functions. However, the user must pass an `AsyncStaticExporter` asynchronous exporter instead of a synchronous one by building it via `StaticExportBuilder`'s `build_async` method.
+
+> Note: Both sync and async exporters are available whenever a `static_export_*` feature is enabled in `plotly`.
+
+For more details check the [`plotly_static` API Documentation](https://docs.rs/plotly_static/)
+
## Logging Support
Enable logging for debugging and monitoring:
@@ -191,6 +217,9 @@ env_logger::init();
let mut exporter = StaticExporterBuilder::default()
.build()
.expect("Failed to create StaticExporter");
+
+// Always close the exporter to ensure proper release of WebDriver resources
+exporter.close();
```
## Performance Considerations
@@ -198,15 +227,10 @@ 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
-See the [static export example](../../../examples/static_export/) for a complete working example that demonstrates:
+See the [static export example](https://github.com/plotly/plotly.rs/tree/main/examples/static_export) for a complete working example that demonstrates:
- Multiple export formats
- Exporter reuse
diff --git a/docs/book/src/getting_started.md b/docs/book/src/getting_started.md
index 512a279e..a142c70c 100644
--- a/docs/book/src/getting_started.md
+++ b/docs/book/src/getting_started.md
@@ -3,7 +3,7 @@
-
+
diff --git a/docs/book/src/plotly_rs.md b/docs/book/src/plotly_rs.md
index db0c058a..cee27070 100644
--- a/docs/book/src/plotly_rs.md
+++ b/docs/book/src/plotly_rs.md
@@ -3,7 +3,7 @@
-
+
diff --git a/docs/book/src/recipes.md b/docs/book/src/recipes.md
index 5c27ecf2..6cda170c 100644
--- a/docs/book/src/recipes.md
+++ b/docs/book/src/recipes.md
@@ -3,7 +3,7 @@
-
+
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) |  |
| [Sliders](./custom_controls/sliders.md) |  |
+| [Animations](./custom_controls/animations.md) |  |
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 00000000..7c3b8dac
Binary files /dev/null and b/docs/book/src/recipes/img/animation.png differ
diff --git a/docs/book/src/recipes/subplots/subplots.md b/docs/book/src/recipes/subplots/subplots.md
index c4e9fa73..9ed53066 100644
--- a/docs/book/src/recipes/subplots/subplots.md
+++ b/docs/book/src/recipes/subplots/subplots.md
@@ -24,7 +24,7 @@ The `to_inline_html` method is used to produce the html plot displayed in this p
{{#include ../../../../../examples/subplots/src/main.rs:subplots_with_multiple_traces}}
```
-{{#include ../../../../../examples/subplots/output/inline_subplot_with_multiple_traces.html}}
+{{#include ../../../../../examples/subplots/output/inline_subplots_with_multiple_traces.html}}
## Custom Sized Subplot
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/examples/customization/consistent_static_format_export/README.md b/examples/customization/consistent_static_format_export/README.md
index f8fc63ba..b40a40ba 100644
--- a/examples/customization/consistent_static_format_export/README.md
+++ b/examples/customization/consistent_static_format_export/README.md
@@ -4,22 +4,9 @@ This example demonstrates exporting a plot to SVG, PNG, and PDF using plotly.rs
This example is based on [GitHub Issue #171](https://github.com/plotly/plotly.rs/issues/171).
-
-**Summary:**
-
For consistent font rendering across browsers and export formats, always set the `font.family` property explicitly in your plot configuration. Relying on default or generic font settings can lead to differences in appearance, especially for font size and legend layout, depending on the browser or export backend.
-**Recommendation:**
-
-Always set the `font.family` property (e.g., to `"Times New Roman, serif"`) for all text elements (titles, axes, legends) to ensure consistent results in all output formats.
-
-## Overview
-
-This example creates a line and scatter plot with custom styling, including:
-- Large font sizes for titles, legends, and axes
-- Custom legend positioning and styling
-- Border shapes around the plot
-- Export to multiple formats (PDF, SVG, PNG)
+The issue reported in [#171](https://github.com/plotly/plotly.rs/issues/171)is solved when the `font.family` property is set explicitly (e.g., to `"Times New Roman, serif"`) for all text elements (titles, axes, legends) which ensures consistent results in all output formats across different browsers.
## Running the Example
@@ -29,6 +16,6 @@ cargo run
```
This will generate three output files:
-- `Data_plot.pdf` - PDF format (typically renders correctly)
-- `Data_plot.svg` - SVG format (may have font/legend issues)
-- `Data_plot.png` - PNG format (typically renders correctly)
+- `Data_plot.png`
+- `Data_plot.svg` - will render fonts differently when compared with PNG if font family not fully specified
+- `Data_plot.pdf` - uses SVG for PDF generation and will inherit the same issue as SVG
diff --git a/examples/customization/consistent_static_format_export/src/main.rs b/examples/customization/consistent_static_format_export/src/main.rs
index 813d5571..1e8a923a 100644
--- a/examples/customization/consistent_static_format_export/src/main.rs
+++ b/examples/customization/consistent_static_format_export/src/main.rs
@@ -3,6 +3,7 @@ use plotly::color::{NamedColor, Rgb};
use plotly::common::{Anchor, Font, Line, Marker, MarkerSymbol, Mode, Title};
use plotly::layout::{Axis, ItemSizing, Legend, Margin, Shape, ShapeLine, ShapeType};
use plotly::plotly_static::{ImageFormat, StaticExporterBuilder};
+use plotly::prelude::*;
use plotly::{Layout, Plot, Scatter};
fn line_and_scatter_plot(
@@ -149,19 +150,25 @@ fn line_and_scatter_plot(
.unwrap();
info!("Exporting to PNG format...");
- plot.write_image_with_exporter(&mut exporter, file_name, ImageFormat::PNG, 1280, 960, 1.0)
+ exporter
+ .write_image(&plot, file_name, ImageFormat::PNG, 1280, 960, 1.0)
.unwrap();
info!("Exporting to SVG format...");
- plot.write_image_with_exporter(&mut exporter, file_name, ImageFormat::SVG, 1280, 960, 1.0)
+ exporter
+ .write_image(&plot, file_name, ImageFormat::SVG, 1280, 960, 1.0)
.unwrap();
info!("Exporting to PDF format...");
- plot.write_image_with_exporter(&mut exporter, file_name, ImageFormat::PDF, 1280, 960, 1.0)
+ exporter
+ .write_image(&plot, file_name, ImageFormat::PDF, 1280, 960, 1.0)
.unwrap();
info!("Export complete. Check the output files:");
info!(" - {file_name}.pdf");
info!(" - {file_name}.svg");
info!(" - {file_name}.png");
+
+ // Always close the exporter to ensure proper release of WebDriver resources
+ exporter.close();
}
fn read_from_file(file_path: &str) -> Vec> {
diff --git a/examples/static_export/Cargo.toml b/examples/static_export/Cargo.toml
index 71c75304..a6de5cde 100644
--- a/examples/static_export/Cargo.toml
+++ b/examples/static_export/Cargo.toml
@@ -5,8 +5,10 @@ authors = ["Andrei Gherghescu andrei-ng@protonmail.com"]
edition = "2021"
description = "Example demonstrating static image export using plotly_static with WebDriver"
readme = "README.md"
+default-run = "sync"
[dependencies]
plotly = { path = "../../plotly", features = ["static_export_default"] }
-env_logger = "0.10"
-log = "0.4"
+env_logger = "0.11"
+log = "0.4"
+tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
diff --git a/examples/static_export/README.md b/examples/static_export/README.md
index 5c5149b3..c5f03936 100644
--- a/examples/static_export/README.md
+++ b/examples/static_export/README.md
@@ -6,13 +6,13 @@ The `plotly_static` provides a interface for converting Plotly plots into variou
In this example it is shown how to use the `StaticExporter` with the old style Kaleido API and also with the new style API. Using the former API is fine for one time static exports, but that API will crate an instance of the `StaticExporter` for each `write_image` call. The new style API is recommended for performance as the same instance of the `StaticExporter` can be reused across multiple exports.
-See also the `Static Image Export` section in the book for a more detailed description.
+When any of the `plotly` static export features are enabled (`static_export_chromedriver`, `static_export_geckodriver`, or `static_export_default`), both `StaticExporter` (sync) and `AsyncStaticExporter` (async) are available via `plotly::plotly_static`. This example includes separate `sync` and `async` bins demonstrating both. Refer to the [`plotly_static` API Documentation](https://docs.rs/plotly_static/) a more detailed description.
## Overview
-
## Features
+- **Async/Sync API**
- **Multiple Export Formats**: PNG, JPEG, SVG, PDF
- **Exporter Reuse (new API)**: Efficient reuse of a single `StaticExporter` instance
- **String Export**: Base64 and SVG string output for web applications
@@ -45,17 +45,32 @@ plotly = { version = "0.13", features = ["static_export_geckodriver"] }
plotly = { version = "0.13", features = ["static_export_chromedriver"] }
```
-## Running the Example
+## Running the Example(s)
+
+To run the `sync` API example
+
+```bash
+# Basic run
+cargo run --bin sync
+
+# With debug logging
+RUST_LOG=debug cargo run --bin sync
+
+# With custom WebDriver path
+WEBDRIVER_PATH=/path/to/chromedriver cargo run --bin sync
+```
+
+To run the `async` API example
```bash
# Basic run
-cargo run
+cargo run --bin async
# With debug logging
-RUST_LOG=debug cargo run
+RUST_LOG=debug cargo run --bin async
# With custom WebDriver path
-WEBDRIVER_PATH=/path/to/chromedriver cargo run
+WEBDRIVER_PATH=/path/to/chromedriver cargo run --bin async
```
## Output
diff --git a/examples/static_export/src/bin/async.rs b/examples/static_export/src/bin/async.rs
new file mode 100644
index 00000000..8ba78394
--- /dev/null
+++ b/examples/static_export/src/bin/async.rs
@@ -0,0 +1,78 @@
+use log::info;
+use plotly::plotly_static::{ImageFormat, StaticExporterBuilder};
+use plotly::prelude::*;
+use plotly::{Plot, Scatter};
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
+
+ // Create some plots
+ let mut plot1 = Plot::new();
+ plot1.add_trace(Scatter::new(vec![1, 2, 3, 4], vec![10, 15, 13, 17]).name("trace1"));
+
+ let mut plot2 = Plot::new();
+ plot2.add_trace(Scatter::new(vec![2, 3, 4, 5], vec![16, 5, 11, 9]).name("trace2"));
+
+ std::fs::create_dir_all("./output").unwrap();
+
+ info!("Creating AsyncStaticExporter with default configuration...");
+ let mut exporter = StaticExporterBuilder::default()
+ .webdriver_port(5111)
+ .build_async()
+ .expect("Failed to create AsyncStaticExporter");
+
+ info!("Exporting multiple plots using a single AsyncStaticExporter...");
+ exporter
+ .write_image(
+ &plot1,
+ "./output/plot1_async_api",
+ ImageFormat::PNG,
+ 800,
+ 600,
+ 1.0,
+ )
+ .await?;
+ exporter
+ .write_image(
+ &plot1,
+ "./output/plot1_async_api",
+ ImageFormat::JPEG,
+ 800,
+ 600,
+ 1.0,
+ )
+ .await?;
+ exporter
+ .write_image(
+ &plot2,
+ "./output/plot2_async_api",
+ ImageFormat::SVG,
+ 800,
+ 600,
+ 1.0,
+ )
+ .await?;
+ exporter
+ .write_image(
+ &plot2,
+ "./output/plot2_async_api",
+ ImageFormat::PDF,
+ 800,
+ 600,
+ 1.0,
+ )
+ .await?;
+
+ info!("Exporting to base64 and SVG strings with async API...");
+ let _base64_data = exporter
+ .to_base64(&plot1, ImageFormat::PNG, 400, 300, 1.0)
+ .await?;
+ let _svg_data = exporter.to_svg(&plot1, 400, 300, 1.0).await?;
+
+ // Always close the exporter to ensure proper release of WebDriver resources
+ exporter.close().await;
+
+ info!("Async exports completed successfully!");
+ Ok(())
+}
diff --git a/examples/static_export/src/main.rs b/examples/static_export/src/bin/sync.rs
similarity index 84%
rename from examples/static_export/src/main.rs
rename to examples/static_export/src/bin/sync.rs
index 391ed41c..8cb93a98 100644
--- a/examples/static_export/src/main.rs
+++ b/examples/static_export/src/bin/sync.rs
@@ -1,5 +1,6 @@
use log::info;
use plotly::plotly_static::{ImageFormat, StaticExporterBuilder};
+use plotly::prelude::*;
use plotly::{Plot, Scatter};
fn main() -> Result<(), Box> {
@@ -28,36 +29,39 @@ fn main() -> Result<(), Box> {
1.0,
)?;
plot3.write_image("./output/plot3_legacy_api", ImageFormat::SVG, 800, 600, 1.0)?;
- plot1.write_image("./output/plot3_legacy_api", ImageFormat::PDF, 800, 600, 1.0)?;
+
+ plot1.write_image("./output/plot1_legacy_api", ImageFormat::PDF, 800, 600, 1.0)?;
// Create a single StaticExporter to reuse across all plots
// This is more efficient than creating a new exporter for each plot which
// happens implicitly in the calls above using the old API
info!("Creating StaticExporter with default configuration...");
let mut exporter = StaticExporterBuilder::default()
+ .webdriver_port(5112)
.build()
.expect("Failed to create StaticExporter");
info!("Exporting multiple plots using a single StaticExporter...");
- // Export all plots using the same exporter
- plot1.write_image_with_exporter(
- &mut exporter,
+ // Export all plots using the same exporter (new unified naming via extension
+ // trait)
+ exporter.write_image(
+ &plot1,
"./output/plot1_new_api",
ImageFormat::PNG,
800,
600,
1.0,
)?;
- plot2.write_image_with_exporter(
- &mut exporter,
+ exporter.write_image(
+ &plot2,
"./output/plot2_new_api",
ImageFormat::JPEG,
800,
600,
1.0,
)?;
- plot3.write_image_with_exporter(
- &mut exporter,
+ exporter.write_image(
+ &plot3,
"./output/plot3_new_api",
ImageFormat::SVG,
800,
@@ -65,8 +69,8 @@ fn main() -> Result<(), Box> {
1.0,
)?;
- plot1.write_image_with_exporter(
- &mut exporter,
+ exporter.write_image(
+ &plot1,
"./output/plot1_new_api",
ImageFormat::PDF,
800,
@@ -77,11 +81,10 @@ fn main() -> Result<(), Box> {
// Demonstrate string-based export
info!("Exporting to base64 and SVG strings...");
// Get base64 data (useful for embedding in HTML or APIs)
- let base64_data =
- plot1.to_base64_with_exporter(&mut exporter, ImageFormat::PNG, 400, 300, 1.0)?;
+ let base64_data = exporter.to_base64(&plot1, ImageFormat::PNG, 400, 300, 1.0)?;
info!("Base64 data length: {}", base64_data.len());
- let svg_data = plot1.to_svg_with_exporter(&mut exporter, 400, 300, 1.0)?;
+ let svg_data = exporter.to_svg(&plot1, 400, 300, 1.0)?;
info!("SVG data starts with: {}", &svg_data[..50]);
info!("All exports completed successfully!");
@@ -108,5 +111,8 @@ fn main() -> Result<(), Box> {
.expect("Failed to create custom StaticExporter");
*/
+ // Always close the exporter to ensure proper release of WebDriver resources
+ exporter.close();
+
Ok(())
}
diff --git a/plotly/Cargo.toml b/plotly/Cargo.toml
index d173285a..24340dcc 100644
--- a/plotly/Cargo.toml
+++ b/plotly/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "plotly"
-version = "0.13.0"
+version = "0.13.5"
description = "A plotting library powered by Plotly.js"
authors = [
"Ioannis Giagkiozis ",
@@ -17,20 +17,28 @@ keywords = ["plot", "chart", "plotly"]
exclude = ["target/*"]
[features]
-# DEPRECATED: kaleido feature will be removed in version 0.14.0. Use `static_export_*` features instead.
-kaleido = ["plotly_kaleido"]
-# DEPRECATED: kaleido_download feature will be removed in version 0.14.0. Use `static_export_wd_download` instead.
-kaleido_download = ["plotly_kaleido/download"]
-
-static_export_chromedriver = ["plotly_static", "plotly_static/chromedriver"]
-static_export_geckodriver = ["plotly_static", "plotly_static/geckodriver"]
+static_export_chromedriver = [
+ "plotly_static",
+ "plotly_static/chromedriver",
+ "async-trait",
+]
+static_export_geckodriver = [
+ "plotly_static",
+ "plotly_static/geckodriver",
+ "async-trait",
+]
static_export_wd_download = ["plotly_static/webdriver_download"]
static_export_default = [
"plotly_static",
"plotly_static/chromedriver",
"plotly_static/webdriver_download",
+ "async-trait",
]
+plotly_ndarray = ["ndarray"]
+plotly_image = ["image"]
+plotly_embed_js = []
+
# All non-conflicting features
all = [
"plotly_ndarray",
@@ -41,9 +49,11 @@ all = [
# This is used for enabling extra debugging messages and debugging functionality
debug = ["plotly_static?/debug"]
-plotly_ndarray = ["ndarray"]
-plotly_image = ["image"]
-plotly_embed_js = []
+# DEPRECATED: kaleido feature will be removed in version 0.14.0. Use `static_export_*` features instead.
+kaleido = ["plotly_kaleido"]
+# DEPRECATED: kaleido_download feature will be removed in version 0.14.0. Use `static_export_wd_download` instead.
+kaleido_download = ["plotly_kaleido/download"]
+
[dependencies]
askama = { version = "0.14.0", features = ["serde_json"] }
@@ -51,7 +61,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.1", path = "../plotly_static", optional = true }
+plotly_static = { version = "0.1", path = "../plotly_static", optional = true }
plotly_kaleido = { version = "0.13", path = "../plotly_kaleido", optional = true }
ndarray = { version = "0.16", optional = true }
once_cell = "1"
@@ -59,10 +69,13 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
serde_with = ">=2, <4"
-rand = "0.9"
+rand = { version = "0.9", default-features = false, features = [
+ "small_rng",
+ "alloc",
+] }
+async-trait = { version = "0.1", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
-getrandom = { version = "0.3", features = ["wasm_js"] }
wasm-bindgen-futures = { version = "0.4" }
wasm-bindgen = { version = "0.2" }
serde-wasm-bindgen = { version = "0.6.3" }
diff --git a/plotly/src/common/mod.rs b/plotly/src/common/mod.rs
index cd866c3a..df2dc9ba 100644
--- a/plotly/src/common/mod.rs
+++ b/plotly/src/common/mod.rs
@@ -1180,7 +1180,10 @@ pub struct Marker {
size_mode: Option,
line: Option,
gradient: Option,
+ /// Marker option specific for Scatter and other common traces
color: Option>>,
+ /// Marker option specific for Pie charts to set the colors of the sectors
+ colors: Option>>,
cauto: Option,
cmin: Option,
cmax: Option,
@@ -1260,6 +1263,11 @@ impl Marker {
self
}
+ pub fn colors(mut self, colors: Vec) -> Self {
+ self.colors = Some(ColorArray(colors).into());
+ self
+ }
+
pub fn color_array(mut self, colors: Vec) -> Self {
self.color = Some(Dim::Vector(ColorArray(colors).into()));
self
@@ -2330,6 +2338,7 @@ mod tests {
.line(Line::new())
.gradient(Gradient::new(GradientType::Radial, "#FFFFFF"))
.color(NamedColor::Blue)
+ .colors(vec![NamedColor::Black, NamedColor::Blue])
.color_array(vec![NamedColor::Black, NamedColor::Blue])
.cauto(true)
.cmin(0.0)
@@ -2359,6 +2368,7 @@ mod tests {
"line": {},
"gradient": {"type": "radial", "color": "#FFFFFF"},
"color": ["black", "blue"],
+ "colors": ["black", "blue"],
"colorbar": {},
"cauto": true,
"cmin": 0.0,
diff --git a/plotly/src/export.rs b/plotly/src/export.rs
new file mode 100644
index 00000000..c91ea41f
--- /dev/null
+++ b/plotly/src/export.rs
@@ -0,0 +1,339 @@
+#[cfg(feature = "plotly_static")]
+pub mod sync {
+ use std::path::Path;
+
+ use crate::{plot::Plot, ImageFormat};
+
+ /// Extension methods for exporting plots using a synchronous exporter.
+ pub trait ExporterSyncExt {
+ /// Convert the `Plot` to a static image of the given image format and
+ /// save at the given location using a provided StaticExporter.
+ ///
+ /// This method allows you to reuse a StaticExporter instance across
+ /// multiple plots, which is more efficient than creating a new one for
+ /// each operation.
+ ///
+ /// This method requires the usage of the `plotly_static` crate using
+ /// one of the available feature flags. For advanced usage (parallelism, exporter reuse, custom config), see the [plotly_static documentation](https://docs.rs/plotly_static/).
+ ///
+ /// # Arguments
+ ///
+ /// * `exporter` - A mutable reference to a StaticExporter instance
+ /// * `filename` - The destination path for the output file
+ /// * `format` - The desired output image format
+ /// * `width` - The width of the output image in pixels
+ /// * `height` - The height of the output image in pixels
+ /// * `scale` - The scale factor for the image (1.0 = normal size)
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use plotly::{Plot, Scatter};
+ /// use plotly::export::sync::ExporterSyncExt as _;
+ /// use plotly::plotly_static::{StaticExporterBuilder, ImageFormat};
+ ///
+ /// let mut plot = Plot::new();
+ /// plot.add_trace(Scatter::new(vec![1, 2, 3], vec![4, 5, 6]));
+ ///
+ /// let mut exporter = StaticExporterBuilder::default()
+ /// .build()
+ /// .expect("Failed to create StaticExporter");
+ ///
+ /// // Export multiple plots using the same exporter
+ /// exporter.write_image(&plot, "plot1", ImageFormat::PNG, 800, 600, 1.0)
+ /// .expect("Failed to export plot");
+ ///
+ /// exporter.close();
+ /// ```
+ fn write_image>(
+ &mut self,
+ plot: &Plot,
+ filename: P,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result<(), Box>;
+
+ /// Convert the `Plot` to a static image and return the image as a
+ /// `base64` string. Supported formats are [ImageFormat::JPEG],
+ /// [ImageFormat::PNG] and [ImageFormat::WEBP].
+ ///
+ /// This method allows you to reuse the same StaticExporter instance
+ /// across multiple plots, which is more efficient than creating
+ /// a new one for each operation.
+ ///
+ /// For advanced usage (parallelism, exporter reuse, custom config), see the [plotly_static documentation](https://docs.rs/plotly_static/).
+ ///
+ /// # Arguments
+ ///
+ /// * `format` - The desired output image format
+ /// * `width` - The width of the output image in pixels
+ /// * `height` - The height of the output image in pixels
+ /// * `scale` - The scale factor for the image (1.0 = normal size)
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use plotly::{Plot, Scatter};
+ /// use plotly::export::sync::ExporterSyncExt as _;
+ /// use plotly::plotly_static::{StaticExporterBuilder, ImageFormat};
+ ///
+ /// let mut plot = Plot::new();
+ /// plot.add_trace(Scatter::new(vec![1, 2, 3], vec![4, 5, 6]));
+ ///
+ /// let mut exporter = StaticExporterBuilder::default()
+ /// .build()
+ /// .expect("Failed to create StaticExporter");
+ ///
+ /// let base64_data = exporter.to_base64(&plot, ImageFormat::PNG, 800, 600, 1.0)
+ /// .expect("Failed to export plot");
+ ///
+ /// exporter.close();
+ /// ```
+ fn to_base64(
+ &mut self,
+ plot: &Plot,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result>;
+
+ /// Convert the `Plot` to SVG and return it as a String.
+ ///
+ /// This method allows you to reuse the same StaticExporter instance
+ /// across multiple plots, which is more efficient than creating
+ /// a new one for each operation.
+ ///
+ /// For advanced usage (parallelism, exporter reuse, custom config), see the [plotly_static documentation](https://docs.rs/plotly_static/).
+ ///
+ /// # Arguments
+ ///
+ /// * `width` - The width of the output image in pixels
+ /// * `height` - The height of the output image in pixels
+ /// * `scale` - The scale factor for the image (1.0 = normal size)
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use plotly::{Plot, Scatter};
+ /// use plotly::export::sync::ExporterSyncExt as _;
+ /// use plotly::plotly_static::{StaticExporterBuilder, ImageFormat};
+ ///
+ /// let mut plot = Plot::new();
+ /// plot.add_trace(Scatter::new(vec![1, 2, 3], vec![4, 5, 6]));
+ ///
+ /// let mut exporter = StaticExporterBuilder::default()
+ /// .build()
+ /// .expect("Failed to create StaticExporter");
+ ///
+ /// let svg_data = exporter.to_svg(&plot, 800, 600, 1.0)
+ /// .expect("Failed to export plot");
+ ///
+ /// exporter.close();
+ /// ```
+ fn to_svg(
+ &mut self,
+ plot: &Plot,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result>;
+ }
+
+ impl ExporterSyncExt for plotly_static::StaticExporter {
+ fn write_image>(
+ &mut self,
+ plot: &Plot,
+ filename: P,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result<(), Box> {
+ self.write_fig(
+ filename.as_ref(),
+ &serde_json::to_value(plot)?,
+ format,
+ width,
+ height,
+ scale,
+ )
+ }
+
+ fn to_base64(
+ &mut self,
+ plot: &Plot,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result> {
+ match format {
+ ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => self.write_to_string(
+ &serde_json::to_value(plot)?,
+ format,
+ width,
+ height,
+ scale,
+ ),
+ _ => Err(format!(
+ "Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP"
+ )
+ .into()),
+ }
+ }
+
+ fn to_svg(
+ &mut self,
+ plot: &Plot,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result> {
+ self.write_to_string(
+ &serde_json::to_value(plot)?,
+ ImageFormat::SVG,
+ width,
+ height,
+ scale,
+ )
+ }
+ }
+}
+
+#[cfg(feature = "plotly_static")]
+pub mod r#async {
+ use std::path::Path;
+
+ use async_trait::async_trait;
+
+ use crate::{plot::Plot, ImageFormat};
+
+ /// Extension methods for exporting plots using an asynchronous exporter.
+ #[async_trait(?Send)]
+ pub trait ExporterAsyncExt {
+ /// Convert the `Plot` to a static image of the given format and save at
+ /// the given location using the asynchronous exporter.
+ ///
+ /// The exporter must have been built with the `build_async` method of
+ /// the StaticExporterBuilder.
+ ///
+ /// Functionally signature equivalent to the sync version in
+ /// [`crate::export::sync::ExporterSyncExt::write_image`], but meant for
+ /// async contexts.
+ ///
+ /// For more details see the [plotly_static documentation](https://docs.rs/plotly_static/).
+ async fn write_image>(
+ &mut self,
+ plot: &Plot,
+ filename: P,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result<(), Box>;
+
+ /// Convert the `Plot` to a static image and return the image as a
+ /// `base64` string using the asynchronous exporter.
+ ///
+ /// The exporter must have been built with the `build_async` method of
+ /// the StaticExporterBuilder.
+ ///
+ /// Functionally signature equivalent to the sync version in
+ /// [`crate::export::sync::ExporterSyncExt::to_base64`], but meant for
+ /// async contexts.
+ ///
+ /// For more details see the [plotly_static documentation](https://docs.rs/plotly_static/).
+ async fn to_base64(
+ &mut self,
+ plot: &Plot,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result>;
+
+ /// Convert the `Plot` to SVG and return it as a String using the
+ /// asynchronous exporter.
+ ///
+ /// Functionally signature equivalent to the sync version in
+ /// [`crate::export::sync::ExporterSyncExt::to_svg`], but meant for
+ /// async contexts.
+ ///
+ /// For more details see the [plotly_static documentation](https://docs.rs/plotly_static/).
+ async fn to_svg(
+ &mut self,
+ plot: &Plot,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result>;
+ }
+
+ #[async_trait(?Send)]
+ impl ExporterAsyncExt for plotly_static::AsyncStaticExporter {
+ async fn write_image>(
+ &mut self,
+ plot: &Plot,
+ filename: P,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result<(), Box> {
+ self.write_fig(
+ filename.as_ref(),
+ &serde_json::to_value(plot)?,
+ format,
+ width,
+ height,
+ scale,
+ )
+ .await
+ }
+
+ async fn to_base64(
+ &mut self,
+ plot: &Plot,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result> {
+ match format {
+ ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => self
+ .write_to_string(
+ &serde_json::to_value(plot)?,
+ format,
+ width,
+ height,
+ scale,
+ )
+ .await,
+ _ => Err(format!(
+ "Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP"
+ )
+ .into()),
+ }
+ }
+
+ async fn to_svg(
+ &mut self,
+ plot: &Plot,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result> {
+ self.write_to_string(
+ &serde_json::to_value(plot)?,
+ ImageFormat::SVG,
+ width,
+ height,
+ scale,
+ )
+ .await
+ }
+ }
+}
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/rangebreaks.rs b/plotly/src/layout/rangebreaks.rs
index 16470b31..43dcb57c 100644
--- a/plotly/src/layout/rangebreaks.rs
+++ b/plotly/src/layout/rangebreaks.rs
@@ -4,7 +4,7 @@ use serde::Serialize;
use crate::private::NumOrString;
/// Struct representing a rangebreak for Plotly axes.
-/// See: https://plotly.com/python/reference/layout/xaxis/#layout-xaxis-rangebreaks
+/// See:
#[derive(Debug, Clone, Serialize, PartialEq, FieldSetter)]
pub struct RangeBreak {
/// Sets the lower and upper bounds for this range break, e.g. ["sat",
diff --git a/plotly/src/layout/scene.rs b/plotly/src/layout/scene.rs
index cd721e39..01cf5084 100644
--- a/plotly/src/layout/scene.rs
+++ b/plotly/src/layout/scene.rs
@@ -357,7 +357,7 @@ impl Rotation {
pub struct Projection {
#[serde(rename = "type")]
projection_type: Option,
- /// Sets the rotation of the map projection. See https://plotly.com/python/reference/layout/geo/#layout-geo-projection-rotation
+ // Sets the rotation of the map projection. See https://plotly.com/python/reference/layout/geo/#layout-geo-projection-rotation
#[serde(rename = "rotation")]
rotation: Option,
}
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