From 29b68d57701e8e4a0746426cd8cbd2f194adc9cf Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 29 Jan 2026 19:59:34 +0000 Subject: [PATCH] Redesign funnel to ovary shape with three distinct zones - Bulbous top section (overflow zone) with curved sides - Straight middle section (healthy operating zone between min/max) - Angled/tapered bottom section narrowing to spout (outcomes) - Side handles positioned at overflow zone for funnel-to-funnel flows - Bottom handle at narrow spout for outcome/deliverable flows - Animated overflow arrows when overflowing - Threshold lines with MIN/MAX labels - Fluid fill with ripple effect animation Co-Authored-By: Claude Opus 4.5 --- components/nodes/FunnelNode.tsx | 250 ++++++++++++++++++++++++-------- 1 file changed, 191 insertions(+), 59 deletions(-) diff --git a/components/nodes/FunnelNode.tsx b/components/nodes/FunnelNode.tsx index ba47ea6..dd75b8f 100644 --- a/components/nodes/FunnelNode.tsx +++ b/components/nodes/FunnelNode.tsx @@ -34,11 +34,23 @@ function FunnelNode({ data, selected, id }: NodeProps) { const isCritical = currentValue < minThreshold const fillPercent = Math.min(100, (currentValue / maxCapacity) * 100) - // Simplified funnel dimensions - const width = 140 - const height = 100 - const topWidth = 120 - const bottomWidth = 40 + // Funnel dimensions - ovary-like shape + const width = 160 + const height = 140 + + // Calculate zone heights based on thresholds + const minPercent = minThreshold / maxCapacity + const maxPercent = maxThreshold / maxCapacity + + // Zone heights (from top to bottom) + const overflowZoneHeight = (1 - maxPercent) * height * 0.4 + 15 // Bulbous top + const healthyZoneHeight = (maxPercent - minPercent) * height * 0.8 + 30 // Straight middle + const drainZoneHeight = height - overflowZoneHeight - healthyZoneHeight // Angled bottom + + // Widths + const topWidth = 130 // Bulbous overflow area + const midWidth = 100 // Straight healthy zone + const bottomWidth = 30 // Narrow spout for outcomes // Double-click to edit const handleDoubleClick = useCallback((e: React.MouseEvent) => { @@ -331,7 +343,7 @@ function FunnelNode({ data, selected, id }: NodeProps) { ${selected ? 'border-blue-500 shadow-blue-200' : 'border-slate-200'} ${isEditing ? 'ring-2 ring-blue-400 ring-offset-2' : ''} `} - style={{ width: width + 40 }} + style={{ width: width + 60 }} onDoubleClick={handleDoubleClick} > {/* TOP Handle - INFLOWS */} @@ -354,50 +366,86 @@ function FunnelNode({ data, selected, id }: NodeProps) { - {/* Simplified Funnel View */} + {/* Ovary-shaped Funnel View */}
{/* Inflow indicator */} -
- In +
- + + + Inflow + +
- {/* Simple funnel shape with fill */} - + {/* Ovary-shaped funnel with three zones */} + + {/* Gradient for fluid fill */} - + - + + {/* Clip path for the ovary funnel shape */} + - {/* Funnel background */} + {/* Zone backgrounds */} + {/* Overflow zone (bulbous top) - amber */} - {/* Fill level */} - + {/* Healthy zone (straight middle) - green */} + + + {/* Drain zone (angled bottom) - darker */} + + + {/* Fluid fill - animated */} + + + {/* Ripple effect at top of fluid */} + + + - {/* Funnel outline */} + {/* Threshold lines */} + {/* Max threshold line */} + + MAX + + {/* Min threshold line */} + + MIN + + {/* Outline stroke for whole shape */} + + {/* Overflow arrows on sides */} + {isOverflowing && hasOverflow && ( + <> + {/* Left overflow arrow */} + + + + + + {/* Right overflow arrow */} + + + + + + + )} + + {/* Outcome flow arrow at bottom */} + {hasSpending && ( + + + + + + )} {/* Value */} @@ -438,27 +570,27 @@ function FunnelNode({ data, selected, id }: NodeProps) {
- {/* Simplified allocation bars */} -
- {/* Outflow (sides) */} -
- Out - {hasOverflow ? ( - renderSimpleBars(overflowAllocations, OVERFLOW_COLORS, 'horizontal') - ) : ( -
- )} -
+ {/* Flow indicators */} +
+ {/* Outflows (sides) */} + {hasOverflow && ( +
+ ← Out → +
+ {renderSimpleBars(overflowAllocations, OVERFLOW_COLORS, 'horizontal')} +
+
+ )} {/* Outcomes (bottom) */} -
- Spend - {hasSpending ? ( - renderSimpleBars(spendingAllocations, SPENDING_COLORS, 'horizontal') - ) : ( -
- )} -
+ {hasSpending && ( +
+ ↓ Fund +
+ {renderSimpleBars(spendingAllocations, SPENDING_COLORS, 'horizontal')} +
+
+ )}
@@ -466,20 +598,20 @@ function FunnelNode({ data, selected, id }: NodeProps) {
- {/* SIDE Handles - OUTFLOWS to other funnels */} + {/* SIDE Handles - OUTFLOWS at bulbous overflow zone */} {/* BOTTOM Handle - OUTCOMES/DELIVERABLES */}