computeFluentSankeyLayout function

FluentSankeyLayoutResult computeFluentSankeyLayout({
  1. required FluentSankeyChartData data,
  2. required Size size,
  3. required double titleHeight,
  4. required bool isRtl,
  5. List<Color>? colorsForNodes,
  6. List<Color>? borderColorsForNodes,
})

Runs the whole upstream layout pipeline.

Ports _normalizeSankeyData (SankeyChart.tsx:681-731). The order matters: the d3 layout runs (:695), the one-percent normalisation rewrites node and link values (:714), the padding is retuned (:715), and then the layout runs a second time (:719). Upstream's comment at :716-718 says the second pass is what makes links hoverable; in fact it is also what makes the lifted values reach the geometry, because _computeNodeValues recomputes every node weight from its links.

Implementation

FluentSankeyLayoutResult computeFluentSankeyLayout({
  required FluentSankeyChartData data,
  required Size size,
  required double titleHeight,
  required bool isRtl,
  List<Color>? colorsForNodes,
  List<Color>? borderColorsForNodes,
}) {
  final colours = assignSankeyNodeColors(
    data.nodes,
    colorsForNodes: colorsForNodes,
    borderColorsForNodes: borderColorsForNodes,
  );
  if (data.nodes.isEmpty || data.links.isEmpty) {
    return FluentSankeyLayoutResult(
      data: data,
      nodes: const <SankeyLayoutNode>[],
      links: const <SankeyLayoutLink>[],
      nodeActualValues: const <double>[],
      linkUnnormalisedValues: const <double>[],
      nodeColors: colours.fills,
      nodeBorderColors: colours.borders,
      columnCount: 0,
      size: size,
      titleHeight: titleHeight,
    );
  }

  // `:286-299` — upstream clones the caller's data; here the layout objects are
  // new by construction, so the caller's immutable model is never touched.
  final nodes = <SankeyLayoutNode>[
    for (final node in data.nodes) SankeyLayoutNode(id: node.nodeId),
  ];
  final links = <SankeyLayoutLink>[
    for (final link in data.links)
      SankeyLayoutLink(
        source: nodes[link.source],
        target: nodes[link.target],
        value: link.value,
      ),
  ];

  // `:336-342` — the margins are fixed and not overridable by props.
  final sankey = Sankey()
    ..nodeWidth(kSankeyNodeWidth)
    ..extentOf(
      kSankeyMarginHorizontal,
      titleHeight,
      size.width - kSankeyMarginHorizontal,
      size.height - kSankeyMarginBottom,
    )
    ..nodeAlign(isRtl ? sankeyRight : sankeyJustify);

  sankey(nodes, links);

  final columns = groupSankeyNodesByColumn(nodes);
  final columnCount = columns.length;
  // `:712-713` — capture the post-first-pass values before anything mutates
  // them.
  final nodeValues = <double>[for (final node in nodes) node.value];
  final linkValues = <double>[for (final link in links) link.value];
  final actualValues = List<double>.filled(nodes.length, 0);
  final unnormalised = List<double?>.filled(links.length, null);

  adjustSankeyOnePercentHeightNodes(
    columns: columns,
    nodeValues: nodeValues,
    linkValues: linkValues,
    actualValues: actualValues,
    unnormalisedValues: unnormalised,
  );
  // `:715` passes the container height minus the margins, NOT the plot height
  // the layout returned. Reproduced verbatim.
  adjustSankeyPadding(
    sankey,
    size.height - titleHeight - kSankeyMarginBottom,
    columns,
  );
  sankey(nodes, links);

  return FluentSankeyLayoutResult(
    data: data,
    nodes: nodes,
    links: links,
    nodeActualValues: actualValues,
    // `:245-247` — `if (!link.unnormalizedValue)`, so a link the normalisation
    // never touched keeps its own value.
    linkUnnormalisedValues: <double>[
      for (var i = 0; i < links.length; i++) unnormalised[i] ?? linkValues[i],
    ],
    nodeColors: colours.fills,
    nodeBorderColors: colours.borders,
    columnCount: columnCount,
    size: size,
    titleHeight: titleHeight,
  );
}