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
|
// Overflow branches — from lip positions to target vessel top
|
||||||
const overflowBranches: BranchLayout[] = [];
|
const overflowBranches: BranchLayout[] = [];
|
||||||
funnelNodes.forEach((n) => {
|
funnelNodes.forEach((n) => {
|
||||||
|
|
@ -306,9 +324,10 @@ function computeLayout(nodes: FlowNode[]): RiverLayout {
|
||||||
const allocations = data.spendingAllocations || [];
|
const allocations = data.spendingAllocations || [];
|
||||||
if (allocations.length === 0) return;
|
if (allocations.length === 0) return;
|
||||||
const percentages = allocations.map((a) => a.percentage);
|
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 drainCx = parentLayout.x + parentLayout.vesselWidth / 2;
|
||||||
const startX = drainCx - DRAIN_WIDTH;
|
const startX = drainCx - drainSpan / 2;
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
allocations.forEach((alloc, i) => {
|
allocations.forEach((alloc, i) => {
|
||||||
const outcomeLayout = outcomeLayouts.find((o) => o.id === alloc.targetId);
|
const outcomeLayout = outcomeLayouts.find((o) => o.id === alloc.targetId);
|
||||||
|
|
@ -368,8 +387,9 @@ function renderWaterfall(wf: WaterfallLayout): string {
|
||||||
const height = wf.yEnd - wf.yStart;
|
const height = wf.yEnd - wf.yStart;
|
||||||
if (height <= 0) return "";
|
if (height <= 0) return "";
|
||||||
|
|
||||||
const topWidth = isInflow ? wf.farEndWidth : wf.riverEndWidth;
|
// Constant width throughout — Sankey-style ribbon (no taper → no S-curve)
|
||||||
const bottomWidth = isInflow ? wf.riverEndWidth : wf.farEndWidth;
|
const topWidth = wf.width;
|
||||||
|
const bottomWidth = wf.width;
|
||||||
const topCx = isInflow ? wf.xSource : wf.x;
|
const topCx = isInflow ? wf.xSource : wf.x;
|
||||||
const bottomCx = isInflow ? wf.x : wf.xSource;
|
const bottomCx = isInflow ? wf.x : wf.xSource;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue