diff --git a/CHANGELOG.md b/CHANGELOG.md index 480e84b9..e6f7948c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - [[#412](https://github.com/plotly/plotly.rs/pull/412)] Add `Violin` trace type with box, mean line, KDE span, and split/grouped support - [[#414](https://github.com/plotly/plotly.rs/issues/414)] Add `DensityMap` (MapLibre `map` subplot) trace type — density heatmaps with full color-scale and hover support - [[#417](https://github.com/plotly/plotly.rs/issues/417)] Add `ScatterMap` (MapLibre `map` subplot) trace type — the modern counterpart to `ScatterMapbox` +- Add `Funnel` trace type (bar-like, cartesian) with `Connector` and `FunnelHoverInfo` options, plus the layout-level `funnelmode`/`funnelgap`/`funnelgroupgap` attributes and a `FunnelMode` enum - [[#418](https://github.com/plotly/plotly.rs/issues/418)] Add native point clustering to `ScatterMap` via a `Cluster` option ### Changed diff --git a/plotly/src/common/mod.rs b/plotly/src/common/mod.rs index db2fa08e..818fb3f1 100644 --- a/plotly/src/common/mod.rs +++ b/plotly/src/common/mod.rs @@ -222,6 +222,7 @@ pub enum PlotType { Choropleth, ChoroplethMap, Contour, + Funnel, HeatMap, Histogram, Histogram2dContour, diff --git a/plotly/src/layout/mod.rs b/plotly/src/layout/mod.rs index 1aef0bb0..4dce794b 100644 --- a/plotly/src/layout/mod.rs +++ b/plotly/src/layout/mod.rs @@ -44,7 +44,8 @@ pub use self::legend::{GroupClick, ItemClick, ItemSizing, Legend, TraceOrder}; pub use self::map::{LayoutMap, MapBounds, MapStyle}; pub use self::mapbox::{Center, Mapbox, MapboxStyle}; pub use self::modes::{ - AspectMode, BarMode, BarNorm, BoxMode, ClickMode, UniformTextMode, ViolinMode, WaterfallMode, + AspectMode, BarMode, BarNorm, BoxMode, ClickMode, FunnelMode, UniformTextMode, ViolinMode, + WaterfallMode, }; pub use self::polar::{ AngularAxis, AngularAxisType, AutoRange, AutoRangeOptions, AutoTypeNumbers, AxisLayer, @@ -379,6 +380,12 @@ pub struct LayoutFields { waterfall_gap: Option, #[serde(rename = "waterfallgroupgap")] waterfall_group_gap: Option, + #[serde(rename = "funnelmode")] + funnel_mode: Option, + #[serde(rename = "funnelgap")] + funnel_gap: Option, + #[serde(rename = "funnelgroupgap")] + funnel_group_gap: Option, #[serde(rename = "piecolorway")] pie_colorway: Option>>, #[serde(rename = "extendpiecolors")] diff --git a/plotly/src/layout/modes.rs b/plotly/src/layout/modes.rs index 6c15f65c..25b5d35b 100644 --- a/plotly/src/layout/modes.rs +++ b/plotly/src/layout/modes.rs @@ -71,6 +71,14 @@ pub enum WaterfallMode { Overlay, } +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FunnelMode { + Stack, + Group, + Overlay, +} + #[derive(Debug, Clone)] pub enum UniformTextMode { False, @@ -146,6 +154,13 @@ mod tests { assert_eq!(to_value(WaterfallMode::Overlay).unwrap(), json!("overlay")); } + #[test] + fn serialize_funnel_mode() { + assert_eq!(to_value(FunnelMode::Stack).unwrap(), json!("stack")); + assert_eq!(to_value(FunnelMode::Group).unwrap(), json!("group")); + assert_eq!(to_value(FunnelMode::Overlay).unwrap(), json!("overlay")); + } + #[test] fn serialize_aspect_mode() { let aspect_mode = AspectMode::default(); diff --git a/plotly/src/lib.rs b/plotly/src/lib.rs index 7086da40..f3795710 100644 --- a/plotly/src/lib.rs +++ b/plotly/src/lib.rs @@ -60,13 +60,14 @@ pub use layout::Layout; pub use plot::{Plot, Trace, Traces}; // Also provide easy access to modules which contain additional trace-specific types pub use traces::{ - box_plot, choropleth, choropleth_map, contour, density_map, heat_map, histogram, image, mesh3d, - sankey, scatter, scatter3d, scatter_map, scatter_mapbox, sunburst, surface, treemap, violin, + box_plot, choropleth, choropleth_map, contour, density_map, funnel, heat_map, histogram, image, + mesh3d, sankey, scatter, scatter3d, scatter_map, scatter_mapbox, sunburst, surface, treemap, + violin, }; // Bring the different trace types into the top-level scope pub use traces::{ Bar, BoxPlot, Candlestick, Choropleth, ChoroplethMap, Contour, DensityMap, DensityMapbox, - HeatMap, Histogram, Image, Mesh3D, Ohlc, Pie, Sankey, Scatter, Scatter3D, ScatterGeo, + Funnel, HeatMap, Histogram, Image, Mesh3D, Ohlc, Pie, Sankey, Scatter, Scatter3D, ScatterGeo, ScatterMap, ScatterMapbox, ScatterPolar, Sunburst, Surface, Table, Treemap, Violin, }; diff --git a/plotly/src/traces/funnel.rs b/plotly/src/traces/funnel.rs new file mode 100644 index 00000000..32bee231 --- /dev/null +++ b/plotly/src/traces/funnel.rs @@ -0,0 +1,368 @@ +//! Funnel trace + +use plotly_derive::FieldSetter; +use serde::Serialize; + +use crate::{ + color::Color, + common::{ + ConstrainText, Dim, Font, Label, LegendGroupTitle, Line, Marker, Orientation, PlotType, + TextAnchor, TextPosition, Visible, XAxisId, YAxisId, + }, + Trace, +}; + +/// Determines which trace information appears on hover for a funnel trace. +/// +/// Unlike the generic [`HoverInfo`](crate::common::HoverInfo), the funnel +/// schema has no `z` flag and adds the funnel-specific `percent initial`, +/// `percent previous` and `percent total` flags. plotly.js accepts any +/// `+`-joined combination of these; the variants below cover the flags +/// individually plus the combinations that are useful in practice. +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum FunnelHoverInfo { + Name, + X, + Y, + Text, + #[serde(rename = "x+y")] + XAndY, + #[serde(rename = "x+y+text")] + XAndYAndText, + #[serde(rename = "percent initial")] + PercentInitial, + #[serde(rename = "percent previous")] + PercentPrevious, + #[serde(rename = "percent total")] + PercentTotal, + #[serde(rename = "percent initial+percent previous+percent total")] + AllPercents, + All, + None, + Skip, +} + +/// Visually connects consecutive funnel stages. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct Connector { + visible: Option, + line: Option, + #[serde(rename = "fillcolor")] + fill_color: Option>, +} + +impl Connector { + pub fn new() -> Self { + Default::default() + } +} + +/// Construct a funnel trace. +/// +/// # Examples +/// +/// ``` +/// use plotly::Funnel; +/// +/// let x = vec![100, 60, 40]; +/// let y = vec!["Visits", "Signups", "Purchases"]; +/// +/// let trace = Funnel::new(x, y) +/// .orientation(plotly::common::Orientation::Horizontal) +/// .text_info("value+percent previous"); +/// +/// let expected = serde_json::json!({ +/// "type": "funnel", +/// "x": [100, 60, 40], +/// "y": ["Visits", "Signups", "Purchases"], +/// "orientation": "h", +/// "textinfo": "value+percent previous" +/// }); +/// +/// assert_eq!(serde_json::to_value(trace).unwrap(), expected); +/// ``` +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug, Clone, FieldSetter)] +#[field_setter(box_self, kind = "trace")] +pub struct Funnel +where + X: Serialize + Clone, + Y: Serialize + Clone, +{ + #[field_setter(default = "PlotType::Funnel")] + r#type: PlotType, + x: Option>, + y: Option>, + name: Option, + visible: Option, + #[serde(rename = "showlegend")] + show_legend: Option, + #[serde(rename = "legendgroup")] + legend_group: Option, + #[serde(rename = "legendgrouptitle")] + legend_group_title: Option, + opacity: Option, + ids: Option>, + /// Sets the bar width (in position axis units). Unlike `Bar`, the funnel + /// trace does not accept a per-point array here. + width: Option, + /// Shifts the position where the bar is drawn (in position axis units). + /// Unlike `Bar`, the funnel trace does not accept a per-point array here. + offset: Option, + orientation: Option, + text: Option>, + #[serde(rename = "textposition")] + text_position: Option>, + /// Determines which trace information appears on the graph. Plotly.js + /// expects a `+`-joined flaglist built from any of `label`, `text`, + /// `percent initial`, `percent previous`, `percent total` and `value`, or + /// the single value `none`. + #[serde(rename = "textinfo")] + text_info: Option, + #[serde(rename = "texttemplate")] + text_template: Option>, + #[serde(rename = "texttemplatefallback")] + text_template_fallback: Option>, + #[serde(rename = "textangle")] + text_angle: Option, + #[serde(rename = "textfont")] + text_font: Option, + #[serde(rename = "insidetextfont")] + inside_text_font: Option, + #[serde(rename = "outsidetextfont")] + outside_text_font: Option, + #[serde(rename = "insidetextanchor")] + inside_text_anchor: Option, + #[serde(rename = "constraintext")] + constrain_text: Option, + #[serde(rename = "cliponaxis")] + clip_on_axis: Option, + #[serde(rename = "hovertext")] + hover_text: Option>, + #[serde(rename = "hoverinfo")] + hover_info: Option, + #[serde(rename = "hovertemplate")] + hover_template: Option>, + #[serde(rename = "hovertemplatefallback")] + hover_template_fallback: Option>, + #[serde(rename = "hoverlabel")] + hover_label: Option