computeFluentSankeyLayout function
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,
);
}