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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-23 17:26:05 -07:00
parent 6bda676680
commit 6ab9790373
1 changed files with 14 additions and 15 deletions

View File

@ -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 });
}