Skip to content

Commit 2f7405f

Browse files
authored
Support Sankey Diagrams (#69)
* working basic example * fix examples * implement the rest of sankey * fix Domain * improve test * add sankey tests * add test for Domain * fix tests * add Sankey to book
1 parent 1987454 commit 2f7405f

File tree

9 files changed

+830
-7
lines changed

9 files changed

+830
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Support for layout templates and pre-defined themes
1212
- Support for WASM environments
1313
- Lots of tests
14+
- Support for `Sankey` diagrams
1415
### Changed
1516
- Improve implementation of `private::NumOrString` to support more primitive types ([Issue
1617
#47](https://github.com/igiagkiozis/plotly/issues/47))

docs/book/src/recipes/basic_charts.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Kind | Link
77
Scatter Plots |[![Scatter Plots](./img/line_and_scatter_plot.png)](./basic_charts/scatter_plots.md)
88
Line Charts | [![Line Charts](./img/line_shape_options_for_interpolation.png)](./basic_charts/line_charts.md)
99
Bar Charts | [![Scatter Plots](./img/bar_chart_with_error_bars.png)](./basic_charts/scatter_plots.md)
10+
Sankey Diagrams | [![Scatter Plots](./img/basic_sankey.png)](./basic_charts/sankey_diagrams.md)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Line Charts
2+
3+
The following imports have been used to produce the plots below:
4+
5+
```rust
6+
use itertools_num::linspace;
7+
use plotly::common::{
8+
ColorScale, ColorScalePalette, DashType, Fill, Font, Line, LineShape, Marker, Mode, Title,
9+
};
10+
use plotly::layout::{Axis, BarMode, Layout, Legend, TicksDirection};
11+
use plotly::Sankey;
12+
use rand_distr::{Distribution, Normal, Uniform};
13+
```
14+
15+
The `to_inline_html` method is used to produce the html plot displayed in this page.
16+
17+
18+
## Adding Names to Line and Scatter Plot
19+
```rust
20+
let trace = Sankey::new()
21+
.orientation(Orientation::Horizontal)
22+
.node(
23+
Node::new()
24+
.pad(15)
25+
.thickness(30)
26+
.line(SankeyLine::new().color(NamedColor::Black).width(0.5))
27+
.label(vec!["A1", "A2", "B1", "B2", "C1", "C2"])
28+
.color_array(vec![
29+
NamedColor::Blue,
30+
NamedColor::Blue,
31+
NamedColor::Blue,
32+
NamedColor::Blue,
33+
NamedColor::Blue,
34+
NamedColor::Blue,
35+
]),
36+
)
37+
.link(
38+
Link::new()
39+
.value(vec![8, 4, 2, 8, 4, 2])
40+
.source(vec![0, 1, 0, 2, 3, 3])
41+
.target(vec![2, 3, 3, 4, 4, 5]),
42+
);
43+
44+
let layout = Layout::new()
45+
.title("Basic Sankey".into())
46+
.font(Font::new().size(10));
47+
48+
let mut plot = Plot::new();
49+
plot.add_trace(trace);
50+
plot.set_layout(layout);
51+
52+
if show {
53+
plot.show();
54+
}
55+
56+
println!("{}", plot.to_inline_html(Some("basic_sankey_diagram")));
57+
}
58+
```
59+
<div id="basic_sankey_diagram" class="plotly-graph-div" style="height:100%; width:100%;"></div>
60+
<script type="text/javascript">
61+
window.PLOTLYENV=window.PLOTLYENV || {};
62+
if (document.getElementById("basic_sankey_diagram")) {
63+
var image_element = document.getElementById('image-export')
64+
65+
Plotly.newPlot('basic_sankey_diagram', {
66+
"data": [
67+
{
68+
"type": "sankey",
69+
"orientation": "h",
70+
"node": {
71+
"color": [
72+
"blue",
73+
"blue",
74+
"blue",
75+
"blue",
76+
"blue",
77+
"blue"
78+
],
79+
"label": [
80+
"A1",
81+
"A2",
82+
"B1",
83+
"B2",
84+
"C1",
85+
"C2"
86+
],
87+
"line": {
88+
"color": "black",
89+
"width": 0.5
90+
},
91+
"pad": 15,
92+
"thickness": 30
93+
},
94+
"link": {
95+
"source": [
96+
0,
97+
1,
98+
0,
99+
2,
100+
3,
101+
3
102+
],
103+
"target": [
104+
2,
105+
3,
106+
3,
107+
4,
108+
4,
109+
5
110+
],
111+
"value": [
112+
8,
113+
4,
114+
2,
115+
8,
116+
4,
117+
2
118+
]
119+
}
120+
}
121+
],
122+
"layout": {
123+
"title": {
124+
"text": "Basic Sankey"
125+
},
126+
"font": {
127+
"size": 10
128+
}
129+
},
130+
"config": {}
131+
});
132+
};
133+
</script>
20 KB
Loading

plotly/examples/basic_charts.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use itertools_num::linspace;
22
use plotly::common::{
3-
ColorScale, ColorScalePalette, DashType, Fill, Font, Line, LineShape, Marker, Mode, Title,
3+
ColorScale, ColorScalePalette, DashType, Fill, Font, Line, LineShape, Marker, Mode,
4+
Orientation, Title,
45
};
56
use plotly::layout::{Axis, BarMode, Layout, Legend, TicksDirection, TraceOrder};
6-
use plotly::{Bar, NamedColor, Plot, Rgb, Rgba, Scatter, ScatterPolar};
7+
use plotly::sankey::{Line as SankeyLine, Link, Node};
8+
use plotly::{Bar, NamedColor, Plot, Rgb, Rgba, Sankey, Scatter, ScatterPolar};
79
use rand_distr::{Distribution, Normal, Uniform};
810

911
// Scatter Plots
@@ -610,6 +612,48 @@ fn stacked_bar_chart(show: bool) {
610612
println!("{}", plot.to_inline_html(Some("stacked_bar_chart")));
611613
}
612614

615+
// Sankey Diagrams
616+
fn basic_sankey_diagram(show: bool) {
617+
// https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram
618+
let trace = Sankey::new()
619+
.orientation(Orientation::Horizontal)
620+
.node(
621+
Node::new()
622+
.pad(15)
623+
.thickness(30)
624+
.line(SankeyLine::new().color(NamedColor::Black).width(0.5))
625+
.label(vec!["A1", "A2", "B1", "B2", "C1", "C2"])
626+
.color_array(vec![
627+
NamedColor::Blue,
628+
NamedColor::Blue,
629+
NamedColor::Blue,
630+
NamedColor::Blue,
631+
NamedColor::Blue,
632+
NamedColor::Blue,
633+
]),
634+
)
635+
.link(
636+
Link::new()
637+
.value(vec![8, 4, 2, 8, 4, 2])
638+
.source(vec![0, 1, 0, 2, 3, 3])
639+
.target(vec![2, 3, 3, 4, 4, 5]),
640+
);
641+
642+
let layout = Layout::new()
643+
.title("Basic Sankey".into())
644+
.font(Font::new().size(10));
645+
646+
let mut plot = Plot::new();
647+
plot.add_trace(trace);
648+
plot.set_layout(layout);
649+
650+
if show {
651+
plot.show();
652+
}
653+
654+
println!("{}", plot.to_inline_html(Some("basic_sankey_diagram")));
655+
}
656+
613657
fn main() -> std::io::Result<()> {
614658
// Scatter Plots
615659
simple_scatter_plot(true);
@@ -633,5 +677,8 @@ fn main() -> std::io::Result<()> {
633677
basic_bar_chart(true);
634678
grouped_bar_chart(true);
635679
stacked_bar_chart(true);
680+
681+
// Sankey Diagrams
682+
basic_sankey_diagram(true);
636683
Ok(())
637684
}

plotly/src/common/mod.rs

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,65 @@ pub enum HoverInfo {
5555
Skip,
5656
}
5757

58+
#[derive(Serialize, Clone, Debug, Default)]
59+
pub struct LegendGroupTitle {
60+
text: String,
61+
#[serde(skip_serializing_if = "Option::is_none")]
62+
font: Option<Font>,
63+
}
64+
65+
impl LegendGroupTitle {
66+
pub fn new(text: &str) -> Self {
67+
Self {
68+
text: text.to_string(),
69+
..Default::default()
70+
}
71+
}
72+
73+
pub fn font(mut self, font: Font) -> Self {
74+
self.font = Some(font);
75+
self
76+
}
77+
}
78+
79+
#[derive(Serialize, Clone, Debug, Default)]
80+
pub struct Domain {
81+
#[serde(skip_serializing_if = "Option::is_none")]
82+
column: Option<usize>,
83+
#[serde(skip_serializing_if = "Option::is_none")]
84+
row: Option<usize>,
85+
#[serde(skip_serializing_if = "Option::is_none")]
86+
x: Option<[f64; 2]>,
87+
#[serde(skip_serializing_if = "Option::is_none")]
88+
y: Option<[f64; 2]>,
89+
}
90+
91+
impl Domain {
92+
pub fn new() -> Self {
93+
Default::default()
94+
}
95+
96+
pub fn column(mut self, column: usize) -> Self {
97+
self.column = Some(column);
98+
self
99+
}
100+
101+
pub fn row(mut self, row: usize) -> Self {
102+
self.row = Some(row);
103+
self
104+
}
105+
106+
pub fn x(mut self, x: &[f64; 2]) -> Self {
107+
self.x = Some(x.to_owned());
108+
self
109+
}
110+
111+
pub fn y(mut self, y: &[f64; 2]) -> Self {
112+
self.y = Some(y.to_owned());
113+
self
114+
}
115+
}
116+
58117
#[derive(Serialize, Clone, Debug)]
59118
#[serde(rename_all = "lowercase")]
60119
pub enum TextPosition {
@@ -74,9 +133,10 @@ pub enum ConstrainText {
74133
}
75134

76135
#[derive(Serialize, Clone, Debug)]
77-
#[serde(rename_all = "lowercase")]
78136
pub enum Orientation {
137+
#[serde(rename = "v")]
79138
Vertical,
139+
#[serde(rename = "h")]
80140
Horizontal,
81141
}
82142

@@ -148,6 +208,7 @@ pub enum PlotType {
148208
Histogram,
149209
Histogram2dContour,
150210
Ohlc,
211+
Sankey,
151212
Surface,
152213
}
153214

@@ -1472,6 +1533,19 @@ mod tests {
14721533
use super::*;
14731534
use crate::NamedColor;
14741535

1536+
#[test]
1537+
fn test_serialize_domain() {
1538+
let domain = Domain::new().column(0).row(0).x(&[0., 1.]).y(&[0., 1.]);
1539+
let expected = json!({
1540+
"column": 0,
1541+
"row": 0,
1542+
"x": [0.0, 1.0],
1543+
"y": [0.0, 1.0],
1544+
});
1545+
1546+
assert_eq!(to_value(domain).unwrap(), expected);
1547+
}
1548+
14751549
#[test]
14761550
fn test_serialize_direction() {
14771551
// TODO: I think `Direction` would be better as a struct, with `fillcolor` and `line` attributes
@@ -1519,8 +1593,8 @@ mod tests {
15191593
#[test]
15201594
#[rustfmt::skip]
15211595
fn test_serialize_orientation() {
1522-
assert_eq!(to_value(Orientation::Vertical).unwrap(), json!("vertical"));
1523-
assert_eq!(to_value(Orientation::Horizontal).unwrap(), json!("horizontal"));
1596+
assert_eq!(to_value(Orientation::Vertical).unwrap(), json!("v"));
1597+
assert_eq!(to_value(Orientation::Horizontal).unwrap(), json!("h"));
15241598
}
15251599

15261600
#[test]
@@ -1583,6 +1657,7 @@ mod tests {
15831657
assert_eq!(to_value(PlotType::Histogram).unwrap(), json!("histogram"));
15841658
assert_eq!(to_value(PlotType::Histogram2dContour).unwrap(), json!("histogram2dcontour"));
15851659
assert_eq!(to_value(PlotType::Ohlc).unwrap(), json!("ohlc"));
1660+
assert_eq!(to_value(PlotType::Sankey).unwrap(), json!("sankey"));
15861661
assert_eq!(to_value(PlotType::Surface).unwrap(), json!("surface"));
15871662
}
15881663

plotly/src/layout.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3513,7 +3513,7 @@ mod tests {
35133513
"bordercolor": "#321321",
35143514
"borderwidth": 500,
35153515
"font": {},
3516-
"orientation": "vertical",
3516+
"orientation": "v",
35173517
"traceorder": "normal",
35183518
"tracegroupgap": 10,
35193519
"itemsizing": "trace",
@@ -3980,7 +3980,7 @@ mod tests {
39803980
.color("#000FFF")
39813981
.active_color("#AAABBB");
39823982
let expected = json!({
3983-
"orientation": "horizontal",
3983+
"orientation": "h",
39843984
"bgcolor": "#FFF000",
39853985
"color": "#000FFF",
39863986
"activecolor": "#AAABBB",

plotly/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod contour;
2626
pub mod heat_map;
2727
pub mod histogram;
2828
pub mod ohlc;
29+
pub mod sankey;
2930
pub mod scatter;
3031
pub mod scatter_polar;
3132
pub mod surface;
@@ -42,6 +43,7 @@ pub use crate::contour::Contour;
4243
pub use crate::heat_map::HeatMap;
4344
pub use crate::histogram::Histogram;
4445
pub use crate::ohlc::Ohlc;
46+
pub use crate::sankey::Sankey;
4547
pub use crate::scatter::Scatter;
4648
pub use crate::scatter_polar::ScatterPolar;
4749
pub use crate::surface::Surface;

0 commit comments

Comments
 (0)