diff --git a/components/FlowCanvas.tsx b/components/FlowCanvas.tsx
index bd219ca..7d646cc 100644
--- a/components/FlowCanvas.tsx
+++ b/components/FlowCanvas.tsx
@@ -15,181 +15,184 @@ import {
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
-import SourceNode from './nodes/SourceNode'
-import ThresholdNode from './nodes/ThresholdNode'
-import RecipientNode from './nodes/RecipientNode'
-import type { FlowNode, FlowEdge, SourceNodeData, ThresholdNodeData, RecipientNodeData } from '@/lib/types'
+import FunnelNode from './nodes/FunnelNode'
+import type { FlowNode, FlowEdge, FunnelNodeData } from '@/lib/types'
const nodeTypes = {
- source: SourceNode,
- threshold: ThresholdNode,
- recipient: RecipientNode,
+ funnel: FunnelNode,
}
-// Vertical layout - sources at top, recipients at bottom
+// Color palette for allocations
+const COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#06b6d4']
+
const initialNodes: FlowNode[] = [
- // Top row - Source
{
- id: 'source-1',
- type: 'source',
- position: { x: 350, y: 0 },
+ id: 'treasury',
+ type: 'funnel',
+ position: { x: 400, y: 0 },
data: {
label: 'Treasury',
- balance: 100000,
- flowRate: 1000,
+ currentValue: 85000,
+ minThreshold: 20000,
+ maxThreshold: 70000,
+ maxCapacity: 100000,
+ inflowRate: 1000,
+ outflowAllocations: [
+ { targetId: 'public-goods', percentage: 40, color: COLORS[0] },
+ { targetId: 'research', percentage: 35, color: COLORS[1] },
+ { targetId: 'emergency', percentage: 25, color: COLORS[2] },
+ ],
},
},
- // Middle row - Threshold funnels
{
- id: 'threshold-1',
- type: 'threshold',
- position: { x: 100, y: 200 },
+ id: 'public-goods',
+ type: 'funnel',
+ position: { x: 100, y: 350 },
data: {
label: 'Public Goods',
+ currentValue: 45000,
minThreshold: 15000,
- maxThreshold: 60000,
- currentValue: 72000, // Overflowing
+ maxThreshold: 50000,
+ maxCapacity: 70000,
+ inflowRate: 400,
+ outflowAllocations: [
+ { targetId: 'project-alpha', percentage: 60, color: COLORS[0] },
+ { targetId: 'project-beta', percentage: 40, color: COLORS[1] },
+ ],
},
},
{
- id: 'threshold-2',
- type: 'threshold',
- position: { x: 400, y: 200 },
+ id: 'research',
+ type: 'funnel',
+ position: { x: 400, y: 350 },
data: {
label: 'Research',
+ currentValue: 28000,
minThreshold: 20000,
- maxThreshold: 50000,
- currentValue: 35000, // Healthy
+ maxThreshold: 45000,
+ maxCapacity: 60000,
+ inflowRate: 350,
+ outflowAllocations: [
+ { targetId: 'project-gamma', percentage: 70, color: COLORS[0] },
+ { targetId: 'project-beta', percentage: 30, color: COLORS[1] },
+ ],
},
},
{
- id: 'threshold-3',
- type: 'threshold',
- position: { x: 700, y: 200 },
+ id: 'emergency',
+ type: 'funnel',
+ position: { x: 700, y: 350 },
data: {
label: 'Emergency',
- minThreshold: 30000,
- maxThreshold: 80000,
- currentValue: 18000, // Critical
+ currentValue: 12000,
+ minThreshold: 25000,
+ maxThreshold: 60000,
+ maxCapacity: 80000,
+ inflowRate: 250,
+ outflowAllocations: [
+ { targetId: 'reserve', percentage: 100, color: COLORS[0] },
+ ],
},
},
- // Bottom row - Recipients
{
- id: 'recipient-1',
- type: 'recipient',
- position: { x: 50, y: 620 },
+ id: 'project-alpha',
+ type: 'funnel',
+ position: { x: 0, y: 700 },
data: {
label: 'Project Alpha',
- received: 24500,
- target: 30000,
+ currentValue: 18000,
+ minThreshold: 10000,
+ maxThreshold: 30000,
+ maxCapacity: 40000,
+ inflowRate: 240,
+ outflowAllocations: [],
},
},
{
- id: 'recipient-2',
- type: 'recipient',
- position: { x: 300, y: 620 },
+ id: 'project-beta',
+ type: 'funnel',
+ position: { x: 300, y: 700 },
data: {
label: 'Project Beta',
- received: 18000,
- target: 25000,
+ currentValue: 22000,
+ minThreshold: 15000,
+ maxThreshold: 35000,
+ maxCapacity: 45000,
+ inflowRate: 265,
+ outflowAllocations: [],
},
},
{
- id: 'recipient-3',
- type: 'recipient',
- position: { x: 550, y: 620 },
+ id: 'project-gamma',
+ type: 'funnel',
+ position: { x: 600, y: 700 },
data: {
- label: 'Research Lab',
- received: 12000,
- target: 40000,
+ label: 'Project Gamma',
+ currentValue: 8000,
+ minThreshold: 12000,
+ maxThreshold: 28000,
+ maxCapacity: 35000,
+ inflowRate: 245,
+ outflowAllocations: [],
},
},
{
- id: 'recipient-4',
- type: 'recipient',
- position: { x: 800, y: 620 },
+ id: 'reserve',
+ type: 'funnel',
+ position: { x: 900, y: 700 },
data: {
- label: 'Reserve Fund',
- received: 5000,
- target: 50000,
+ label: 'Reserve',
+ currentValue: 5000,
+ minThreshold: 20000,
+ maxThreshold: 50000,
+ maxCapacity: 60000,
+ inflowRate: 250,
+ outflowAllocations: [],
},
},
]
-const initialEdges: FlowEdge[] = [
- // Source to thresholds (top to middle)
- {
- id: 'e-source-t1',
- source: 'source-1',
- target: 'threshold-1',
- animated: true,
- style: { stroke: '#3b82f6', strokeWidth: 3 },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' },
- },
- {
- id: 'e-source-t2',
- source: 'source-1',
- target: 'threshold-2',
- animated: true,
- style: { stroke: '#3b82f6', strokeWidth: 3 },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' },
- },
- {
- id: 'e-source-t3',
- source: 'source-1',
- target: 'threshold-3',
- animated: true,
- style: { stroke: '#3b82f6', strokeWidth: 3 },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' },
- },
- // Threshold to recipients (middle to bottom)
- {
- id: 'e-t1-r1',
- source: 'threshold-1',
- target: 'recipient-1',
- animated: true,
- style: { stroke: '#ec4899', strokeWidth: 2 },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' },
- },
- {
- id: 'e-t1-r2',
- source: 'threshold-1',
- target: 'recipient-2',
- animated: true,
- style: { stroke: '#ec4899', strokeWidth: 2 },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' },
- },
- {
- id: 'e-t2-r3',
- source: 'threshold-2',
- target: 'recipient-3',
- animated: true,
- style: { stroke: '#ec4899', strokeWidth: 2 },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' },
- },
- {
- id: 'e-t3-r4',
- source: 'threshold-3',
- target: 'recipient-4',
- animated: true,
- style: { stroke: '#ec4899', strokeWidth: 2 },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' },
- },
- // Overflow connections (side handles) - from overflowing funnel to neighbors
- {
- id: 'e-overflow-1',
- source: 'threshold-1',
- sourceHandle: 'overflow-right',
- target: 'threshold-2',
- animated: true,
- style: { stroke: '#f59e0b', strokeWidth: 2, strokeDasharray: '5 5' },
- markerEnd: { type: MarkerType.ArrowClosed, color: '#f59e0b' },
- },
-]
+// Generate edges from node allocations with proportional thickness
+function generateEdges(nodes: FlowNode[]): FlowEdge[] {
+ const edges: FlowEdge[] = []
+ const maxAllocation = 100 // Max percentage for scaling
+
+ nodes.forEach((node) => {
+ const data = node.data as FunnelNodeData
+ data.outflowAllocations.forEach((alloc) => {
+ // Calculate stroke width: min 2px, max 12px based on percentage
+ const strokeWidth = 2 + (alloc.percentage / maxAllocation) * 10
+
+ edges.push({
+ id: `e-${node.id}-${alloc.targetId}`,
+ source: node.id,
+ target: alloc.targetId,
+ animated: true,
+ style: {
+ stroke: alloc.color,
+ strokeWidth,
+ opacity: 0.8,
+ },
+ markerEnd: {
+ type: MarkerType.ArrowClosed,
+ color: alloc.color,
+ width: 15 + alloc.percentage / 10,
+ height: 15 + alloc.percentage / 10,
+ },
+ data: {
+ allocation: alloc.percentage,
+ color: alloc.color,
+ },
+ })
+ })
+ })
+
+ return edges
+}
export default function FlowCanvas() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
- const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
+ const [edges, setEdges, onEdgesChange] = useEdgesState(generateEdges(initialNodes))
const [isSimulating, setIsSimulating] = useState(true)
const onConnect = useCallback(
@@ -199,7 +202,7 @@ export default function FlowCanvas() {
{
...params,
animated: true,
- style: { stroke: '#64748b', strokeWidth: 2 },
+ style: { stroke: '#64748b', strokeWidth: 4 },
markerEnd: { type: MarkerType.ArrowClosed, color: '#64748b' },
},
eds
@@ -215,41 +218,16 @@ export default function FlowCanvas() {
const interval = setInterval(() => {
setNodes((nds) =>
nds.map((node) => {
- if (node.type === 'source') {
- const data = node.data as SourceNodeData
- return {
- ...node,
- data: {
- ...data,
- balance: Math.max(0, data.balance - data.flowRate / 3600),
- },
- }
+ const data = node.data as FunnelNodeData
+ // Random walk for demo
+ const change = (Math.random() - 0.45) * 300
+ return {
+ ...node,
+ data: {
+ ...data,
+ currentValue: Math.max(0, Math.min(data.maxCapacity * 1.1, data.currentValue + change)),
+ },
}
- if (node.type === 'threshold') {
- const data = node.data as ThresholdNodeData
- // Random walk for demo
- const change = (Math.random() - 0.4) * 200
- return {
- ...node,
- data: {
- ...data,
- currentValue: Math.max(0, Math.min(100000, data.currentValue + change)),
- },
- }
- }
- if (node.type === 'recipient') {
- const data = node.data as RecipientNodeData
- if (data.received < data.target) {
- return {
- ...node,
- data: {
- ...data,
- received: Math.min(data.target, data.received + Math.random() * 20),
- },
- }
- }
- }
- return node
})
)
}, 500)
@@ -257,6 +235,11 @@ export default function FlowCanvas() {
return () => clearInterval(interval)
}, [isSimulating, setNodes])
+ // Regenerate edges when nodes change (to update proportions if needed)
+ useEffect(() => {
+ setEdges(generateEdges(nodes))
+ }, [nodes, setEdges])
+
return (
@@ -280,7 +259,7 @@ export default function FlowCanvas() {
{/* Title Panel */}
Threshold-Based Flow Funding
- Funds flow top→bottom through funnel thresholds
+ Drag min/max handles • Line thickness = allocation %
{/* Simulation Toggle */}
@@ -299,35 +278,31 @@ export default function FlowCanvas() {
{/* Legend */}
- Flow Types
+ Funnel Zones
-
-
Inflow (from source)
+
+
Overflow (above MAX)
-
-
Outflow (to recipients)
+
+
Healthy (MIN to MAX)
-
-
Overflow (excess)
+
+
Critical (below MIN)
-
Funnel Zones
-
+
Flow Lines
+
-
-
Overflow (above MAX)
+
+
Thin = small allocation
-
-
Healthy (MIN to MAX)
-
-
-
-
Critical (below MIN)
+
+
Thick = large allocation
diff --git a/components/nodes/FunnelNode.tsx b/components/nodes/FunnelNode.tsx
new file mode 100644
index 0000000..54a4959
--- /dev/null
+++ b/components/nodes/FunnelNode.tsx
@@ -0,0 +1,320 @@
+'use client'
+
+import { memo, useState, useCallback, useRef, useEffect } from 'react'
+import { Handle, Position } from '@xyflow/react'
+import type { NodeProps } from '@xyflow/react'
+import type { FunnelNodeData } from '@/lib/types'
+
+// Pie chart colors
+const PIE_COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#06b6d4']
+
+function FunnelNode({ data, selected }: NodeProps) {
+ const nodeData = data as FunnelNodeData
+ const { label, currentValue, maxCapacity, outflowAllocations } = nodeData
+
+ const [minThreshold, setMinThreshold] = useState(nodeData.minThreshold)
+ const [maxThreshold, setMaxThreshold] = useState(nodeData.maxThreshold)
+ const [dragging, setDragging] = useState<'min' | 'max' | null>(null)
+ const sliderRef = useRef
(null)
+
+ // Calculate status
+ const isOverflowing = currentValue > maxThreshold
+ const isCritical = currentValue < minThreshold
+
+ // Funnel dimensions
+ const width = 180
+ const height = 160
+ const topWidth = 160
+ const bottomWidth = 40
+ const padding = 8
+
+ // Calculate Y positions
+ const scaleY = (value: number) => padding + ((maxCapacity - value) / maxCapacity) * (height * 0.65)
+ const maxY = scaleY(maxThreshold)
+ const minY = scaleY(minThreshold)
+ const funnelStartY = minY + 10
+ const balanceY = Math.max(padding, scaleY(Math.min(currentValue, maxCapacity * 1.1)))
+
+ // Funnel shape
+ const leftTop = (width - topWidth) / 2
+ const rightTop = (width + topWidth) / 2
+ const leftBottom = (width - bottomWidth) / 2
+ const rightBottom = (width + bottomWidth) / 2
+
+ const clipPath = `
+ M ${leftTop} ${padding}
+ L ${rightTop} ${padding}
+ L ${rightTop} ${funnelStartY}
+ L ${rightBottom} ${height - padding - 10}
+ L ${rightBottom} ${height - padding}
+ L ${leftBottom} ${height - padding}
+ L ${leftBottom} ${height - padding - 10}
+ L ${leftTop} ${funnelStartY}
+ Z
+ `
+
+ // Dual range slider logic
+ const handleSliderMouseDown = useCallback((e: React.MouseEvent, type: 'min' | 'max') => {
+ e.stopPropagation()
+ setDragging(type)
+ }, [])
+
+ const handleSliderMouseMove = useCallback((e: MouseEvent) => {
+ if (!dragging || !sliderRef.current) return
+
+ const rect = sliderRef.current.getBoundingClientRect()
+ const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left))
+ const value = Math.round((x / rect.width) * maxCapacity)
+
+ if (dragging === 'min') {
+ setMinThreshold(Math.min(value, maxThreshold - 1000))
+ } else {
+ setMaxThreshold(Math.max(value, minThreshold + 1000))
+ }
+ }, [dragging, maxCapacity, minThreshold, maxThreshold])
+
+ const handleSliderMouseUp = useCallback(() => {
+ setDragging(null)
+ }, [])
+
+ useEffect(() => {
+ if (dragging) {
+ window.addEventListener('mousemove', handleSliderMouseMove)
+ window.addEventListener('mouseup', handleSliderMouseUp)
+ return () => {
+ window.removeEventListener('mousemove', handleSliderMouseMove)
+ window.removeEventListener('mouseup', handleSliderMouseUp)
+ }
+ }
+ }, [dragging, handleSliderMouseMove, handleSliderMouseUp])
+
+ // Pie chart calculations
+ const pieRadius = 24
+ const pieCenter = { x: pieRadius + 4, y: pieRadius + 4 }
+
+ const getPieSlices = () => {
+ if (outflowAllocations.length === 0) return []
+
+ let currentAngle = -90 // Start at top
+ return outflowAllocations.map((alloc, idx) => {
+ const angle = (alloc.percentage / 100) * 360
+ const startAngle = currentAngle
+ const endAngle = currentAngle + angle
+ currentAngle = endAngle
+
+ const startRad = (startAngle * Math.PI) / 180
+ const endRad = (endAngle * Math.PI) / 180
+
+ const x1 = pieCenter.x + pieRadius * Math.cos(startRad)
+ const y1 = pieCenter.y + pieRadius * Math.sin(startRad)
+ const x2 = pieCenter.x + pieRadius * Math.cos(endRad)
+ const y2 = pieCenter.y + pieRadius * Math.sin(endRad)
+
+ const largeArc = angle > 180 ? 1 : 0
+
+ return {
+ path: `M ${pieCenter.x} ${pieCenter.y} L ${x1} ${y1} A ${pieRadius} ${pieRadius} 0 ${largeArc} 1 ${x2} ${y2} Z`,
+ color: alloc.color || PIE_COLORS[idx % PIE_COLORS.length],
+ percentage: alloc.percentage,
+ targetId: alloc.targetId,
+ }
+ })
+ }
+
+ const pieSlices = getPieSlices()
+
+ return (
+
+ {/* Top Handle - Inflow */}
+
+
+ {/* Header */}
+
+
+ {label}
+
+ {isOverflowing ? 'OVERFLOW' : isCritical ? 'CRITICAL' : 'HEALTHY'}
+
+
+
+
+ {/* Main content - Funnel and Pie side by side */}
+
+ {/* Funnel SVG */}
+
+
+ {/* Pie chart for outflow allocation */}
+ {outflowAllocations.length > 0 && (
+
+
Outflow
+
+ {/* Mini legend */}
+
+ {outflowAllocations.slice(0, 3).map((alloc, idx) => (
+
+
+
{alloc.percentage}%
+
+ ))}
+
+
+ )}
+
+
+ {/* Value display */}
+
+
+ ${Math.floor(currentValue).toLocaleString()}
+
+
+
+ {/* Dual range slider */}
+
+
+ MIN: ${(minThreshold/1000).toFixed(0)}k
+ MAX: ${(maxThreshold/1000).toFixed(0)}k
+
+
e.stopPropagation()}
+ >
+ {/* Track background */}
+
+ {/* Red zone (0 to min) */}
+
+ {/* Green zone (min to max) */}
+
+ {/* Amber zone (max to capacity) */}
+
+
+
+ {/* Min handle */}
+
handleSliderMouseDown(e, 'min')}
+ />
+
+ {/* Max handle */}
+
handleSliderMouseDown(e, 'max')}
+ />
+
+
+
+ {/* Bottom Handle - Outflow */}
+
+
+ {/* Side Handles - Overflow */}
+
+
+
+ )
+}
+
+export default memo(FunnelNode)
diff --git a/components/nodes/RecipientNode.tsx b/components/nodes/RecipientNode.tsx
deleted file mode 100644
index ca0d404..0000000
--- a/components/nodes/RecipientNode.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-'use client'
-
-import { memo } from 'react'
-import { Handle, Position } from '@xyflow/react'
-import type { NodeProps } from '@xyflow/react'
-import type { RecipientNodeData } from '@/lib/types'
-
-function RecipientNode({ data, selected }: NodeProps) {
- const { label, received, target } = data as RecipientNodeData
- const progress = Math.min(100, (received / target) * 100)
- const isFunded = received >= target
-
- return (
-
- {/* Input Handle - Top for vertical flow */}
-
-
- {/* Header */}
-
-
-
-
{label}
- {isFunded && (
-
- )}
-
-
-
- {/* Body */}
-
-
- Received
-
- ${Math.floor(received).toLocaleString()}
-
-
-
- {/* Progress bar */}
-
-
- Progress
- {progress.toFixed(0)}%
-
-
-
-
-
- Target
-
- ${target.toLocaleString()}
-
-
-
-
- )
-}
-
-export default memo(RecipientNode)
diff --git a/components/nodes/SourceNode.tsx b/components/nodes/SourceNode.tsx
deleted file mode 100644
index 6cc0c87..0000000
--- a/components/nodes/SourceNode.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client'
-
-import { memo } from 'react'
-import { Handle, Position } from '@xyflow/react'
-import type { NodeProps } from '@xyflow/react'
-import type { SourceNodeData } from '@/lib/types'
-
-function SourceNode({ data, selected }: NodeProps) {
- const { label, balance, flowRate } = data as SourceNodeData
-
- return (
-
- {/* Header */}
-
-
- {/* Body */}
-
-
- Balance
-
- ${Math.floor(balance).toLocaleString()}
-
-
-
- Flow Rate
-
- ${flowRate}/hr
-
-
- {/* Flow indicator */}
-
-
-
- {/* Output Handle - Bottom for vertical flow */}
-
-
- )
-}
-
-export default memo(SourceNode)
diff --git a/components/nodes/ThresholdNode.tsx b/components/nodes/ThresholdNode.tsx
deleted file mode 100644
index b2a6375..0000000
--- a/components/nodes/ThresholdNode.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-'use client'
-
-import { memo, useState } from 'react'
-import { Handle, Position } from '@xyflow/react'
-import type { NodeProps } from '@xyflow/react'
-import type { ThresholdNodeData } from '@/lib/types'
-
-function ThresholdNode({ data, selected }: NodeProps) {
- const nodeData = data as ThresholdNodeData
- const [minThreshold, setMinThreshold] = useState(nodeData.minThreshold)
- const [maxThreshold, setMaxThreshold] = useState(nodeData.maxThreshold)
- const currentValue = nodeData.currentValue
- const maxCapacity = 100000
-
- // Calculate status
- const isOverflowing = currentValue > maxThreshold
- const isCritical = currentValue < minThreshold
- const isHealthy = !isOverflowing && !isCritical
-
- // Funnel dimensions
- const width = 160
- const height = 200
- const topWidth = 140
- const bottomWidth = 30
- const padding = 10
-
- // Calculate Y positions for thresholds and fill
- const maxY = padding + ((maxCapacity - maxThreshold) / maxCapacity) * (height * 0.6)
- const minY = padding + ((maxCapacity - minThreshold) / maxCapacity) * (height * 0.6)
- const funnelStartY = minY + 15
- const balanceY = Math.max(padding, padding + ((maxCapacity - Math.min(currentValue, maxCapacity * 1.1)) / maxCapacity) * (height * 0.6))
-
- // Funnel shape calculations
- const leftTop = (width - topWidth) / 2
- const rightTop = (width + topWidth) / 2
- const leftBottom = (width - bottomWidth) / 2
- const rightBottom = (width + bottomWidth) / 2
-
- // Clip path for liquid fill
- const clipPath = `
- M ${leftTop} ${padding}
- L ${rightTop} ${padding}
- L ${rightTop} ${funnelStartY}
- L ${rightBottom} ${height - padding - 15}
- L ${rightBottom} ${height - padding}
- L ${leftBottom} ${height - padding}
- L ${leftBottom} ${height - padding - 15}
- L ${leftTop} ${funnelStartY}
- Z
- `
-
- return (
-
- {/* Top Handle - Inflow */}
-
-
- {/* Header */}
-
-
{nodeData.label}
-
- {isOverflowing ? 'OVERFLOW' : isCritical ? 'CRITICAL' : 'HEALTHY'}
-
-
-
- {/* Funnel SVG */}
-
-
- {/* Value display */}
-
-
- ${Math.floor(currentValue).toLocaleString()}
-
-
-
- {/* Threshold sliders */}
-
-
-
- Min
- ${minThreshold.toLocaleString()}
-
-
setMinThreshold(Number(e.target.value))}
- className="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-red-500"
- />
-
-
-
- Max
- ${maxThreshold.toLocaleString()}
-
-
setMaxThreshold(Number(e.target.value))}
- className="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-amber-500"
- />
-
-
-
- {/* Bottom Handle - Outflow */}
-
-
- {/* Side Handles - Overflow */}
- {isOverflowing && (
- <>
-
-
- >
- )}
-
- )
-}
-
-export default memo(ThresholdNode)
diff --git a/lib/types.ts b/lib/types.ts
index df87227..55c9ba7 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,26 +1,28 @@
import type { Node, Edge } from '@xyflow/react'
-export interface SourceNodeData {
- label: string
- balance: number
- flowRate: number
- [key: string]: unknown
+export interface OutflowAllocation {
+ targetId: string
+ percentage: number // 0-100
+ color: string
}
-export interface ThresholdNodeData {
+export interface FunnelNodeData {
label: string
+ currentValue: number
minThreshold: number
maxThreshold: number
- currentValue: number
+ maxCapacity: number
+ inflowRate: number
+ outflowAllocations: OutflowAllocation[]
[key: string]: unknown
}
-export interface RecipientNodeData {
- label: string
- received: number
- target: number
+export type FlowNode = Node
+
+export interface FlowEdgeData {
+ allocation: number // percentage 0-100
+ color: string
[key: string]: unknown
}
-export type FlowNode = Node
-export type FlowEdge = Edge<{ animated?: boolean }>
+export type FlowEdge = Edge