diff --git a/modules/rflows/components/folk-flow-river.ts b/modules/rflows/components/folk-flow-river.ts index 828c356..2f24e1a 100644 --- a/modules/rflows/components/folk-flow-river.ts +++ b/modules/rflows/components/folk-flow-river.ts @@ -121,6 +121,9 @@ function computeLayout(nodes: FlowNode[]): RiverLayout { const funnelLayouts: FunnelLayout[] = []; + // Find max desiredOutflow to normalize pipe widths + const maxOutflow = Math.max(1, ...funnelNodes.map((n) => (n.data as FunnelNodeData).desiredOutflow || (n.data as FunnelNodeData).inflowRate || 1)); + for (let layer = 0; layer <= maxLayer; layer++) { const layerNodes = layerGroups.get(layer) || []; const layerY = funnelStartY + layer * (LAYER_HEIGHT + WATERFALL_HEIGHT + GAP); @@ -128,10 +131,13 @@ function computeLayout(nodes: FlowNode[]): RiverLayout { layerNodes.forEach((n, i) => { const data = n.data as FunnelNodeData; - const fillRatio = Math.min(1, data.currentValue / (data.maxCapacity || 1)); - // Pipe width = capacity (always full size), inner flow = fillRatio - const capacityRatio = Math.min(1, (data.maxCapacity || 1) / 90000); // normalize to largest typical capacity - const riverWidth = MIN_RIVER_WIDTH + capacityRatio * (MAX_RIVER_WIDTH - MIN_RIVER_WIDTH); + const outflow = data.desiredOutflow || data.inflowRate || 1; + const inflow = data.inflowRate || 0; + // Pipe width = desiredOutflow (what they need) + const outflowRatio = Math.min(1, outflow / maxOutflow); + const riverWidth = MIN_RIVER_WIDTH + outflowRatio * (MAX_RIVER_WIDTH - MIN_RIVER_WIDTH); + // Fill ratio = inflowRate / desiredOutflow (how funded they are) + const fillRatio = Math.min(1, inflow / (outflow || 1)); const x = -totalWidth / 2 + i * (SEGMENT_LENGTH + GAP * 2); const status: "healthy" | "overflow" | "critical" = data.currentValue > data.maxThreshold ? "overflow" : @@ -335,34 +341,38 @@ function renderFunnel(f: FunnelLayout): string { const threshold = f.data.sufficientThreshold ?? f.data.maxThreshold; const isSufficient = f.sufficiency === "sufficient" || f.sufficiency === "abundant"; - // Pipe capacity = full riverWidth (outer boundary) - // Active flow = inner height proportional to fillRatio + // Pipe = desiredOutflow (what they need), Flow = inflowRate (what they get) + const outflow = f.data.desiredOutflow || f.data.inflowRate || 1; + const inflow = f.data.inflowRate || 0; const flowHeight = Math.max(2, f.riverWidth * f.fillRatio); - const flowY = f.y + (f.riverWidth - flowHeight); // flow fills from bottom + const flowY = f.y + (f.riverWidth - flowHeight) / 2; // center the flow vertically + const fundingPct = Math.round(f.fillRatio * 100); + const underfunded = f.fillRatio < 0.95; + const flowColor = underfunded ? "#ef4444" : colors[0]; // red tint when underfunded return ` - - - + + + - - - + + + ${isSufficient ? `` : ""} - + - + - ${f.fillRatio > 0.02 ? [0, 1, 2].map((i) => ``).join("") : ""} + ${f.fillRatio > 0.02 ? [0, 1, 2].map((i) => ``).join("") : ""} ${esc(f.label)} - $${Math.floor(f.data.currentValue).toLocaleString()} / $${Math.floor(threshold).toLocaleString()} ${isSufficient ? "✨" : ""} + $${Math.floor(inflow).toLocaleString()} → $${Math.floor(outflow).toLocaleString()}/mo ${underfunded ? `(${fundingPct}%)` : "✨"} - `; + `; } function renderOutcome(o: OutcomeLayout): string {