From 6ab9790373aed43020bae0aa7f3ab96bce2cc6fa Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 17:26:05 -0700 Subject: [PATCH] fix(rflows): match overflow pipe widths to edge width formula Compute overflow/spending pipe widths as proportional shares of outflowWidthPx (matching edge formula: outflowWidthPx * flow/total) instead of independent globalMaxFlow scaling. Co-Authored-By: Claude Opus 4.6 --- modules/rflows/components/folk-flows-app.ts | 29 ++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/modules/rflows/components/folk-flows-app.ts b/modules/rflows/components/folk-flows-app.ts index 01d66fe..b437c92 100644 --- a/modules/rflows/components/folk-flows-app.ts +++ b/modules/rflows/components/folk-flows-app.ts @@ -2452,28 +2452,27 @@ class FolkFlowsApp extends HTMLElement { const inflowFillRatio = neededInflow > 0 ? Math.min(nf.totalInflow / neededInflow, 1) : 0; const inflowWidthPx = nf.totalInflow > 0 ? MIN_PX + (nf.totalInflow / globalMaxFlow) * (MAX_PX - MIN_PX) : MIN_PX; - // Overflow pipe width: excess beyond max threshold (funnels) or funding target (outcomes) - let overflowAmount = 0; - if (n.type === "funnel") { - const d = n.data as FunnelNodeData; - overflowAmount = Math.max(0, d.currentValue - d.maxThreshold); - } else if (n.type === "outcome") { - const d = n.data as OutcomeNodeData; - overflowAmount = Math.max(0, d.fundingReceived - d.fundingTarget); - } - const overflowWidthPx = overflowAmount > 0 ? MIN_PX + (overflowAmount / globalMaxFlow) * (MAX_PX - MIN_PX) : MIN_PX; - - // Spending pipe width: drain rate for funnels - let spendingAmount = 0; + // Overflow/spending pipe widths as proportional shares of outflowWidthPx + // (matches how edges compute: outflowWidthPx * edgeFlow / totalOutflow) + let overflowFlow = 0; + let spendingFlow = 0; if (n.type === "funnel") { const d = n.data as FunnelNodeData; + const excess = Math.max(0, d.currentValue - d.maxThreshold); + for (const alloc of d.overflowAllocations) overflowFlow += excess * (alloc.percentage / 100); let rateMultiplier: number; if (d.currentValue > d.maxThreshold) rateMultiplier = 0.8; else if (d.currentValue >= d.minThreshold) rateMultiplier = 0.5; else rateMultiplier = 0.1; - spendingAmount = d.inflowRate * rateMultiplier; + const drain = d.inflowRate * rateMultiplier; + for (const alloc of d.spendingAllocations) spendingFlow += drain * (alloc.percentage / 100); + } else if (n.type === "outcome") { + const d = n.data as OutcomeNodeData; + const excess = Math.max(0, d.fundingReceived - d.fundingTarget); + for (const alloc of (d.overflowAllocations || [])) overflowFlow += excess * (alloc.percentage / 100); } - const spendingWidthPx = spendingAmount > 0 ? MIN_PX + (spendingAmount / globalMaxFlow) * (MAX_PX - MIN_PX) : MIN_PX; + const overflowWidthPx = nf.totalOutflow > 0 ? outflowWidthPx * (overflowFlow / nf.totalOutflow) : 0; + const spendingWidthPx = nf.totalOutflow > 0 ? outflowWidthPx * (spendingFlow / nf.totalOutflow) : 0; this._currentFlowWidths.set(n.id, { totalOutflow: nf.totalOutflow, totalInflow: nf.totalInflow, outflowWidthPx, inflowWidthPx, inflowFillRatio, overflowWidthPx, spendingWidthPx }); }