diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d820f52..6913daa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,13 @@ 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 +- Add `Waterfall` trace type (bar-like, cartesian) with a typed `Measure` enum (`absolute`/`relative`/`total`), per-class `increasing`/`decreasing`/`totals` styling via `MeasureStyle`, a `Connector` with `ConnectorMode`, and a `WaterfallHoverInfo` enum. The layout-level `waterfallmode`/`waterfallgap`/`waterfallgroupgap` attributes and the `WaterfallMode` enum already existed - [[#418](https://github.com/plotly/plotly.rs/issues/418)] Add native point clustering to `ScatterMap` via a `Cluster` option - [[#421](https://github.com/plotly/plotly.rs/pull/421)] Backfill trace attributes for trace types to bring them to parity with plotly.js 3.7 - [[#422](https://github.com/plotly/plotly.rs/issues/422)] Add `Indicator`, `Histogram2d`, `Icicle` trace types - [[#425](https://github.com/plotly/plotly.rs/issues/425)] Add `Splom` and `Parcats` trace types (scatter-plot matrix and parallel categories) +- [[#432](https://github.com/plotly/plotly.rs/issues/432)] Add `Funnel` and `Waterfall` trace types ### Changed @@ -31,6 +34,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - [[#387](https://github.com/plotly/plotly.rs/pull/387)] Fix HeatMap hovertext/text dimensions - [[#385](https://github.com/plotly/plotly.rs/pull/385)] Add `{x,y}gap` parameter to heatmaps - [[#381](https://github.com/plotly/plotly.rs/pull/381)] Make `Plot::default()` behave like `Plot::new()` +- [[#432](https://github.com/plotly/plotly.rs/issues/432)] Fixed mdBook table of contents ordering + ## [0.14.0] - 2026-01-10 diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 27cd5a47..de051e4b 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -14,12 +14,13 @@ - [Scatter Plots](./recipes/basic_charts/scatter_plots.md) - [Line Charts](./recipes/basic_charts/line_charts.md) - [Bar Charts](./recipes/basic_charts/bar_charts.md) + - [Table Charts](./recipes/basic_charts/table_charts.md) - [Pie Charts](./recipes/basic_charts/pie_charts.md) - - [Sankey Diagrams](./recipes/basic_charts/sankey_diagrams.md) - - [Parallel Categories](./recipes/basic_charts/parcats_charts.md) - [Treemap Charts](./recipes/basic_charts/treemap_charts.md) - [Sunburst Charts](./recipes/basic_charts/sunburst_charts.md) - [Icicle Charts](./recipes/basic_charts/icicle_charts.md) + - [Sankey Diagrams](./recipes/basic_charts/sankey_diagrams.md) + - [Parallel Categories](./recipes/basic_charts/parcats_charts.md) - [Indicator Charts](./recipes/basic_charts/indicator_charts.md) - [Statistical Charts](./recipes/statistical_charts.md) - [Error Bars](./recipes/statistical_charts/error_bars.md) @@ -35,10 +36,16 @@ - [Time Series and Date Axes](./recipes/financial_charts/time_series_and_date_axes.md) - [Candlestick Charts](./recipes/financial_charts/candlestick_charts.md) - [OHLC Charts](./recipes/financial_charts/ohlc_charts.md) + - [Waterfall Charts](./recipes/financial_charts/waterfall_charts.md) + - [Funnel Charts](./recipes/financial_charts/funnel_charts.md) - [Rangebreaks](./recipes/financial_charts/rangebreaks.md) - [3D Charts](./recipes/3dcharts.md) - [Scatter 3D](./recipes/3dcharts/3dcharts.md) + - [Surface Plots](./recipes/3dcharts/surface_plots.md) + - [Mesh3D](./recipes/3dcharts/mesh3d_plots.md) - [Maps](./recipes/maps.md) + - [Scatter Maps](./recipes/maps/scatter_maps.md) + - [Density Maps](./recipes/maps/density_maps.md) - [Choropleth Maps](./recipes/maps/choropleth_maps.md) - [Subplots](./recipes/subplots.md) - [Subplots](./recipes/subplots/subplots.md) diff --git a/docs/book/src/recipes/3dcharts.md b/docs/book/src/recipes/3dcharts.md index 6229ff17..a95eae43 100644 --- a/docs/book/src/recipes/3dcharts.md +++ b/docs/book/src/recipes/3dcharts.md @@ -5,3 +5,5 @@ The complete source code for the following examples can also be found [here](htt Kind | Link :---|:----: Scatter3D |[![Scatter 3D Charts](./img/basic_scatter3d.png)](./3dcharts/3dcharts.md) +Surface Plots | [![Surface Plots](./img/surface_plot.png)](./3dcharts/surface_plots.md) +Mesh3D | [![Mesh3D](./img/mesh3d.png)](./3dcharts/mesh3d_plots.md) diff --git a/docs/book/src/recipes/3dcharts/mesh3d_plots.md b/docs/book/src/recipes/3dcharts/mesh3d_plots.md new file mode 100644 index 00000000..a758c9d6 --- /dev/null +++ b/docs/book/src/recipes/3dcharts/mesh3d_plots.md @@ -0,0 +1,21 @@ +# Mesh3D Plots + +A `Mesh3D` trace draws a triangulated 3D mesh from vertex coordinates and triangle +indices. An optional `intensity` array per vertex drives the color scale. + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::common::ColorScalePalette; +use plotly::{Mesh3D, Plot}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + +## Basic Mesh3D Plot + +```rust,no_run +{{#include ../../../../../examples/3d_charts/src/main.rs:mesh_3d_plot}} +``` + +{{#include ../../../../../examples/3d_charts/output/inline_mesh_3d_plot.html}} diff --git a/docs/book/src/recipes/3dcharts/surface_plots.md b/docs/book/src/recipes/3dcharts/surface_plots.md new file mode 100644 index 00000000..16975d3f --- /dev/null +++ b/docs/book/src/recipes/3dcharts/surface_plots.md @@ -0,0 +1,21 @@ +# Surface Plots + +A `Surface` trace renders a 3D surface from a matrix of z-values. Optional `x` and +`y` vectors define the grid coordinates; when omitted, indices are used. + +The following imports have been used to produce the plots below: + +```rust,no_run +use ndarray::Array; +use plotly::{Plot, Surface}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + +## Basic Surface Plot + +```rust,no_run +{{#include ../../../../../examples/3d_charts/src/main.rs:surface_plot}} +``` + +{{#include ../../../../../examples/3d_charts/output/inline_surface_plot.html}} diff --git a/docs/book/src/recipes/basic_charts.md b/docs/book/src/recipes/basic_charts.md index 26e1e6ac..b217e777 100644 --- a/docs/book/src/recipes/basic_charts.md +++ b/docs/book/src/recipes/basic_charts.md @@ -4,13 +4,14 @@ The source code for the following examples can also be found [here](https://gith Kind | Link :---|:----: -Scatter Plots |[![Scatter Plots](./img/line_and_scatter_plot.png)](./basic_charts/scatter_plots.md) +Scatter Plots | [![Scatter Plots](./img/line_and_scatter_plot.png)](./basic_charts/scatter_plots.md) Line Charts | [![Line Charts](./img/line_shape_options_for_interpolation.png)](./basic_charts/line_charts.md) -Bar Charts | [![Bar Charts](./img/bar_chart_with_error_bars.png)](./basic_charts/scatter_plots.md) +Bar Charts | [![Bar Charts](./img/bar_chart_with_error_bars.png)](./basic_charts/bar_charts.md) +Table Charts | [![Table Charts](./img/table.png)](./basic_charts/table_charts.md) Pie Charts | [![Pie Charts](./img/pie_charts.png)](./basic_charts/pie_charts.md) -Sankey Diagrams | [![Sankey Diagrams](./img/basic_sankey.png)](./basic_charts/sankey_diagrams.md) -Parallel Categories | [![Parallel Categories](./img/parallelcat.png)](./basic_charts/parcats_charts.md) Treemap Charts | [![Treemap Charts](./img/treemap.png)](./basic_charts/treemap_charts.md) Sunburst Charts | [![Sunburst Charts](./img/sunburst.png)](./basic_charts/sunburst_charts.md) Icicle Charts | [![Icicle Charts](./img/icicle.png)](./basic_charts/icicle_charts.md) +Sankey Diagrams | [![Sankey Diagrams](./img/basic_sankey.png)](./basic_charts/sankey_diagrams.md) +Parallel Categories | [![Parallel Categories](./img/parallelcat.png)](./basic_charts/parcats_charts.md) Indicator Charts | [![Indicator Charts](./img/indicator.png)](./basic_charts/indicator_charts.md) diff --git a/docs/book/src/recipes/basic_charts/bar_charts.md b/docs/book/src/recipes/basic_charts/bar_charts.md index f4a0fd3e..395f05d4 100644 --- a/docs/book/src/recipes/basic_charts/bar_charts.md +++ b/docs/book/src/recipes/basic_charts/bar_charts.md @@ -35,3 +35,17 @@ The `to_inline_html` method is used to produce the html plot displayed in this p ``` {{#include ../../../../../examples/basic_charts/output/inline_stacked_bar_chart.html}} + +## Category Order Bar Chart +```rust,no_run +{{#include ../../../../../examples/basic_charts/src/main.rs:category_order_bar_chart}} +``` + +{{#include ../../../../../examples/basic_charts/output/inline_category_order_bar_chart.html}} + +## Bar Chart with Pattern Fills +```rust,no_run +{{#include ../../../../../examples/basic_charts/src/main.rs:bar_chart_with_pattern_fills}} +``` + +{{#include ../../../../../examples/basic_charts/output/inline_bar_chart_with_pattern_fills.html}} diff --git a/docs/book/src/recipes/basic_charts/sankey_diagrams.md b/docs/book/src/recipes/basic_charts/sankey_diagrams.md index 26c39355..2f77fba8 100644 --- a/docs/book/src/recipes/basic_charts/sankey_diagrams.md +++ b/docs/book/src/recipes/basic_charts/sankey_diagrams.md @@ -22,7 +22,7 @@ The `to_inline_html` method is used to produce the html plot displayed in this p {{#include ../../../../../examples/basic_charts/output/inline_basic_sankey_diagram.html}} -## Skankey diagram with defined node position +## Sankey diagram with defined node position ```rust,no_run {{#include ../../../../../examples/basic_charts/src/main.rs:custom_node_sankey_diagram}} ``` diff --git a/docs/book/src/recipes/basic_charts/scatter_plots.md b/docs/book/src/recipes/basic_charts/scatter_plots.md index b2af1bd7..c387caa5 100644 --- a/docs/book/src/recipes/basic_charts/scatter_plots.md +++ b/docs/book/src/recipes/basic_charts/scatter_plots.md @@ -74,3 +74,10 @@ The `to_inline_html` method is used to produce the html plot displayed in this p ``` {{#include ../../../../../examples/basic_charts/output/inline_large_data_sets.html}} + +## Categories Scatter Chart +```rust,no_run +{{#include ../../../../../examples/basic_charts/src/main.rs:categories_scatter_chart}} +``` + +{{#include ../../../../../examples/basic_charts/output/inline_categories_scatter_chart.html}} diff --git a/docs/book/src/recipes/basic_charts/table_charts.md b/docs/book/src/recipes/basic_charts/table_charts.md new file mode 100644 index 00000000..918563ea --- /dev/null +++ b/docs/book/src/recipes/basic_charts/table_charts.md @@ -0,0 +1,23 @@ +# Table Charts + +A `Table` trace renders structured data as an HTML table inside the plot. Unlike cartesian traces, +it does not use x/y axes — you supply a styled `Header` and `Cells` block. + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::color::NamedColor; +use plotly::traces::table::{ + Align as TableAlign, Cells, Fill as TableFill, Font as TableFont, Header, Line as TableLine, +}; +use plotly::{Plot, Table}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + +## Basic Table +```rust,no_run +{{#include ../../../../../examples/basic_charts/src/main.rs:table_chart}} +``` + +{{#include ../../../../../examples/basic_charts/output/inline_table_chart.html}} diff --git a/docs/book/src/recipes/financial_charts.md b/docs/book/src/recipes/financial_charts.md index 06156011..fd12ee62 100644 --- a/docs/book/src/recipes/financial_charts.md +++ b/docs/book/src/recipes/financial_charts.md @@ -4,7 +4,9 @@ The source code for the following examples can also be found [here](https://gith Kind | Link :---|:----: -Time Series and Date Axes |[![Time Series and Date Axes](./img/time_series_and_date_axes.png)](./financial_charts/time_series_and_date_axes.md) +Time Series and Date Axes | [![Time Series and Date Axes](./img/time_series_and_date_axes.png)](./financial_charts/time_series_and_date_axes.md) Candlestick Charts | [![Candlestick Charts](./img/candlestick_chart.png)](./financial_charts/candlestick_charts.md) OHLC Charts | [![OHLC Charts](./img/ohlc_chart.png)](./financial_charts/ohlc_charts.md) +Waterfall Charts | [![Waterfall Charts](./img/waterfall.png)](./financial_charts/waterfall_charts.md) +Funnel Charts | [![Funnel Charts](./img/funnel.png)](./financial_charts/funnel_charts.md) Rangebreaks | [![Rangebreaks](./img/rangebreaks.png)](./financial_charts/rangebreaks.md) diff --git a/docs/book/src/recipes/financial_charts/funnel_charts.md b/docs/book/src/recipes/financial_charts/funnel_charts.md new file mode 100644 index 00000000..9b5afde8 --- /dev/null +++ b/docs/book/src/recipes/financial_charts/funnel_charts.md @@ -0,0 +1,34 @@ +# Funnel Charts + +A funnel chart shows how a quantity narrows as it passes through a sequence of stages. It is a +*containment* form: each stage is understood to be a subset of the one above it, and the band +widths carry that claim — so it suits pipelines that genuinely nest, such as a conversion funnel. + +Stages are fed **upstream-first**: plotly draws index 0 at the top, which is the opposite of a +plain category axis. + +`text_info` takes a `+`-joined flaglist, so a band can show its own value alongside its conversion +from the previous stage (`"value+percent previous"`), the share of the first stage +(`"percent initial"`), or the share of the total (`"percent total"`). + +For a sequence whose steps do *not* nest — independent totals, or contributions that can be +negative — see [Waterfall Charts](./waterfall_charts.md) instead. + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::color::NamedColor; +use plotly::common::Marker; +use plotly::funnel::Connector as FunnelConnector; +use plotly::layout::Layout; +use plotly::{Funnel, Plot}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + +## Basic Funnel +```rust,no_run +{{#include ../../../../../examples/financial_charts/src/main.rs:basic_funnel}} +``` + +{{#include ../../../../../examples/financial_charts/output/inline_basic_funnel.html}} diff --git a/docs/book/src/recipes/financial_charts/waterfall_charts.md b/docs/book/src/recipes/financial_charts/waterfall_charts.md new file mode 100644 index 00000000..bba75c1b --- /dev/null +++ b/docs/book/src/recipes/financial_charts/waterfall_charts.md @@ -0,0 +1,32 @@ +# Waterfall Charts + +A waterfall chart shows how a running total is built up from a starting value and a +sequence of signed contributions. Each bar's role is set by its `Measure`: + +- `Measure::Absolute` resets the running total and draws a bar from zero to it, +- `Measure::Relative` adds a signed delta, drawn as a floating bar, +- `Measure::Total` draws the running total accumulated so far. + +The value supplied for a `Total` bar is ignored — plotly.js re-derives it — but the slot +must still be present, because the label, value and `measure` arrays are read positionally. + +For nested stage-to-stage containment rather than signed contributions, see +[Funnel Charts](./funnel_charts.md) instead. + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::color::NamedColor; +use plotly::layout::Layout; +use plotly::waterfall::{Marker as WaterfallMarker, Measure, MeasureStyle}; +use plotly::{Plot, Waterfall}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + +## Basic Waterfall +```rust,no_run +{{#include ../../../../../examples/financial_charts/src/main.rs:basic_waterfall}} +``` + +{{#include ../../../../../examples/financial_charts/output/inline_basic_waterfall.html}} diff --git a/docs/book/src/recipes/img/choropleth_maps.png b/docs/book/src/recipes/img/choropleth_maps.png new file mode 100644 index 00000000..bcdd4e83 Binary files /dev/null and b/docs/book/src/recipes/img/choropleth_maps.png differ diff --git a/docs/book/src/recipes/img/density_maps.png b/docs/book/src/recipes/img/density_maps.png new file mode 100644 index 00000000..07ba032e Binary files /dev/null and b/docs/book/src/recipes/img/density_maps.png differ diff --git a/docs/book/src/recipes/img/funnel.png b/docs/book/src/recipes/img/funnel.png new file mode 100644 index 00000000..7b510776 Binary files /dev/null and b/docs/book/src/recipes/img/funnel.png differ diff --git a/docs/book/src/recipes/img/mesh3d.png b/docs/book/src/recipes/img/mesh3d.png new file mode 100644 index 00000000..ae351b5c Binary files /dev/null and b/docs/book/src/recipes/img/mesh3d.png differ diff --git a/docs/book/src/recipes/img/scatter_maps.png b/docs/book/src/recipes/img/scatter_maps.png new file mode 100644 index 00000000..057e1c69 Binary files /dev/null and b/docs/book/src/recipes/img/scatter_maps.png differ diff --git a/docs/book/src/recipes/img/table.png b/docs/book/src/recipes/img/table.png new file mode 100644 index 00000000..ad3777e4 Binary files /dev/null and b/docs/book/src/recipes/img/table.png differ diff --git a/docs/book/src/recipes/img/waterfall.png b/docs/book/src/recipes/img/waterfall.png new file mode 100644 index 00000000..6fc5e872 Binary files /dev/null and b/docs/book/src/recipes/img/waterfall.png differ diff --git a/docs/book/src/recipes/maps.md b/docs/book/src/recipes/maps.md index 1fc2c6b3..1afeddd6 100644 --- a/docs/book/src/recipes/maps.md +++ b/docs/book/src/recipes/maps.md @@ -4,4 +4,6 @@ The source code for the following examples can also be found [here](https://gith Kind | Link :---|:----: -Choropleth Maps | [Choropleth Maps](./maps/choropleth_maps.md) +Scatter Maps | [![Scatter Maps](./img/scatter_maps.png)](./maps/scatter_maps.md) +Density Maps | [![Density Maps](./img/density_maps.png)](./maps/density_maps.md) +Choropleth Maps | [![Choropleth Maps](./img/choropleth_maps.png)](./maps/choropleth_maps.md) diff --git a/docs/book/src/recipes/maps/density_maps.md b/docs/book/src/recipes/maps/density_maps.md new file mode 100644 index 00000000..c2e0d136 --- /dev/null +++ b/docs/book/src/recipes/maps/density_maps.md @@ -0,0 +1,25 @@ +# Density Maps + +A [`DensityMapbox`](https://docs.rs/plotly/latest/plotly/struct.DensityMapbox.html) +trace renders a kernel density estimate as a heatmap layer on a Mapbox-style +basemap. + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::{ + layout::{Center, DragMode, Mapbox, MapboxStyle}, + Configuration, DensityMapbox, Layout, Plot, +}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this +page. The rendered map requires an internet connection for the basemap tiles. + +## Basic Density Mapbox + +```rust,no_run +{{#include ../../../../../examples/maps/src/main.rs:density_mapbox}} +``` + +{{#include ../../../../../examples/maps/output/inline_density_mapbox.html}} diff --git a/docs/book/src/recipes/maps/scatter_maps.md b/docs/book/src/recipes/maps/scatter_maps.md new file mode 100644 index 00000000..bbe76e5b --- /dev/null +++ b/docs/book/src/recipes/maps/scatter_maps.md @@ -0,0 +1,48 @@ +# Scatter Maps + +Scatter map traces place markers or lines on geographic basemaps. Two trace types +are available: + +- [`ScatterGeo`](https://docs.rs/plotly/latest/plotly/struct.ScatterGeo.html) — + drawn on the built-in `geo` subplot + ([`LayoutGeo`](https://docs.rs/plotly/latest/plotly/layout/struct.LayoutGeo.html)). +- [`ScatterMapbox`](https://docs.rs/plotly/latest/plotly/struct.ScatterMapbox.html) — + drawn on a Mapbox-style basemap via + [`Mapbox`](https://docs.rs/plotly/latest/plotly/layout/struct.Mapbox.html). + +The following imports are used in the examples below: + +```rust,no_run +use plotly::{ + common::{Line, Marker, Mode}, + layout::{ + Axis, Center, DragMode, GeoResolution, LayoutGeo, Mapbox, MapboxStyle, Projection, + Rotation, + }, + color::Rgb, + Configuration, Layout, Plot, ScatterGeo, ScatterMapbox, +}; +``` + +The `to_inline_html` method is used to produce the html plots displayed in this +page. The Mapbox example requires an internet connection for the basemap tiles. + +## Scatter on a Mapbox basemap + +```rust,no_run +{{#include ../../../../../examples/maps/src/main.rs:scatter_mapbox}} +``` + +{{#include ../../../../../examples/maps/output/inline_scatter_mapbox.html}} + +## Lines on an orthographic geo subplot + +The example below downloads contour data from the Plotly datasets repository. If +the fetch fails (for example during an offline build), the example is skipped with +a warning rather than panicking. + +```rust,no_run +{{#include ../../../../../examples/maps/src/main.rs:scatter_geo}} +``` + +{{#include ../../../../../examples/maps/output/inline_scatter_geo.html}} diff --git a/docs/book/src/recipes/statistical_charts.md b/docs/book/src/recipes/statistical_charts.md index 2000329b..963996f2 100644 --- a/docs/book/src/recipes/statistical_charts.md +++ b/docs/book/src/recipes/statistical_charts.md @@ -4,9 +4,9 @@ The complete source code for the following examples can also be found [here](htt Kind | Link :---|:----: -Error Bars |[![Scatter Plots](./img/error_bars.png)](./statistical_charts/error_bars.md) -Box Plots | [![Line Charts](./img/box_plot.png)](./statistical_charts/box_plots.md) +Error Bars | [![Error Bars](./img/error_bars.png)](./statistical_charts/error_bars.md) +Box Plots | [![Box Plots](./img/box_plot.png)](./statistical_charts/box_plots.md) Violin Plots | [![Violin Plots](./img/violin_plot.png)](./statistical_charts/violin_plots.md) -Histograms | [![Scatter Plots](./img/overlaid_histogram.png)](./statistical_charts/histograms.md) +Histograms | [![Histograms](./img/overlaid_histogram.png)](./statistical_charts/histograms.md) 2D Histograms | [![2D Histograms](./img/basic_histogram2d.png)](./statistical_charts/histogram2d.md) SPLOM | [![SPLOM](./img/splom.png)](./statistical_charts/splom.md) diff --git a/examples/basic_charts/src/main.rs b/examples/basic_charts/src/main.rs index a8e2ce0d..a7d872ad 100644 --- a/examples/basic_charts/src/main.rs +++ b/examples/basic_charts/src/main.rs @@ -357,6 +357,54 @@ fn large_data_sets(show: bool, file_name: &str) { } // ANCHOR_END: large_data_sets +/// Scatter plot showing y axis categories and category ordering. +// ANCHOR: categories_scatter_chart +fn categories_scatter_chart(show: bool, file_name: &str) { + // Categories are ordered on the y axis from bottom to top. + let categories = vec!["Unknown", "Off", "On"]; + + let x = vec![ + "2024-10-30T08:30:05.05Z", + "2024-10-30T08:35:05.05Z", + "2024-10-30T08:50:05.05Z", + "2024-10-30T08:50:20.05Z", + "2024-10-30T09:00:05.05Z", + "2024-10-30T09:05:05.05Z", + "2024-10-30T09:10:05.05Z", + "2024-10-30T09:10:20.05Z", + ]; + let y = vec![ + "On", + "Off", + "Unknown", + "Off", + "On", + "Off", + // Categories that aren't in the category_array follow the Trace order. + "NewCategory", + "Off", + ]; + + let trace = Scatter::new(x, y).line(Line::new().shape(LineShape::Hv)); + + let layout = Layout::new().y_axis( + Axis::new() + .category_order(CategoryOrder::Array) + .category_array(categories), + ); + + let mut plot = Plot::new(); + plot.add_trace(trace); + + plot.set_layout(layout); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: categories_scatter_chart + // Line Charts // ANCHOR: adding_names_to_line_and_scatter_plot fn adding_names_to_line_and_scatter_plot(show: bool, file_name: &str) { @@ -642,44 +690,109 @@ fn filled_lines(show: bool, file_name: &str) { } // ANCHOR_END: filled_lines -/// Scatter plot showing y axis categories and category ordering. -// ANCHOR: categories_scatter_chart -fn categories_scatter_chart(show: bool, file_name: &str) { - // Categories are ordered on the y axis from bottom to top. - let categories = vec!["Unknown", "Off", "On"]; +// ANCHOR: set_lower_or_upper_bound_on_axis +fn set_lower_or_upper_bound_on_axis(show: bool, file_name: &str) { + use std::fs::File; + use std::io::BufReader; - let x = vec![ - "2024-10-30T08:30:05.05Z", - "2024-10-30T08:35:05.05Z", - "2024-10-30T08:50:05.05Z", - "2024-10-30T08:50:20.05Z", - "2024-10-30T09:00:05.05Z", - "2024-10-30T09:05:05.05Z", - "2024-10-30T09:10:05.05Z", - "2024-10-30T09:10:20.05Z", - ]; - let y = vec![ - "On", - "Off", - "Unknown", - "Off", - "On", - "Off", - // Categories that aren't in the category_array follow the Trace order. - "NewCategory", - "Off", - ]; + // Read the iris dataset + let file = File::open("assets/iris.csv").expect("Failed to open iris.csv"); + let reader = BufReader::new(file); + let mut csv_reader = csv::Reader::from_reader(reader); - let trace = Scatter::new(x, y).line(Line::new().shape(LineShape::Hv)); + // Parse the data + let mut sepal_width = Vec::new(); + let mut sepal_length = Vec::new(); + let mut species = Vec::new(); - let layout = Layout::new().y_axis( - Axis::new() - .category_order(CategoryOrder::Array) - .category_array(categories), - ); + for result in csv_reader.records() { + let record = result.expect("Failed to read CSV record"); + sepal_width.push(record[1].parse::().unwrap()); + sepal_length.push(record[0].parse::().unwrap()); + species.push(record[4].to_string()); + } + + // Create separate traces for each species + let mut traces = Vec::new(); + let unique_species: Vec = species + .iter() + .cloned() + .collect::>() + .into_iter() + .collect(); + + for (i, species_name) in unique_species.iter().enumerate() { + let mut x = Vec::new(); + let mut y = Vec::new(); + + for (j, s) in species.iter().enumerate() { + if s == species_name { + x.push(sepal_width[j]); + y.push(sepal_length[j]); + } + } + + let trace = Scatter::new(x, y) + .name(species_name) + .mode(plotly::common::Mode::Markers) + .x_axis(format!("x{}", i + 1)) + .y_axis(format!("y{}", i + 1)); + traces.push(trace); + } let mut plot = Plot::new(); - plot.add_trace(trace); + for trace in traces { + plot.add_trace(trace); + } + + // Create layout with subplots + let mut layout = Layout::new() + .title("Iris Dataset - Subplots by Species") + .grid( + LayoutGrid::new() + .rows(1) + .columns(3) + .pattern(plotly::layout::GridPattern::Independent), + ); + + // Set x-axis range for all subplots: [None, 4.5] + layout = layout + .x_axis( + Axis::new() + .title("sepal_width") + // Can be set using a vec! of two optional values + .range(vec![None, Some(4.5)]), + ) + .x_axis2( + Axis::new() + .title("sepal_width") + // Or can be set using AxisRange::upper(4.5) + .range(AxisRange::upper(4.5)), + ) + .x_axis3( + Axis::new() + .title("sepal_width") + // Or can be set using AxisRange::upper(4.5) + .range(AxisRange::upper(4.5)), + ); + + // Set y-axis range for all subplots: [3, None] + layout = layout + .y_axis( + Axis::new() + .title("sepal_length") + .range(vec![Some(3.0), None]), + ) + .y_axis2( + Axis::new() + .title("sepal_length") + .range(vec![Some(3.0), None]), + ) + .y_axis3( + Axis::new() + .title("sepal_length") + .range(vec![Some(3.0), None]), + ); plot.set_layout(layout); @@ -688,8 +801,7 @@ fn categories_scatter_chart(show: bool, file_name: &str) { plot.show_html(path); } } -// ANCHOR_END: categories_scatter_chart - +// ANCHOR_END: set_lower_or_upper_bound_on_axis // Bar Charts // ANCHOR: basic_bar_chart fn basic_bar_chart(show: bool, file_name: &str) { @@ -811,206 +923,56 @@ fn bar_chart_with_pattern_fills(show: bool, file_name: &str) { } // ANCHOR_END: bar_chart_with_pattern_fills -// Sankey Diagrams -// ANCHOR: basic_sankey_diagram -fn basic_sankey_diagram(show: bool, file_name: &str) { - // https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram - let trace = Sankey::new() - .orientation(Orientation::Horizontal) - .node( - Node::new() - .pad(15) - .thickness(30) - .line(SankeyLine::new().color(NamedColor::Black).width(0.5)) - .label(vec!["A1", "A2", "B1", "B2", "C1", "C2"]) - .color_array(vec![ - NamedColor::Blue, - NamedColor::Blue, - NamedColor::Blue, - NamedColor::Blue, - NamedColor::Blue, - NamedColor::Blue, - ]), - ) - .link( - Link::new() - .value(vec![8, 4, 2, 8, 4, 2]) - .source(vec![0, 1, 0, 2, 3, 3]) - .target(vec![2, 3, 3, 4, 4, 5]), - ); +// Table Charts +// ANCHOR: table_chart +fn table_chart(show: bool, file_name: &str) { + let trace = Table::new( + Header::new(vec![String::from("col1"), String::from("col2")]) + .font(TableFont::new().color_array(vec![NamedColor::Black, NamedColor::Blue])) + .align_array(vec![TableAlign::Left, TableAlign::Right]), + Cells::new(vec![vec![1, 2], vec![2, 3]]) + .align_matrix(vec![ + vec![TableAlign::Left, TableAlign::Right], + vec![TableAlign::Right, TableAlign::Left], + ]) + .fill(TableFill::new().color_matrix(vec![ + vec![NamedColor::LightBlue, NamedColor::LightCoral], + vec![NamedColor::LightGreen, NamedColor::LightYellow], + ])) + .line( + TableLine::new() + .color_matrix(vec![ + vec![NamedColor::Black, NamedColor::Blue], + vec![NamedColor::Green, NamedColor::Yellow], + ]) + .width_matrix(vec![vec![2.5, 3.3], vec![4.5, 5.3]]), + ), + ); + let mut plot = Plot::new(); + plot.add_trace(trace); - let layout = Layout::new() - .title("Basic Sankey") - .font(Font::new().size(10)); + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: table_chart +// Pie Charts +// ANCHOR: basic_pie_chart +fn basic_pie_chart(show: bool, file_name: &str) { + let values = vec![2, 3, 4]; + let labels = vec!["giraffes", "orangutans", "monkeys"]; + let t = Pie::new(values).labels(labels); let mut plot = Plot::new(); - plot.add_trace(trace); - plot.set_layout(layout); + plot.add_trace(t); let path = write_example_to_html(&plot, file_name); if show { plot.show_html(path); } } -// ANCHOR_END: basic_sankey_diagram - -// ANCHOR: custom_node_sankey_diagram -fn custom_node_sankey_diagram(show: bool, file_name: &str) { - // https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram - let trace = Sankey::new() - .orientation(Orientation::Horizontal) - .arrangement(plotly::sankey::Arrangement::Snap) - .node( - Node::new() - .pad(15) - .thickness(30) - .line(SankeyLine::new().color(NamedColor::Black).width(0.5)) - .label(vec!["A", "B", "C", "D", "E", "F"]) - .x(vec![0.2, 0.1, 0.5, 0.7, 0.3, 0.5]) - .y(vec![0.2, 0.1, 0.5, 0.7, 0.3, 0.5]) - .pad(20), - ) - .link( - Link::new() - .source(vec![0, 0, 1, 2, 5, 4, 3, 5]) - .target(vec![5, 3, 4, 3, 0, 2, 2, 3]) - .value(vec![1, 2, 1, 1, 1, 1, 1, 2]), - ); - - let layout = Layout::new() - .title("Define Node Position") - .font(Font::new().size(10)); - - let mut plot = Plot::new(); - plot.add_trace(trace); - plot.set_layout(layout); - - let path = write_example_to_html(&plot, file_name); - if show { - plot.show_html(path); - } -} -// ANCHOR_END: custom_node_sankey_diagram - -// Parallel Categories -// ANCHOR: basic_parcats -fn basic_parcats(show: bool, file_name: &str) { - let trace = Parcats::new() - .name("survey responses") - .dimensions(vec![ - ParcatsDimension::new() - .label("Region") - .values(vec!["North", "South", "North", "East", "West", "South"]), - ParcatsDimension::new() - .label("Product") - .values(vec!["A", "B", "A", "C", "B", "A"]), - ParcatsDimension::new().label("Channel").values(vec![ - "Online", "Retail", "Online", "Retail", "Online", "Retail", - ]), - ]) - .arrangement(ParcatsArrangement::Perpendicular); - - let layout = Layout::new().title("Basic Parallel Categories"); - let mut plot = Plot::new(); - plot.set_layout(layout); - plot.add_trace(trace); - - let path = write_example_to_html(&plot, file_name); - if show { - plot.show_html(path); - } -} -// ANCHOR_END: basic_parcats - -// ANCHOR: styled_parcats -fn styled_parcats(show: bool, file_name: &str) { - let trace = Parcats::new() - .name("passengers") - .dimensions(vec![ - ParcatsDimension::new() - .label("Sex") - .values(vec!["M", "F", "F", "M", "F", "M"]), - ParcatsDimension::new() - .label("Class") - .values(vec!["First", "Second", "Third", "Third", "First", "Second"]), - ParcatsDimension::new() - .label("Survived") - .values(vec!["yes", "no", "yes", "no", "yes", "no"]), - ]) - .counts_array(vec![1.0, 2.0, 1.0, 3.0, 1.0, 2.0]) - .line( - ParcatsLine::new() - .color(NamedColor::SteelBlue) - .shape(ParcatsLineShape::Hspline) - .show_scale(true), - ) - .arrangement(ParcatsArrangement::Perpendicular) - .bundle_colors(true) - .hover_on(ParcatsHoverOn::Category) - .hover_info(ParcatsHoverInfo::CountAndProbability) - .domain(Domain::new()); - - let layout = Layout::new().title("Styled Parallel Categories"); - let mut plot = Plot::new(); - plot.set_layout(layout); - plot.add_trace(trace); - - let path = write_example_to_html(&plot, file_name); - if show { - plot.show_html(path); - } -} -// ANCHOR_END: styled_parcats - -// ANCHOR: table_chart -fn table_chart(show: bool, file_name: &str) { - let trace = Table::new( - Header::new(vec![String::from("col1"), String::from("col2")]) - .font(TableFont::new().color_array(vec![NamedColor::Black, NamedColor::Blue])) - .align_array(vec![TableAlign::Left, TableAlign::Right]), - Cells::new(vec![vec![1, 2], vec![2, 3]]) - .align_matrix(vec![ - vec![TableAlign::Left, TableAlign::Right], - vec![TableAlign::Right, TableAlign::Left], - ]) - .fill(TableFill::new().color_matrix(vec![ - vec![NamedColor::LightBlue, NamedColor::LightCoral], - vec![NamedColor::LightGreen, NamedColor::LightYellow], - ])) - .line( - TableLine::new() - .color_matrix(vec![ - vec![NamedColor::Black, NamedColor::Blue], - vec![NamedColor::Green, NamedColor::Yellow], - ]) - .width_matrix(vec![vec![2.5, 3.3], vec![4.5, 5.3]]), - ), - ); - let mut plot = Plot::new(); - plot.add_trace(trace); - - let path = write_example_to_html(&plot, file_name); - if show { - plot.show_html(path); - } -} -// ANCHOR_END: table_chart - -// Pie Charts -// ANCHOR: basic_pie_chart -fn basic_pie_chart(show: bool, file_name: &str) { - let values = vec![2, 3, 4]; - let labels = vec!["giraffes", "orangutans", "monkeys"]; - let t = Pie::new(values).labels(labels); - let mut plot = Plot::new(); - plot.add_trace(t); - - let path = write_example_to_html(&plot, file_name); - if show { - plot.show_html(path); - } -} -// ANCHOR_END: basic_pie_chart +// ANCHOR_END: basic_pie_chart // ANCHOR: basic_pie_chart_labels fn basic_pie_chart_labels(show: bool, file_name: &str) { @@ -1128,6 +1090,7 @@ fn grouped_donout_pie_charts(show: bool, file_name: &str) { } // ANCHOR_END: grouped_donout_pie_charts +// Treemap Charts // ANCHOR: basic_treemap fn basic_treemap(show: bool, file_name: &str) { let labels = vec![ @@ -1189,6 +1152,7 @@ fn styled_treemap(show: bool, file_name: &str) { } // ANCHOR_END: styled_treemap +// Sunburst Charts // ANCHOR: basic_sunburst fn basic_sunburst(show: bool, file_name: &str) { let labels = vec![ @@ -1293,6 +1257,157 @@ fn styled_icicle(show: bool, file_name: &str) { } // ANCHOR_END: styled_icicle +// Sankey Diagrams +// ANCHOR: basic_sankey_diagram +fn basic_sankey_diagram(show: bool, file_name: &str) { + // https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram + let trace = Sankey::new() + .orientation(Orientation::Horizontal) + .node( + Node::new() + .pad(15) + .thickness(30) + .line(SankeyLine::new().color(NamedColor::Black).width(0.5)) + .label(vec!["A1", "A2", "B1", "B2", "C1", "C2"]) + .color_array(vec![ + NamedColor::Blue, + NamedColor::Blue, + NamedColor::Blue, + NamedColor::Blue, + NamedColor::Blue, + NamedColor::Blue, + ]), + ) + .link( + Link::new() + .value(vec![8, 4, 2, 8, 4, 2]) + .source(vec![0, 1, 0, 2, 3, 3]) + .target(vec![2, 3, 3, 4, 4, 5]), + ); + + let layout = Layout::new() + .title("Basic Sankey") + .font(Font::new().size(10)); + + let mut plot = Plot::new(); + plot.add_trace(trace); + plot.set_layout(layout); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: basic_sankey_diagram + +// ANCHOR: custom_node_sankey_diagram +fn custom_node_sankey_diagram(show: bool, file_name: &str) { + // https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram + let trace = Sankey::new() + .orientation(Orientation::Horizontal) + .arrangement(plotly::sankey::Arrangement::Snap) + .node( + Node::new() + .pad(15) + .thickness(30) + .line(SankeyLine::new().color(NamedColor::Black).width(0.5)) + .label(vec!["A", "B", "C", "D", "E", "F"]) + .x(vec![0.2, 0.1, 0.5, 0.7, 0.3, 0.5]) + .y(vec![0.2, 0.1, 0.5, 0.7, 0.3, 0.5]) + .pad(20), + ) + .link( + Link::new() + .source(vec![0, 0, 1, 2, 5, 4, 3, 5]) + .target(vec![5, 3, 4, 3, 0, 2, 2, 3]) + .value(vec![1, 2, 1, 1, 1, 1, 1, 2]), + ); + + let layout = Layout::new() + .title("Define Node Position") + .font(Font::new().size(10)); + + let mut plot = Plot::new(); + plot.add_trace(trace); + plot.set_layout(layout); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: custom_node_sankey_diagram + +// Parallel Categories +// ANCHOR: basic_parcats +fn basic_parcats(show: bool, file_name: &str) { + let trace = Parcats::new() + .name("survey responses") + .dimensions(vec![ + ParcatsDimension::new() + .label("Region") + .values(vec!["North", "South", "North", "East", "West", "South"]), + ParcatsDimension::new() + .label("Product") + .values(vec!["A", "B", "A", "C", "B", "A"]), + ParcatsDimension::new().label("Channel").values(vec![ + "Online", "Retail", "Online", "Retail", "Online", "Retail", + ]), + ]) + .arrangement(ParcatsArrangement::Perpendicular); + + let layout = Layout::new().title("Basic Parallel Categories"); + let mut plot = Plot::new(); + plot.set_layout(layout); + plot.add_trace(trace); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: basic_parcats + +// ANCHOR: styled_parcats +fn styled_parcats(show: bool, file_name: &str) { + let trace = Parcats::new() + .name("passengers") + .dimensions(vec![ + ParcatsDimension::new() + .label("Sex") + .values(vec!["M", "F", "F", "M", "F", "M"]), + ParcatsDimension::new() + .label("Class") + .values(vec!["First", "Second", "Third", "Third", "First", "Second"]), + ParcatsDimension::new() + .label("Survived") + .values(vec!["yes", "no", "yes", "no", "yes", "no"]), + ]) + .counts_array(vec![1.0, 2.0, 1.0, 3.0, 1.0, 2.0]) + .line( + ParcatsLine::new() + .color(NamedColor::SteelBlue) + .shape(ParcatsLineShape::Hspline) + .show_scale(true), + ) + .arrangement(ParcatsArrangement::Perpendicular) + .bundle_colors(true) + .hover_on(ParcatsHoverOn::Category) + .hover_info(ParcatsHoverInfo::CountAndProbability) + .domain(Domain::new()); + + let layout = Layout::new().title("Styled Parallel Categories"); + let mut plot = Plot::new(); + plot.set_layout(layout); + plot.add_trace(trace); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: styled_parcats + // Indicator Charts // ANCHOR: basic_indicator fn basic_indicator(show: bool, file_name: &str) { @@ -1361,119 +1476,6 @@ fn indicator_gauge(show: bool, file_name: &str) { } // ANCHOR_END: indicator_gauge -// ANCHOR: set_lower_or_upper_bound_on_axis -fn set_lower_or_upper_bound_on_axis(show: bool, file_name: &str) { - use std::fs::File; - use std::io::BufReader; - - // Read the iris dataset - let file = File::open("assets/iris.csv").expect("Failed to open iris.csv"); - let reader = BufReader::new(file); - let mut csv_reader = csv::Reader::from_reader(reader); - - // Parse the data - let mut sepal_width = Vec::new(); - let mut sepal_length = Vec::new(); - let mut species = Vec::new(); - - for result in csv_reader.records() { - let record = result.expect("Failed to read CSV record"); - sepal_width.push(record[1].parse::().unwrap()); - sepal_length.push(record[0].parse::().unwrap()); - species.push(record[4].to_string()); - } - - // Create separate traces for each species - let mut traces = Vec::new(); - let unique_species: Vec = species - .iter() - .cloned() - .collect::>() - .into_iter() - .collect(); - - for (i, species_name) in unique_species.iter().enumerate() { - let mut x = Vec::new(); - let mut y = Vec::new(); - - for (j, s) in species.iter().enumerate() { - if s == species_name { - x.push(sepal_width[j]); - y.push(sepal_length[j]); - } - } - - let trace = Scatter::new(x, y) - .name(species_name) - .mode(plotly::common::Mode::Markers) - .x_axis(format!("x{}", i + 1)) - .y_axis(format!("y{}", i + 1)); - traces.push(trace); - } - - let mut plot = Plot::new(); - for trace in traces { - plot.add_trace(trace); - } - - // Create layout with subplots - let mut layout = Layout::new() - .title("Iris Dataset - Subplots by Species") - .grid( - LayoutGrid::new() - .rows(1) - .columns(3) - .pattern(plotly::layout::GridPattern::Independent), - ); - - // Set x-axis range for all subplots: [None, 4.5] - layout = layout - .x_axis( - Axis::new() - .title("sepal_width") - // Can be set using a vec! of two optional values - .range(vec![None, Some(4.5)]), - ) - .x_axis2( - Axis::new() - .title("sepal_width") - // Or can be set using AxisRange::upper(4.5) - .range(AxisRange::upper(4.5)), - ) - .x_axis3( - Axis::new() - .title("sepal_width") - // Or can be set using AxisRange::upper(4.5) - .range(AxisRange::upper(4.5)), - ); - - // Set y-axis range for all subplots: [3, None] - layout = layout - .y_axis( - Axis::new() - .title("sepal_length") - .range(vec![Some(3.0), None]), - ) - .y_axis2( - Axis::new() - .title("sepal_length") - .range(vec![Some(3.0), None]), - ) - .y_axis3( - Axis::new() - .title("sepal_length") - .range(vec![Some(3.0), None]), - ); - - plot.set_layout(layout); - - let path = write_example_to_html(&plot, file_name); - if show { - plot.show_html(path); - } -} -// ANCHOR_END: set_lower_or_upper_bound_on_axis - fn main() { // Change false to true on any of these lines to display the example. @@ -1497,29 +1499,20 @@ fn main() { line_shape_options_for_interpolation(false, "line_shape_options_for_interpolation"); line_dash(false, "line_dash"); filled_lines(false, "filled_lines"); + set_lower_or_upper_bound_on_axis(false, "set_lower_or_upper_bound_on_axis"); // Bar Charts basic_bar_chart(false, "basic_bar_chart"); grouped_bar_chart(false, "grouped_bar_chart"); stacked_bar_chart(false, "stacked_bar_chart"); - table_chart(false, "table_chart"); category_order_bar_chart(false, "category_order_bar_chart"); - bar_chart_with_pattern_fills(false, "bar_chart_with_pattern_fills"); - - // Sankey Diagrams - basic_sankey_diagram(false, "basic_sankey_diagram"); - custom_node_sankey_diagram(false, "custom_node_sankey_diagram"); - - // Parallel Categories - basic_parcats(false, "basic_parcats"); - styled_parcats(false, "styled_parcats"); + table_chart(false, "table_chart"); // Pie Charts basic_pie_chart(false, "basic_pie_chart"); basic_pie_chart_labels(false, "basic_pie_chart_labels"); pie_chart_text_control(false, "pie_chart_text_control"); - grouped_donout_pie_charts(false, "grouped_donout_pie_charts"); // Treemap Charts @@ -1534,10 +1527,15 @@ fn main() { basic_icicle(false, "basic_icicle"); styled_icicle(false, "styled_icicle"); + // Sankey Diagrams + basic_sankey_diagram(false, "basic_sankey_diagram"); + custom_node_sankey_diagram(false, "custom_node_sankey_diagram"); + + // Parallel Categories + basic_parcats(false, "basic_parcats"); + styled_parcats(false, "styled_parcats"); + // Indicator Charts basic_indicator(false, "basic_indicator"); indicator_gauge(false, "indicator_gauge"); - - // Set Lower or Upper Bound on Axis - set_lower_or_upper_bound_on_axis(false, "set_lower_or_upper_bound_on_axis"); } diff --git a/examples/financial_charts/src/main.rs b/examples/financial_charts/src/main.rs index 0b48806a..2e25c92d 100644 --- a/examples/financial_charts/src/main.rs +++ b/examples/financial_charts/src/main.rs @@ -4,9 +4,12 @@ use std::env; use std::path::PathBuf; use chrono::{DateTime, Duration}; -use plotly::common::TickFormatStop; +use plotly::color::NamedColor; +use plotly::common::{Marker, TickFormatStop}; +use plotly::funnel::Connector as FunnelConnector; use plotly::layout::{Axis, RangeSelector, RangeSlider, SelectorButton, SelectorStep, StepMode}; -use plotly::{Candlestick, Layout, Ohlc, Plot, Scatter}; +use plotly::waterfall::{Marker as WaterfallMarker, Measure, MeasureStyle}; +use plotly::{Candlestick, Funnel, Layout, Ohlc, Plot, Scatter, Waterfall}; use plotly_utils::write_example_to_html; use serde::Deserialize; @@ -321,6 +324,59 @@ fn simple_ohlc_chart(show: bool, file_name: &str) { } // ANCHOR_END: simple_ohlc_chart +// Waterfall Charts +// ANCHOR: basic_waterfall +fn basic_waterfall(show: bool, file_name: &str) { + let trace = Waterfall::new( + vec!["Start", "Revenue", "Costs", "End"], + vec![100.0, 40.0, -25.0, 0.0], + ) + .measure(vec![ + Measure::Absolute, + Measure::Relative, + Measure::Relative, + Measure::Total, + ]) + .text_info("label+delta") + .increasing(MeasureStyle::new().marker(WaterfallMarker::new().color(NamedColor::SeaGreen))) + .decreasing(MeasureStyle::new().marker(WaterfallMarker::new().color(NamedColor::Tomato))) + .totals(MeasureStyle::new().marker(WaterfallMarker::new().color(NamedColor::SteelBlue))); + + let layout = Layout::new().title("Basic Waterfall"); + let mut plot = Plot::new(); + plot.set_layout(layout); + plot.add_trace(trace); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: basic_waterfall + +// Funnel Charts +// ANCHOR: basic_funnel +fn basic_funnel(show: bool, file_name: &str) { + let trace = Funnel::new( + vec![100, 60, 40, 25], + vec!["Visits", "Signups", "Trials", "Purchases"], + ) + .text_info("value+percent previous") + .connector(FunnelConnector::new().visible(true)) + .marker(Marker::new().color(NamedColor::SteelBlue)); + + let layout = Layout::new().title("Conversion Funnel"); + let mut plot = Plot::new(); + plot.set_layout(layout); + plot.add_trace(trace); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: basic_funnel + // ANCHOR: series_with_gaps_for_weekends_and_holidays fn series_with_gaps_for_weekends_and_holidays(show: bool, file_name: &str) { let data = load_apple_data(); @@ -504,6 +560,12 @@ fn main() { // OHLC Charts simple_ohlc_chart(false, "simple_ohlc_chart"); + // Waterfall Charts + basic_waterfall(false, "basic_waterfall"); + + // Funnel Charts + basic_funnel(false, "basic_funnel"); + // Rangebreaks usage series_with_gaps_for_weekends_and_holidays(false, "series_with_gaps_for_weekends_and_holidays"); hiding_weekends_and_holidays_with_rangebreaks( diff --git a/examples/maps/src/main.rs b/examples/maps/src/main.rs index 0b514f84..82e15cbe 100644 --- a/examples/maps/src/main.rs +++ b/examples/maps/src/main.rs @@ -13,6 +13,8 @@ use plotly::{ }; use plotly_utils::{write_example_to_html, write_example_to_html_with_inline_config}; +// Scatter Maps +// ANCHOR: scatter_mapbox fn scatter_mapbox(show: bool, file_name: &str) { let trace = ScatterMapbox::new(vec![45.5017], vec![-73.5673]) .marker(Marker::new().size(25).opacity(0.9)); @@ -34,8 +36,10 @@ fn scatter_mapbox(show: bool, file_name: &str) { plot.show_html(path); } } +// ANCHOR_END: scatter_mapbox /// Reproduce the Earth from https://plotly.com/javascript/lines-on-maps/#lines-on-an-orthographic-map +// ANCHOR: scatter_geo fn scatter_geo(show: bool, file_name: &str) { use csv; use reqwest; @@ -161,7 +165,10 @@ fn scatter_geo(show: bool, file_name: &str) { plot.show_html(path); } } +// ANCHOR_END: scatter_geo +// Density Maps +// ANCHOR: density_mapbox fn density_mapbox(show: bool, file_name: &str) { let trace = DensityMapbox::new(vec![45.5017], vec![-73.5673], vec![0.75]).zauto(true); @@ -182,6 +189,7 @@ fn density_mapbox(show: bool, file_name: &str) { plot.show_html(path); } } +// ANCHOR_END: density_mapbox /// Classic choropleth on the `geo` subplot, coloring countries by value using /// ISO-3 country codes. @@ -263,9 +271,15 @@ fn choropleth_map(show: bool, file_name: &str) { fn main() { // Change false to true on any of these lines to display the example. + + // Scatter Maps scatter_mapbox(false, "scatter_mapbox"); scatter_geo(false, "scatter_geo"); + + // Density Maps density_mapbox(false, "density_mapbox"); + + // Choropleth Maps choropleth(false, "choropleth"); choropleth_map(false, "choropleth_map"); } diff --git a/plotly/src/common/mod.rs b/plotly/src/common/mod.rs index 6739612d..5b0e8375 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, Histogram2d, @@ -242,6 +243,7 @@ pub enum PlotType { Treemap, Sunburst, Violin, + Waterfall, } #[derive(Serialize, Clone, Debug)] diff --git a/plotly/src/layout/mod.rs b/plotly/src/layout/mod.rs index 027e3075..b46c9f65 100644 --- a/plotly/src/layout/mod.rs +++ b/plotly/src/layout/mod.rs @@ -45,7 +45,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, @@ -386,6 +387,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 cd278dfd..d40a09d0 100644 --- a/plotly/src/lib.rs +++ b/plotly/src/lib.rs @@ -60,16 +60,16 @@ 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, histogram2d, - icicle, image, indicator, mesh3d, parcats, sankey, scatter, scatter3d, scatter_map, - scatter_mapbox, splom, sunburst, surface, treemap, violin, + box_plot, choropleth, choropleth_map, contour, density_map, funnel, heat_map, histogram, + histogram2d, icicle, image, indicator, mesh3d, parcats, sankey, scatter, scatter3d, + scatter_map, scatter_mapbox, splom, sunburst, surface, treemap, violin, waterfall, }; // Bring the different trace types into the top-level scope pub use traces::{ Bar, BoxPlot, Candlestick, Choropleth, ChoroplethMap, Contour, DensityMap, DensityMapbox, - HeatMap, Histogram, Histogram2d, Icicle, Image, Indicator, Mesh3D, Ohlc, Parcats, Pie, Sankey, - Scatter, Scatter3D, ScatterGeo, ScatterMap, ScatterMapbox, ScatterPolar, Splom, Sunburst, - Surface, Table, Treemap, Violin, + Funnel, HeatMap, Histogram, Histogram2d, Icicle, Image, Indicator, Mesh3D, Ohlc, Parcats, Pie, + Sankey, Scatter, Scatter3D, ScatterGeo, ScatterMap, ScatterMapbox, ScatterPolar, Splom, + Sunburst, Surface, Table, Treemap, Violin, Waterfall, }; pub trait Restyle: serde::Serialize {} 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