|
| 1 | +//! Parallel categories (parcats) trace |
| 2 | +
|
| 3 | +use plotly_derive::FieldSetter; |
| 4 | +use serde::Serialize; |
| 5 | + |
| 6 | +use crate::{ |
| 7 | + color::Color, |
| 8 | + common::{ColorScale, Dim, Domain, HoverInfo, PlotType}, |
| 9 | + layout::CategoryOrder, |
| 10 | + Trace, |
| 11 | +}; |
| 12 | + |
| 13 | +/// Sets the shape of the paths connecting the categories. |
| 14 | +#[derive(Serialize, Clone, Debug)] |
| 15 | +#[serde(rename_all = "lowercase")] |
| 16 | +pub enum ParcatsLineShape { |
| 17 | + Linear, |
| 18 | + Hspline, |
| 19 | +} |
| 20 | + |
| 21 | +/// Sets the drag interaction mode for the categories and dimensions. |
| 22 | +#[derive(Serialize, Clone, Debug)] |
| 23 | +#[serde(rename_all = "lowercase")] |
| 24 | +pub enum ParcatsArrangement { |
| 25 | + Perpendicular, |
| 26 | + Freeform, |
| 27 | + Fixed, |
| 28 | +} |
| 29 | + |
| 30 | +/// Sets the hover interaction mode for the parcats diagram. |
| 31 | +#[derive(Serialize, Clone, Debug)] |
| 32 | +#[serde(rename_all = "lowercase")] |
| 33 | +pub enum ParcatsHoverOn { |
| 34 | + Category, |
| 35 | + Color, |
| 36 | + Dimension, |
| 37 | +} |
| 38 | + |
| 39 | +/// Sets the path sorting algorithm. |
| 40 | +#[derive(Serialize, Clone, Debug)] |
| 41 | +#[serde(rename_all = "lowercase")] |
| 42 | +pub enum ParcatsSortPaths { |
| 43 | + Forward, |
| 44 | + Backward, |
| 45 | +} |
| 46 | + |
| 47 | +/// A single dimension (column) of a [`Parcats`] trace. |
| 48 | +#[serde_with::skip_serializing_none] |
| 49 | +#[derive(Serialize, Debug, Clone, FieldSetter)] |
| 50 | +pub struct ParcatsDimension<V> |
| 51 | +where |
| 52 | + V: Serialize + Clone, |
| 53 | +{ |
| 54 | + /// The shown name of the dimension. |
| 55 | + label: Option<String>, |
| 56 | + /// Sets the category values, one per data point. |
| 57 | + values: Option<Vec<V>>, |
| 58 | + /// Specifies the ordering logic for the categories in the dimension. |
| 59 | + #[serde(rename = "categoryorder")] |
| 60 | + category_order: Option<CategoryOrder>, |
| 61 | + /// Sets the order in which categories in this dimension appear, only used if |
| 62 | + /// `category_order` is set to "array". |
| 63 | + #[serde(rename = "categoryarray")] |
| 64 | + category_array: Option<Vec<V>>, |
| 65 | + /// Sets alternative tick labels for the categories in this dimension. |
| 66 | + ticktext: Option<Vec<String>>, |
| 67 | + /// The display index of the dimension, from left to right, zero indexed, |
| 68 | + /// defaults to dimension index. |
| 69 | + #[serde(rename = "displayindex")] |
| 70 | + display_index: Option<usize>, |
| 71 | + /// Determines whether or not this dimension is visible. |
| 72 | + visible: Option<bool>, |
| 73 | +} |
| 74 | + |
| 75 | +impl<V> ParcatsDimension<V> |
| 76 | +where |
| 77 | + V: Serialize + Clone, |
| 78 | +{ |
| 79 | + pub fn new() -> Self { |
| 80 | + Default::default() |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +/// Styles the paths (lines) connecting the categories of a [`Parcats`] trace. |
| 85 | +#[serde_with::skip_serializing_none] |
| 86 | +#[derive(Serialize, Debug, Clone, FieldSetter)] |
| 87 | +pub struct ParcatsLine { |
| 88 | + /// Sets the line color. It accepts either a specific color or an array of |
| 89 | + /// numbers that are mapped to the colorscale. |
| 90 | + color: Option<Dim<Box<dyn Color>>>, |
| 91 | + /// Sets the colorscale used to map the `color` values to colors. |
| 92 | + #[serde(rename = "colorscale")] |
| 93 | + color_scale: Option<ColorScale>, |
| 94 | + /// Sets the shape of the paths. |
| 95 | + shape: Option<ParcatsLineShape>, |
| 96 | + /// Sets the lower bound of the color domain. |
| 97 | + cmin: Option<f64>, |
| 98 | + /// Sets the upper bound of the color domain. |
| 99 | + cmax: Option<f64>, |
| 100 | + /// Sets the mid-point of the color domain by scaling `cmin` and/or `cmax`. |
| 101 | + cmid: Option<f64>, |
| 102 | + /// Determines whether or not a colorbar is displayed for this trace. |
| 103 | + #[serde(rename = "showscale")] |
| 104 | + show_scale: Option<bool>, |
| 105 | +} |
| 106 | + |
| 107 | +impl ParcatsLine { |
| 108 | + pub fn new() -> Self { |
| 109 | + Default::default() |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +/// Construct a parallel categories (parcats) trace. |
| 114 | +/// |
| 115 | +/// A `Parcats` trace visualizes multi-dimensional categorical data as a set of |
| 116 | +/// parallel axes, one per [`ParcatsDimension`], with ribbons flowing between |
| 117 | +/// the categories. It is domain-based (like Sankey), so it does not share the |
| 118 | +/// cartesian subplot grid. |
| 119 | +/// |
| 120 | +/// # Examples |
| 121 | +/// |
| 122 | +/// ``` |
| 123 | +/// use plotly::Parcats; |
| 124 | +/// use plotly::parcats::{ParcatsArrangement, ParcatsDimension}; |
| 125 | +/// |
| 126 | +/// let trace = Parcats::new() |
| 127 | +/// .dimensions(vec![ |
| 128 | +/// ParcatsDimension::new().label("A").values(vec!["x", "y", "x"]), |
| 129 | +/// ParcatsDimension::new().label("B").values(vec!["p", "q", "p"]), |
| 130 | +/// ]) |
| 131 | +/// .counts_array(vec![1.0, 2.0, 3.0]) |
| 132 | +/// .arrangement(ParcatsArrangement::Perpendicular); |
| 133 | +/// |
| 134 | +/// let expected = serde_json::json!({ |
| 135 | +/// "type": "parcats", |
| 136 | +/// "dimensions": [ |
| 137 | +/// {"label": "A", "values": ["x", "y", "x"]}, |
| 138 | +/// {"label": "B", "values": ["p", "q", "p"]} |
| 139 | +/// ], |
| 140 | +/// "counts": [1.0, 2.0, 3.0], |
| 141 | +/// "arrangement": "perpendicular" |
| 142 | +/// }); |
| 143 | +/// |
| 144 | +/// assert_eq!(serde_json::to_value(trace).unwrap(), expected); |
| 145 | +/// ``` |
| 146 | +#[serde_with::skip_serializing_none] |
| 147 | +#[derive(Serialize, Debug, Clone, FieldSetter)] |
| 148 | +#[field_setter(box_self, kind = "trace")] |
| 149 | +pub struct Parcats<V> |
| 150 | +where |
| 151 | + V: Serialize + Clone, |
| 152 | +{ |
| 153 | + #[field_setter(default = "PlotType::Parcats")] |
| 154 | + r#type: PlotType, |
| 155 | + /// Sets the trace name. The trace name appears as the legend item and on |
| 156 | + /// hover. |
| 157 | + name: Option<String>, |
| 158 | + /// The dimensions (columns) of the parallel-categories diagram. |
| 159 | + dimensions: Option<Vec<ParcatsDimension<V>>>, |
| 160 | + /// Styles the paths connecting the categories. |
| 161 | + line: Option<ParcatsLine>, |
| 162 | + /// The number of observations represented by each state. Defaults to 1 so |
| 163 | + /// that each state represents one observation. This lets callers pass |
| 164 | + /// aggregated per-path weights instead of one row per record. |
| 165 | + counts: Option<Dim<f64>>, |
| 166 | + /// Sets the drag interaction mode for categories and dimensions. |
| 167 | + arrangement: Option<ParcatsArrangement>, |
| 168 | + /// Sort paths so that like colors are bundled together within each category. |
| 169 | + #[serde(rename = "bundlecolors")] |
| 170 | + bundle_colors: Option<bool>, |
| 171 | + /// Sets the hover interaction mode for the parcats diagram. |
| 172 | + #[serde(rename = "hoveron")] |
| 173 | + hover_on: Option<ParcatsHoverOn>, |
| 174 | + /// Sets the path sorting algorithm. |
| 175 | + #[serde(rename = "sortpaths")] |
| 176 | + sort_paths: Option<ParcatsSortPaths>, |
| 177 | + /// Template string used for rendering the information that appear on hover |
| 178 | + /// box. |
| 179 | + // NOTE: parcats also accepts `line.hovertemplate`; trace-level placement is |
| 180 | + // the more general form and is used here. |
| 181 | + #[serde(rename = "hovertemplate")] |
| 182 | + hover_template: Option<String>, |
| 183 | + /// Determines which trace information appears on hover. |
| 184 | + #[serde(rename = "hoverinfo")] |
| 185 | + hover_info: Option<HoverInfo>, |
| 186 | + /// Sets the domain within which this parcats trace is drawn. |
| 187 | + domain: Option<Domain>, |
| 188 | +} |
| 189 | + |
| 190 | +impl<V> Parcats<V> |
| 191 | +where |
| 192 | + V: Serialize + Clone, |
| 193 | +{ |
| 194 | + /// Creates a new empty parcats trace. |
| 195 | + pub fn new() -> Box<Self> { |
| 196 | + Box::default() |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +impl<V> Trace for Parcats<V> |
| 201 | +where |
| 202 | + V: Serialize + Clone, |
| 203 | +{ |
| 204 | + fn to_json(&self) -> String { |
| 205 | + serde_json::to_string(self).unwrap() |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +#[cfg(test)] |
| 210 | +mod tests { |
| 211 | + use serde_json::{json, to_value}; |
| 212 | + |
| 213 | + use super::*; |
| 214 | + use crate::color::NamedColor; |
| 215 | + |
| 216 | + #[test] |
| 217 | + fn serialize_default_parcats() { |
| 218 | + let trace = Parcats::<i32>::default(); |
| 219 | + let expected = json!({"type": "parcats"}).to_string(); |
| 220 | + |
| 221 | + assert_eq!(trace.to_json(), expected); |
| 222 | + } |
| 223 | + |
| 224 | + #[test] |
| 225 | + fn serialize_basic_parcats_trace() { |
| 226 | + let trace = Parcats::new() |
| 227 | + .name("parcats") |
| 228 | + .dimensions(vec![ |
| 229 | + ParcatsDimension::new() |
| 230 | + .label("Sex") |
| 231 | + .values(vec!["M", "F", "F"]), |
| 232 | + ParcatsDimension::new() |
| 233 | + .label("Survived") |
| 234 | + .values(vec!["yes", "no", "yes"]), |
| 235 | + ]) |
| 236 | + .counts_array(vec![1.0, 2.0, 3.0]) |
| 237 | + .arrangement(ParcatsArrangement::Perpendicular) |
| 238 | + .bundle_colors(true) |
| 239 | + .hover_on(ParcatsHoverOn::Category) |
| 240 | + .sort_paths(ParcatsSortPaths::Forward); |
| 241 | + |
| 242 | + let expected = json!({ |
| 243 | + "type": "parcats", |
| 244 | + "name": "parcats", |
| 245 | + "dimensions": [ |
| 246 | + {"label": "Sex", "values": ["M", "F", "F"]}, |
| 247 | + {"label": "Survived", "values": ["yes", "no", "yes"]} |
| 248 | + ], |
| 249 | + "counts": [1.0, 2.0, 3.0], |
| 250 | + "arrangement": "perpendicular", |
| 251 | + "bundlecolors": true, |
| 252 | + "hoveron": "category", |
| 253 | + "sortpaths": "forward" |
| 254 | + }); |
| 255 | + |
| 256 | + assert_eq!(to_value(trace).unwrap(), expected); |
| 257 | + } |
| 258 | + |
| 259 | + #[test] |
| 260 | + fn serialize_parcats_line() { |
| 261 | + let line = ParcatsLine::new() |
| 262 | + .color(NamedColor::Blue) |
| 263 | + .shape(ParcatsLineShape::Hspline) |
| 264 | + .cmin(0.0) |
| 265 | + .cmax(1.0) |
| 266 | + .show_scale(true); |
| 267 | + let expected = json!({ |
| 268 | + "color": "blue", |
| 269 | + "shape": "hspline", |
| 270 | + "cmin": 0.0, |
| 271 | + "cmax": 1.0, |
| 272 | + "showscale": true |
| 273 | + }); |
| 274 | + |
| 275 | + assert_eq!(to_value(line).unwrap(), expected); |
| 276 | + } |
| 277 | + |
| 278 | + #[test] |
| 279 | + fn serialize_parcats_dimension() { |
| 280 | + let dim = ParcatsDimension::new() |
| 281 | + .label("A") |
| 282 | + .values(vec![0, 1, 0]) |
| 283 | + .category_order(CategoryOrder::CategoryAscending) |
| 284 | + .category_array(vec![0, 1]) |
| 285 | + .ticktext(vec!["zero", "one"]) |
| 286 | + .display_index(2) |
| 287 | + .visible(true); |
| 288 | + let expected = json!({ |
| 289 | + "label": "A", |
| 290 | + "values": [0, 1, 0], |
| 291 | + "categoryorder": "category ascending", |
| 292 | + "categoryarray": [0, 1], |
| 293 | + "ticktext": ["zero", "one"], |
| 294 | + "displayindex": 2, |
| 295 | + "visible": true |
| 296 | + }); |
| 297 | + |
| 298 | + assert_eq!(to_value(dim).unwrap(), expected); |
| 299 | + } |
| 300 | +} |
0 commit comments