plotly/traces/
sankey.rs

1//! Sankey trace
2
3use plotly_derive::FieldSetter;
4use serde::Serialize;
5
6use crate::{
7    color::Color,
8    common::{Dim, Domain, Font, HoverInfo, Label, LegendGroupTitle, Orientation, PlotType},
9    Trace,
10};
11
12#[derive(Serialize, Clone)]
13#[serde(rename_all = "lowercase")]
14pub enum Arrangement {
15    Snap,
16    Perpendicular,
17    Freeform,
18    Fixed,
19}
20
21#[serde_with::skip_serializing_none]
22#[derive(Serialize, Clone, FieldSetter)]
23pub struct Line {
24    color: Option<Dim<Box<dyn Color>>>,
25    width: Option<f64>,
26}
27
28impl Line {
29    pub fn new() -> Self {
30        Default::default()
31    }
32}
33
34#[serde_with::skip_serializing_none]
35#[derive(Serialize, Clone, FieldSetter)]
36pub struct Node {
37    // Missing: customdata, groups
38    color: Option<Dim<Box<dyn Color>>>,
39    #[serde(rename = "hoverinfo")]
40    hover_info: Option<HoverInfo>,
41    #[serde(rename = "hoverlabel")]
42    hover_label: Option<Label>,
43    #[serde(rename = "hovertemplate")]
44    hover_template: Option<Dim<String>>,
45    #[field_setter(skip)]
46    label: Option<Vec<String>>,
47    line: Option<Line>,
48    /// Sets the padding (in px) between the `nodes`.
49    pad: Option<usize>,
50    /// Sets the thickness (in px) of the `nodes`.
51    thickness: Option<usize>,
52    /// The normalized horizontal position of the node.
53    x: Option<Vec<f64>>,
54    /// The normalized vertical position of the node.
55    y: Option<Vec<f64>>,
56}
57
58impl Node {
59    pub fn new() -> Self {
60        Default::default()
61    }
62
63    pub fn label(mut self, label: Vec<&str>) -> Self {
64        self.label = Some(label.iter().map(|&el| el.to_string()).collect());
65        self
66    }
67}
68
69#[serde_with::skip_serializing_none]
70#[derive(Serialize, Clone, FieldSetter)]
71pub struct Link<V>
72where
73    V: Serialize + Clone,
74{
75    // Missing: colorscales, customdata
76    color: Option<Dim<Box<dyn Color>>>,
77    #[serde(rename = "hoverinfo")]
78    hover_info: Option<HoverInfo>,
79    #[serde(rename = "hoverlabel")]
80    hover_label: Option<Label>,
81    #[serde(rename = "hovertemplate")]
82    hover_template: Option<Dim<String>>,
83    line: Option<Line>,
84    source: Option<Vec<usize>>,
85    target: Option<Vec<usize>>,
86    value: Option<Vec<V>>,
87}
88
89impl<V> Link<V>
90where
91    V: Serialize + Clone,
92{
93    pub fn new() -> Self {
94        Default::default()
95    }
96}
97
98/// Construct a Sankey trace.
99///
100/// # Examples
101///
102/// ```
103/// use plotly::{
104///     Sankey,
105///     common::Orientation,
106///     sankey::{Line, Link, Node}
107/// };
108///
109/// let line = Line::new().color("#00FF00").width(0.5);
110///
111/// let node = Node::new()
112///     .line(line)
113///     .pad(15)
114///     .thickness(30)
115///     .label(vec!["A1", "A2", "B1", "B2", "C1", "C2"])
116///     .color("#0000FF");
117///
118/// let link = Link::new()
119///     .value(vec![8, 4, 2, 8, 4, 2])
120///     .source(vec![0, 1, 0, 2, 3, 3])
121///     .target(vec![2, 3, 3, 4, 4, 5]);
122///
123/// let trace = Sankey::new()
124///     .node(node)
125///     .link(link)
126///     .orientation(Orientation::Horizontal);
127///
128/// let expected = serde_json::json!({
129///     "type": "sankey",
130///     "orientation": "h",
131///     "node": {
132///         "color": "#0000FF",
133///         "label": ["A1", "A2", "B1", "B2", "C1", "C2"],
134///         "thickness": 30,
135///         "pad": 15,
136///         "line": {
137///             "color": "#00FF00",
138///             "width": 0.5,
139///         }
140///     },
141///     "link": {
142///         "source": [0, 1, 0, 2, 3, 3],
143///         "target": [2, 3, 3, 4, 4, 5],
144///         "value": [8, 4, 2, 8, 4, 2]
145///     }
146/// });
147///
148/// assert_eq!(serde_json::to_value(trace).unwrap(), expected);
149/// ```
150#[serde_with::skip_serializing_none]
151#[derive(Serialize, Clone, FieldSetter)]
152#[field_setter(box_self, kind = "trace")]
153pub struct Sankey<V>
154where
155    V: Serialize + Clone,
156{
157    // Missing: meta, customdata, uirevision
158    #[field_setter(default = "PlotType::Sankey")]
159    r#type: PlotType,
160    /// If value is `snap` (the default), the node arrangement is assisted by
161    /// automatic snapping of elements to preserve space between nodes
162    /// specified via `nodepad`. If value is `perpendicular`, the nodes can
163    /// only move along a line perpendicular to the flow. If value is
164    /// `freeform`, the nodes can freely move on the plane. If value is
165    /// `fixed`, the nodes are stationary.
166    arrangement: Option<Arrangement>,
167    /// Sets the domain within which the Sankey diagram will be drawn.
168    domain: Option<Domain>,
169    /// Assigns id labels to each datum. These ids are for object constancy of
170    /// data points during animation.
171    ids: Option<Vec<String>>,
172    /// Determines which trace information appear on hover. If `none` or `skip`
173    /// are set, no information is displayed upon hovering. But, if `none`
174    /// is set, click and hover events are still fired. Note that this attribute
175    /// is superseded by `node.hover_info` and `link.hover_info` for nodes and
176    /// links respectively.
177    #[serde(rename = "hoverinfo")]
178    hover_info: Option<HoverInfo>,
179    /// Sets the hover label for this trace.
180    #[serde(rename = "hoverlabel")]
181    hover_label: Option<Label>,
182    /// Set and style the title to appear for the legend group
183    #[serde(rename = "legendgrouptitle")]
184    legend_group_title: Option<LegendGroupTitle>,
185    /// Sets the legend rank for this trace. Items and groups with smaller ranks
186    /// are presented on top/left side while with `"reversed"
187    /// `legend.trace_order` they are on bottom/right side. The default
188    /// legendrank is 1000, so that you can use ranks less than 1000 to
189    /// place certain items before all unranked items, and ranks greater
190    /// than 1000 to go after all unranked items.
191    #[serde(rename = "legendrank")]
192    legend_rank: Option<usize>,
193    /// The links of the Sankey diagram.
194    link: Option<Link<V>>,
195    /// Sets the trace name. The trace name appears as the legend item and on
196    /// hover.
197    name: Option<String>,
198    /// The nodes of the Sankey diagram.
199    node: Option<Node>,
200    /// Sets the orientation of the Sankey diagram.
201    orientation: Option<Orientation>,
202    /// Vector containing integer indices of selected points. Has an effect only
203    /// for traces that support selections. Note that an empty vector means
204    /// an empty selection where the `unselected` are turned on for all
205    /// points.
206    #[serde(rename = "selectedpoints")]
207    selected_points: Option<Vec<usize>>,
208    /// Sets the font for node labels.
209    #[serde(rename = "textfont")]
210    text_font: Option<Font>,
211    /// Sets the value formatting rule using d3 formatting mini-languages which
212    /// are very similar to those in Python. For numbers, see: <https://github.com/d3/d3-format/tree/v1.4.5#d3-format>.
213    #[serde(rename = "valueformat")]
214    value_format: Option<String>,
215    /// Adds a unit to follow the value in the hover tooltip. Add a space if a
216    /// separation is necessary from the value.
217    #[serde(rename = "valuesuffix")]
218    value_suffix: Option<String>,
219    /// Determines whether or not this trace is visible. If "legendonly", the
220    /// trace is not drawn, but can appear as a legend item (provided that
221    /// the legend itself is visible).
222    visible: Option<bool>,
223}
224
225impl<V> Sankey<V>
226where
227    V: Serialize + Clone,
228{
229    /// Creates a new empty Sankey diagram.
230    pub fn new() -> Box<Self> {
231        Box::default()
232    }
233}
234
235impl<V> Trace for Sankey<V>
236where
237    V: Serialize + Clone,
238{
239    fn to_json(&self) -> String {
240        serde_json::to_string(self).unwrap()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use serde_json::{json, to_value};
247
248    use super::*;
249    use crate::color::NamedColor;
250
251    #[test]
252    fn serialize_default_sankey() {
253        let trace = Sankey::<i32>::default();
254        let expected = json!({"type": "sankey"});
255
256        assert_eq!(to_value(trace).unwrap(), expected);
257    }
258
259    #[test]
260    fn serialize_basic_sankey_trace() {
261        // Mimic the plot here, minus the layout:
262        // https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram
263        let trace = Sankey::new()
264            .orientation(Orientation::Horizontal)
265            .node(
266                Node::new()
267                    .pad(15)
268                    .thickness(30)
269                    .line(Line::new().color(NamedColor::Black).width(0.5))
270                    .label(vec!["A1", "A2", "B1", "B2", "C1", "C2"])
271                    .color_array(vec![
272                        NamedColor::Blue,
273                        NamedColor::Blue,
274                        NamedColor::Blue,
275                        NamedColor::Blue,
276                        NamedColor::Blue,
277                        NamedColor::Blue,
278                    ]),
279            )
280            .link(
281                Link::new()
282                    .value(vec![8, 4, 2, 8, 4, 2])
283                    .source(vec![0, 1, 0, 2, 3, 3])
284                    .target(vec![2, 3, 3, 4, 4, 5]),
285            );
286
287        let expected = json!({
288            "link": {
289                "source": [0, 1, 0, 2, 3, 3],
290                "target": [2, 3, 3, 4, 4, 5],
291                "value": [8, 4, 2, 8, 4, 2]
292            },
293            "orientation": "h",
294            "type": "sankey",
295            "node": {
296                "color": ["blue", "blue", "blue", "blue", "blue", "blue"],
297                "label": ["A1", "A2", "B1", "B2", "C1", "C2"],
298                "line": {
299                    "color": "black",
300                    "width": 0.5
301                },
302                "pad": 15,
303                "thickness": 30
304            }
305        });
306
307        assert_eq!(to_value(trace).unwrap(), expected);
308    }
309
310    #[test]
311    fn serialize_full_sankey_trace() {
312        let trace = Sankey::<i32>::new()
313            .name("sankey")
314            .visible(true)
315            .legend_rank(1000)
316            .legend_group_title("Legend Group Title")
317            .ids(vec!["one"])
318            .hover_info(HoverInfo::All)
319            .hover_label(Label::new())
320            .domain(Domain::new())
321            .orientation(Orientation::Horizontal)
322            .node(Node::new())
323            .link(Link::new())
324            .text_font(Font::new())
325            .selected_points(vec![0])
326            .arrangement(Arrangement::Fixed)
327            .value_format(".3f")
328            .value_suffix("nT");
329
330        let expected = json!({
331            "type": "sankey",
332            "name": "sankey",
333            "visible": true,
334            "legendrank": 1000,
335            "legendgrouptitle": {"text": "Legend Group Title"},
336            "ids": ["one"],
337            "hoverinfo": "all",
338            "hoverlabel": {},
339            "domain": {},
340            "orientation": "h",
341            "node": {},
342            "link": {},
343            "textfont": {},
344            "selectedpoints": [0],
345            "arrangement": "fixed",
346            "valueformat": ".3f",
347            "valuesuffix": "nT"
348        });
349
350        assert_eq!(to_value(trace).unwrap(), expected);
351    }
352
353    #[test]
354    fn serialize_arrangement() {
355        assert_eq!(to_value(Arrangement::Snap).unwrap(), json!("snap"));
356        assert_eq!(
357            to_value(Arrangement::Perpendicular).unwrap(),
358            json!("perpendicular")
359        );
360        assert_eq!(to_value(Arrangement::Freeform).unwrap(), json!("freeform"));
361        assert_eq!(to_value(Arrangement::Fixed).unwrap(), json!("fixed"));
362    }
363
364    #[test]
365    fn serialize_line() {
366        let line = Line::new()
367            .color_array(vec![NamedColor::Black, NamedColor::Blue])
368            .color(NamedColor::Black)
369            .width(0.1);
370        let expected = json!({
371            "color": "black",
372            "width": 0.1
373        });
374
375        assert_eq!(to_value(line).unwrap(), expected)
376    }
377
378    #[test]
379    fn serialize_node() {
380        let node = Node::new()
381            .color(NamedColor::Blue)
382            .color_array(vec![NamedColor::Blue])
383            .hover_info(HoverInfo::All)
384            .hover_label(Label::new())
385            .hover_template("template")
386            .line(Line::new())
387            .pad(5)
388            .thickness(10)
389            .x(vec![0.5])
390            .y(vec![0.25]);
391        let expected = json!({
392            "color": ["blue"],
393            "hoverinfo": "all",
394            "hoverlabel": {},
395            "hovertemplate": "template",
396            "line": {},
397            "pad": 5,
398            "thickness": 10,
399            "x": [0.5],
400            "y": [0.25]
401        });
402
403        assert_eq!(to_value(node).unwrap(), expected)
404    }
405
406    #[test]
407    fn serialize_link() {
408        let link = Link::new()
409            .color_array(vec![NamedColor::Blue])
410            .color(NamedColor::Blue)
411            .hover_info(HoverInfo::All)
412            .hover_label(Label::new())
413            .hover_template("template")
414            .line(Line::new())
415            .value(vec![2, 2, 2])
416            .source(vec![0, 1, 2])
417            .target(vec![1, 2, 0]);
418        let expected = json!({
419            "color": "blue",
420            "hoverinfo": "all",
421            "hoverlabel": {},
422            "hovertemplate": "template",
423            "line": {},
424            "source": [0, 1, 2],
425            "target": [1, 2, 0],
426            "value": [2, 2, 2],
427        });
428
429        assert_eq!(to_value(link).unwrap(), expected)
430    }
431}