1use std::fmt;
5
6use ravenrs_core::{Point, Rect};
7
8use crate::vector::PathSegment;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12#[non_exhaustive]
13#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
14pub enum SankeyAlign {
15 Left,
17 Right,
19 Center,
22 #[default]
25 Justify,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct SankeyLink {
31 pub source: usize,
33 pub target: usize,
35 pub value: f64,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
41#[non_exhaustive]
42pub struct SankeyStyle {
43 pub node_width: f32,
45 pub node_gap: f32,
47 pub align: SankeyAlign,
49 pub passes: usize,
51}
52
53impl Default for SankeyStyle {
54 fn default() -> Self {
55 Self { node_width: 12.0, node_gap: 14.0, align: SankeyAlign::default(), passes: 16 }
56 }
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum SankeyError {
63 Cycle(Vec<usize>),
65 NoSuchNode(usize),
67}
68
69impl fmt::Display for SankeyError {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 Self::Cycle(nodes) => {
73 let names: Vec<String> = nodes.iter().map(ToString::to_string).collect();
74 write!(f, "the flows go in a circle: {}", names.join(" -> "))
75 }
76 Self::NoSuchNode(node) => write!(f, "a flow names node {node}, which does not exist"),
77 }
78 }
79}
80
81impl std::error::Error for SankeyError {}
82
83#[derive(Clone, Debug, PartialEq)]
85#[non_exhaustive]
86pub struct SankeyNode {
87 pub rect: Rect,
89 pub value: f64,
91 pub column: usize,
93 pub incoming: Vec<usize>,
95 pub outgoing: Vec<usize>,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq)]
101#[non_exhaustive]
102pub struct SankeyFlow {
103 pub source: usize,
105 pub target: usize,
107 pub value: f64,
109 pub from: Point,
111 pub to: Point,
113 pub width: f32,
115}
116
117impl SankeyFlow {
118 fn middle(&self) -> f32 {
120 (self.from.x + self.to.x) / 2.0
121 }
122
123 pub fn path(&self) -> Vec<PathSegment> {
125 let (half, middle) = (self.width / 2.0, self.middle());
126 let at = Point::new;
127 let (x0, x1, y0, y1) = (self.from.x, self.to.x, self.from.y, self.to.y);
128 vec![
129 PathSegment::Move(at(x0, y0 - half)),
130 PathSegment::Cubic(at(middle, y0 - half), at(middle, y1 - half), at(x1, y1 - half)),
131 PathSegment::Line(at(x1, y1 + half)),
132 PathSegment::Cubic(at(middle, y1 + half), at(middle, y0 + half), at(x0, y0 + half)),
133 PathSegment::Close,
134 ]
135 }
136
137 pub fn contains(&self, point: Point) -> bool {
139 if point.x < self.from.x || point.x > self.to.x {
140 return false;
141 }
142 let (x0, x1, middle) = (self.from.x, self.to.x, self.middle());
143 let x_at = |t: f32| {
144 let u = 1.0 - t;
145 u * u * u * x0 + 3.0 * u * t * middle * (u + t) + t * t * t * x1
146 };
147 let (mut low, mut high) = (0.0_f32, 1.0_f32);
149 for _ in 0..16 {
150 let t = (low + high) / 2.0;
151 if x_at(t) < point.x {
152 low = t;
153 } else {
154 high = t;
155 }
156 }
157 let t = (low + high) / 2.0;
158 let share = t * t * (3.0 - 2.0 * t);
159 let y = self.from.y + (self.to.y - self.from.y) * share;
160 (point.y - y).abs() <= self.width / 2.0
161 }
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub enum SankeyHit {
167 Node(usize),
169 Flow(usize),
171}
172
173#[derive(Clone, Debug, Default, PartialEq)]
175#[non_exhaustive]
176pub struct SankeyLayout {
177 pub nodes: Vec<SankeyNode>,
179 pub flows: Vec<SankeyFlow>,
181 pub columns: usize,
183}
184
185impl SankeyLayout {
186 pub fn hit(&self, point: Point) -> Option<SankeyHit> {
188 if let Some(node) = self.nodes.iter().position(|node| node.rect.contains(point)) {
189 return Some(SankeyHit::Node(node));
190 }
191 self.flows.iter().rposition(|flow| flow.contains(point)).map(SankeyHit::Flow)
193 }
194}
195
196fn in_order(count: usize, links: &[SankeyLink]) -> Result<Vec<usize>, SankeyError> {
198 let mut waiting = vec![0_usize; count];
199 for link in links {
200 waiting[link.target] += 1;
201 }
202 let mut ready: Vec<usize> = (0..count).filter(|node| waiting[*node] == 0).collect();
203 let mut order = Vec::with_capacity(count);
204 while let Some(node) = ready.pop() {
205 order.push(node);
206 for link in links.iter().filter(|link| link.source == node) {
207 waiting[link.target] -= 1;
208 if waiting[link.target] == 0 {
209 ready.push(link.target);
210 }
211 }
212 }
213 if order.len() == count {
214 return Ok(order);
215 }
216 let Some(start) = (0..count).find(|node| waiting[*node] > 0) else { return Ok(order) };
219 let mut path = vec![start];
220 loop {
221 let last = path[path.len() - 1];
222 let previous = links.iter().find(|l| l.target == last && waiting[l.source] > 0).map(|l| l.source);
223 let Some(previous) = previous else { return Err(SankeyError::Cycle(path)) };
224 if let Some(seen) = path.iter().position(|node| *node == previous) {
225 let mut circle: Vec<usize> = path[seen..].to_vec();
226 circle.push(previous);
227 circle.reverse();
228 return Err(SankeyError::Cycle(circle));
229 }
230 path.push(previous);
231 }
232}
233
234fn usable(links: &[SankeyLink]) -> Vec<SankeyLink> {
235 links.iter().copied().filter(|link| link.value.is_finite() && link.value > 0.0).collect()
236}
237
238pub fn check(count: usize, links: &[SankeyLink]) -> Result<(), SankeyError> {
240 let links = usable(links);
241 if let Some(link) = links.iter().find(|link| link.source >= count || link.target >= count) {
242 return Err(SankeyError::NoSuchNode(link.source.max(link.target)));
243 }
244 in_order(count, &links).map(|_| ())
245}
246
247fn columns_of(
249 count: usize,
250 links: &[SankeyLink],
251 order: &[usize],
252 align: SankeyAlign,
253) -> (Vec<usize>, usize) {
254 let mut depth = vec![0_usize; count];
255 for node in order {
256 for link in links.iter().filter(|link| link.source == *node) {
257 depth[link.target] = depth[link.target].max(depth[*node] + 1);
258 }
259 }
260 let mut height = vec![0_usize; count];
261 for node in order.iter().rev() {
262 for link in links.iter().filter(|link| link.target == *node) {
263 height[link.source] = height[link.source].max(height[*node] + 1);
264 }
265 }
266 let columns = depth.iter().copied().max().map_or(0, |deepest| deepest + 1);
267 let has_in = |node: usize| links.iter().any(|link| link.target == node);
268 let has_out = |node: usize| links.iter().any(|link| link.source == node);
269 let column = (0..count)
270 .map(|node| match align {
271 SankeyAlign::Left => depth[node],
272 SankeyAlign::Right => columns - 1 - height[node],
273 SankeyAlign::Center if !has_in(node) && has_out(node) => {
274 let first = links.iter().filter(|l| l.source == node).map(|l| depth[l.target]).min();
275 first.map_or(0, |at| at.saturating_sub(1))
276 }
277 SankeyAlign::Center => depth[node],
278 SankeyAlign::Justify if !has_out(node) && has_in(node) => columns - 1,
279 _ => depth[node],
280 })
281 .collect();
282 (column, columns)
283}
284
285fn resolve(ys: &mut [f32], heights: &[f32], column: &mut [usize], area: Rect, gap: f32) {
287 column.sort_by(|a, b| ys[*a].total_cmp(&ys[*b]));
288 let mut next = area.y();
289 for node in column.iter() {
290 ys[*node] = ys[*node].max(next);
291 next = ys[*node] + heights[*node] + gap;
292 }
293 let mut limit = area.max_y();
294 for node in column.iter().rev() {
295 ys[*node] = ys[*node].min(limit - heights[*node]);
296 limit = ys[*node] - gap;
297 }
298}
299
300pub fn layout(
302 count: usize,
303 links: &[SankeyLink],
304 area: Rect,
305 style: &SankeyStyle,
306) -> Result<SankeyLayout, SankeyError> {
307 check(count, links)?;
308 let links = usable(links);
309 let order = in_order(count, &links)?;
310 let (column_of, columns) = columns_of(count, &links, &order, style.align);
311 if columns == 0 {
312 return Ok(SankeyLayout::default());
313 }
314 let mut value = vec![0.0_f64; count];
315 for (node, slot) in value.iter_mut().enumerate() {
316 let coming: f64 = links.iter().filter(|l| l.target == node).map(|l| l.value).sum();
317 let going: f64 = links.iter().filter(|l| l.source == node).map(|l| l.value).sum();
318 *slot = coming.max(going);
319 }
320 let mut by_column: Vec<Vec<usize>> = vec![Vec::new(); columns];
321 for node in 0..count {
322 by_column[column_of[node]].push(node);
323 }
324 let scale = by_column
326 .iter()
327 .filter(|column| !column.is_empty())
328 .map(|column| {
329 let total: f64 = column.iter().map(|node| value[*node]).sum();
330 let room = area.height() - style.node_gap * (column.len() - 1) as f32;
331 if total > 0.0 { room.max(1.0) / total as f32 } else { f32::INFINITY }
332 })
333 .fold(f32::INFINITY, f32::min);
334 let scale = if scale.is_finite() { scale } else { 1.0 };
335 let heights: Vec<f32> = value.iter().map(|v| (*v as f32 * scale).max(1.0)).collect();
336
337 let mut ys = vec![area.y(); count];
340 for column in &by_column {
341 let total: f32 = column.iter().map(|node| heights[*node]).sum::<f32>()
342 + style.node_gap * column.len().saturating_sub(1) as f32;
343 let mut y = area.y() + (area.height() - total).max(0.0) / 2.0;
344 for node in column {
345 ys[*node] = y;
346 y += heights[*node] + style.node_gap;
347 }
348 }
349 for pass in 0..style.passes {
350 let strength = 0.99_f32.powi(pass as i32) * 0.8;
351 for column in (1..columns).chain((0..columns.saturating_sub(1)).rev()) {
352 let forward = pass % 2 == 0;
353 let nodes: Vec<usize> = by_column[column].clone();
354 for node in nodes {
355 let toward = |other: usize| ys[other] + heights[other] / 2.0;
356 let (mut sum, mut weight) = (0.0_f32, 0.0_f32);
357 for link in &links {
358 let other = if forward && link.target == node {
359 Some(link.source)
360 } else if !forward && link.source == node {
361 Some(link.target)
362 } else {
363 None
364 };
365 if let Some(other) = other {
366 sum += toward(other) * link.value as f32;
367 weight += link.value as f32;
368 }
369 }
370 if weight > 0.0 {
371 let wanted = sum / weight - heights[node] / 2.0;
372 ys[node] += (wanted - ys[node]) * strength;
373 }
374 }
375 resolve(&mut ys, &heights, &mut by_column[column], area, style.node_gap);
376 }
377 }
378
379 let node_width = style.node_width.min(area.width() / (columns * 2) as f32);
381 let span = (area.width() - node_width).max(0.0);
382 let x_of = |column: usize| {
383 if columns > 1 { area.x() + span * column as f32 / (columns - 1) as f32 } else { area.x() }
384 };
385 let mut nodes: Vec<SankeyNode> = (0..count)
386 .map(|node| SankeyNode {
387 rect: Rect::new(x_of(column_of[node]), ys[node], node_width, heights[node]),
388 value: value[node],
389 column: column_of[node],
390 incoming: Vec::new(),
391 outgoing: Vec::new(),
392 })
393 .collect();
394 for (place, link) in links.iter().enumerate() {
395 nodes[link.source].outgoing.push(place);
396 nodes[link.target].incoming.push(place);
397 }
398 let middle = |node: usize| ys[node] + heights[node] / 2.0;
399 let mut starts = vec![0.0_f32; links.len()];
400 let mut ends = vec![0.0_f32; links.len()];
401 for node in nodes.iter_mut() {
402 node.outgoing.sort_by(|a, b| middle(links[*a].target).total_cmp(&middle(links[*b].target)));
405 node.incoming.sort_by(|a, b| middle(links[*a].source).total_cmp(&middle(links[*b].source)));
406 let mut y = node.rect.y();
407 for link in &node.outgoing {
408 starts[*link] = y + links[*link].value as f32 * scale / 2.0;
409 y += links[*link].value as f32 * scale;
410 }
411 let mut y = node.rect.y();
412 for link in &node.incoming {
413 ends[*link] = y + links[*link].value as f32 * scale / 2.0;
414 y += links[*link].value as f32 * scale;
415 }
416 }
417 let flows = links
418 .iter()
419 .enumerate()
420 .map(|(place, link)| SankeyFlow {
421 source: link.source,
422 target: link.target,
423 value: link.value,
424 from: Point::new(nodes[link.source].rect.max_x(), starts[place]),
425 to: Point::new(nodes[link.target].rect.x(), ends[place]),
426 width: link.value as f32 * scale,
427 })
428 .collect();
429 Ok(SankeyLayout { nodes, flows, columns })
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 const AREA: Rect = Rect::new(0.0, 0.0, 400.0, 300.0);
437
438 fn links(list: &[(usize, usize, f64)]) -> Vec<SankeyLink> {
439 list.iter()
440 .map(|(source, target, value)| SankeyLink { source: *source, target: *target, value: *value })
441 .collect()
442 }
443
444 fn laid(count: usize, list: &[(usize, usize, f64)]) -> SankeyLayout {
445 layout(count, &links(list), AREA, &SankeyStyle::default()).expect("a layout")
446 }
447
448 #[test]
449 fn a_chain_goes_from_left_to_right_with_a_column_each() {
450 let graph = laid(3, &[(0, 1, 10.0), (1, 2, 10.0)]);
451 assert_eq!(graph.columns, 3);
452 assert_eq!(graph.nodes[0].rect.x(), 0.0);
453 assert!((graph.nodes[2].rect.max_x() - AREA.max_x()).abs() < 0.01);
454 assert!(graph.nodes[0].rect.max_x() < graph.nodes[1].rect.x());
455 let flow = graph.flows[0];
456 assert_eq!(flow.from.x, graph.nodes[0].rect.max_x());
457 assert_eq!(flow.to.x, graph.nodes[1].rect.x());
458 }
459
460 #[test]
461 fn a_circle_says_which_nodes_it_goes_through() {
462 let error = layout(
463 4,
464 &links(&[(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (3, 1, 1.0)]),
465 AREA,
466 &SankeyStyle::default(),
467 );
468 let Err(SankeyError::Cycle(nodes)) = error else { panic!("{error:?}") };
469 assert_eq!(nodes.first(), nodes.last(), "it closes on itself");
470 assert_eq!(nodes.len(), 4, "{nodes:?}");
471 assert!(!nodes.contains(&0), "the node leading to it is not part of it");
472 for pair in nodes.windows(2) {
473 let found = [(1, 2), (2, 3), (3, 1)].contains(&(pair[0], pair[1]));
474 assert!(found, "{pair:?} in the direction of the flows");
475 }
476 }
477
478 #[test]
479 fn a_node_that_flows_into_itself_is_a_circle() {
480 assert!(matches!(check(2, &links(&[(1, 1, 1.0)])), Err(SankeyError::Cycle(_))));
481 }
482
483 #[test]
484 fn a_flow_to_nowhere_is_an_error_and_says_so() {
485 let error = check(2, &links(&[(0, 5, 1.0)])).expect_err("no such node");
486 assert_eq!(error, SankeyError::NoSuchNode(5));
487 assert!(error.to_string().contains('5'));
488 }
489
490 #[test]
491 fn a_flow_with_no_value_does_not_count() {
492 let graph = laid(3, &[(0, 1, 5.0), (1, 2, 0.0), (1, 2, f64::NAN), (2, 0, -1.0)]);
493 assert_eq!(graph.flows.len(), 1, "and the negative one closes no circle");
494 }
495
496 #[test]
497 fn what_goes_through_a_node_fills_its_side() {
498 let graph = laid(4, &[(0, 2, 30.0), (1, 2, 10.0), (2, 3, 40.0)]);
499 let middle = &graph.nodes[2];
500 let width: f32 = middle.incoming.iter().map(|f| graph.flows[*f].width).sum();
501 assert!((width - middle.rect.height()).abs() < 0.01);
502 let width: f32 = middle.outgoing.iter().map(|f| graph.flows[*f].width).sum();
503 assert!((width - middle.rect.height()).abs() < 0.01);
504 assert!(graph.nodes[0].rect.height() > graph.nodes[1].rect.height() * 2.0);
505 }
506
507 #[test]
508 fn nodes_of_a_column_stay_inside_and_apart() {
509 let list: Vec<(usize, usize, f64)> = (1..12).map(|i| (0, i, f64::from(i as u32))).collect();
510 let graph = laid(12, &list);
511 let mut column: Vec<Rect> = graph.nodes[1..].iter().map(|n| n.rect).collect();
512 column.sort_by(|a, b| a.y().total_cmp(&b.y()));
513 for pair in column.windows(2) {
514 assert!(pair[0].max_y() <= pair[1].y() + 0.01, "{pair:?}");
515 }
516 assert!(column[0].y() >= AREA.y() - 0.01);
517 assert!(column[column.len() - 1].max_y() <= AREA.max_y() + 0.01);
518 }
519
520 #[test]
521 fn the_alignment_decides_where_a_short_branch_goes() {
522 let list = links(&[(0, 1, 5.0), (1, 2, 5.0), (3, 2, 5.0), (0, 4, 5.0)]);
523 let of = |align| {
524 let style = SankeyStyle { align, ..SankeyStyle::default() };
525 layout(5, &list, AREA, &style).expect("a layout").nodes
526 };
527 assert_eq!(of(SankeyAlign::Left)[4].column, 1, "as soon as it can");
528 assert_eq!(of(SankeyAlign::Justify)[4].column, 2, "nothing goes out of it: last");
529 assert_eq!(of(SankeyAlign::Right)[3].column, 1, "right before what it feeds");
530 assert_eq!(of(SankeyAlign::Left)[3].column, 0);
531 assert_eq!(of(SankeyAlign::Center)[3].column, 1);
532 }
533
534 #[test]
535 fn the_pointer_finds_a_node_over_a_flow() {
536 let graph = laid(2, &[(0, 1, 10.0)]);
537 let node = graph.nodes[0].rect.center();
538 assert_eq!(graph.hit(node), Some(SankeyHit::Node(0)));
539 let flow = graph.flows[0];
540 let halfway = Point::new((flow.from.x + flow.to.x) / 2.0, (flow.from.y + flow.to.y) / 2.0);
541 assert_eq!(graph.hit(halfway), Some(SankeyHit::Flow(0)));
542 assert_eq!(graph.hit(Point::new(200.0, -10.0)), None);
543 }
544
545 #[test]
546 fn a_ribbon_is_where_its_outline_says_at_any_point_across() {
547 let graph = laid(3, &[(0, 2, 10.0), (1, 2, 30.0)]);
548 let flow = graph.flows[0];
549 let quarter = flow.from.x + (flow.to.x - flow.from.x) * 0.25;
550 let inside = (0..300).map(|y| Point::new(quarter, y as f32)).filter(|p| flow.contains(*p)).count();
551 assert!((inside as f32 - flow.width).abs() <= 2.0, "{inside} {}", flow.width);
552 }
553
554 #[test]
555 fn nothing_lays_out_as_nothing() {
556 assert_eq!(laid(0, &[]).nodes.len(), 0);
557 let alone = laid(2, &[]);
558 assert_eq!(alone.flows.len(), 0);
559 assert_eq!(alone.nodes.len(), 2);
560 }
561
562 #[test]
563 fn many_columns_in_little_room_still_flow_forward() {
564 let list: Vec<(usize, usize, f64)> = (0..200).map(|i| (i, i + 1, 3.0)).collect();
565 let graph = laid(201, &list);
566 assert_eq!(graph.columns, 201);
567 assert!(graph.flows.iter().all(|f| f.from.x < f.to.x));
568 }
569}