1use crate::aes::Aesthetic;
6use crate::coord::Coord;
7use crate::data::{DataFrame, Value};
8use crate::position::identity::PositionIdentity;
9use crate::position::Position;
10use crate::render::backend::{DrawBackend, FontFace, RectStyle, TextAnchor, TextStyle};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::sankey::StatSankey;
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::{Geom, GeomParams};
18
19pub struct GeomSankey {
22 pub node_width: f64,
24 pub node_padding: f64,
26 pub justify: bool,
28 pub fill: (u8, u8, u8),
30 pub color: Option<(u8, u8, u8)>,
32 pub node_alpha: f64,
33 pub link_alpha: f64,
34 pub labels: bool,
36 pub label_size: f64,
37 pub label_color: (u8, u8, u8),
38 pub label_values: bool,
40 pub label_halo: Option<(u8, u8, u8)>,
42}
43
44impl Default for GeomSankey {
45 fn default() -> Self {
46 GeomSankey {
47 node_width: 0.12,
48 node_padding: 0.08,
49 justify: false,
50 fill: (97, 156, 255),
51 color: None,
52 node_alpha: 1.0,
53 link_alpha: 0.4,
54 labels: true,
55 label_size: 10.0,
56 label_color: (40, 40, 40),
57 label_values: false,
58 label_halo: Some((255, 255, 255)),
59 }
60 }
61}
62
63const LINK_STEPS: usize = 32;
65
66impl Geom for GeomSankey {
67 fn draw(
68 &self,
69 data: &DataFrame,
70 coord: &dyn Coord,
71 scales: &ScaleSet,
72 theme: &Theme,
73 backend: &mut dyn DrawBackend,
74 ) -> Result<(), RenderError> {
75 let col = |n: &str| {
76 data.column(n)
77 .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
78 };
79 let (part, xmin, xmax, ymin, ymax) = (
80 col("part")?,
81 col("xmin")?,
82 col("xmax")?,
83 col("ymin")?,
84 col("ymax")?,
85 );
86 let (emin, emax) = (col("yend_min")?, col("yend_max")?);
87 let (node, from, to, value) = (col("node")?, col("from")?, col("to")?, col("value")?);
88 let column = data.column("column");
89 let fill_col = data.column("fill");
90 let area = backend.plot_area();
91 let mx = |v: f64| {
92 scales
93 .get(&Aesthetic::X)
94 .map(|s| s.map(&Value::Float(v)))
95 .unwrap_or(0.0)
96 };
97 let my = |v: f64| {
98 scales
99 .get(&Aesthetic::Y)
100 .map(|s| s.map(&Value::Float(v)))
101 .unwrap_or(0.0)
102 };
103 let f = |c: &[Value], i: usize| c[i].as_f64().unwrap_or(f64::NAN);
104 let fill_of = |i: usize| {
105 fill_col
106 .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
107 .unwrap_or(self.fill)
108 };
109 let is = |i: usize, p: &str| part[i].key_str() == p;
110
111 for i in (0..data.nrows()).filter(|&i| is(i, "link")) {
113 let (x0, x1) = (f(xmin, i), f(xmax, i));
114 let (s0, s1, t0, t1) = (f(ymin, i), f(ymax, i), f(emin, i), f(emax, i));
115 if ![x0, x1, s0, s1, t0, t1].iter().all(|v| v.is_finite()) {
116 continue;
117 }
118 let ease = |t: f64| t * t * (3.0 - 2.0 * t);
120 let mut pts = Vec::with_capacity(2 * (LINK_STEPS + 1));
121 for k in 0..=LINK_STEPS {
122 let t = k as f64 / LINK_STEPS as f64;
123 let y = s1 + (t1 - s1) * ease(t);
124 pts.push(coord.transform((mx(x0 + (x1 - x0) * t), my(y)), &area));
125 }
126 for k in (0..=LINK_STEPS).rev() {
127 let t = k as f64 / LINK_STEPS as f64;
128 let y = s0 + (t0 - s0) * ease(t);
129 pts.push(coord.transform((mx(x0 + (x1 - x0) * t), my(y)), &area));
130 }
131 let (a, b) = (super::tip_value(&from[i]), super::tip_value(&to[i]));
132 super::set_mark(
133 backend,
134 Some(format!("{a} → {b}: {}", super::tip_value(&value[i]))),
135 Some(a.clone()),
136 Some(a),
137 super::raw_value(&value[i]),
138 );
139 backend.draw_polygon(
140 &pts,
141 &RectStyle {
142 fill: Some(fill_of(i)),
143 stroke: None,
144 stroke_width: 0.0,
145 alpha: self.link_alpha,
146 clip: true,
147 corner_radius: 0.0,
148 },
149 )?;
150 }
151
152 let last_column = column
153 .map(|c| c.iter().filter_map(|v| v.as_f64()).fold(0.0, f64::max))
154 .unwrap_or(0.0);
155 let mut labels = Vec::new();
156 for i in (0..data.nrows()).filter(|&i| is(i, "node")) {
157 let (x0, x1, y0, y1) = (f(xmin, i), f(xmax, i), f(ymin, i), f(ymax, i));
158 let name = super::tip_value(&node[i]);
159 super::set_mark(
160 backend,
161 Some(format!("{name}: {}", super::tip_value(&value[i]))),
162 Some(name.clone()),
163 Some(name.clone()),
164 super::raw_value(&value[i]),
165 );
166 let (px0, py0) = coord.transform((mx(x0), my(y1)), &area);
167 let (px1, py1) = coord.transform((mx(x1), my(y0)), &area);
168 backend.draw_rect(
169 (px0.min(px1), py0.min(py1)),
170 (px0.max(px1), py0.max(py1)),
171 &RectStyle {
172 fill: Some(fill_of(i)),
173 stroke: self.color,
174 stroke_width: if self.color.is_some() { 0.5 } else { 0.0 },
175 alpha: self.node_alpha,
176 clip: true,
177 corner_radius: 0.0,
178 },
179 )?;
180 if self.labels {
181 let at_end =
183 column.and_then(|c| c[i].as_f64()) == Some(last_column) && last_column > 0.0;
184 let ymid = my((y0 + y1) / 2.0);
185 let (pos, anchor) = if at_end {
186 (coord.transform((mx(x0), ymid), &area), TextAnchor::End)
187 } else {
188 (coord.transform((mx(x1), ymid), &area), TextAnchor::Start)
189 };
190 let dx = if at_end { -4.0 } else { 4.0 };
191 let text = if self.label_values {
192 format!("{name} ({})", super::tip_value(&value[i]))
193 } else {
194 name
195 };
196 labels.push((text, (pos.0 + dx, pos.1), anchor));
197 }
198 }
199 super::clear_mark(backend);
200 let family = super::resolve_font_family("", theme);
201 for (text, pos, anchor) in labels {
202 backend.draw_text(
203 &text,
204 pos,
205 &TextStyle {
206 color: self.label_color,
207 size: self.label_size,
208 anchor,
209 angle: 0.0,
210 family: family.clone(),
211 face: FontFace::Plain,
212 bg_colour: self.label_halo,
213 bg_r: 1.5,
214 },
215 )?;
216 }
217 Ok(())
218 }
219
220 fn required_aes(&self) -> Vec<Aesthetic> {
221 vec![Aesthetic::From, Aesthetic::To]
222 }
223
224 fn default_stat(&self) -> Box<dyn Stat> {
225 Box::new(StatSankey {
226 node_width: self.node_width,
227 node_padding: self.node_padding,
228 justify: self.justify,
229 ..Default::default()
230 })
231 }
232
233 fn default_position(&self) -> Box<dyn Position> {
234 Box::new(PositionIdentity)
235 }
236
237 fn default_params(&self) -> GeomParams {
238 GeomParams::default()
239 }
240
241 fn name(&self) -> &str {
242 "sankey"
243 }
244
245 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
246 if let Some(ink) = el.ink {
247 self.label_color = ink;
248 }
249 if let (Some(paper), Some(_)) = (el.paper, self.label_halo) {
250 self.label_halo = Some(paper);
251 }
252 }
253}