Skip to main content

gpui_base/plot/shape/
sankey.rs

1// @reference: https://github.com/d3/d3-sankey
2
3use gpui::{Path, PathBuilder, Pixels, Point, px};
4
5use crate::plot::origin_point;
6
7/// Vertical offset, as a fraction of node height, applied to stagger runs of
8/// equal-height single-node columns so their otherwise-flat ribbons curve.
9const STAGGER_RATIO: f32 = 0.15;
10
11/// Horizontal alignment of nodes across layers.
12///
13/// Mirrors d3-sankey's `sankeyLeft` / `sankeyRight` / `sankeyCenter` /
14/// `sankeyJustify`.
15#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
16pub enum SankeyAlign {
17    Left,
18    Right,
19    Center,
20    #[default]
21    Justify,
22}
23
24/// An input link of a Sankey diagram.
25///
26/// `source` and `target` are indices into the node list (d3-sankey's default
27/// `nodeId`), `value` is the flow amount.
28#[derive(Clone, Copy, Debug, PartialEq)]
29#[non_exhaustive]
30pub struct SankeyLink {
31    pub source: usize,
32    pub target: usize,
33    pub value: f64,
34}
35
36impl SankeyLink {
37    /// Create a link from the `source` node index to the `target` node index
38    /// carrying `value`.
39    pub fn new(source: usize, target: usize, value: f64) -> Self {
40        Self {
41            source,
42            target,
43            value,
44        }
45    }
46}
47
48/// A node with computed layout (d3-sankey's computed node fields).
49#[derive(Clone, Debug, Default)]
50#[non_exhaustive]
51pub struct SankeyNodeLayout {
52    pub index: usize,
53    /// The node's throughput in the layout's value space: max(sum of incoming,
54    /// sum of outgoing). With a non-linear [`SankeyValueScale`] this is in
55    /// scaled units, not raw values — read raw values from the input links.
56    pub value: f64,
57    /// Topological distance from any source node (longest path).
58    pub depth: usize,
59    /// Topological distance to any sink node (longest path).
60    pub height: usize,
61    /// Horizontal column index after alignment.
62    pub layer: usize,
63    pub x0: f32,
64    pub x1: f32,
65    pub y0: f32,
66    pub y1: f32,
67    /// Indices into [`SankeyGraph::links`] of the outgoing links.
68    pub source_links: Vec<usize>,
69    /// Indices into [`SankeyGraph::links`] of the incoming links.
70    pub target_links: Vec<usize>,
71}
72
73/// A link with computed layout.
74///
75/// Like d3-sankey, `y0` and `y1` are the vertical centers of the ribbon at
76/// the source and target end. Each end has its own width: the links of a
77/// node's side share the node height in proportion to their values, so both
78/// sides of every node are always fully covered. On a balanced graph
79/// (incoming sum == outgoing sum everywhere) the two ends are equal; on an
80/// imbalanced one (e.g. sqrt-compressed values) the ribbon transitions
81/// smoothly between the two widths.
82#[derive(Clone, Debug)]
83#[non_exhaustive]
84pub struct SankeyLinkLayout {
85    pub index: usize,
86    pub source: usize,
87    pub target: usize,
88    /// The flow value in the layout's value space (scaled by
89    /// [`SankeyValueScale`]; equals the raw input value under `Linear`).
90    pub value: f64,
91    pub y0: f32,
92    pub y1: f32,
93    /// The nominal width from the global value scale, used by the layout
94    /// relaxation; equals both end widths on a balanced graph.
95    pub width: f32,
96    /// The ribbon width at the source end.
97    pub source_width: f32,
98    /// The ribbon width at the target end.
99    pub target_width: f32,
100}
101
102/// The computed Sankey layout.
103#[derive(Clone, Debug, Default)]
104#[non_exhaustive]
105pub struct SankeyGraph {
106    pub nodes: Vec<SankeyNodeLayout>,
107    pub links: Vec<SankeyLinkLayout>,
108}
109
110impl SankeyGraph {
111    /// Number of layers (max layer + 1), 0 for an empty graph.
112    pub fn layer_count(&self) -> usize {
113        self.nodes
114            .iter()
115            .map(|node| node.layer + 1)
116            .max()
117            .unwrap_or(0)
118    }
119}
120
121/// A reason a Sankey layout could not be computed.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum SankeyError {
124    /// A link references a node index out of range.
125    MissingNode(usize),
126    /// The graph contains a circular link.
127    CircularLink,
128}
129
130impl std::fmt::Display for SankeyError {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Self::MissingNode(index) => write!(f, "missing node: {}", index),
134            Self::CircularLink => write!(f, "circular link"),
135        }
136    }
137}
138
139impl std::error::Error for SankeyError {}
140
141/// How flow values map to node heights and ribbon widths.
142#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
143pub enum SankeyValueScale {
144    /// Height is proportional to the value (standard sankey semantics).
145    #[default]
146    Linear,
147    /// Height is proportional to the square root of the value, compressing a
148    /// wide value range so a dominant flow doesn't dwarf the small ones (and
149    /// the small ones stay visible) without the caller pre-transforming data.
150    Sqrt,
151}
152
153impl SankeyValueScale {
154    fn apply(self, value: f64) -> f64 {
155        match self {
156            Self::Linear => value,
157            // Guard against tiny negatives from bad data.
158            Self::Sqrt => value.max(0.).sqrt(),
159        }
160    }
161}
162
163/// The Sankey layout generator.
164pub struct Sankey {
165    node_width: f32,
166    node_padding: f32,
167    align: SankeyAlign,
168    iterations: usize,
169    value_scale: SankeyValueScale,
170    x0: f32,
171    y0: f32,
172    x1: f32,
173    y1: f32,
174}
175
176impl Default for Sankey {
177    fn default() -> Self {
178        Self {
179            node_width: 24.,
180            node_padding: 8.,
181            align: SankeyAlign::default(),
182            iterations: 6,
183            value_scale: SankeyValueScale::default(),
184            x0: 0.,
185            y0: 0.,
186            x1: 1.,
187            y1: 1.,
188        }
189    }
190}
191
192impl Sankey {
193    /// Create a generator with the d3-sankey defaults (see [`Sankey::default`]).
194    pub fn new() -> Self {
195        Self::default()
196    }
197
198    /// Set the node rectangle width. Defaults to 24.
199    pub fn node_width(mut self, node_width: f32) -> Self {
200        self.node_width = node_width;
201        self
202    }
203
204    /// Set the vertical gap between nodes in a column. Defaults to 8.
205    pub fn node_padding(mut self, node_padding: f32) -> Self {
206        self.node_padding = node_padding;
207        self
208    }
209
210    /// Set the node alignment. Defaults to [`SankeyAlign::Justify`].
211    pub fn node_align(mut self, align: SankeyAlign) -> Self {
212        self.align = align;
213        self
214    }
215
216    /// Set the number of relaxation passes. Defaults to 6.
217    pub fn iterations(mut self, iterations: usize) -> Self {
218        self.iterations = iterations;
219        self
220    }
221
222    /// Set how values map to heights. Defaults to [`SankeyValueScale::Linear`].
223    pub fn value_scale(mut self, value_scale: SankeyValueScale) -> Self {
224        self.value_scale = value_scale;
225        self
226    }
227
228    /// Set the layout bounds as `[[x0, y0], [x1, y1]]`. Defaults to `[[0, 0], [1, 1]]`.
229    pub fn extent(mut self, x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
230        self.x0 = x0;
231        self.y0 = y0;
232        self.x1 = x1;
233        self.y1 = y1;
234        self
235    }
236
237    /// Equivalent to `extent(0., 0., width, height)`.
238    pub fn size(self, width: f32, height: f32) -> Self {
239        self.extent(0., 0., width, height)
240    }
241
242    /// Compute the topology only: node `value`, `depth`, `height`, `layer`
243    /// and horizontal positions, without the vertical placement. Much cheaper
244    /// than [`Sankey::layout`] when only the column structure is needed
245    /// (e.g. to measure labels before fixing the extent).
246    ///
247    /// `node_count` is the number of nodes; links reference nodes by index
248    /// (d3-sankey's default `nodeId`). Returns an error if a link references
249    /// a node out of range or the graph contains a cycle.
250    pub fn topology(
251        &self,
252        node_count: usize,
253        links: &[SankeyLink],
254    ) -> Result<SankeyGraph, SankeyError> {
255        for link in links {
256            if link.source >= node_count {
257                return Err(SankeyError::MissingNode(link.source));
258            }
259            if link.target >= node_count {
260                return Err(SankeyError::MissingNode(link.target));
261            }
262        }
263
264        let mut graph = SankeyGraph {
265            nodes: (0..node_count)
266                .map(|index| SankeyNodeLayout {
267                    index,
268                    ..Default::default()
269                })
270                .collect(),
271            links: links
272                .iter()
273                .enumerate()
274                .map(|(index, link)| SankeyLinkLayout {
275                    index,
276                    source: link.source,
277                    target: link.target,
278                    // Layout works in scaled value space; all downstream math
279                    // (node values, widths, breadths) stays coherent because
280                    // it is additive in this space, so nodes remain exactly
281                    // filled by their ribbons regardless of the scale.
282                    value: self.value_scale.apply(link.value),
283                    y0: 0.,
284                    y1: 0.,
285                    width: 0.,
286                    source_width: 0.,
287                    target_width: 0.,
288                })
289                .collect(),
290        };
291        if node_count == 0 {
292            return Ok(graph);
293        }
294
295        compute_node_links(&mut graph);
296        compute_node_values(&mut graph);
297        compute_node_ranks(&mut graph)?;
298        self.compute_node_layers(&mut graph);
299
300        Ok(graph)
301    }
302
303    /// Compute the full layout: [`Sankey::topology`] plus the vertical node
304    /// placement and link breadths.
305    pub fn layout(
306        &self,
307        node_count: usize,
308        links: &[SankeyLink],
309    ) -> Result<SankeyGraph, SankeyError> {
310        Ok(self.layout_from(self.topology(node_count, links)?))
311    }
312
313    /// Complete the layout for a graph produced by [`Sankey::topology`],
314    /// avoiding a second topology pass when the extent only becomes known
315    /// after measuring against the column structure (e.g. label margins).
316    ///
317    /// The topological fields are extent-independent; the horizontal and
318    /// vertical positions are recomputed for this generator's extent.
319    pub fn layout_from(&self, mut graph: SankeyGraph) -> SankeyGraph {
320        if graph.nodes.is_empty() {
321            return graph;
322        }
323
324        self.compute_node_layers(&mut graph);
325
326        let mut columns = vec![Vec::new(); graph.layer_count()];
327        for node in &graph.nodes {
328            columns[node.layer].push(node.index);
329        }
330
331        self.compute_node_breadths(&mut graph, &mut columns);
332        compute_link_breadths(&mut graph);
333        self.center_columns(&mut graph);
334        self.stagger_flat_columns(&mut graph);
335
336        graph
337    }
338
339    /// Vertically center each column's stack of nodes within the extent.
340    ///
341    /// When a crowded column forces a small scale, the sparser columns don't
342    /// fill the height and the relaxation aligns them to their flows' weighted
343    /// center, which leaves the trunk sitting high with empty space below.
344    /// d3-sankey doesn't correct this; centering each column (translating it,
345    /// so the relaxation's within-column arrangement and the ribbon fits are
346    /// preserved) keeps the diagram balanced on the canvas.
347    fn center_columns(&self, graph: &mut SankeyGraph) {
348        let layers = graph.layer_count();
349        if layers == 0 {
350            return;
351        }
352
353        // Per-layer bounding box, then the offset that centers it.
354        let mut lo = vec![f32::INFINITY; layers];
355        let mut hi = vec![f32::NEG_INFINITY; layers];
356        for node in &graph.nodes {
357            lo[node.layer] = lo[node.layer].min(node.y0);
358            hi[node.layer] = hi[node.layer].max(node.y1);
359        }
360        let offsets: Vec<f32> = (0..layers)
361            .map(|l| {
362                if lo[l].is_finite() && hi[l] > lo[l] {
363                    (self.y0 + self.y1 - lo[l] - hi[l]) / 2.
364                } else {
365                    0.
366                }
367            })
368            .collect();
369
370        apply_layer_offsets(graph, &offsets);
371    }
372
373    /// Nudge runs of adjacent single-node columns of (near-)equal height off
374    /// the shared center line. Centering aligns such columns exactly, so the
375    /// ribbon between them is a flat rectangle; a small alternating stagger
376    /// turns it into a gentle S-curve. Only the flat single-node case is
377    /// touched, so multi-node or unequal columns (which already curve) are
378    /// left alone.
379    fn stagger_flat_columns(&self, graph: &mut SankeyGraph) {
380        let layers = graph.layer_count();
381        if layers < 2 {
382            return;
383        }
384
385        let mut count = vec![0usize; layers];
386        let mut single = vec![usize::MAX; layers];
387        for node in &graph.nodes {
388            count[node.layer] += 1;
389            single[node.layer] = node.index;
390        }
391        let heights: Vec<f32> = (0..layers)
392            .map(|l| {
393                if count[l] == 1 {
394                    let n = &graph.nodes[single[l]];
395                    n.y1 - n.y0
396                } else {
397                    0.
398                }
399            })
400            .collect();
401
402        // Offset the odd columns of each flat run, leaving the even ones on
403        // the center line, so consecutive ribbons bend down then back up.
404        let mut offsets = vec![0f32; layers];
405        let mut run = 0usize;
406        for l in 1..layers {
407            let flat =
408                count[l] == 1 && count[l - 1] == 1 && (heights[l] - heights[l - 1]).abs() < 1e-3;
409            if flat {
410                run += 1;
411                if run % 2 == 1 {
412                    // Bound the nudge by the slack so the node stays inside
413                    // the extent (a column that fills the height can't move).
414                    let slack = (self.y1 - self.y0 - heights[l]).max(0.);
415                    offsets[l] = (heights[l] * STAGGER_RATIO).min(slack / 2.);
416                }
417            } else {
418                run = 0;
419            }
420        }
421
422        apply_layer_offsets(graph, &offsets);
423    }
424
425    fn align_layer(&self, graph: &SankeyGraph, index: usize, n: usize) -> usize {
426        let node = &graph.nodes[index];
427        match self.align {
428            SankeyAlign::Left => node.depth,
429            SankeyAlign::Right => n - 1 - node.height,
430            SankeyAlign::Justify => {
431                if node.source_links.is_empty() {
432                    n - 1
433                } else {
434                    node.depth
435                }
436            }
437            SankeyAlign::Center => {
438                if !node.target_links.is_empty() {
439                    node.depth
440                } else if !node.source_links.is_empty() {
441                    node.source_links
442                        .iter()
443                        .map(|&link| graph.nodes[graph.links[link].target].depth)
444                        .min()
445                        .unwrap_or(1)
446                        .saturating_sub(1)
447                } else {
448                    0
449                }
450            }
451        }
452    }
453
454    fn compute_node_layers(&self, graph: &mut SankeyGraph) {
455        let n = graph
456            .nodes
457            .iter()
458            .map(|node| node.depth + 1)
459            .max()
460            .unwrap_or(0);
461        let kx = if n > 1 {
462            (self.x1 - self.x0 - self.node_width) / (n - 1) as f32
463        } else {
464            0.
465        };
466
467        let layers: Vec<usize> = (0..graph.nodes.len())
468            .map(|index| self.align_layer(graph, index, n).min(n - 1))
469            .collect();
470
471        for (index, layer) in layers.into_iter().enumerate() {
472            let node = &mut graph.nodes[index];
473            node.layer = layer;
474            node.x0 = self.x0 + layer as f32 * kx;
475            node.x1 = node.x0 + self.node_width;
476        }
477    }
478
479    fn compute_node_breadths(&self, graph: &mut SankeyGraph, columns: &mut [Vec<usize>]) {
480        let max_column_len = columns.iter().map(|column| column.len()).max().unwrap_or(0);
481        let py = if max_column_len > 1 {
482            self.node_padding
483                .min((self.y1 - self.y0) / (max_column_len - 1) as f32)
484        } else {
485            self.node_padding
486        };
487
488        self.initialize_node_breadths(graph, columns, py);
489
490        for i in 0..self.iterations {
491            let alpha = 0.99_f32.powi(i as i32);
492            let beta = (1. - alpha).max((i + 1) as f32 / self.iterations as f32);
493            self.relax_right_to_left(graph, columns, alpha, beta, py);
494            self.relax_left_to_right(graph, columns, alpha, beta, py);
495        }
496    }
497
498    fn initialize_node_breadths(&self, graph: &mut SankeyGraph, columns: &[Vec<usize>], py: f32) {
499        // Scale factor between flow value and pixels. d3 lets an over-crowded
500        // column produce a negative ky; clamp to zero so heights never invert.
501        let mut ky = f32::INFINITY;
502        for column in columns {
503            let value_sum: f64 = column.iter().map(|&index| graph.nodes[index].value).sum();
504            if value_sum > 0. {
505                let k = (self.y1 - self.y0 - (column.len() - 1) as f32 * py) / value_sum as f32;
506                ky = ky.min(k);
507            }
508        }
509        if !ky.is_finite() {
510            ky = 0.;
511        }
512        ky = ky.max(0.);
513
514        for column in columns {
515            let mut y = self.y0;
516            for &index in column {
517                let node_height = graph.nodes[index].value as f32 * ky;
518                let node = &mut graph.nodes[index];
519                node.y0 = y;
520                node.y1 = y + node_height;
521                y = node.y1 + py;
522            }
523
524            // Distribute the leftover vertical space evenly (d3 keeps this
525            // signed: an over-crowded column shifts nodes back up).
526            let leftover = (self.y1 - y + py) / (column.len() + 1) as f32;
527            for (i, &index) in column.iter().enumerate() {
528                let node = &mut graph.nodes[index];
529                let dy = leftover * (i + 1) as f32;
530                node.y0 += dy;
531                node.y1 += dy;
532            }
533        }
534
535        for link in &mut graph.links {
536            link.width = link.value as f32 * ky;
537        }
538
539        for column in columns {
540            for &index in column {
541                sort_source_links(graph, index);
542                sort_target_links(graph, index);
543            }
544        }
545    }
546
547    /// Reposition each node based on its incoming links.
548    fn relax_left_to_right(
549        &self,
550        graph: &mut SankeyGraph,
551        columns: &mut [Vec<usize>],
552        alpha: f32,
553        beta: f32,
554        py: f32,
555    ) {
556        for i in 1..columns.len() {
557            for &target in &columns[i] {
558                let mut y = 0.;
559                let mut w = 0.;
560                for &link_index in &graph.nodes[target].target_links {
561                    let link = &graph.links[link_index];
562                    let v = link.value as f32
563                        * (graph.nodes[target].layer as f32
564                            - graph.nodes[link.source].layer as f32);
565                    y += target_top(graph, link.source, target, py) * v;
566                    w += v;
567                }
568                if w <= 0. {
569                    continue;
570                }
571                let dy = (y / w - graph.nodes[target].y0) * alpha;
572                graph.nodes[target].y0 += dy;
573                graph.nodes[target].y1 += dy;
574                reorder_node_links(graph, target);
575            }
576            sort_column(graph, &mut columns[i]);
577            self.resolve_collisions(graph, &columns[i], beta, py);
578        }
579    }
580
581    /// Reposition each node based on its outgoing links.
582    fn relax_right_to_left(
583        &self,
584        graph: &mut SankeyGraph,
585        columns: &mut [Vec<usize>],
586        alpha: f32,
587        beta: f32,
588        py: f32,
589    ) {
590        for i in (0..columns.len().saturating_sub(1)).rev() {
591            for &source in &columns[i] {
592                let mut y = 0.;
593                let mut w = 0.;
594                for &link_index in &graph.nodes[source].source_links {
595                    let link = &graph.links[link_index];
596                    let v = link.value as f32
597                        * (graph.nodes[link.target].layer as f32
598                            - graph.nodes[source].layer as f32);
599                    y += source_top(graph, source, link.target, py) * v;
600                    w += v;
601                }
602                if w <= 0. {
603                    continue;
604                }
605                let dy = (y / w - graph.nodes[source].y0) * alpha;
606                graph.nodes[source].y0 += dy;
607                graph.nodes[source].y1 += dy;
608                reorder_node_links(graph, source);
609            }
610            sort_column(graph, &mut columns[i]);
611            self.resolve_collisions(graph, &columns[i], beta, py);
612        }
613    }
614
615    /// d3's middle-out collision resolution: push nodes away from the middle
616    /// node, then clamp the column against the extent edges.
617    fn resolve_collisions(&self, graph: &mut SankeyGraph, column: &[usize], beta: f32, py: f32) {
618        if column.is_empty() {
619            return;
620        }
621
622        let i = column.len() >> 1;
623        let subject_y0 = graph.nodes[column[i]].y0;
624        let subject_y1 = graph.nodes[column[i]].y1;
625        push_up(graph, &column[..i], subject_y0 - py, beta, py);
626        push_down(graph, &column[i + 1..], subject_y1 + py, beta, py);
627        push_up(graph, column, self.y1, beta, py);
628        push_down(graph, column, self.y0, beta, py);
629    }
630}
631
632/// Shift every node (and its attached link ends) by its layer's offset.
633/// Used by both column centering and flat-run staggering.
634fn apply_layer_offsets(graph: &mut SankeyGraph, offsets: &[f32]) {
635    // Precompute per-node offsets so the link loop doesn't borrow `nodes`
636    // while mutating `links`.
637    let node_offset: Vec<f32> = graph.nodes.iter().map(|n| offsets[n.layer]).collect();
638    for node in &mut graph.nodes {
639        let dy = node_offset[node.index];
640        node.y0 += dy;
641        node.y1 += dy;
642    }
643    for link in &mut graph.links {
644        link.y0 += node_offset[link.source];
645        link.y1 += node_offset[link.target];
646    }
647}
648
649fn compute_node_links(graph: &mut SankeyGraph) {
650    for index in 0..graph.links.len() {
651        let (source, target) = (graph.links[index].source, graph.links[index].target);
652        graph.nodes[source].source_links.push(index);
653        graph.nodes[target].target_links.push(index);
654    }
655}
656
657fn compute_node_values(graph: &mut SankeyGraph) {
658    for index in 0..graph.nodes.len() {
659        let outgoing: f64 = graph.nodes[index]
660            .source_links
661            .iter()
662            .map(|&link| graph.links[link].value)
663            .sum();
664        let incoming: f64 = graph.nodes[index]
665            .target_links
666            .iter()
667            .map(|&link| graph.links[link].value)
668            .sum();
669        graph.nodes[index].value = outgoing.max(incoming);
670    }
671}
672
673/// Assign the longest-path `depth` (from any source) and `height` (to any
674/// sink) from a single topological ordering. A node left unordered means its
675/// incoming links never resolved, i.e. a cycle.
676fn compute_node_ranks(graph: &mut SankeyGraph) -> Result<(), SankeyError> {
677    let n = graph.nodes.len();
678    let mut incoming: Vec<usize> = graph
679        .nodes
680        .iter()
681        .map(|node| node.target_links.len())
682        .collect();
683    // Doubles as the traversal queue and, once drained, the topological order.
684    let mut order: Vec<usize> = (0..n).filter(|&index| incoming[index] == 0).collect();
685
686    // Ranks live in their own arrays rather than in the nodes: the traversal
687    // hops between them by index, and the node struct is an order of magnitude
688    // wider than a `usize`.
689    let mut depths = vec![0usize; n];
690    let mut visited = 0;
691    while visited < order.len() {
692        let index = order[visited];
693        visited += 1;
694        let depth = depths[index] + 1;
695        for &link in &graph.nodes[index].source_links {
696            let target = graph.links[link].target;
697            depths[target] = depths[target].max(depth);
698            incoming[target] -= 1;
699            if incoming[target] == 0 {
700                order.push(target);
701            }
702        }
703    }
704    if order.len() != n {
705        return Err(SankeyError::CircularLink);
706    }
707
708    // Walking the order backwards means every target's height is already final
709    // when its sources are visited.
710    let mut heights = vec![0usize; n];
711    for &index in order.iter().rev() {
712        for &link in &graph.nodes[index].source_links {
713            heights[index] = heights[index].max(heights[graph.links[link].target] + 1);
714        }
715    }
716
717    for (node, (depth, height)) in graph.nodes.iter_mut().zip(depths.into_iter().zip(heights)) {
718        node.depth = depth;
719        node.height = height;
720    }
721    Ok(())
722}
723
724/// Sort a node's outgoing links by the target node's `y0` (then link index).
725fn sort_source_links(graph: &mut SankeyGraph, index: usize) {
726    let mut links = std::mem::take(&mut graph.nodes[index].source_links);
727    // The link-index tie-break makes the order total, so an unstable sort
728    // is deterministic and avoids the stable sort's scratch allocation.
729    links.sort_unstable_by(|&a, &b| {
730        let ya = graph.nodes[graph.links[a].target].y0;
731        let yb = graph.nodes[graph.links[b].target].y0;
732        ya.partial_cmp(&yb)
733            .unwrap_or(std::cmp::Ordering::Equal)
734            .then(a.cmp(&b))
735    });
736    graph.nodes[index].source_links = links;
737}
738
739/// Sort a node's incoming links by the source node's `y0` (then link index).
740fn sort_target_links(graph: &mut SankeyGraph, index: usize) {
741    let mut links = std::mem::take(&mut graph.nodes[index].target_links);
742    links.sort_unstable_by(|&a, &b| {
743        let ya = graph.nodes[graph.links[a].source].y0;
744        let yb = graph.nodes[graph.links[b].source].y0;
745        ya.partial_cmp(&yb)
746            .unwrap_or(std::cmp::Ordering::Equal)
747            .then(a.cmp(&b))
748    });
749    graph.nodes[index].target_links = links;
750}
751
752/// After a node moved, re-sort the link lists of its neighbors on the
753/// opposite ends (d3's reorderNodeLinks).
754///
755/// Iterates by position to avoid cloning the link lists on this hot path;
756/// the sorts only mutate the neighbors' lists, never the one being walked.
757fn reorder_node_links(graph: &mut SankeyGraph, index: usize) {
758    for i in 0..graph.nodes[index].target_links.len() {
759        let link = graph.nodes[index].target_links[i];
760        let source = graph.links[link].source;
761        sort_source_links(graph, source);
762    }
763    for i in 0..graph.nodes[index].source_links.len() {
764        let link = graph.nodes[index].source_links[i];
765        let target = graph.links[link].target;
766        sort_target_links(graph, target);
767    }
768}
769
770fn sort_column(graph: &SankeyGraph, column: &mut [usize]) {
771    column.sort_by(|&a, &b| {
772        graph.nodes[a]
773            .y0
774            .partial_cmp(&graph.nodes[b].y0)
775            .unwrap_or(std::cmp::Ordering::Equal)
776    });
777}
778
779/// Push overlapping nodes down (d3's resolveCollisionsTopToBottom).
780fn push_down(graph: &mut SankeyGraph, column: &[usize], mut y: f32, alpha: f32, py: f32) {
781    for &index in column {
782        let node = &mut graph.nodes[index];
783        let dy = (y - node.y0) * alpha;
784        if dy > 1e-6 {
785            node.y0 += dy;
786            node.y1 += dy;
787        }
788        y = node.y1 + py;
789    }
790}
791
792/// Push overlapping nodes up (d3's resolveCollisionsBottomToTop).
793fn push_up(graph: &mut SankeyGraph, column: &[usize], mut y: f32, alpha: f32, py: f32) {
794    for &index in column.iter().rev() {
795        let node = &mut graph.nodes[index];
796        let dy = (node.y1 - y) * alpha;
797        if dy > 1e-6 {
798            node.y0 -= dy;
799            node.y1 -= dy;
800        }
801        y = node.y0 - py;
802    }
803}
804
805/// The ideal `y0` for `target` so that its ribbon from `source` lines up
806/// with the slot the ribbon occupies in the source's outgoing stack
807/// (d3's targetTop).
808fn target_top(graph: &SankeyGraph, source: usize, target: usize, py: f32) -> f32 {
809    let source_node = &graph.nodes[source];
810    let mut y = source_node.y0 - source_node.source_links.len().saturating_sub(1) as f32 * py / 2.;
811    for &link_index in &source_node.source_links {
812        let link = &graph.links[link_index];
813        if link.target == target {
814            break;
815        }
816        y += link.width + py;
817    }
818    for &link_index in &graph.nodes[target].target_links {
819        let link = &graph.links[link_index];
820        if link.source == source {
821            break;
822        }
823        y -= link.width;
824    }
825    y
826}
827
828/// The ideal `y0` for `source` so that its ribbon to `target` lines up with
829/// the slot the ribbon occupies in the target's incoming stack
830/// (d3's sourceTop).
831fn source_top(graph: &SankeyGraph, source: usize, target: usize, py: f32) -> f32 {
832    let target_node = &graph.nodes[target];
833    let mut y = target_node.y0 - target_node.target_links.len().saturating_sub(1) as f32 * py / 2.;
834    for &link_index in &target_node.target_links {
835        let link = &graph.links[link_index];
836        if link.source == source {
837            break;
838        }
839        y += link.width + py;
840    }
841    for &link_index in &graph.nodes[source].source_links {
842        let link = &graph.links[link_index];
843        if link.target == target {
844            break;
845        }
846        y -= link.width;
847    }
848    y
849}
850
851/// Assign each link's `y0`/`y1` (ribbon centers) and per-end widths by
852/// stacking the sorted link lists within each node. Each side shares the
853/// node height in proportion to the link values, so both sides of a node
854/// are fully covered even when the graph is imbalanced (equivalent to the
855/// nominal `width` stacking when it is balanced).
856fn compute_link_breadths(graph: &mut SankeyGraph) {
857    for index in 0..graph.nodes.len() {
858        let node = &graph.nodes[index];
859        let node_y0 = node.y0;
860        let node_height = node.y1 - node.y0;
861
862        let outgoing: f64 = node
863            .source_links
864            .iter()
865            .map(|&link| graph.links[link].value)
866            .sum();
867        let mut y0 = node_y0;
868        for i in 0..graph.nodes[index].source_links.len() {
869            let link = &mut graph.links[graph.nodes[index].source_links[i]];
870            let width = if outgoing > 0. {
871                (link.value / outgoing) as f32 * node_height
872            } else {
873                0.
874            };
875            link.source_width = width;
876            link.y0 = y0 + width / 2.;
877            y0 += width;
878        }
879
880        let node = &graph.nodes[index];
881        let incoming: f64 = node
882            .target_links
883            .iter()
884            .map(|&link| graph.links[link].value)
885            .sum();
886        let mut y1 = node_y0;
887        for i in 0..graph.nodes[index].target_links.len() {
888            let link = &mut graph.links[graph.nodes[index].target_links[i]];
889            let width = if incoming > 0. {
890                (link.value / incoming) as f32 * node_height
891            } else {
892                0.
893            };
894            link.target_width = width;
895            link.y1 = y1 + width / 2.;
896            y1 += width;
897        }
898    }
899}
900
901/// Build the filled ribbon path for a link — the equivalent of d3-sankey's
902/// `sankeyLinkHorizontal()`: a horizontal cubic bezier with control points at
903/// the horizontal midpoint, thickened to the per-end link widths (clamped to
904/// `min_width` so tiny flows stay visible).
905pub fn sankey_link_path(
906    source: &SankeyNodeLayout,
907    target: &SankeyNodeLayout,
908    link: &SankeyLinkLayout,
909    min_width: f32,
910    origin: Point<Pixels>,
911) -> Option<Path<Pixels>> {
912    let source_half = link.source_width.max(min_width) / 2.;
913    let target_half = link.target_width.max(min_width) / 2.;
914    let sx = source.x1;
915    let tx = target.x0;
916    let mx = (sx + tx) / 2.;
917
918    let mut builder = PathBuilder::fill();
919    builder.move_to(origin_point(px(sx), px(link.y0 - source_half), origin));
920    builder.cubic_bezier_to(
921        origin_point(px(tx), px(link.y1 - target_half), origin),
922        origin_point(px(mx), px(link.y0 - source_half), origin),
923        origin_point(px(mx), px(link.y1 - target_half), origin),
924    );
925    builder.line_to(origin_point(px(tx), px(link.y1 + target_half), origin));
926    builder.cubic_bezier_to(
927        origin_point(px(sx), px(link.y0 + source_half), origin),
928        origin_point(px(mx), px(link.y1 + target_half), origin),
929        origin_point(px(mx), px(link.y0 + source_half), origin),
930    );
931    builder.close();
932    builder.build().ok()
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938
939    const EPSILON: f32 = 1e-3;
940
941    fn links(links: &[(usize, usize, f64)]) -> Vec<SankeyLink> {
942        links
943            .iter()
944            .map(|&(source, target, value)| SankeyLink::new(source, target, value))
945            .collect()
946    }
947
948    #[test]
949    fn test_sankey_builder() {
950        let sankey = Sankey::new();
951        assert_eq!(sankey.node_width, 24.);
952        assert_eq!(sankey.node_padding, 8.);
953        assert_eq!(sankey.align, SankeyAlign::Justify);
954        assert_eq!(sankey.iterations, 6);
955        assert_eq!(
956            (sankey.x0, sankey.y0, sankey.x1, sankey.y1),
957            (0., 0., 1., 1.)
958        );
959
960        let sankey = Sankey::new()
961            .node_width(12.)
962            .node_padding(10.)
963            .node_align(SankeyAlign::Left)
964            .iterations(10)
965            .size(400., 300.);
966        assert_eq!(sankey.node_width, 12.);
967        assert_eq!(sankey.node_padding, 10.);
968        assert_eq!(sankey.align, SankeyAlign::Left);
969        assert_eq!(sankey.iterations, 10);
970        assert_eq!(
971            (sankey.x0, sankey.y0, sankey.x1, sankey.y1),
972            (0., 0., 400., 300.)
973        );
974
975        let sankey = Sankey::new().extent(10., 20., 30., 40.);
976        assert_eq!(
977            (sankey.x0, sankey.y0, sankey.x1, sankey.y1),
978            (10., 20., 30., 40.)
979        );
980    }
981
982    #[test]
983    fn test_sankey_layout_chain() {
984        // A -> B -> C
985        let graph = Sankey::new()
986            .node_width(10.)
987            .size(100., 100.)
988            .layout(3, &links(&[(0, 1, 5.), (1, 2, 5.)]))
989            .unwrap();
990
991        let depths: Vec<usize> = graph.nodes.iter().map(|n| n.depth).collect();
992        let heights: Vec<usize> = graph.nodes.iter().map(|n| n.height).collect();
993        let layers: Vec<usize> = graph.nodes.iter().map(|n| n.layer).collect();
994        assert_eq!(depths, vec![0, 1, 2]);
995        assert_eq!(heights, vec![2, 1, 0]);
996        assert_eq!(layers, vec![0, 1, 2]);
997        assert_eq!(graph.layer_count(), 3);
998
999        assert_eq!(graph.nodes[0].x0, 0.);
1000        assert_eq!(graph.nodes[1].x0, 45.);
1001        assert_eq!(graph.nodes[2].x0, 90.);
1002        for node in &graph.nodes {
1003            assert_eq!(node.x1 - node.x0, 10.);
1004            assert_eq!(node.value, 5.);
1005            // Every node carries the full flow, so all span the full height.
1006            assert!((node.y1 - node.y0 - 100.).abs() < EPSILON);
1007        }
1008        for link in &graph.links {
1009            assert!((link.width - 100.).abs() < EPSILON);
1010            // The chain is balanced, so both ribbon ends span the nodes.
1011            assert!((link.source_width - 100.).abs() < EPSILON);
1012            assert!((link.target_width - 100.).abs() < EPSILON);
1013        }
1014
1015        // `topology` agrees with `layout` on the topological fields.
1016        let topology = Sankey::new()
1017            .node_width(10.)
1018            .size(100., 100.)
1019            .topology(3, &links(&[(0, 1, 5.), (1, 2, 5.)]))
1020            .unwrap();
1021        assert_eq!(topology.layer_count(), 3);
1022        for (a, b) in topology.nodes.iter().zip(&graph.nodes) {
1023            assert_eq!(a.depth, b.depth);
1024            assert_eq!(a.height, b.height);
1025            assert_eq!(a.layer, b.layer);
1026            assert_eq!(a.value, b.value);
1027            assert_eq!(a.x0, b.x0);
1028        }
1029
1030        // Completing a unit-extent topology on the final extent (the chart's
1031        // two-pass flow) matches a direct layout on that extent.
1032        let topology = Sankey::new()
1033            .node_width(10.)
1034            .topology(3, &links(&[(0, 1, 5.), (1, 2, 5.)]))
1035            .unwrap();
1036        let completed = Sankey::new()
1037            .node_width(10.)
1038            .size(100., 100.)
1039            .layout_from(topology);
1040        for (a, b) in completed.nodes.iter().zip(&graph.nodes) {
1041            assert_eq!((a.x0, a.y0, a.x1, a.y1), (b.x0, b.y0, b.x1, b.y1));
1042        }
1043        for (a, b) in completed.links.iter().zip(&graph.links) {
1044            assert_eq!((a.y0, a.y1, a.width), (b.y0, b.y1, b.width));
1045        }
1046    }
1047
1048    #[test]
1049    fn test_sankey_topology_large_chain() {
1050        const NODE_COUNT: usize = 50_000;
1051        let links: Vec<SankeyLink> = (0..NODE_COUNT - 1)
1052            .map(|source| SankeyLink::new(source, source + 1, 1.))
1053            .collect();
1054        let graph = Sankey::new().topology(NODE_COUNT, &links).unwrap();
1055
1056        assert_eq!(graph.nodes[0].height, NODE_COUNT - 1);
1057        assert_eq!(graph.nodes[NODE_COUNT - 1].depth, NODE_COUNT - 1);
1058    }
1059
1060    #[test]
1061    fn test_sankey_alignment() {
1062        // A -> B -> C, plus a short branch A -> D.
1063        let links = links(&[(0, 1, 1.), (1, 2, 1.), (0, 3, 1.)]);
1064        let layers = |align: SankeyAlign| -> Vec<usize> {
1065            Sankey::new()
1066                .node_align(align)
1067                .size(100., 100.)
1068                .layout(4, &links)
1069                .unwrap()
1070                .nodes
1071                .iter()
1072                .map(|n| n.layer)
1073                .collect()
1074        };
1075
1076        assert_eq!(layers(SankeyAlign::Left), vec![0, 1, 2, 1]);
1077        assert_eq!(layers(SankeyAlign::Right), vec![0, 1, 2, 2]);
1078        assert_eq!(layers(SankeyAlign::Justify), vec![0, 1, 2, 2]);
1079        assert_eq!(layers(SankeyAlign::Center), vec![0, 1, 2, 1]);
1080    }
1081
1082    #[test]
1083    fn test_sankey_link_offsets() {
1084        // One source fanning out into two targets.
1085        let graph = Sankey::new()
1086            .node_width(10.)
1087            .size(100., 100.)
1088            .layout(3, &links(&[(0, 1, 30.), (0, 2, 10.)]))
1089            .unwrap();
1090
1091        let source = &graph.nodes[0];
1092        let source_height = source.y1 - source.y0;
1093        let total_width: f32 = graph.links.iter().map(|l| l.width).sum();
1094        assert!((total_width - source_height).abs() < EPSILON);
1095
1096        // Widths are proportional to values.
1097        assert!((graph.links[0].width / graph.links[1].width - 3.).abs() < EPSILON);
1098
1099        // Outgoing ribbons stack contiguously within the source node.
1100        let (first, second) = if graph.links[0].y0 < graph.links[1].y0 {
1101            (&graph.links[0], &graph.links[1])
1102        } else {
1103            (&graph.links[1], &graph.links[0])
1104        };
1105        assert!((first.y0 - first.source_width / 2. - source.y0).abs() < EPSILON);
1106        assert!(
1107            (first.y0 + first.source_width / 2. - (second.y0 - second.source_width / 2.)).abs()
1108                < EPSILON
1109        );
1110
1111        // Each target has a single incoming ribbon filling its full height.
1112        for link in &graph.links {
1113            let target = &graph.nodes[link.target];
1114            assert!((link.y1 - link.target_width / 2. - target.y0).abs() < EPSILON);
1115            assert!((link.y1 + link.target_width / 2. - target.y1).abs() < EPSILON);
1116        }
1117    }
1118
1119    #[test]
1120    fn test_sankey_imbalanced_link_widths() {
1121        // A -> B (10) but B -> C (7): B is sized by its incoming flow, and
1122        // its single outgoing ribbon is stretched to cover its outgoing side
1123        // while the ribbon's target end matches C's height.
1124        let graph = Sankey::new()
1125            .size(100., 100.)
1126            .layout(3, &links(&[(0, 1, 10.), (1, 2, 7.)]))
1127            .unwrap();
1128
1129        let node_b = &graph.nodes[1];
1130        let node_c = &graph.nodes[2];
1131        let out_link = &graph.links[1];
1132        assert!((out_link.source_width - (node_b.y1 - node_b.y0)).abs() < EPSILON);
1133        assert!((out_link.target_width - (node_c.y1 - node_c.y0)).abs() < EPSILON);
1134        // The two ends differ: B is taller (value 10) than C (value 7).
1135        assert!(out_link.source_width > out_link.target_width);
1136
1137        // Both ends stay centered on their nodes' filled ranges.
1138        assert!((out_link.y0 - (node_b.y0 + node_b.y1) / 2.).abs() < EPSILON);
1139        assert!((out_link.y1 - (node_c.y0 + node_c.y1) / 2.).abs() < EPSILON);
1140    }
1141
1142    #[test]
1143    fn test_sankey_sqrt_scale_fills_nodes() {
1144        // With the sqrt scale, a node's children must still exactly fill it:
1145        // the incoming and outgoing ribbon widths each sum to the node height
1146        // (no gaps), and the diagram is compressed vs linear.
1147        let links = links(&[(0, 1, 90.), (1, 2, 40.), (1, 3, 50.)]);
1148        let sqrt = Sankey::new()
1149            .value_scale(SankeyValueScale::Sqrt)
1150            .size(100., 100.)
1151            .layout(4, &links)
1152            .unwrap();
1153
1154        for node in &sqrt.nodes {
1155            let node_height = node.y1 - node.y0;
1156            let incoming: f32 = node
1157                .target_links
1158                .iter()
1159                .map(|&l| sqrt.links[l].target_width)
1160                .sum();
1161            let outgoing: f32 = node
1162                .source_links
1163                .iter()
1164                .map(|&l| sqrt.links[l].source_width)
1165                .sum();
1166            if !node.target_links.is_empty() {
1167                assert!((incoming - node_height).abs() < EPSILON);
1168            }
1169            if !node.source_links.is_empty() {
1170                assert!((outgoing - node_height).abs() < EPSILON);
1171            }
1172        }
1173
1174        // Two leaf nodes (50 and 40) show the sqrt compression directly:
1175        // their height ratio is sqrt(50/40), not the linear 50/40.
1176        let ratio = (sqrt.nodes[3].y1 - sqrt.nodes[3].y0) / (sqrt.nodes[2].y1 - sqrt.nodes[2].y0);
1177        assert!((ratio - (50f32 / 40.).sqrt()).abs() < 0.02);
1178    }
1179
1180    #[test]
1181    fn test_sankey_value_conservation() {
1182        // Incoming 10, outgoing 7: node value takes the max.
1183        let graph = Sankey::new()
1184            .size(100., 100.)
1185            .layout(3, &links(&[(0, 1, 10.), (1, 2, 7.)]))
1186            .unwrap();
1187
1188        assert_eq!(graph.nodes[1].value, 10.);
1189
1190        for node in &graph.nodes {
1191            assert!(node.y0 <= node.y1);
1192            assert!(node.y0 >= -EPSILON);
1193            assert!(node.y1 <= 100. + EPSILON);
1194        }
1195    }
1196
1197    #[test]
1198    fn test_sankey_vertical_centering() {
1199        // Each column's stack is centered in the extent: its midpoint equals
1200        // the extent midpoint. Uses multi-node columns so the flat-run stagger
1201        // (which only touches equal single-node columns) does not apply.
1202        let graph = Sankey::new()
1203            .node_padding(20.)
1204            .extent(0., 10., 100., 90.)
1205            .layout(
1206                5,
1207                &links(&[(0, 2, 40.), (1, 2, 10.), (2, 3, 25.), (2, 4, 25.)]),
1208            )
1209            .unwrap();
1210
1211        let layers = graph.layer_count();
1212        let mut lo = vec![f32::INFINITY; layers];
1213        let mut hi = vec![f32::NEG_INFINITY; layers];
1214        for node in &graph.nodes {
1215            lo[node.layer] = lo[node.layer].min(node.y0);
1216            hi[node.layer] = hi[node.layer].max(node.y1);
1217        }
1218        // Extent is [10, 90], midpoint 50; every column's midpoint matches.
1219        for l in 0..layers {
1220            assert!(((lo[l] + hi[l]) / 2. - 50.).abs() < EPSILON);
1221        }
1222        // Centering keeps every node within the extent.
1223        for node in &graph.nodes {
1224            assert!(node.y0 >= 10. - EPSILON);
1225            assert!(node.y1 <= 90. + EPSILON);
1226        }
1227    }
1228
1229    #[test]
1230    fn test_sankey_stagger_flat_columns() {
1231        // Two equal single-node columns feeding a fan-out: the sparse trunk
1232        // columns don't fill the height, so the equal pair is staggered off
1233        // the center line to curve the otherwise-flat ribbon between them.
1234        let graph = Sankey::new()
1235            .node_padding(20.)
1236            .size(100., 100.)
1237            .layout(
1238                6,
1239                &links(&[
1240                    (0, 1, 100.),
1241                    (1, 2, 40.),
1242                    (1, 3, 30.),
1243                    (1, 4, 20.),
1244                    (1, 5, 10.),
1245                ]),
1246            )
1247            .unwrap();
1248
1249        // Nodes 0 and 1 are the equal single-node columns; one is nudged off
1250        // center so their centers differ (the ribbon is no longer flat), but
1251        // both stay within the extent.
1252        let c0 = (graph.nodes[0].y0 + graph.nodes[0].y1) / 2.;
1253        let c1 = (graph.nodes[1].y0 + graph.nodes[1].y1) / 2.;
1254        assert!((c0 - c1).abs() > EPSILON);
1255        for node in &graph.nodes {
1256            assert!(node.y0 >= -EPSILON);
1257            assert!(node.y1 <= 100. + EPSILON);
1258        }
1259
1260        // After the stagger's per-layer shift, each ribbon end must still be
1261        // attached to its node: `y0` centered on the source node's outgoing
1262        // stack, `y1` on the target node's incoming stack. (Guards against a
1263        // source/target mix-up in `apply_layer_offsets`.)
1264        for node in &graph.nodes {
1265            let mut y = node.y0;
1266            for &l in &node.source_links {
1267                let link = &graph.links[l];
1268                assert!((link.y0 - (y + link.source_width / 2.)).abs() < EPSILON);
1269                y += link.source_width;
1270            }
1271            let mut y = node.y0;
1272            for &l in &node.target_links {
1273                let link = &graph.links[l];
1274                assert!((link.y1 - (y + link.target_width / 2.)).abs() < EPSILON);
1275                y += link.target_width;
1276            }
1277        }
1278    }
1279
1280    #[test]
1281    fn test_sankey_circular_link() {
1282        let sankey = Sankey::new().size(100., 100.);
1283
1284        assert_eq!(
1285            sankey
1286                .layout(2, &links(&[(0, 1, 1.), (1, 0, 1.)]))
1287                .unwrap_err(),
1288            SankeyError::CircularLink
1289        );
1290        assert_eq!(
1291            sankey.layout(1, &links(&[(0, 0, 1.)])).unwrap_err(),
1292            SankeyError::CircularLink
1293        );
1294        assert_eq!(
1295            sankey.layout(2, &links(&[(0, 5, 1.)])).unwrap_err(),
1296            SankeyError::MissingNode(5)
1297        );
1298    }
1299
1300    #[test]
1301    fn test_sankey_degenerate() {
1302        // Empty graph.
1303        let graph = Sankey::new().size(100., 100.).layout(0, &[]).unwrap();
1304        assert!(graph.nodes.is_empty());
1305        assert!(graph.links.is_empty());
1306        assert_eq!(graph.layer_count(), 0);
1307
1308        // All-zero link values must not produce NaN coordinates.
1309        let graph = Sankey::new()
1310            .size(100., 100.)
1311            .layout(2, &links(&[(0, 1, 0.)]))
1312            .unwrap();
1313        for node in &graph.nodes {
1314            assert!((node.y1 - node.y0).abs() < EPSILON);
1315            assert!(node.x0.is_finite() && node.x1.is_finite());
1316            assert!(node.y0.is_finite() && node.y1.is_finite());
1317        }
1318
1319        // Isolated nodes collapse into a single column without dividing by zero.
1320        let graph = Sankey::new().size(100., 100.).layout(2, &[]).unwrap();
1321        assert_eq!(graph.layer_count(), 1);
1322        for node in &graph.nodes {
1323            assert_eq!(node.x0, 0.);
1324            assert!(node.y0.is_finite() && node.y1.is_finite());
1325        }
1326    }
1327}