From a4b71f11063599e545a719e5339962376bbf1443 Mon Sep 17 00:00:00 2001 From: RedZapdos123 Date: Tue, 28 Apr 2026 12:57:46 +0530 Subject: [PATCH 1/2] feat: type trace axis ids Signed-off-by: RedZapdos123 --- CHANGELOG.md | 6 ++ plotly/src/common/mod.rs | 124 ++++++++++++++++++++++++++++++ plotly/src/traces/bar.rs | 10 +-- plotly/src/traces/box_plot.rs | 10 +-- plotly/src/traces/candlestick.rs | 9 ++- plotly/src/traces/contour.rs | 18 ++--- plotly/src/traces/heat_map.rs | 9 ++- plotly/src/traces/histogram.rs | 10 +-- plotly/src/traces/image.rs | 10 +-- plotly/src/traces/scatter.rs | 29 +++++-- plotly_derive/src/field_setter.rs | 28 +++++-- 11 files changed, 215 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec42fd0..4785b6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a https://github.com/plotly/plotly.rs/pull/350 +## [Unreleased] + +### Fixed + +- [#5583](https://github.com/plotly/plotly.rs/issues/5583) Add typed axis-id enums for trace `x_axis` and `y_axis` setters + ## [0.14.1] - 2026-02-15 ### Fixed diff --git a/plotly/src/common/mod.rs b/plotly/src/common/mod.rs index 28e3bd2b..19d1cb14 100644 --- a/plotly/src/common/mod.rs +++ b/plotly/src/common/mod.rs @@ -893,6 +893,114 @@ pub enum Reference { Paper, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum XAxisId { + X1, + X2, + X3, + X4, + Custom(String), +} + +impl XAxisId { + fn as_str(&self) -> &str { + match self { + Self::X1 => "x", + Self::X2 => "x2", + Self::X3 => "x3", + Self::X4 => "x4", + Self::Custom(value) => value.as_str(), + } + } +} + +impl Serialize for XAxisId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl From<&str> for XAxisId { + fn from(value: &str) -> Self { + match value { + "x" | "x1" => Self::X1, + "x2" => Self::X2, + "x3" => Self::X3, + "x4" => Self::X4, + _ => Self::Custom(value.to_string()), + } + } +} + +impl From for XAxisId { + fn from(value: String) -> Self { + Self::from(value.as_str()) + } +} + +impl From for String { + fn from(value: XAxisId) -> Self { + value.as_str().to_string() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum YAxisId { + Y1, + Y2, + Y3, + Y4, + Custom(String), +} + +impl YAxisId { + fn as_str(&self) -> &str { + match self { + Self::Y1 => "y", + Self::Y2 => "y2", + Self::Y3 => "y3", + Self::Y4 => "y4", + Self::Custom(value) => value.as_str(), + } + } +} + +impl Serialize for YAxisId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl From<&str> for YAxisId { + fn from(value: &str) -> Self { + match value { + "y" | "y1" => Self::Y1, + "y2" => Self::Y2, + "y3" => Self::Y3, + "y4" => Self::Y4, + _ => Self::Custom(value.to_string()), + } + } +} + +impl From for YAxisId { + fn from(value: String) -> Self { + Self::from(value.as_str()) + } +} + +impl From for String { + fn from(value: YAxisId) -> Self { + value.as_str().to_string() + } +} + #[derive(Serialize, Clone, Debug)] pub struct Pad { t: usize, @@ -1799,6 +1907,22 @@ mod tests { assert_eq!(to_value(Reference::Paper).unwrap(), json!("paper")); } + #[test] + fn serialize_axis_id() { + assert_eq!(to_value(XAxisId::X1).unwrap(), json!("x")); + assert_eq!(to_value(XAxisId::X3).unwrap(), json!("x3")); + assert_eq!( + to_value(XAxisId::Custom("x7".to_string())).unwrap(), + json!("x7") + ); + assert_eq!(to_value(YAxisId::Y1).unwrap(), json!("y")); + assert_eq!(to_value(YAxisId::Y4).unwrap(), json!("y4")); + assert_eq!( + to_value(YAxisId::Custom("y8".to_string())).unwrap(), + json!("y8") + ); + } + #[test] #[rustfmt::skip] fn serialize_legend_group_title() { diff --git a/plotly/src/traces/bar.rs b/plotly/src/traces/bar.rs index 01d70527..7def1efc 100644 --- a/plotly/src/traces/bar.rs +++ b/plotly/src/traces/bar.rs @@ -6,7 +6,7 @@ use serde::Serialize; use crate::{ common::{ Calendar, ConstrainText, Dim, ErrorData, Font, HoverInfo, Label, LegendGroupTitle, Marker, - Orientation, PlotType, TextAnchor, TextPosition, Visible, + Orientation, PlotType, TextAnchor, TextPosition, Visible, XAxisId, YAxisId, }, Trace, }; @@ -69,9 +69,9 @@ where #[serde(rename = "hovertemplate")] hover_template: Option>, #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, orientation: Option, #[serde(rename = "alignmentgroup")] alignment_group: Option, @@ -179,9 +179,9 @@ mod tests { .text_template_array(vec!["text_template"]) .visible(Visible::LegendOnly) .width(999.0) - .x_axis("xaxis") + .x_axis(XAxisId::from("xaxis")) .x_calendar(Calendar::Nanakshahi) - .y_axis("yaxis") + .y_axis(YAxisId::from("yaxis")) .y_calendar(Calendar::Ummalqura); let expected = json!({ diff --git a/plotly/src/traces/box_plot.rs b/plotly/src/traces/box_plot.rs index 604eb1d3..8d21dadd 100644 --- a/plotly/src/traces/box_plot.rs +++ b/plotly/src/traces/box_plot.rs @@ -7,7 +7,7 @@ use crate::{ color::Color, common::{ Calendar, Dim, HoverInfo, Label, LegendGroupTitle, Line, Marker, Orientation, PlotType, - Visible, + Visible, XAxisId, YAxisId, }, Trace, }; @@ -124,9 +124,9 @@ where #[serde(rename = "hovertemplate")] hover_template: Option>, #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, orientation: Option, #[serde(rename = "alignmentgroup")] alignment_group: Option, @@ -307,9 +307,9 @@ mod tests { .visible(Visible::LegendOnly) .whisker_width(0.2) .width(50) - .x_axis("xaxis") + .x_axis(XAxisId::from("xaxis")) .x_calendar(Calendar::Chinese) - .y_axis("yaxis") + .y_axis(YAxisId::from("yaxis")) .y_calendar(Calendar::Coptic); let expected = json!({ diff --git a/plotly/src/traces/candlestick.rs b/plotly/src/traces/candlestick.rs index 9eaacce4..ea53a8e5 100644 --- a/plotly/src/traces/candlestick.rs +++ b/plotly/src/traces/candlestick.rs @@ -7,6 +7,7 @@ use crate::{ color::NamedColor, common::{ Calendar, Dim, Direction, HoverInfo, Label, LegendGroupTitle, Line, PlotType, Visible, + XAxisId, YAxisId, }, Trace, }; @@ -72,9 +73,9 @@ where #[serde(rename = "hoverinfo")] hover_info: Option, #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, line: Option, #[serde(rename = "whiskerwidth")] whisker_width: Option, @@ -151,8 +152,8 @@ mod tests { .hover_text_array(vec!["hover", "text"]) .hover_text("hover text") .hover_info(HoverInfo::Skip) - .x_axis("x1") - .y_axis("y1") + .x_axis(XAxisId::from("x1")) + .y_axis(YAxisId::from("y1")) .line(Line::new()) .whisker_width(0.4) .increasing(Direction::Increasing { line: Line::new() }) diff --git a/plotly/src/traces/contour.rs b/plotly/src/traces/contour.rs index 359eaf37..61ce8ec0 100644 --- a/plotly/src/traces/contour.rs +++ b/plotly/src/traces/contour.rs @@ -7,7 +7,7 @@ use crate::{ color::Color, common::{ Calendar, ColorBar, ColorScale, Dim, Font, HoverInfo, Label, LegendGroupTitle, Line, - PlotType, Visible, + PlotType, Visible, XAxisId, YAxisId, }, private, Trace, }; @@ -137,9 +137,9 @@ where #[serde(rename = "hovertemplate")] hover_template: Option>, #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, line: Option, #[serde(rename = "colorbar")] color_bar: Option, @@ -403,8 +403,8 @@ where Box::new(self) } - pub fn x_axis(mut self, axis: &str) -> Box { - self.x_axis = Some(axis.to_string()); + pub fn x_axis(mut self, axis: impl Into) -> Box { + self.x_axis = Some(axis.into()); Box::new(self) } @@ -418,8 +418,8 @@ where Box::new(self) } - pub fn y_axis(mut self, axis: &str) -> Box { - self.y_axis = Some(axis.to_string()); + pub fn y_axis(mut self, axis: impl Into) -> Box { + self.y_axis = Some(axis.into()); Box::new(self) } @@ -598,11 +598,11 @@ mod tests { .transpose(true) .visible(Visible::True) .x(vec![0.0, 1.0]) - .x_axis("x0") + .x_axis(XAxisId::from("x0")) .x_calendar(Calendar::Ethiopian) .x0(0.) .y(vec![2.0, 3.0]) - .y_axis("y0") + .y_axis(YAxisId::from("y0")) .y_calendar(Calendar::Gregorian) .y0(0.) .zauto(false) diff --git a/plotly/src/traces/heat_map.rs b/plotly/src/traces/heat_map.rs index b5514784..e9e86f5b 100644 --- a/plotly/src/traces/heat_map.rs +++ b/plotly/src/traces/heat_map.rs @@ -6,6 +6,7 @@ use serde::Serialize; use crate::{ common::{ Calendar, ColorBar, ColorScale, Dim, HoverInfo, Label, LegendGroupTitle, PlotType, Visible, + XAxisId, YAxisId, }, private::{NumOrString, NumOrStringCollection}, Trace, @@ -106,14 +107,14 @@ where visible: Option, x: Option>, #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, #[serde(rename = "xcalendar")] x_calendar: Option, #[serde(rename = "xgap")] x_gap: Option, y: Option>, #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, #[serde(rename = "ycalendar")] y_calendar: Option, #[serde(rename = "ygap")] @@ -227,10 +228,10 @@ mod tests { .text_array(vec!["te", "xt"]) .transpose(true) .visible(Visible::LegendOnly) - .x_axis("x") + .x_axis(XAxisId::from("x")) .x_calendar(Calendar::Hebrew) .x_gap(1.0) - .y_axis("y") + .y_axis(YAxisId::from("y")) .y_calendar(Calendar::Islamic) .y_gap("10") .zauto(true) diff --git a/plotly/src/traces/histogram.rs b/plotly/src/traces/histogram.rs index e1e8c422..bf884192 100644 --- a/plotly/src/traces/histogram.rs +++ b/plotly/src/traces/histogram.rs @@ -10,7 +10,7 @@ use crate::ndarray::ArrayTraces; use crate::{ common::{ Calendar, Dim, ErrorData, HoverInfo, Label, LegendGroupTitle, Marker, Orientation, - PlotType, Visible, + PlotType, Visible, XAxisId, YAxisId, }, Trace, }; @@ -155,14 +155,14 @@ where visible: Option, x: Option>, #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, #[serde(rename = "xbins")] x_bins: Option, #[serde(rename = "xcalendar")] x_calendar: Option, y: Option>, #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, #[serde(rename = "ybins")] y_bins: Option, #[serde(rename = "ycalendar")] @@ -419,10 +419,10 @@ mod tests { .text("text") .text_array(vec!["text_1", "text_2"]) .visible(Visible::True) - .x_axis("xaxis") + .x_axis(XAxisId::from("xaxis")) .x_bins(Bins::new(1.0, 2.0, 1.0)) .x_calendar(Calendar::Julian) - .y_axis("yaxis") + .y_axis(YAxisId::from("yaxis")) .y_bins(Bins::new(2.0, 3.0, 4.0)) .y_calendar(Calendar::Mayan); diff --git a/plotly/src/traces/image.rs b/plotly/src/traces/image.rs index 7fb4dd08..1afb8de9 100644 --- a/plotly/src/traces/image.rs +++ b/plotly/src/traces/image.rs @@ -8,7 +8,7 @@ use plotly_derive::FieldSetter; use serde::Serialize; use crate::color::{Rgb, Rgba}; -use crate::common::{Dim, HoverInfo, Label, LegendGroupTitle, PlotType, Visible}; +use crate::common::{Dim, HoverInfo, Label, LegendGroupTitle, PlotType, Visible, XAxisId, YAxisId}; use crate::private::{NumOrString, NumOrStringCollection}; use crate::Trace; @@ -280,13 +280,13 @@ pub struct Image { /// `Layout::x_axis`. If "x2", the x coordinates /// refer to `Layout::x_axis2`, and so on. #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, /// Sets a reference between this trace's y coordinates and a 2D cartesian y /// axis. If "y" (the default value), the y coordinates refer to /// `Layout::y_axis`. If "y2", the y coordinates /// refer to `Layout::y_axis2`, and so on. #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, /// Color model used to map the numerical color components described in `z` /// into colors. If `source` is specified, this attribute will be set to @@ -425,8 +425,8 @@ mod tests { .hover_template_array(vec!["hover_template"]) .meta("meta") .custom_data(vec!["custom_data"]) - .x_axis("x2") - .y_axis("y2") + .x_axis(XAxisId::from("x2")) + .y_axis(YAxisId::from("y2")) .color_model(ColorModel::RGBA) .z_max(vec![vec![w, w, w, w, w], vec![w, w, w, w, w]]) .z_min(vec![vec![b, b, b, b, b], vec![b, b, b, b, b]]) diff --git a/plotly/src/traces/scatter.rs b/plotly/src/traces/scatter.rs index e96784c3..5a3d4aa6 100644 --- a/plotly/src/traces/scatter.rs +++ b/plotly/src/traces/scatter.rs @@ -11,7 +11,7 @@ use crate::{ color::Color, common::{ Calendar, Dim, ErrorData, Fill, Font, HoverInfo, HoverOn, Label, LegendGroupTitle, Line, - Marker, Mode, Orientation, PlotType, Position, Visible, + Marker, Mode, Orientation, PlotType, Position, Visible, XAxisId, YAxisId, }, private::{NumOrString, NumOrStringCollection}, Trace, @@ -180,13 +180,13 @@ where /// `Layout::x_axis`. If "x2", the x coordinates /// refer to `Layout::x_axis2`, and so on. #[serde(rename = "xaxis")] - x_axis: Option, + x_axis: Option, /// Sets a reference between this trace's y coordinates and a 2D cartesian y /// axis. If "y" (the default value), the y coordinates refer to /// `Layout::y_axis`. If "y2", the y coordinates /// refer to `Layout::y_axis2`, and so on. #[serde(rename = "yaxis")] - y_axis: Option, + y_axis: Option, /// Only relevant when `stackgroup` is used, and only the first /// `orientation` found in the `stackgroup` will be used - including if /// `visible` is "legendonly" but not if it is `false`. @@ -473,10 +473,10 @@ mod tests { .text_template("text_template") .text_template_array(vec!["text_template"]) .visible(Visible::True) - .x_axis("x_axis") + .x_axis(XAxisId::from("x_axis")) .x_calendar(Calendar::Chinese) .x0(0) - .y_axis("y_axis") + .y_axis(YAxisId::from("y_axis")) .y_calendar(Calendar::Coptic) .y0(2) .web_gl_mode(true); @@ -528,4 +528,23 @@ mod tests { assert_eq!(to_value(trace).unwrap(), expected); } + + #[test] + fn serialize_scatter_axis_ids() { + use crate::common::{XAxisId, YAxisId}; + + let trace = Scatter::new(vec![0, 1], vec![2, 3]) + .x_axis(XAxisId::X2) + .y_axis(YAxisId::Y3); + + let expected = json!({ + "type": "scatter", + "x": [0, 1], + "y": [2, 3], + "xaxis": "x2", + "yaxis": "y3" + }); + + assert_eq!(to_value(trace).unwrap(), expected); + } } diff --git a/plotly_derive/src/field_setter.rs b/plotly_derive/src/field_setter.rs index 7f6b6b2b..f54c88cb 100644 --- a/plotly_derive/src/field_setter.rs +++ b/plotly_derive/src/field_setter.rs @@ -336,12 +336,28 @@ impl FieldReceiver { crate::color::ColorArray(value).into() )], ), - FieldType::OptionString => ( - quote![impl AsRef], - quote![value.as_ref().to_owned()], - quote![], - ), - FieldType::OptionOther(inner_ty) => (quote![#inner_ty], quote![value], quote![]), + FieldType::OptionString => { + if matches!(kind, Kind::Trace) + && matches!(field_ident.to_string().as_str(), "x_axis" | "y_axis") + { + (quote![impl Into], quote![value.into()], quote![]) + } else { + ( + quote![impl AsRef], + quote![value.as_ref().to_owned()], + quote![], + ) + } + } + FieldType::OptionOther(inner_ty) => { + if matches!(kind, Kind::Trace) + && matches!(field_ident.to_string().as_str(), "x_axis" | "y_axis") + { + (quote![impl Into<#inner_ty>], quote![value.into()], quote![]) + } else { + (quote![#inner_ty], quote![value], quote![]) + } + } FieldType::OptionVecString => ( quote![Vec>], quote![value.into_iter().map(|v| v.as_ref().to_owned()).collect()], From 778ee1fd96aedc6f3fdd9383dcb1360c4ae51d2e Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Fri, 1 May 2026 11:43:43 +0000 Subject: [PATCH 2/2] ci: fix driver downloads, use pre-installed - Export WEBDRIVER_PATH to reuse pre-installed runners drivers. - Remove harmful stub fallback causing OS Error 193. Signed-off-by: Mridankan Mandal --- .github/workflows/build.yml | 18 ++++++++++++++---- plotly_static/build.rs | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58403644..0a19afc1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,8 +43,11 @@ jobs: components: clippy targets: wasm32-unknown-unknown # lint plotly_static for all features - - run: cargo clippy -p plotly_static --features geckodriver,webdriver_download -- -D warnings -A deprecated - - run: cargo clippy -p plotly_static --features chromedriver,webdriver_download -- -D warnings -A deprecated + - run: | + export WEBDRIVER_PATH="${{ steps.setup-chrome.outputs.chromedriver-path }}" + export BROWSER_PATH="${{ steps.setup-chrome.outputs.chrome-path }}" + cargo clippy -p plotly_static --features geckodriver,webdriver_download -- -D warnings -A deprecated + cargo clippy -p plotly_static --features chromedriver,webdriver_download -- -D warnings -A deprecated # lint the main library workspace for non-wasm target - run: cargo clippy --features all -- -D warnings -A deprecated # lint the non-wasm examples @@ -132,7 +135,10 @@ jobs: # Run tests on Ubuntu with Chrome - name: Run tests (${{ matrix.os }} - Chrome) if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome' - run: cargo test --workspace --features ${{ matrix.features }} --exclude plotly_kaleido + run: | + export WEBDRIVER_PATH="${{ steps.setup-chrome.outputs.chromedriver-path }}" + export BROWSER_PATH="${{ steps.setup-chrome.outputs.chrome-path }}" + cargo test --workspace --features ${{ matrix.features }} --exclude plotly_kaleido # Install xvfb for Firefox WebGL support - name: Install xvfb @@ -146,6 +152,7 @@ jobs: if: matrix.os == 'ubuntu-latest' && matrix.browser == 'firefox' run: | # Set environment variables for Firefox WebDriver + export WEBDRIVER_PATH="/usr/local/share/gecko_driver" export BROWSER_PATH="${{ steps.setup-firefox.outputs.firefox-path }}" export RUST_LOG="debug" export RUST_BACKTRACE="1" @@ -159,7 +166,10 @@ jobs: # Run tests on macOS with Chrome - name: Run tests (${{ matrix.os }} - Chrome) if: matrix.os == 'macos-latest' && matrix.browser == 'chrome' - run: cargo test --workspace --features ${{ matrix.features }} --exclude plotly_kaleido + run: | + export WEBDRIVER_PATH="${{ steps.setup-chrome.outputs.chromedriver-path }}" + export BROWSER_PATH="${{ steps.setup-chrome.outputs.chrome-path }}" + cargo test --workspace --features ${{ matrix.features }} --exclude plotly_kaleido # Run tests on Windows with Chrome WebDriver - name: Run tests (${{ matrix.os }} - Chrome) diff --git a/plotly_static/build.rs b/plotly_static/build.rs index f8b2c076..aeaa4828 100644 --- a/plotly_static/build.rs +++ b/plotly_static/build.rs @@ -174,19 +174,19 @@ fn setup_driver(config: &WebdriverDownloadConfig) -> Result<()> { match config.driver_name { CHROMEDRIVER_NAME => { let driver_info = ChromedriverInfo::new(webdriver_bin.clone(), browser_path); - runtime - .block_on(async { download_with_retry(&driver_info, false, true, 1).await }) - .with_context(|| { - format!("Failed to download and install {}", config.driver_name) - })?; + let dl_res = runtime + .block_on(async { download_with_retry(&driver_info, false, true, 1).await }); + if let Err(e) = dl_res { + return Err(anyhow!("Failed to download and install chromedriver").context(e)); + } } GECKODRIVER_NAME => { let driver_info = GeckodriverInfo::new(webdriver_bin.clone(), browser_path); - runtime - .block_on(async { download_with_retry(&driver_info, false, true, 1).await }) - .with_context(|| { - format!("Failed to download and install {}", config.driver_name) - })?; + let dl_res = runtime + .block_on(async { download_with_retry(&driver_info, false, true, 1).await }); + if let Err(e) = dl_res { + return Err(anyhow!("Failed to download and install geckodriver").context(e)); + } } _ => return Err(anyhow!("Unsupported driver type: {}", config.driver_name)), }