diff --git a/CHANGELOG.md b/CHANGELOG.md index b37b671f..d7dfba59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ### Added +- [[#412](https://github.com/plotly/plotly.rs/pull/412)] Add `Violin` trace type with box, mean line, KDE span, and split/grouped support - [[#406](https://github.com/plotly/plotly.rs/issues/406)] Expose `plotly.js` 3.1–3.6 attributes ### Changed diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 8a9b7e7c..927a1393 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -21,6 +21,7 @@ - [Statistical Charts](./recipes/statistical_charts.md) - [Error Bars](./recipes/statistical_charts/error_bars.md) - [Box Plots](./recipes/statistical_charts/box_plots.md) + - [Violin Plots](./recipes/statistical_charts/violin_plots.md) - [Histograms](./recipes/statistical_charts/histograms.md) - [Scientific Charts](./recipes/scientific_charts.md) - [Contour Plots](./recipes/scientific_charts/contour_plots.md) diff --git a/docs/book/src/recipes/img/violin_plot.png b/docs/book/src/recipes/img/violin_plot.png new file mode 100644 index 00000000..d6c679ab Binary files /dev/null and b/docs/book/src/recipes/img/violin_plot.png differ diff --git a/docs/book/src/recipes/statistical_charts.md b/docs/book/src/recipes/statistical_charts.md index 4f446d1d..136aa7bc 100644 --- a/docs/book/src/recipes/statistical_charts.md +++ b/docs/book/src/recipes/statistical_charts.md @@ -6,4 +6,5 @@ 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) +Violin Plots | [![Violin Plots](./img/violin_plot.png)](./statistical_charts/violin_plots.md) Histograms | [![Scatter Plots](./img/overlaid_histogram.png)](./statistical_charts/histograms.md) diff --git a/docs/book/src/recipes/statistical_charts/violin_plots.md b/docs/book/src/recipes/statistical_charts/violin_plots.md new file mode 100644 index 00000000..739886ad --- /dev/null +++ b/docs/book/src/recipes/statistical_charts/violin_plots.md @@ -0,0 +1,36 @@ +# Violin Plots + +The following imports have been used to produce the plots below: + +```rust,no_run +use plotly::common::{Line, Orientation}; +use plotly::layout::{Layout, ViolinMode}; +use plotly::violin::{MeanLine, ViolinBox, ViolinPoints, ViolinSide}; +use plotly::{color::NamedColor, Plot, Violin}; +``` + +The `to_inline_html` method is used to produce the html plot displayed in this page. + + +## Basic Violin Plot +```rust,no_run +{{#include ../../../../../examples/statistical_charts/src/main.rs:basic_violin_plot}} +``` + +{{#include ../../../../../examples/statistical_charts/output/inline_basic_violin_plot.html}} + + +## Horizontal Violin Plot +```rust,no_run +{{#include ../../../../../examples/statistical_charts/src/main.rs:horizontal_violin_plot}} +``` + +{{#include ../../../../../examples/statistical_charts/output/inline_horizontal_violin_plot.html}} + + +## Split Violin Plot +```rust,no_run +{{#include ../../../../../examples/statistical_charts/src/main.rs:split_violin_plot}} +``` + +{{#include ../../../../../examples/statistical_charts/output/inline_split_violin_plot.html}} diff --git a/examples/statistical_charts/src/main.rs b/examples/statistical_charts/src/main.rs index f4947cc7..2b4d2da9 100644 --- a/examples/statistical_charts/src/main.rs +++ b/examples/statistical_charts/src/main.rs @@ -6,8 +6,9 @@ use plotly::{ color::{NamedColor, Rgb, Rgba}, common::{ErrorData, ErrorType, Line, Marker, Mode, Orientation}, histogram::{Bins, Cumulative, HistFunc, HistNorm}, - layout::{Axis, BarMode, BoxMode, Layout, Margin}, - Bar, BoxPlot, Histogram, Plot, Scatter, + layout::{Axis, BarMode, BoxMode, Layout, Margin, ViolinMode}, + violin::{MeanLine, ViolinBox, ViolinPoints, ViolinSide}, + Bar, BoxPlot, Histogram, Plot, Scatter, Violin, }; use plotly_utils::write_example_to_html; use rand_distr::{Distribution, Normal, Uniform}; @@ -477,6 +478,98 @@ fn fully_styled_box_plot(show: bool, file_name: &str) { } // ANCHOR_END: fully_styled_box_plot +// Violin Plots +// ANCHOR: basic_violin_plot +fn basic_violin_plot(show: bool, file_name: &str) { + let y = vec![ + 0.2, 0.2, 0.6, 1.0, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3, 0.8, 0.4, 0.6, + ]; + + let trace = Violin::new(y) + .box_plot(ViolinBox::new().visible(true)) + .mean_line(MeanLine::new().visible(true)) + .name("Total"); + + let layout = Layout::new().title("Basic Violin Plot"); + + 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_violin_plot + +// ANCHOR: horizontal_violin_plot +fn horizontal_violin_plot(show: bool, file_name: &str) { + let x = vec![1.4, 2.1, 1.9, 3.2, 2.7, 2.2, 1.8, 2.5, 3.1, 2.0, 2.6, 1.7]; + + let trace = Violin::::default() + .x(x) + .points(ViolinPoints::All) + .box_plot(ViolinBox::new().visible(true)) + .mean_line(MeanLine::new().visible(true)) + .orientation(Orientation::Horizontal) + .name("Score"); + + let layout = Layout::new().title("Horizontal Violin Plot"); + + 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: horizontal_violin_plot + +// ANCHOR: split_violin_plot +fn split_violin_plot(show: bool, file_name: &str) { + let x = vec![ + "Mon", "Mon", "Mon", "Mon", "Tue", "Tue", "Tue", "Tue", "Wed", "Wed", "Wed", "Wed", + ]; + + let trace1 = Violin::new_xy( + x.clone(), + vec![0.6, 0.9, 0.4, 0.7, 0.8, 1.1, 0.6, 0.9, 1.0, 1.3, 0.8, 1.1], + ) + .legend_group("Yes") + .scale_group("Yes") + .name("Yes") + .side(ViolinSide::Negative) + .line(Line::new().color(NamedColor::Blue)); + + let trace2 = Violin::new_xy( + x, + vec![0.4, 0.7, 0.3, 0.5, 0.6, 0.9, 0.4, 0.7, 0.8, 1.1, 0.6, 0.9], + ) + .legend_group("No") + .scale_group("No") + .name("No") + .side(ViolinSide::Positive) + .line(Line::new().color(NamedColor::Green)); + + let layout = Layout::new() + .title("Split Violin Plot") + .violin_mode(ViolinMode::Overlay); + + let mut plot = Plot::new(); + plot.set_layout(layout); + plot.add_trace(trace1); + plot.add_trace(trace2); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} +// ANCHOR_END: split_violin_plot + // Histograms fn sample_normal_distribution(n: usize, mean: f64, std_dev: f64) -> Vec { let mut rng = rand::rng(); @@ -729,6 +822,11 @@ fn main() { grouped_horizontal_box_plot(false, "grouped_horizontal_box_plot"); fully_styled_box_plot(false, "fully_styled_box_plot"); + // Violin Plots + basic_violin_plot(false, "basic_violin_plot"); + horizontal_violin_plot(false, "horizontal_violin_plot"); + split_violin_plot(false, "split_violin_plot"); + // Histograms basic_histogram(false, "basic_histogram"); horizontal_histogram(false, "horizontal_histogram"); diff --git a/plotly/src/common/mod.rs b/plotly/src/common/mod.rs index 9ab8786f..2e719e6f 100644 --- a/plotly/src/common/mod.rs +++ b/plotly/src/common/mod.rs @@ -232,6 +232,7 @@ pub enum PlotType { Pie, Treemap, Sunburst, + Violin, } #[derive(Serialize, Clone, Debug)] diff --git a/plotly/src/lib.rs b/plotly/src/lib.rs index 1340271b..348db5eb 100644 --- a/plotly/src/lib.rs +++ b/plotly/src/lib.rs @@ -61,13 +61,13 @@ pub use plot::{Plot, Trace, Traces}; // Also provide easy access to modules which contain additional trace-specific types pub use traces::{ box_plot, contour, heat_map, histogram, image, mesh3d, sankey, scatter, scatter3d, - scatter_mapbox, sunburst, surface, treemap, + scatter_mapbox, sunburst, surface, treemap, violin, }; // Bring the different trace types into the top-level scope pub use traces::{ Bar, BoxPlot, Candlestick, Contour, DensityMapbox, HeatMap, Histogram, Image, Mesh3D, Ohlc, Pie, Sankey, Scatter, Scatter3D, ScatterGeo, ScatterMapbox, ScatterPolar, Sunburst, Surface, - Table, Treemap, + Table, Treemap, Violin, }; pub trait Restyle: serde::Serialize {} diff --git a/plotly/src/traces/mod.rs b/plotly/src/traces/mod.rs index adc2efb4..6a8f9158 100644 --- a/plotly/src/traces/mod.rs +++ b/plotly/src/traces/mod.rs @@ -21,6 +21,7 @@ pub mod sunburst; pub mod surface; pub mod table; pub mod treemap; +pub mod violin; pub use bar::Bar; pub use box_plot::BoxPlot; @@ -42,5 +43,6 @@ pub use sunburst::Sunburst; pub use surface::Surface; pub use table::Table; pub use treemap::Treemap; +pub use violin::Violin; pub use self::image::Image; diff --git a/plotly/src/traces/violin.rs b/plotly/src/traces/violin.rs new file mode 100644 index 00000000..f5575b4b --- /dev/null +++ b/plotly/src/traces/violin.rs @@ -0,0 +1,502 @@ +//! Violin trace + +use plotly_derive::FieldSetter; +use serde::{Serialize, Serializer}; + +// Re-use the box plot's quartile method, whose values (linear/exclusive/inclusive) +// are identical to the violin trace's `quartilemethod` attribute. +pub use super::box_plot::QuartileMethod; +use crate::{ + color::Color, + common::{ + Dim, HoverInfo, Label, LegendGroupTitle, Line, Marker, Orientation, PlotType, Visible, + XAxisId, YAxisId, + }, + private::{NumOrString, NumOrStringCollection}, + Trace, +}; + +/// Determines which sample points are shown alongside the violin(s). +#[derive(Debug, Clone)] +pub enum ViolinPoints { + All, + Outliers, + SuspectedOutliers, + False, +} + +impl Serialize for ViolinPoints { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Self::All => serializer.serialize_str("all"), + Self::Outliers => serializer.serialize_str("outliers"), + Self::SuspectedOutliers => serializer.serialize_str("suspectedoutliers"), + Self::False => serializer.serialize_bool(false), + } + } +} + +/// Sets the metric by which the width of each violin is determined. +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ScaleMode { + Width, + Count, +} + +/// Sets the method by which the span in data space (where the density function +/// is computed) is determined. +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum SpanMode { + Soft, + Hard, + Manual, +} + +/// Determines on which side of the position value the density function making +/// up one half of a violin is plotted. +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ViolinSide { + Both, + Positive, + Negative, +} + +/// Determines what the hover interactions highlight. +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum HoverOn { + Violins, + Points, + Kde, + #[serde(rename = "violins+points")] + ViolinsAndPoints, + #[serde(rename = "violins+points+kde")] + ViolinsPointsAndKde, + All, +} + +/// A miniature box plot drawn inside the violins. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct ViolinBox { + visible: Option, + width: Option, + #[serde(rename = "fillcolor")] + fill_color: Option>, + line: Option, +} + +impl ViolinBox { + pub fn new() -> Self { + Default::default() + } +} + +/// A line corresponding to the sample's mean, drawn inside the violins. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct MeanLine { + visible: Option, + color: Option>, + width: Option, +} + +impl MeanLine { + pub fn new() -> Self { + Default::default() + } +} + +/// Marker styling for selected or unselected points. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct SelectionMarker { + color: Option>, + size: Option, + opacity: Option, +} + +impl SelectionMarker { + pub fn new() -> Self { + Default::default() + } +} + +/// Sets the styling of selected or unselected points. +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct Selection { + marker: Option, +} + +impl Selection { + pub fn new() -> Self { + Default::default() + } +} + +/// Construct a violin trace. +/// +/// # Examples +/// +/// ``` +/// use plotly::Violin; +/// +/// let trace = Violin::new(vec![0, 1, 2, 3, 4, 5]) +/// .box_plot(plotly::violin::ViolinBox::new().visible(true)) +/// .mean_line(plotly::violin::MeanLine::new().visible(true)); +/// +/// let expected = serde_json::json!({ +/// "type": "violin", +/// "y": [0, 1, 2, 3, 4, 5], +/// "box": {"visible": true}, +/// "meanline": {"visible": true} +/// }); +/// +/// 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 Violin +where + X: Serialize + Clone, + Y: Serialize + Clone, +{ + #[field_setter(default = "PlotType::Violin")] + r#type: PlotType, + x: Option>, + y: Option>, + x0: Option, + y0: 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>, + width: Option, + text: 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 = "xhoverformat")] + x_hover_format: Option, + #[serde(rename = "yhoverformat")] + y_hover_format: Option, + #[serde(rename = "xaxis")] + x_axis: Option, + #[serde(rename = "yaxis")] + y_axis: Option, + orientation: Option, + #[serde(rename = "alignmentgroup")] + alignment_group: Option, + #[serde(rename = "offsetgroup")] + offset_group: Option, + marker: Option, + line: Option, + #[serde(rename = "fillcolor")] + fill_color: Option>, + points: Option, + jitter: Option, + #[serde(rename = "pointpos")] + point_pos: Option, + selected: Option, + unselected: Option, + #[serde(rename = "hoverlabel")] + hover_label: Option