Skip to content

Commit bc97fc7

Browse files
jqnatividadclaude
andcommitted
feat(traces): add Splom + Parcats trace types
Add typed scatter-plot-matrix (splom) and parallel-categories (parcats) traces following the established trace-struct recipe. - Splom<V>: SplomDimension/SplomAxis/SplomDiagonal sub-structs, showupperhalf/showlowerhalf controls, reuses common Marker. - Parcats<V>: ParcatsDimension/ParcatsLine sub-structs, counts weighting, and ParcatsLineShape/ParcatsArrangement/ParcatsHoverOn/ParcatsSortPaths enums. arrangement includes the runtime-supported "fixed" value. - Register PlotType::{Splom, Parcats} (serialize to "splom"/"parcats"), re-export modules + traces from traces/mod.rs and lib.rs. - All attributes verified against bundled plotly.js 3.7.0 schema. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9722338 commit bc97fc7

6 files changed

Lines changed: 548 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
1414
- [[#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
1515
- [[#417](https://github.com/plotly/plotly.rs/issues/417)] Add `ScatterMap` (MapLibre `map` subplot) trace type — the modern counterpart to `ScatterMapbox`
1616
- [[#418](https://github.com/plotly/plotly.rs/issues/418)] Add native point clustering to `ScatterMap` via a `Cluster` option
17+
- Add `Splom` trace type (scatter-plot matrix) with `SplomDimension`/`SplomAxis`/`SplomDiagonal` config and `showupperhalf`/`showlowerhalf` controls
18+
- Add `Parcats` trace type (parallel categories) with `ParcatsDimension`/`ParcatsLine` config and `ParcatsLineShape`/`ParcatsArrangement`/`ParcatsHoverOn`/`ParcatsSortPaths` enums
1719

1820
### Changed
1921

plotly/src/common/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,9 @@ pub enum PlotType {
228228
Image,
229229
Mesh3D,
230230
Ohlc,
231+
Parcats,
231232
Sankey,
233+
Splom,
232234
Surface,
233235
DensityMapbox,
234236
DensityMap,

plotly/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@ pub use plot::{Plot, Trace, Traces};
6161
// Also provide easy access to modules which contain additional trace-specific types
6262
pub use traces::{
6363
box_plot, choropleth, choropleth_map, contour, density_map, heat_map, histogram, image, mesh3d,
64-
sankey, scatter, scatter3d, scatter_map, scatter_mapbox, sunburst, surface, treemap, violin,
64+
parcats, sankey, scatter, scatter3d, scatter_map, scatter_mapbox, splom, sunburst, surface,
65+
treemap, violin,
6566
};
6667
// Bring the different trace types into the top-level scope
6768
pub use traces::{
6869
Bar, BoxPlot, Candlestick, Choropleth, ChoroplethMap, Contour, DensityMap, DensityMapbox,
69-
HeatMap, Histogram, Image, Mesh3D, Ohlc, Pie, Sankey, Scatter, Scatter3D, ScatterGeo,
70-
ScatterMap, ScatterMapbox, ScatterPolar, Sunburst, Surface, Table, Treemap, Violin,
70+
HeatMap, Histogram, Image, Mesh3D, Ohlc, Parcats, Pie, Sankey, Scatter, Scatter3D, ScatterGeo,
71+
ScatterMap, ScatterMapbox, ScatterPolar, Splom, Sunburst, Surface, Table, Treemap, Violin,
7172
};
7273

7374
pub trait Restyle: serde::Serialize {}

plotly/src/traces/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod histogram;
1313
pub mod image;
1414
pub mod mesh3d;
1515
mod ohlc;
16+
pub mod parcats;
1617
pub mod pie;
1718
pub mod sankey;
1819
pub mod scatter;
@@ -21,6 +22,7 @@ pub mod scatter_geo;
2122
pub mod scatter_map;
2223
pub mod scatter_mapbox;
2324
mod scatter_polar;
25+
pub mod splom;
2426
pub mod sunburst;
2527
pub mod surface;
2628
pub mod table;
@@ -39,6 +41,7 @@ pub use heat_map::HeatMap;
3941
pub use histogram::Histogram;
4042
pub use mesh3d::Mesh3D;
4143
pub use ohlc::Ohlc;
44+
pub use parcats::Parcats;
4245
pub use pie::Pie;
4346
pub use sankey::Sankey;
4447
pub use scatter::Scatter;
@@ -47,6 +50,7 @@ pub use scatter_geo::ScatterGeo;
4750
pub use scatter_map::ScatterMap;
4851
pub use scatter_mapbox::ScatterMapbox;
4952
pub use scatter_polar::ScatterPolar;
53+
pub use splom::Splom;
5054
pub use sunburst::Sunburst;
5155
pub use surface::Surface;
5256
pub use table::Table;

plotly/src/traces/parcats.rs

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
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

Comments
 (0)