fix(rflows): Sankey-style constant-width L-shaped ribbons for waterfalls
Three fixes for S-curve appearance: - Constant width throughout each waterfall (no taper between source/river) - Stack inflow waterfalls side-by-side at funnel top proportionally - Widen spending drain to 80% of vessel bottom width Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aa4a200f32
commit
8b43df5ce9
|
|
@ -266,6 +266,24 @@ function computeLayout(nodes: FlowNode[]): RiverLayout {
|
|||
});
|
||||
}
|
||||
|
||||
// Distribute inflows side-by-side at each funnel's top (Sankey stacking)
|
||||
const inflowsPerFunnel = new Map<string, WaterfallLayout[]>();
|
||||
sourceWaterfalls.forEach(wf => {
|
||||
if (!inflowsPerFunnel.has(wf.targetId)) inflowsPerFunnel.set(wf.targetId, []);
|
||||
inflowsPerFunnel.get(wf.targetId)!.push(wf);
|
||||
});
|
||||
inflowsPerFunnel.forEach((wfs, targetId) => {
|
||||
const target = funnelLayouts.find(f => f.id === targetId);
|
||||
if (!target || wfs.length <= 1) return;
|
||||
const totalW = wfs.reduce((s, w) => s + w.width, 0);
|
||||
const cx = target.x + target.vesselWidth / 2;
|
||||
let offset = -totalW / 2;
|
||||
wfs.forEach(wf => {
|
||||
wf.x = cx + offset + wf.width / 2;
|
||||
offset += wf.width;
|
||||
});
|
||||
});
|
||||
|
||||
// Overflow branches — from lip positions to target vessel top
|
||||
const overflowBranches: BranchLayout[] = [];
|
||||
funnelNodes.forEach((n) => {
|
||||
|
|
@ -306,9 +324,10 @@ function computeLayout(nodes: FlowNode[]): RiverLayout {
|
|||
const allocations = data.spendingAllocations || [];
|
||||
if (allocations.length === 0) return;
|
||||
const percentages = allocations.map((a) => a.percentage);
|
||||
const slotWidths = distributeWidths(percentages, DRAIN_WIDTH * 2, MIN_WATERFALL_WIDTH);
|
||||
const drainSpan = parentLayout.vesselBottomWidth * 0.8;
|
||||
const slotWidths = distributeWidths(percentages, drainSpan, MIN_WATERFALL_WIDTH);
|
||||
const drainCx = parentLayout.x + parentLayout.vesselWidth / 2;
|
||||
const startX = drainCx - DRAIN_WIDTH;
|
||||
const startX = drainCx - drainSpan / 2;
|
||||
let offsetX = 0;
|
||||
allocations.forEach((alloc, i) => {
|
||||
const outcomeLayout = outcomeLayouts.find((o) => o.id === alloc.targetId);
|
||||
|
|
@ -368,8 +387,9 @@ function renderWaterfall(wf: WaterfallLayout): string {
|
|||
const height = wf.yEnd - wf.yStart;
|
||||
if (height <= 0) return "";
|
||||
|
||||
const topWidth = isInflow ? wf.farEndWidth : wf.riverEndWidth;
|
||||
const bottomWidth = isInflow ? wf.riverEndWidth : wf.farEndWidth;
|
||||
// Constant width throughout — Sankey-style ribbon (no taper → no S-curve)
|
||||
const topWidth = wf.width;
|
||||
const bottomWidth = wf.width;
|
||||
const topCx = isInflow ? wf.xSource : wf.x;
|
||||
const bottomCx = isInflow ? wf.x : wf.xSource;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue