Add funnel-style TBFF visualization with draggable thresholds

New FundingFunnel component with three distinct zones:
- Overflow zone (above MAX): Straight walls, excess redistribution
- Healthy zone (MIN to MAX): Straight walls, normal operations
- Critical zone (below MIN): Narrowing funnel, restricted outflow

Features:
- Draggable MIN/MAX threshold lines with real-time updates
- Animated inflow particles (blue) dropping from top
- Animated outflow particles (pink) from bottom spout
- Gradient liquid fill with glow effect
- Live balance simulation based on flow rates
- Status panel showing current balance, rates, progress
- Single/Multi-funnel view toggle
- Zone color coding (rose/emerald/amber)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-01-29 18:05:58 +00:00
parent 14ebf0853d
commit 4b0491e999
3 changed files with 887 additions and 238 deletions

398
DESIGN.md
View File

@ -2,268 +2,232 @@
## Core Concept: Threshold-Based Flow Funding (TBFF)
Unlike traditional discrete grants, TBFF enables **continuous resource flows** that respond dynamically to conditions. Funds stream like water through a network, with thresholds acting as gates that open, close, or modulate flow rates.
Unlike traditional discrete grants, TBFF enables **continuous resource flows** that respond dynamically to threshold conditions. The funnel visualization makes these dynamics intuitive and interactive.
---
## Visual Metaphors
## The Funnel Metaphor
### 1. The River System
- **Sources** = Springs/tributaries (funding origins)
- **Flows** = Rivers/streams (animated fund movement)
- **Recipients** = Lakes/deltas (fund destinations)
- **Thresholds** = Dams/locks (control points)
The funding funnel has three distinct zones defined by minimum and maximum thresholds:
### 2. The Circulatory System
- **Sources** = Heart (pumping resources)
- **Flows** = Arteries/veins (distribution network)
- **Recipients** = Organs (entities needing resources)
- **Thresholds** = Valves (flow regulation)
```
════════════════════════
│ │ OVERFLOW ZONE
│ Excess funds │ (above MAX)
│ redistribute │
MAX ─ ─ ─ ─ ─ ─ ┼──────────────────────┼ ─ ─ ─ ─ ─ ─
│ │
│ HEALTHY ZONE │ STRAIGHT WALLS
│ Normal operations │ (MIN to MAX)
│ │
MIN ─ ─ ─ ─ ─ ─ ┼──────────────────────┼ ─ ─ ─ ─ ─ ─
╲ CRITICAL ZONE NARROWING FUNNEL
╲ Restricted (below MIN)
╲ outflow
╲________
║║
outflow
```
### 3. The Electrical Grid
- **Sources** = Power plants (generation)
- **Flows** = Transmission lines (distribution)
- **Recipients** = Consumers (utilization)
- **Thresholds** = Circuit breakers (protection/regulation)
### Zone Behaviors
| Zone | Shape | Behavior |
|------|-------|----------|
| **Overflow** (above MAX) | Straight walls | Excess funds spill over, can redirect to other pools |
| **Healthy** (MIN to MAX) | Straight walls | Normal flow rate, balanced operations |
| **Critical** (below MIN) | Narrowing funnel | Outflow restricted, conservation mode |
---
## Key Visual Elements
## Visual Elements
### Flow Nodes
### Animated Flows
**Inflow (blue particles)**
- Drops falling from above into the funnel
- Speed/density indicates inflow rate
- Particles merge into the liquid surface
**Outflow (pink particles)**
- Drops falling from the bottom spout
- Throttled in critical zone (fewer/slower)
- Full rate in healthy zone
**Liquid Fill**
- Gradient fill showing current balance level
- Subtle glow/pulse animation
- Wave effect at surface
### Threshold Indicators
**Minimum Threshold (Rose/Red)**
```
┌─────────────────────────────────────────────────────────┐
│ SOURCE NODE │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ╭──────────────╮ │ │
│ │ │ Treasury │ Balance: $50,000 │ │
│ │ │ 💰 │ Flow Rate: $100/hr │ │
│ │ ╰──────┬───────╯ Status: ● Active │ │
│ │ │ │ │
│ │ ═════╧═════▶ (animated particles) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ RECIPIENT NODE │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ═════╤═════▶ (incoming flow) │ │
│ │ │ │ │
│ │ ╭──────┴───────╮ │ │
│ │ │ Project │ Received: $12,340 │ │
│ │ │ 🎯 │ Rate: $85/hr │ │
│ │ ╰──────────────╯ Health: ████████░░ 80% │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
════════════╳════════════ ← Draggable handle
MIN $20,000
```
### Threshold Visualization
**Maximum Threshold (Amber/Yellow)**
```
THRESHOLD STATES:
Below Threshold At Threshold Above Threshold
(Flow Blocked) (Flow Starting) (Full Flow)
╱╲ ╱╲ ╱╲
⛔ ╲ ⚠️ ╲ ✅ ╲
╱──────╲ ╱──────╲ ╱──────╲
│░░░░░░│ │▓▓▓░░░│ │██████│
│░░░░░░│ │▓▓▓░░░│ │██████│
└──────┘ └──────┘ └──────┘
$2,000 $4,500 $8,000
─────── ─────── ───────
Threshold: $5,000 Threshold: $5,000 Threshold: $5,000
════════════╳════════════ ← Draggable handle
MAX $80,000
```
### Flow Animation Styles
### Status Panel
```
1. PARTICLE FLOW (recommended)
○───○───○───○───○───▶
Dots flowing along the path, speed indicates rate
2. GRADIENT PULSE
████▓▓▒▒░░░░░░░░░░░▶
Color gradient pulses along the line
3. DASHED ANIMATION
─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶
Animated dashes moving toward recipient
4. THICKNESS ENCODING
═══════════════════▶ High flow
────────────────────▶ Medium flow
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ Low flow
┌─────────────────────────────┐
│ Current Balance │
│ ████████████████░░░░░░░░ │
│ $45,000 │
│ ● Healthy Range │
├─────────────────────────────┤
│ Flow Rates │
│ ↓ Inflow: +$500/hr │
│ ↑ Outflow: -$300/hr │
│ ═ Net: +$200/hr │
├─────────────────────────────┤
│ Progress │
│ To MIN: ████████░░ 80% │
│ To MAX: ██████░░░░ 60% │
└─────────────────────────────┘
```
---
## Interactive Features
## Interaction Design
### 1. Threshold Slider
Users can drag thresholds to see how changes affect flow dynamics:
### Dragging Thresholds
1. **Hover** over threshold line → cursor changes to resize
2. **Click & drag** up/down to adjust value
3. **Real-time preview** of zone changes
4. **Constraints**: MIN cannot exceed MAX, minimum gap enforced
### Visual Feedback During Drag
```
┌────────────────────────────────────────────────────────┐
│ THRESHOLD CONTROL │
│ │
│ Min ├────────────●────────────────────────────┤ Max │
│ $0 $5,000 $50,000 │
│ ▲ │
│ │ │
│ Current: $5,000 │
│ │
│ Flow activates when source balance > threshold │
└────────────────────────────────────────────────────────┘
Before drag: During drag: After release:
════════════ ════════════════ ════════════
│ ║ (thicker) │
│ ║ │
│ New value shown │
```
### 2. Flow Rate Dial
Adjust how fast funds flow when threshold is met:
### Responsive Behaviors
```
╭─────────────╮
│ ╲
│ ────●──── │
╲ │
│ ╲
╰──────┴──────╯
$10 $100 $1000
/hr
Current Rate: $50/hr
```
### 3. Time Simulation
Scrub through time to see how flows evolve:
```
┌─────────────────────────────────────────────────────────┐
│ ◀◀ ◀ ▶ ▶▶ │▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░│ │
│ ▲ │
│ NOW │
│ │
│ Jan 2025 ─────────────────────────────────── Dec 2025 │
└─────────────────────────────────────────────────────────┘
```
---
## Screen Layouts
### Main Canvas View
```
┌──────────────────────────────────────────────────────────────────┐
│ FLOW FUNDING [+] [⚙] [?] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ╭─────────╮ ╭─────────╮ │
│ │ Treasury│ │Project A│ │
│ │ $50K │══════════════════════════│ $12K │ │
│ ╰────┬────╯ ╰─────────╯ │
│ │
│ │ ┌──────┐ │
│ ├═════│ GATE │═══════╗ ╭─────────╮ │
│ │ │$5K TH│ ║ │Project B│ │
│ │ └──────┘ ╚══════════│ $8K │ │
│ │ ╰─────────╯ │
│ │ │
│ │ ╭─────────╮ │
│ ╰═══════════════════════════════│Project C│ │
│ │ $5K │ │
│ ╰─────────╯ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ Total Flowing: $300/hr │ Active Flows: 3 │ Blocked: 1 │
└──────────────────────────────────────────────────────────────────┘
```
### Detail Panel (on node click)
```
┌─────────────────────────────────────┐
│ PROJECT A [×] │
├─────────────────────────────────────┤
│ │
│ Current Balance $12,340.50 │
│ Incoming Rate $85.00/hr │
│ ──────────────────────────────── │
│ │
│ THRESHOLD STATUS │
│ ████████████████░░░░ 80% │
│ $12,340 / $15,000 threshold │
│ │
│ When threshold met: │
│ → Unlock outgoing flow to Proj D │
│ → Increase rate to $150/hr │
│ │
│ ──────────────────────────────── │
│ FLOW HISTORY │
│ ┌─────────────────────────────┐ │
│ │ ╱╲ ╱╲ │ │
│ │ ╲ ╱╲ │ │
│ │ ╲╱ ╲ ╲ │ │
│ │ ╲╱ ╲ │ │
│ └─────────────────────────────┘ │
│ Jan Feb Mar Apr May Jun │
│ │
└─────────────────────────────────────┘
```
| Balance State | Visual Response |
|---------------|-----------------|
| Approaching MIN | Rose tint intensifies, warning pulse |
| Crossing MIN ↓ | Funnel narrows animation, outflow slows |
| Crossing MIN ↑ | Funnel widens animation, outflow normalizes |
| Approaching MAX | Amber tint at top, overflow warning |
| Crossing MAX ↑ | Overflow particles, excess redistribution |
---
## Color System
| State | Color | Hex | Usage |
|-------|-------|-----|-------|
| Active Flow | Electric Blue | `#3B82F6` | Flowing connections |
| Threshold Met | Emerald | `#10B981` | Success states |
| Near Threshold | Amber | `#F59E0B` | Warning/attention |
| Below Threshold | Rose | `#F43F5E` | Blocked/inactive |
| Pending | Purple | `#8B5CF6` | Processing states |
| Background | Slate | `#0F172A` | Canvas background |
| Element | Color | Hex | Usage |
|---------|-------|-----|-------|
| Inflow | Blue | `#3B82F6` | Incoming fund particles |
| Outflow | Pink | `#EC4899` | Outgoing fund particles |
| Fill Gradient | Blue→Purple→Pink | gradient | Liquid level |
| Critical Zone | Rose | `#F43F5E` | Below minimum |
| Healthy Zone | Emerald | `#10B981` | Normal range |
| Overflow Zone | Amber | `#F59E0B` | Above maximum |
| Background | Slate | `#0F172A` | Canvas |
---
## Interaction Patterns
## Multi-Funnel View
### Creating a New Flow
1. Click source node → drag to recipient
2. Release to open threshold configuration
3. Set threshold amount and flow rate
4. Flow appears with animation
When displaying multiple funding pools:
### Adjusting Threshold
1. Click on threshold gate icon
2. Drag slider or enter exact value
3. See real-time preview of flow state change
4. Changes apply immediately or on confirm
```
┌─────────────────────────────────────────────────────────────────┐
│ Threshold-Based Flow Funding │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ╔═══════╗ ╔═══════╗ ╔═══════╗ │
│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │
│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │
│ ║░░░░░░░║ ║▓▓▓▓▓▓▓║ ║███████║ │
│ ║░░░░░░░║ ╚═══╤═══╝ ╚═══╤═══╝ │
│ ╚═══╤═══╝ │ │ │
│ │ │ │ │
│ Public Goods Research Emergency │
│ $12K / $30K $45K / $60K $85K / $70K │
│ ● Critical ● Healthy ● Overflow │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### Time Travel
1. Pause current state
2. Drag timeline scrubber
3. See historical or projected flow states
4. Resume real-time or stay at selected time
### Interconnected Flows
Future enhancement: Connect funnels so overflow from one feeds into another:
```
Fund A Fund B
╔═════╗ ╔═════╗
║█████║ ═══overflow═══▶ ║░░░░░║
║█████║ ║░░░░░║
╚══╤══╝ ╚══╤══╝
```
---
## Technical Implementation
### React Flow Integration
- Custom node types: `SourceNode`, `RecipientNode`, `ThresholdGate`
- Custom edge type: `AnimatedFlowEdge` with particle animation
- Minimap for large networks
- Pan/zoom controls
### Component Architecture
### Animation System
- Canvas-based particle system for flow animation
- 60fps smooth animations
- Performance-optimized for 100+ nodes
```
<FundingFunnel>
├── <SVG Canvas>
│ ├── Zone backgrounds (overflow, healthy, critical)
│ ├── Funnel outline path
│ ├── Liquid fill path (animated)
│ ├── Inflow particles (animated)
│ ├── Outflow particles (animated)
│ ├── Threshold lines (draggable)
│ └── Balance indicator
└── <Stats Panel>
├── Current balance display
├── Flow rates
└── Progress bars
```
### State Management
- Real-time flow calculations
- Threshold state machine
- Time-series data for history/projections
```typescript
interface FunnelState {
balance: number // Current fund level
minThreshold: number // Minimum threshold (draggable)
maxThreshold: number // Maximum threshold (draggable)
inflowRate: number // Funds per hour coming in
outflowRate: number // Funds per hour going out
maxCapacity: number // Visual ceiling
}
```
### Animation Loop
```typescript
// Simulation runs at 10Hz
setInterval(() => {
const netFlow = (inflowRate - outflowRate) / 3600 // per second
balance = clamp(balance + netFlow, 0, maxCapacity * 1.2)
}, 100)
```
---
## Future Enhancements
1. **Cascading Thresholds** — When one fund overflows, automatically route to connected funds
2. **Historical Playback** — Scrub through time to see past flow dynamics
3. **Prediction Mode** — Project future states based on current rates
4. **Smart Routing** — AI-suggested threshold adjustments for optimal distribution
5. **Real Data Integration** — Connect to actual treasury/DAO data sources
6. **Export/Share** — Save configurations and share visualizations

View File

@ -1,45 +1,154 @@
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
// Dynamic import to avoid SSR issues with React Flow
const FlowCanvas = dynamic(() => import('@/components/FlowCanvas'), {
const FundingFunnel = dynamic(() => import('@/components/FundingFunnel'), {
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center bg-slate-950">
<div className="text-slate-400 animate-pulse">Loading flow visualization...</div>
<div className="w-[500px] h-[600px] flex items-center justify-center bg-slate-900 rounded-2xl">
<div className="text-slate-400 animate-pulse">Loading visualization...</div>
</div>
),
})
export default function Home() {
const [view, setView] = useState<'single' | 'multi'>('single')
return (
<main className="h-screen w-screen overflow-hidden">
<main className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 p-8">
{/* Header */}
<header className="absolute top-0 left-0 right-0 z-10 bg-slate-900/80 backdrop-blur border-b border-slate-700">
<div className="px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-emerald-400 bg-clip-text text-transparent">
Flow Funding
<header className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 bg-clip-text text-transparent">
Threshold-Based Flow Funding
</h1>
<span className="text-xs text-slate-500 border border-slate-700 px-2 py-0.5 rounded">
TBFF Demo
</span>
<p className="text-slate-400 mt-2">
Interactive visualization of funding flows with minimum and maximum thresholds
</p>
</div>
<div className="flex items-center gap-4">
<button className="px-3 py-1.5 text-sm text-slate-300 hover:text-white transition">
Reset
<div className="flex gap-2">
<button
onClick={() => setView('single')}
className={`px-4 py-2 rounded-lg text-sm transition ${
view === 'single'
? 'bg-purple-600 text-white'
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
}`}
>
Single Funnel
</button>
<button className="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition">
+ Add Node
<button
onClick={() => setView('multi')}
className={`px-4 py-2 rounded-lg text-sm transition ${
view === 'multi'
? 'bg-purple-600 text-white'
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
}`}
>
Multi-Funnel
</button>
</div>
</div>
</header>
{/* Flow Canvas */}
<div className="pt-16 h-full">
<FlowCanvas />
{/* Legend */}
<div className="mb-8 flex items-center gap-8 text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-rose-500/30 border border-rose-500" />
<span className="text-slate-400">Critical Zone (below min)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-emerald-500/30 border border-emerald-500" />
<span className="text-slate-400">Healthy Zone (min to max)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded bg-amber-500/30 border border-amber-500" />
<span className="text-slate-400">Overflow Zone (above max)</span>
</div>
</div>
{/* Funnel Visualizations */}
{view === 'single' ? (
<div className="flex justify-center">
<FundingFunnel
name="Community Treasury"
currentBalance={35000}
minThreshold={20000}
maxThreshold={80000}
inflowRate={500}
outflowRate={300}
maxCapacity={100000}
/>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8 justify-items-center">
<FundingFunnel
name="Public Goods Fund"
currentBalance={45000}
minThreshold={25000}
maxThreshold={75000}
inflowRate={400}
outflowRate={350}
maxCapacity={100000}
/>
<FundingFunnel
name="Research Grant Pool"
currentBalance={12000}
minThreshold={30000}
maxThreshold={60000}
inflowRate={200}
outflowRate={150}
maxCapacity={80000}
/>
<FundingFunnel
name="Emergency Reserve"
currentBalance={85000}
minThreshold={50000}
maxThreshold={70000}
inflowRate={300}
outflowRate={100}
maxCapacity={100000}
/>
</div>
)}
{/* Instructions */}
<div className="mt-12 max-w-2xl mx-auto">
<div className="bg-slate-800/50 rounded-xl p-6 border border-slate-700">
<h3 className="text-lg font-semibold text-white mb-4">How It Works</h3>
<ul className="space-y-3 text-slate-300">
<li className="flex items-start gap-3">
<span className="text-rose-400 font-bold">1.</span>
<span>
<strong className="text-rose-400">Minimum Threshold</strong> Below this level,
the funnel narrows, restricting outflow. Funds are conserved until the minimum is reached.
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-emerald-400 font-bold">2.</span>
<span>
<strong className="text-emerald-400">Healthy Range</strong> Between min and max,
the funnel has straight walls. Normal operations with balanced in/out flows.
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-amber-400 font-bold">3.</span>
<span>
<strong className="text-amber-400">Maximum Threshold</strong> Above this level,
excess funds overflow and can be redistributed to other pools or purposes.
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-purple-400 font-bold"></span>
<span>
<strong className="text-purple-400">Drag the threshold lines</strong> to adjust
minimum and maximum levels interactively.
</span>
</li>
</ul>
</div>
</div>
</main>
)

View File

@ -0,0 +1,576 @@
'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
interface FundingFunnelProps {
name: string
currentBalance: number
minThreshold: number
maxThreshold: number
inflowRate: number // per hour
outflowRate: number // per hour
maxCapacity?: number
onMinThresholdChange?: (value: number) => void
onMaxThresholdChange?: (value: number) => void
}
export default function FundingFunnel({
name,
currentBalance,
minThreshold: initialMin,
maxThreshold: initialMax,
inflowRate,
outflowRate,
maxCapacity = 100000,
onMinThresholdChange,
onMaxThresholdChange,
}: FundingFunnelProps) {
const [minThreshold, setMinThreshold] = useState(initialMin)
const [maxThreshold, setMaxThreshold] = useState(initialMax)
const [balance, setBalance] = useState(currentBalance)
const [isDraggingMin, setIsDraggingMin] = useState(false)
const [isDraggingMax, setIsDraggingMax] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
// Funnel dimensions
const width = 300
const height = 500
const funnelTopWidth = 280
const funnelNarrowWidth = 80
const padding = 10
// Calculate Y positions for thresholds (inverted - 0 is at bottom)
const minY = height - (minThreshold / maxCapacity) * height
const maxY = height - (maxThreshold / maxCapacity) * height
const balanceY = height - (balance / maxCapacity) * height
// Funnel shape points
const funnelPath = `
M ${padding} ${maxY}
L ${padding} ${minY}
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
L ${width - padding} ${minY}
L ${width - padding} ${maxY}
L ${padding} ${maxY}
`
// Overflow zone (above max)
const overflowPath = `
M ${padding} ${padding}
L ${padding} ${maxY}
L ${width - padding} ${maxY}
L ${width - padding} ${padding}
Z
`
// Calculate fill path based on current balance
const getFillPath = () => {
if (balance <= 0) return ''
const fillY = Math.max(balanceY, padding)
if (balance >= maxThreshold) {
// Overflow zone - straight walls above max
const overflowY = Math.max(fillY, padding)
return `
M ${padding} ${minY}
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
L ${width - padding} ${minY}
L ${width - padding} ${overflowY}
L ${padding} ${overflowY}
Z
`
} else if (balance >= minThreshold) {
// Between min and max - straight walls
return `
M ${padding} ${minY}
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
L ${width - padding} ${minY}
L ${width - padding} ${fillY}
L ${padding} ${fillY}
Z
`
} else {
// Below min - in the funnel narrowing section
const ratio = balance / minThreshold
const bottomWidth = funnelNarrowWidth
const topWidth = funnelTopWidth - 2 * padding
const currentWidth = bottomWidth + (topWidth - bottomWidth) * ratio
const leftX = (width - currentWidth) / 2
const rightX = (width + currentWidth) / 2
return `
M ${(width - funnelNarrowWidth) / 2} ${height - padding}
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
L ${rightX} ${fillY}
L ${leftX} ${fillY}
Z
`
}
}
// Simulate balance changes
useEffect(() => {
const interval = setInterval(() => {
setBalance((prev) => {
const netFlow = (inflowRate - outflowRate) / 3600 // per second
const newBalance = prev + netFlow
return Math.max(0, Math.min(maxCapacity * 1.2, newBalance))
})
}, 100)
return () => clearInterval(interval)
}, [inflowRate, outflowRate, maxCapacity])
// Handle threshold dragging
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const y = e.clientY - rect.top
const value = Math.max(0, Math.min(maxCapacity, ((height - y) / height) * maxCapacity))
if (isDraggingMin) {
const newMin = Math.min(value, maxThreshold - 1000)
setMinThreshold(Math.max(0, newMin))
onMinThresholdChange?.(Math.max(0, newMin))
} else if (isDraggingMax) {
const newMax = Math.max(value, minThreshold + 1000)
setMaxThreshold(Math.min(maxCapacity, newMax))
onMaxThresholdChange?.(Math.min(maxCapacity, newMax))
}
},
[isDraggingMin, isDraggingMax, maxThreshold, minThreshold, maxCapacity, onMinThresholdChange, onMaxThresholdChange]
)
const handleMouseUp = useCallback(() => {
setIsDraggingMin(false)
setIsDraggingMax(false)
}, [])
useEffect(() => {
if (isDraggingMin || isDraggingMax) {
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
}
}
}, [isDraggingMin, isDraggingMax, handleMouseMove, handleMouseUp])
// Determine zone status
const getZoneStatus = () => {
if (balance < minThreshold) return { zone: 'critical', color: '#F43F5E', label: 'Below Minimum' }
if (balance > maxThreshold) return { zone: 'overflow', color: '#F59E0B', label: 'Overflow' }
return { zone: 'healthy', color: '#10B981', label: 'Healthy Range' }
}
const status = getZoneStatus()
return (
<div className="flex flex-col items-center">
<h3 className="text-xl font-bold text-white mb-2">{name}</h3>
<div className="flex gap-8">
{/* Main Funnel Visualization */}
<div
ref={containerRef}
className="relative bg-slate-900 rounded-2xl p-4 border border-slate-700"
style={{ width: width + 80, height: height + 40 }}
>
<svg width={width} height={height} className="overflow-visible">
<defs>
{/* Gradient for the fill */}
<linearGradient id={`fill-gradient-${name}`} x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" stopColor="#3B82F6" stopOpacity="0.9" />
<stop offset="50%" stopColor="#8B5CF6" stopOpacity="0.8" />
<stop offset="100%" stopColor="#EC4899" stopOpacity="0.7" />
</linearGradient>
{/* Glow filter */}
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
{/* Wave pattern for liquid effect */}
<pattern id="wave" x="0" y="0" width="20" height="10" patternUnits="userSpaceOnUse">
<path
d="M0 5 Q5 0 10 5 T20 5"
fill="none"
stroke="rgba(255,255,255,0.1)"
strokeWidth="1"
/>
</pattern>
</defs>
{/* Background zones */}
{/* Overflow zone (above max) */}
<rect
x={padding}
y={padding}
width={width - 2 * padding}
height={maxY - padding}
fill="#F59E0B"
fillOpacity="0.1"
stroke="#F59E0B"
strokeWidth="1"
strokeDasharray="4 4"
/>
{/* Healthy zone (between min and max) - straight walls */}
<rect
x={padding}
y={maxY}
width={width - 2 * padding}
height={minY - maxY}
fill="#10B981"
fillOpacity="0.1"
/>
{/* Funnel zone (below min) */}
<path
d={`
M ${padding} ${minY}
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
L ${width - padding} ${minY}
Z
`}
fill="#F43F5E"
fillOpacity="0.1"
/>
{/* Funnel outline */}
<path
d={`
M ${padding} ${padding}
L ${padding} ${minY}
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
L ${width - padding} ${minY}
L ${width - padding} ${padding}
`}
fill="none"
stroke="#475569"
strokeWidth="2"
/>
{/* Fill (current balance) */}
<path
d={getFillPath()}
fill={`url(#fill-gradient-${name})`}
filter="url(#glow)"
>
<animate
attributeName="opacity"
values="0.8;1;0.8"
dur="2s"
repeatCount="indefinite"
/>
</path>
{/* Animated inflow particles */}
{inflowRate > 0 && (
<>
{[...Array(5)].map((_, i) => (
<circle
key={`inflow-${i}`}
r="4"
fill="#3B82F6"
opacity="0.8"
>
<animate
attributeName="cy"
values={`-10;${balanceY}`}
dur={`${1 + i * 0.2}s`}
repeatCount="indefinite"
begin={`${i * 0.2}s`}
/>
<animate
attributeName="cx"
values={`${width / 2 - 20 + i * 10};${width / 2 - 10 + i * 5}`}
dur={`${1 + i * 0.2}s`}
repeatCount="indefinite"
begin={`${i * 0.2}s`}
/>
<animate
attributeName="opacity"
values="0.8;0.8;0"
dur={`${1 + i * 0.2}s`}
repeatCount="indefinite"
begin={`${i * 0.2}s`}
/>
</circle>
))}
</>
)}
{/* Animated outflow particles */}
{outflowRate > 0 && balance > 0 && (
<>
{[...Array(3)].map((_, i) => (
<circle
key={`outflow-${i}`}
r="3"
fill="#EC4899"
opacity="0.8"
>
<animate
attributeName="cy"
values={`${height - padding};${height + 30}`}
dur={`${0.8 + i * 0.15}s`}
repeatCount="indefinite"
begin={`${i * 0.25}s`}
/>
<animate
attributeName="cx"
values={`${width / 2 - 10 + i * 10};${width / 2 - 15 + i * 15}`}
dur={`${0.8 + i * 0.15}s`}
repeatCount="indefinite"
begin={`${i * 0.25}s`}
/>
<animate
attributeName="opacity"
values="0.8;0.6;0"
dur={`${0.8 + i * 0.15}s`}
repeatCount="indefinite"
begin={`${i * 0.25}s`}
/>
</circle>
))}
</>
)}
{/* Max threshold line (draggable) */}
<g
className="cursor-ns-resize"
onMouseDown={() => setIsDraggingMax(true)}
>
<line
x1={0}
y1={maxY}
x2={width}
y2={maxY}
stroke="#F59E0B"
strokeWidth={isDraggingMax ? 4 : 2}
strokeDasharray="8 4"
/>
<rect
x={width - 8}
y={maxY - 12}
width={16}
height={24}
rx={4}
fill="#F59E0B"
className="cursor-ns-resize"
/>
<text
x={width + 12}
y={maxY + 5}
fill="#F59E0B"
fontSize="12"
fontFamily="monospace"
>
MAX ${maxThreshold.toLocaleString()}
</text>
</g>
{/* Min threshold line (draggable) */}
<g
className="cursor-ns-resize"
onMouseDown={() => setIsDraggingMin(true)}
>
<line
x1={padding}
y1={minY}
x2={width - padding}
y2={minY}
stroke="#F43F5E"
strokeWidth={isDraggingMin ? 4 : 2}
strokeDasharray="8 4"
/>
<rect
x={width - 8}
y={minY - 12}
width={16}
height={24}
rx={4}
fill="#F43F5E"
className="cursor-ns-resize"
/>
<text
x={width + 12}
y={minY + 5}
fill="#F43F5E"
fontSize="12"
fontFamily="monospace"
>
MIN ${minThreshold.toLocaleString()}
</text>
</g>
{/* Current balance indicator */}
<g>
<line
x1={0}
y1={balanceY}
x2={padding - 2}
y2={balanceY}
stroke={status.color}
strokeWidth={3}
/>
<polygon
points={`${padding - 2},${balanceY - 6} ${padding - 2},${balanceY + 6} ${padding + 6},${balanceY}`}
fill={status.color}
/>
</g>
</svg>
{/* Zone labels */}
<div
className="absolute text-xs text-amber-400/60 font-medium"
style={{ right: 8, top: maxY / 2 + 20 }}
>
OVERFLOW
</div>
<div
className="absolute text-xs text-emerald-400/60 font-medium"
style={{ right: 8, top: (maxY + minY) / 2 + 20 }}
>
HEALTHY
</div>
<div
className="absolute text-xs text-rose-400/60 font-medium"
style={{ right: 8, top: (minY + height) / 2 + 10 }}
>
CRITICAL
</div>
</div>
{/* Stats Panel */}
<div className="flex flex-col gap-4 min-w-[200px]">
{/* Current Balance */}
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
<div className="text-sm text-slate-400 mb-1">Current Balance</div>
<div className="text-3xl font-bold font-mono" style={{ color: status.color }}>
${balance.toLocaleString(undefined, { maximumFractionDigits: 0 })}
</div>
<div
className="text-sm mt-2 px-2 py-1 rounded-full inline-block"
style={{ backgroundColor: `${status.color}20`, color: status.color }}
>
{status.label}
</div>
</div>
{/* Flow Rates */}
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
<div className="text-sm text-slate-400 mb-3">Flow Rates</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<span className="text-slate-300">Inflow</span>
</div>
<span className="font-mono text-blue-400">+${inflowRate}/hr</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-pink-500" />
<span className="text-slate-300">Outflow</span>
</div>
<span className="font-mono text-pink-400">-${outflowRate}/hr</span>
</div>
<div className="border-t border-slate-600 pt-3">
<div className="flex items-center justify-between">
<span className="text-slate-300">Net Flow</span>
<span
className={`font-mono font-bold ${
inflowRate - outflowRate >= 0 ? 'text-emerald-400' : 'text-rose-400'
}`}
>
{inflowRate - outflowRate >= 0 ? '+' : ''}${inflowRate - outflowRate}/hr
</span>
</div>
</div>
</div>
</div>
{/* Thresholds */}
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
<div className="text-sm text-slate-400 mb-3">Thresholds</div>
<div className="space-y-3">
<div>
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-rose-400">Minimum</span>
<span className="font-mono text-rose-400">${minThreshold.toLocaleString()}</span>
</div>
<div className="text-xs text-slate-500">
Drag the red line to adjust
</div>
</div>
<div>
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-amber-400">Maximum</span>
<span className="font-mono text-amber-400">${maxThreshold.toLocaleString()}</span>
</div>
<div className="text-xs text-slate-500">
Drag the yellow line to adjust
</div>
</div>
</div>
</div>
{/* Progress to thresholds */}
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
<div className="text-sm text-slate-400 mb-3">Progress</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">To Minimum</span>
<span className="text-slate-400">
{Math.min(100, (balance / minThreshold) * 100).toFixed(0)}%
</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-rose-500 transition-all duration-300"
style={{ width: `${Math.min(100, (balance / minThreshold) * 100)}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">To Maximum</span>
<span className="text-slate-400">
{Math.min(100, (balance / maxThreshold) * 100).toFixed(0)}%
</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-amber-500 transition-all duration-300"
style={{ width: `${Math.min(100, (balance / maxThreshold) * 100)}%` }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}