diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed9a42a..1d820f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - [[#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` - [[#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 on trace types to bring them to parity with plotly.js 3.7 +- [[#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) ### Changed diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 974fa378..27cd5a47 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -16,6 +16,7 @@ - [Bar Charts](./recipes/basic_charts/bar_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) @@ -26,6 +27,7 @@ - [Violin Plots](./recipes/statistical_charts/violin_plots.md) - [Histograms](./recipes/statistical_charts/histograms.md) - [2D Histograms](./recipes/statistical_charts/histogram2d.md) + - [SPLOM](./recipes/statistical_charts/splom.md) - [Scientific Charts](./recipes/scientific_charts.md) - [Contour Plots](./recipes/scientific_charts/contour_plots.md) - [Heatmaps](./recipes/scientific_charts/heatmaps.md) diff --git a/docs/book/src/recipes/basic_charts.md b/docs/book/src/recipes/basic_charts.md index bfcc9365..26e1e6ac 100644 --- a/docs/book/src/recipes/basic_charts.md +++ b/docs/book/src/recipes/basic_charts.md @@ -9,7 +9,8 @@ Line Charts | [![Line Charts](./img/line_shape_options_for_interpolation.png)](. Bar Charts | [![Bar Charts](./img/bar_chart_with_error_bars.png)](./basic_charts/scatter_plots.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) -Treemap Charts | [Treemap Charts](./basic_charts/treemap_charts.md) -Sunburst Charts | [Sunburst Charts](./basic_charts/sunburst_charts.md) -Icicle Charts | [Icicle Charts](./basic_charts/icicle_charts.md) -Indicator Charts | [Indicator Charts](./basic_charts/indicator_charts.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) +Indicator Charts | [![Indicator Charts](./img/indicator.png)](./basic_charts/indicator_charts.md) diff --git a/docs/book/src/recipes/basic_charts/parcats_charts.md b/docs/book/src/recipes/basic_charts/parcats_charts.md new file mode 100644 index 00000000..8b9cffa3 --- /dev/null +++ b/docs/book/src/recipes/basic_charts/parcats_charts.md @@ -0,0 +1,38 @@ +# Parallel Categories + +A parallel categories (`parcats`) chart visualizes multi-dimensional categorical +data as a set of parallel axes, one per category column, with ribbons flowing +between the categories. Like Sankey diagrams, it is domain-based rather than +cartesian — each [`ParcatsDimension`](https://docs.rs/plotly/latest/plotly/parcats/struct.ParcatsDimension.html) +defines one axis, and optional `counts` let you weight aggregated paths instead +of plotting one row per observation. + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::color::NamedColor; +use plotly::common::Domain; +use plotly::layout::Layout; +use plotly::parcats::{ + ParcatsArrangement, ParcatsDimension, ParcatsHoverInfo, ParcatsHoverOn, ParcatsLine, + ParcatsLineShape, +}; +use plotly::{Parcats, Plot}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + +## Basic Parallel Categories +```rust,no_run +{{#include ../../../../../examples/basic_charts/src/main.rs:basic_parcats}} +``` + +{{#include ../../../../../examples/basic_charts/output/inline_basic_parcats.html}} + + +## Styled Parallel Categories +```rust,no_run +{{#include ../../../../../examples/basic_charts/src/main.rs:styled_parcats}} +``` + +{{#include ../../../../../examples/basic_charts/output/inline_styled_parcats.html}} diff --git a/docs/book/src/recipes/img/icicle.png b/docs/book/src/recipes/img/icicle.png new file mode 100644 index 00000000..8ee14210 Binary files /dev/null and b/docs/book/src/recipes/img/icicle.png differ diff --git a/docs/book/src/recipes/img/indicator.png b/docs/book/src/recipes/img/indicator.png new file mode 100644 index 00000000..7e1f5d9d Binary files /dev/null and b/docs/book/src/recipes/img/indicator.png differ diff --git a/docs/book/src/recipes/img/parallelcat.png b/docs/book/src/recipes/img/parallelcat.png new file mode 100644 index 00000000..5b9f2aa3 Binary files /dev/null and b/docs/book/src/recipes/img/parallelcat.png differ diff --git a/docs/book/src/recipes/img/splom.png b/docs/book/src/recipes/img/splom.png new file mode 100644 index 00000000..5c89daf0 Binary files /dev/null and b/docs/book/src/recipes/img/splom.png differ diff --git a/docs/book/src/recipes/img/sunburst.png b/docs/book/src/recipes/img/sunburst.png new file mode 100644 index 00000000..c4ec27b6 Binary files /dev/null and b/docs/book/src/recipes/img/sunburst.png differ diff --git a/docs/book/src/recipes/img/treemap.png b/docs/book/src/recipes/img/treemap.png new file mode 100644 index 00000000..7b3b853b Binary files /dev/null and b/docs/book/src/recipes/img/treemap.png differ diff --git a/docs/book/src/recipes/statistical_charts.md b/docs/book/src/recipes/statistical_charts.md index 7a210865..2000329b 100644 --- a/docs/book/src/recipes/statistical_charts.md +++ b/docs/book/src/recipes/statistical_charts.md @@ -9,3 +9,4 @@ Box Plots | [![Line Charts](./img/box_plot.png)](./statistical_charts/box_plots. Violin Plots | [![Violin Plots](./img/violin_plot.png)](./statistical_charts/violin_plots.md) Histograms | [![Scatter Plots](./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/docs/book/src/recipes/statistical_charts/splom.md b/docs/book/src/recipes/statistical_charts/splom.md new file mode 100644 index 00000000..bc79767f --- /dev/null +++ b/docs/book/src/recipes/statistical_charts/splom.md @@ -0,0 +1,34 @@ +# SPLOM + +A SPLOM (scatter plot matrix) visualizes multivariate data as a grid of scatter +subplots — one cell for each pair of dimensions. Each column of your dataset +becomes a [`SplomDimension`](https://docs.rs/plotly/latest/plotly/splom/struct.SplomDimension.html), +and Plotly.js lays out the axis grid automatically, so you do not assign explicit +`xaxis`/`yaxis` ids as you would for cartesian traces. + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::color::NamedColor; +use plotly::common::Marker; +use plotly::layout::Layout; +use plotly::splom::{SplomDiagonal, SplomDimension}; +use plotly::{Plot, Splom}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + +## Basic SPLOM +```rust,no_run +{{#include ../../../../../examples/statistical_charts/src/main.rs:basic_splom}} +``` + +{{#include ../../../../../examples/statistical_charts/output/inline_basic_splom.html}} + + +## Styled SPLOM +```rust,no_run +{{#include ../../../../../examples/statistical_charts/src/main.rs:styled_splom}} +``` + +{{#include ../../../../../examples/statistical_charts/output/inline_styled_splom.html}} diff --git a/examples/basic_charts/src/main.rs b/examples/basic_charts/src/main.rs index 9af85696..a8e2ce0d 100644 --- a/examples/basic_charts/src/main.rs +++ b/examples/basic_charts/src/main.rs @@ -16,6 +16,10 @@ use plotly::{ LayoutPolar, Legend, PolarAxisAttributes, PolarAxisTicks, PolarDirection, RadialAxis, TicksDirection, TraceOrder, }, + parcats::{ + ParcatsArrangement, ParcatsDimension, ParcatsHoverInfo, ParcatsHoverOn, ParcatsLine, + ParcatsLineShape, + }, sankey::{Line as SankeyLine, Link, Node}, sunburst::{InsideTextOrientation, Leaf}, traces::indicator::{ @@ -26,7 +30,8 @@ use plotly::{ Align as TableAlign, Cells, Fill as TableFill, Font as TableFont, Header, Line as TableLine, }, treemap::{BranchValues, Marker as TreemapMarker, Packing, PathBar, Side, Tiling}, - Bar, Icicle, Indicator, Pie, Plot, Sankey, Scatter, ScatterPolar, Sunburst, Table, Treemap, + Bar, Icicle, Indicator, Parcats, Pie, Plot, Sankey, Scatter, ScatterPolar, Sunburst, Table, + Treemap, }; use plotly_utils::write_example_to_html; use rand_distr::{Distribution, Normal, Uniform}; @@ -887,6 +892,76 @@ fn custom_node_sankey_diagram(show: bool, file_name: &str) { } // 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( @@ -1436,6 +1511,10 @@ fn main() { 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"); + // Pie Charts basic_pie_chart(false, "basic_pie_chart"); basic_pie_chart_labels(false, "basic_pie_chart_labels"); diff --git a/examples/statistical_charts/src/main.rs b/examples/statistical_charts/src/main.rs index deb9425a..b81c8a8f 100644 --- a/examples/statistical_charts/src/main.rs +++ b/examples/statistical_charts/src/main.rs @@ -9,8 +9,9 @@ use plotly::{ }, histogram::{Bins, Cumulative, HistFunc, HistNorm}, layout::{Axis, BarMode, BoxMode, Layout, Margin, ViolinMode}, + splom::{SplomDiagonal, SplomDimension}, violin::{MeanLine, ViolinBox, ViolinPoints, ViolinSide}, - Bar, BoxPlot, Histogram, Histogram2d, Plot, Scatter, Violin, + Bar, BoxPlot, Histogram, Histogram2d, Plot, Scatter, Splom, Violin, }; use plotly_utils::write_example_to_html; use rand_distr::{Distribution, Normal, Uniform}; @@ -868,6 +869,118 @@ fn histogram2d_aggregation(show: bool, file_name: &str) { } // ANCHOR_END: histogram2d_aggregation +// SPLOM +fn splom_axis() -> Axis { + Axis::new() + .show_line(false) + .zero_line(false) + .grid_color("#ffffff") + .tick_length(4) +} + +fn splom_layout(title: &str, dimensions: usize) -> Layout { + let axis = splom_axis; + let mut layout = Layout::new() + .title(title) + .height(800) + .width(800) + .auto_size(false) + .x_axis(axis()) + .y_axis(axis()); + + if dimensions >= 2 { + layout = layout.x_axis2(axis()).y_axis2(axis()); + } + if dimensions >= 3 { + layout = layout.x_axis3(axis()).y_axis3(axis()); + } + if dimensions >= 4 { + layout = layout.x_axis4(axis()).y_axis4(axis()); + } + if dimensions >= 5 { + layout = layout.x_axis5(axis()).y_axis5(axis()); + } + if dimensions >= 6 { + layout = layout.x_axis6(axis()).y_axis6(axis()); + } + if dimensions >= 7 { + layout = layout.x_axis7(axis()).y_axis7(axis()); + } + if dimensions >= 8 { + layout = layout.x_axis8(axis()).y_axis8(axis()); + } + + layout +} + +// ANCHOR: basic_splom +fn basic_splom(show: bool, file_name: &str) { + let n = 200; + let sepal_length = sample_normal_distribution(n, 5.8, 0.8); + let sepal_width = sample_normal_distribution(n, 3.0, 0.4); + let petal_length = sample_normal_distribution(n, 3.8, 1.8); + let petal_width = sample_normal_distribution(n, 1.2, 0.8); + + let trace = Splom::new().name("flower measurements").dimensions(vec![ + SplomDimension::new() + .label("Sepal Length") + .values(sepal_length), + SplomDimension::new() + .label("Sepal Width") + .values(sepal_width), + SplomDimension::new() + .label("Petal Length") + .values(petal_length), + SplomDimension::new() + .label("Petal Width") + .values(petal_width), + ]); + + let mut plot = Plot::new(); + plot.set_layout(splom_layout("Flower Measurements", 4)); + plot.add_trace(trace); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: basic_splom + +// ANCHOR: styled_splom +fn styled_splom(show: bool, file_name: &str) { + let n = 150; + let x = sample_uniform_distribution(n, 0.0, 10.0); + let y = sample_uniform_distribution(n, 0.0, 10.0); + let z = sample_normal_distribution(n, 5.0, 2.0); + + let trace = Splom::new() + .name("styled splom") + .dimensions(vec![ + SplomDimension::new().label("X").values(x), + SplomDimension::new().label("Y").values(y), + SplomDimension::new().label("Z").values(z), + ]) + .diagonal(SplomDiagonal::new().visible(false)) + .show_upper_half(false) + .marker( + Marker::new() + .size(4) + .opacity(0.6) + .color(NamedColor::SteelBlue), + ); + + let mut plot = Plot::new(); + plot.set_layout(splom_layout("Styled SPLOM", 3)); + plot.add_trace(trace); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: styled_splom + fn main() { // Change false to true on any of these lines to display the example. @@ -925,4 +1038,8 @@ fn main() { basic_histogram2d(false, "basic_histogram2d"); styled_histogram2d(false, "styled_histogram2d"); histogram2d_aggregation(false, "histogram2d_aggregation"); + + // SPLOM + basic_splom(false, "basic_splom"); + styled_splom(false, "styled_splom"); } diff --git a/plotly/src/common/mod.rs b/plotly/src/common/mod.rs index b02792fc..6739612d 100644 --- a/plotly/src/common/mod.rs +++ b/plotly/src/common/mod.rs @@ -231,7 +231,9 @@ pub enum PlotType { Indicator, Mesh3D, Ohlc, + Parcats, Sankey, + Splom, Surface, DensityMapbox, DensityMap, diff --git a/plotly/src/lib.rs b/plotly/src/lib.rs index abe8e813..cd278dfd 100644 --- a/plotly/src/lib.rs +++ b/plotly/src/lib.rs @@ -61,15 +61,15 @@ 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, sankey, scatter, scatter3d, scatter_map, scatter_mapbox, - sunburst, surface, treemap, violin, + icicle, image, indicator, mesh3d, parcats, sankey, scatter, scatter3d, scatter_map, + scatter_mapbox, splom, 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, Histogram2d, Icicle, Image, Indicator, Mesh3D, Ohlc, Pie, Sankey, Scatter, - Scatter3D, ScatterGeo, ScatterMap, ScatterMapbox, ScatterPolar, Sunburst, Surface, Table, - Treemap, Violin, + HeatMap, Histogram, Histogram2d, Icicle, Image, Indicator, Mesh3D, Ohlc, Parcats, Pie, Sankey, + Scatter, Scatter3D, ScatterGeo, ScatterMap, ScatterMapbox, ScatterPolar, Splom, Sunburst, + Surface, Table, Treemap, Violin, }; pub trait Restyle: serde::Serialize {} diff --git a/plotly/src/traces/mod.rs b/plotly/src/traces/mod.rs index 79e7dca4..45def695 100644 --- a/plotly/src/traces/mod.rs +++ b/plotly/src/traces/mod.rs @@ -16,6 +16,7 @@ pub mod image; pub mod indicator; pub mod mesh3d; mod ohlc; +pub mod parcats; pub mod pie; pub mod sankey; pub mod scatter; @@ -24,6 +25,7 @@ pub mod scatter_geo; pub mod scatter_map; pub mod scatter_mapbox; mod scatter_polar; +pub mod splom; pub mod sunburst; pub mod surface; pub mod table; @@ -45,6 +47,7 @@ pub use icicle::Icicle; pub use indicator::Indicator; pub use mesh3d::Mesh3D; pub use ohlc::Ohlc; +pub use parcats::Parcats; pub use pie::Pie; pub use sankey::Sankey; pub use scatter::Scatter; @@ -53,6 +56,7 @@ pub use scatter_geo::ScatterGeo; pub use scatter_map::ScatterMap; pub use scatter_mapbox::ScatterMapbox; pub use scatter_polar::ScatterPolar; +pub use splom::Splom; pub use sunburst::Sunburst; pub use surface::Surface; pub use table::Table; diff --git a/plotly/src/traces/parcats.rs b/plotly/src/traces/parcats.rs new file mode 100644 index 00000000..78158e87 --- /dev/null +++ b/plotly/src/traces/parcats.rs @@ -0,0 +1,335 @@ +//! Parallel categories (parcats) trace + +use plotly_derive::FieldSetter; +use serde::Serialize; + +use crate::{ + color::Color, + common::{ColorScale, Dim, Domain, PlotType}, + layout::CategoryOrder, + Trace, +}; + +/// Sets the shape of the paths connecting the categories. +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ParcatsLineShape { + Linear, + Hspline, +} + +/// Sets the drag interaction mode for the categories and dimensions. +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ParcatsArrangement { + Perpendicular, + Freeform, + Fixed, +} + +/// Sets the hover interaction mode for the parcats diagram. +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ParcatsHoverOn { + Category, + Color, + Dimension, +} + +/// Sets the path sorting algorithm. +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ParcatsSortPaths { + Forward, + Backward, +} + +/// Determines which trace information appears on hover for a parcats trace. +/// +/// Unlike the cartesian [`HoverInfo`](crate::common::HoverInfo), parcats hover +/// info is limited to `count`/`probability` (plus `all`/`none`/`skip`). +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ParcatsHoverInfo { + Count, + Probability, + #[serde(rename = "count+probability")] + CountAndProbability, + All, + None, + Skip, +} + +/// A single dimension (column) of a [`Parcats`] trace. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug, Clone, FieldSetter)] +pub struct ParcatsDimension +where + V: Serialize + Clone, +{ + /// The shown name of the dimension. + label: Option, + /// Sets the category values, one per data point. + values: Option>, + /// Specifies the ordering logic for the categories in the dimension. + #[serde(rename = "categoryorder")] + category_order: Option, + /// Sets the order in which categories in this dimension appear, only used + /// if `category_order` is set to "array". + #[serde(rename = "categoryarray")] + category_array: Option>, + /// Sets alternative tick labels for the categories in this dimension. + ticktext: Option>, + /// The display index of the dimension, from left to right, zero indexed, + /// defaults to dimension index. + #[serde(rename = "displayindex")] + display_index: Option, + /// Determines whether or not this dimension is visible. + visible: Option, +} + +impl ParcatsDimension +where + V: Serialize + Clone, +{ + pub fn new() -> Self { + Default::default() + } +} + +/// Styles the paths (lines) connecting the categories of a [`Parcats`] trace. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug, Clone, FieldSetter)] +pub struct ParcatsLine { + /// Sets the line color. It accepts either a specific color or an array of + /// numbers that are mapped to the colorscale. + color: Option>>, + /// Sets the colorscale used to map the `color` values to colors. + #[serde(rename = "colorscale")] + color_scale: Option, + /// Sets the shape of the paths. + shape: Option, + /// Sets the lower bound of the color domain. + cmin: Option, + /// Sets the upper bound of the color domain. + cmax: Option, + /// Sets the mid-point of the color domain by scaling `cmin` and/or `cmax`. + cmid: Option, + /// Determines whether or not a colorbar is displayed for this trace. + #[serde(rename = "showscale")] + show_scale: Option, +} + +impl ParcatsLine { + pub fn new() -> Self { + Default::default() + } +} + +/// Construct a parallel categories (parcats) trace. +/// +/// A `Parcats` trace visualizes multi-dimensional categorical data as a set of +/// parallel axes, one per [`ParcatsDimension`], with ribbons flowing between +/// the categories. It is domain-based (like Sankey), so it does not share the +/// cartesian subplot grid. +/// +/// # Examples +/// +/// ``` +/// use plotly::Parcats; +/// use plotly::parcats::{ParcatsArrangement, ParcatsDimension}; +/// +/// let trace = Parcats::new() +/// .dimensions(vec![ +/// ParcatsDimension::new().label("A").values(vec!["x", "y", "x"]), +/// ParcatsDimension::new().label("B").values(vec!["p", "q", "p"]), +/// ]) +/// .counts_array(vec![1.0, 2.0, 3.0]) +/// .arrangement(ParcatsArrangement::Perpendicular); +/// +/// let expected = serde_json::json!({ +/// "type": "parcats", +/// "dimensions": [ +/// {"label": "A", "values": ["x", "y", "x"]}, +/// {"label": "B", "values": ["p", "q", "p"]} +/// ], +/// "counts": [1.0, 2.0, 3.0], +/// "arrangement": "perpendicular" +/// }); +/// +/// 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 Parcats +where + V: Serialize + Clone, +{ + #[field_setter(default = "PlotType::Parcats")] + r#type: PlotType, + /// Sets the trace name. The trace name appears as the legend item and on + /// hover. + name: Option, + /// The dimensions (columns) of the parallel-categories diagram. + dimensions: Option>>, + /// Styles the paths connecting the categories. + line: Option, + /// The number of observations represented by each state. Defaults to 1 so + /// that each state represents one observation. This lets callers pass + /// aggregated per-path weights instead of one row per record. + counts: Option>, + /// Sets the drag interaction mode for categories and dimensions. + arrangement: Option, + /// Sort paths so that like colors are bundled together within each + /// category. + #[serde(rename = "bundlecolors")] + bundle_colors: Option, + /// Sets the hover interaction mode for the parcats diagram. + #[serde(rename = "hoveron")] + hover_on: Option, + /// Sets the path sorting algorithm. + #[serde(rename = "sortpaths")] + sort_paths: Option, + /// Template string used for rendering the information that appear on hover + /// box. + // NOTE: parcats also accepts `line.hovertemplate`; trace-level placement is + // the more general form and is used here. + #[serde(rename = "hovertemplate")] + hover_template: Option, + /// Determines which trace information appears on hover. Limited to + /// `count`/`probability` (plus `all`/`none`/`skip`). + #[serde(rename = "hoverinfo")] + hover_info: Option, + /// Sets the domain within which this parcats trace is drawn. + domain: Option, +} + +impl Parcats +where + V: Serialize + Clone, +{ + /// Creates a new empty parcats trace. + pub fn new() -> Box { + Box::default() + } +} + +impl Trace for Parcats +where + V: Serialize + Clone, +{ + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +#[cfg(test)] +mod tests { + use serde_json::{json, to_value}; + + use super::*; + use crate::color::NamedColor; + + #[test] + fn serialize_default_parcats() { + let trace = Parcats::::default(); + let expected = json!({"type": "parcats"}).to_string(); + + assert_eq!(trace.to_json(), expected); + } + + #[test] + fn serialize_basic_parcats_trace() { + let trace = Parcats::new() + .name("parcats") + .dimensions(vec![ + ParcatsDimension::new() + .label("Sex") + .values(vec!["M", "F", "F"]), + ParcatsDimension::new() + .label("Survived") + .values(vec!["yes", "no", "yes"]), + ]) + .counts_array(vec![1.0, 2.0, 3.0]) + .arrangement(ParcatsArrangement::Perpendicular) + .bundle_colors(true) + .hover_on(ParcatsHoverOn::Category) + .sort_paths(ParcatsSortPaths::Forward); + + let expected = json!({ + "type": "parcats", + "name": "parcats", + "dimensions": [ + {"label": "Sex", "values": ["M", "F", "F"]}, + {"label": "Survived", "values": ["yes", "no", "yes"]} + ], + "counts": [1.0, 2.0, 3.0], + "arrangement": "perpendicular", + "bundlecolors": true, + "hoveron": "category", + "sortpaths": "forward" + }); + + assert_eq!(to_value(trace).unwrap(), expected); + } + + #[test] + fn serialize_parcats_line() { + let line = ParcatsLine::new() + .color(NamedColor::Blue) + .shape(ParcatsLineShape::Hspline) + .cmin(0.0) + .cmax(1.0) + .show_scale(true); + let expected = json!({ + "color": "blue", + "shape": "hspline", + "cmin": 0.0, + "cmax": 1.0, + "showscale": true + }); + + assert_eq!(to_value(line).unwrap(), expected); + } + + #[test] + fn serialize_parcats_hover_info() { + assert_eq!(to_value(ParcatsHoverInfo::Count).unwrap(), json!("count")); + assert_eq!( + to_value(ParcatsHoverInfo::Probability).unwrap(), + json!("probability") + ); + assert_eq!( + to_value(ParcatsHoverInfo::CountAndProbability).unwrap(), + json!("count+probability") + ); + assert_eq!(to_value(ParcatsHoverInfo::Skip).unwrap(), json!("skip")); + + let trace = Parcats::::new().hover_info(ParcatsHoverInfo::Count); + assert_eq!(to_value(trace).unwrap()["hoverinfo"], json!("count")); + } + + #[test] + fn serialize_parcats_dimension() { + let dim = ParcatsDimension::new() + .label("A") + .values(vec![0, 1, 0]) + .category_order(CategoryOrder::CategoryAscending) + .category_array(vec![0, 1]) + .ticktext(vec!["zero", "one"]) + .display_index(2) + .visible(true); + let expected = json!({ + "label": "A", + "values": [0, 1, 0], + "categoryorder": "category ascending", + "categoryarray": [0, 1], + "ticktext": ["zero", "one"], + "displayindex": 2, + "visible": true + }); + + assert_eq!(to_value(dim).unwrap(), expected); + } +} diff --git a/plotly/src/traces/splom.rs b/plotly/src/traces/splom.rs new file mode 100644 index 00000000..a762c7f3 --- /dev/null +++ b/plotly/src/traces/splom.rs @@ -0,0 +1,237 @@ +//! Scatter-plot matrix (SPLOM) trace + +use plotly_derive::FieldSetter; +use serde::Serialize; + +use crate::{ + common::{Dim, HoverInfo, LegendGroupTitle, Marker, PlotType, Visible}, + layout::AxisType, + Trace, +}; + +/// Per-dimension axis settings for a [`Splom`] dimension. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug, Clone, FieldSetter)] +pub struct SplomAxis { + /// Sets the axis type for this dimension's row/column. + #[serde(rename = "type")] + r#type: Option, + /// Determines whether or not the x & y axes generated by this dimension + /// match their corresponding axes on the other dimensions. + matches: Option, +} + +impl SplomAxis { + pub fn new() -> Self { + Default::default() + } +} + +/// A single dimension (column) of a [`Splom`] trace. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug, Clone, FieldSetter)] +pub struct SplomDimension +where + V: Serialize + Clone, +{ + /// Sets the label corresponding to this splom dimension. + label: Option, + /// Sets the dimension values to be plotted. + values: Option>, + /// Determines whether or not this dimension is shown on the graph. Note + /// that even visible false dimensions contribute to the default grid + /// generation. + visible: Option, + /// Per-dimension axis settings. + axis: Option, +} + +impl SplomDimension +where + V: Serialize + Clone, +{ + pub fn new() -> Self { + Default::default() + } +} + +/// Controls the rendering of the SPLOM diagonal (where a dimension is plotted +/// against itself). +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug, Clone, FieldSetter)] +pub struct SplomDiagonal { + /// Determines whether or not subplots on the diagonal are displayed. + visible: Option, +} + +impl SplomDiagonal { + pub fn new() -> Self { + Default::default() + } +} + +/// Construct a scatter-plot matrix (SPLOM) trace. +/// +/// A `Splom` renders an N×N grid of scatter subplots, plotting every pair of +/// the supplied [`SplomDimension`]s against each other. Plotly.js generates the +/// axis grid automatically, so — unlike cartesian traces — you do not assign +/// explicit `xaxis`/`yaxis` ids. +/// +/// # Examples +/// +/// ``` +/// use plotly::Splom; +/// use plotly::splom::SplomDimension; +/// +/// let trace = Splom::new().dimensions(vec![ +/// SplomDimension::new().label("A").values(vec![1, 2, 3]), +/// SplomDimension::new().label("B").values(vec![4, 5, 6]), +/// ]); +/// +/// let expected = serde_json::json!({ +/// "type": "splom", +/// "dimensions": [ +/// {"label": "A", "values": [1, 2, 3]}, +/// {"label": "B", "values": [4, 5, 6]} +/// ] +/// }); +/// +/// 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 Splom +where + V: Serialize + Clone, +{ + #[field_setter(default = "PlotType::Splom")] + r#type: PlotType, + /// Sets the trace name. The trace name appears as the legend item and on + /// hover. + name: Option, + /// Determines whether or not this trace is visible. If "legendonly", the + /// trace is not drawn, but can appear as a legend item. + visible: Option, + /// Determines whether or not an item corresponding to this trace is shown + /// in the legend. + #[serde(rename = "showlegend")] + show_legend: Option, + /// Sets the legend group for this trace. Traces part of the same legend + /// group show/hide at the same time when toggling legend items. + #[serde(rename = "legendgroup")] + legend_group: Option, + /// Set and style the title to appear for the legend group. + #[serde(rename = "legendgrouptitle")] + legend_group_title: Option, + /// Sets the opacity of the trace. + opacity: Option, + /// The dimensions (columns) plotted against each other in the matrix. + dimensions: Option>>, + /// Controls the rendering of the subplots on the diagonal. + diagonal: Option, + /// Determines whether or not subplots on the upper half of the matrix are + /// displayed. + #[serde(rename = "showupperhalf")] + show_upper_half: Option, + /// Determines whether or not subplots on the lower half of the matrix are + /// displayed. + #[serde(rename = "showlowerhalf")] + show_lower_half: Option, + /// Sets the marker style for the scatter points. + marker: Option, + /// Sets the list of x axes corresponding to dimensions of this splom trace. + /// Usually left unset so Plotly.js auto-lays-out the grid. + xaxes: Option>, + /// Sets the list of y axes corresponding to dimensions of this splom trace. + /// Usually left unset so Plotly.js auto-lays-out the grid. + yaxes: Option>, + /// Sets text elements associated with each (x,y) pair to appear on hover. + text: Option>, + /// Determines which trace information appears on hover. + #[serde(rename = "hoverinfo")] + hover_info: Option, + /// Template string used for rendering the information that appear on hover + /// box. + #[serde(rename = "hovertemplate")] + hover_template: Option>, +} + +impl Splom +where + V: Serialize + Clone, +{ + /// Creates a new empty SPLOM trace. + pub fn new() -> Box { + Box::default() + } +} + +impl Trace for Splom +where + V: Serialize + Clone, +{ + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +#[cfg(test)] +mod tests { + use serde_json::{json, to_value}; + + use super::*; + + #[test] + fn serialize_default_splom() { + let trace = Splom::::default(); + let expected = json!({"type": "splom"}).to_string(); + + assert_eq!(trace.to_json(), expected); + } + + #[test] + fn serialize_basic_splom_trace() { + let trace = Splom::new() + .name("splom") + .dimensions(vec![ + SplomDimension::new().label("A").values(vec![1.0, 2.0, 3.0]), + SplomDimension::new().label("B").values(vec![4.0, 5.0, 6.0]), + ]) + .diagonal(SplomDiagonal::new().visible(false)) + .show_upper_half(false) + .marker(Marker::new().size(4).opacity(0.6)); + + let expected = json!({ + "type": "splom", + "name": "splom", + "dimensions": [ + {"label": "A", "values": [1.0, 2.0, 3.0]}, + {"label": "B", "values": [4.0, 5.0, 6.0]} + ], + "diagonal": {"visible": false}, + "showupperhalf": false, + "marker": {"size": 4, "opacity": 0.6} + }); + + assert_eq!(to_value(trace).unwrap(), expected); + } + + #[test] + fn serialize_splom_dimension_axis() { + let dim = SplomDimension::new() + .label("A") + .values(vec![1, 2, 3]) + .visible(true) + .axis(SplomAxis::new().r#type(AxisType::Log).matches(true)); + + let expected = json!({ + "label": "A", + "values": [1, 2, 3], + "visible": true, + "axis": {"type": "log", "matches": true} + }); + + assert_eq!(to_value(dim).unwrap(), expected); + } +}