import React, { useState } from "react"; import type { GraphSpec } from "./types"; import { useViewport } from "./useViewport"; import { RegionLayer } from "./RegionLayer"; import { PolylineLayer } from "./PolylineLayer"; import { CurveLayer } from "./CurveLayer"; import { GuideLayer } from "./GuideLayer"; import { ArrowLayer } from "./ArrowLayer"; import { PointLayer } from "./PointLayer"; import { GraphTooltip } from "./GraphTooltip"; interface EconGraphProps { spec: GraphSpec; width?: number; height?: number; className?: string; } function computeTicks(max: number, customStep?: number): number[] { if (customStep && customStep > 0) { const ticks: number[] = []; for (let v = 0; v <= max + 1e-6; v += customStep) { ticks.push(Number(v.toFixed(4))); } return ticks; } let step = 5; if (max <= 5) step = 1; else if (max <= 12) step = 2; else if (max <= 25) step = 5; else if (max <= 50) step = 10; else if (max <= 120) step = 20; else step = Math.max(1, Math.round(max / 5)); const ticks: number[] = []; for (let v = 0; v <= max + 1e-6; v += step) { ticks.push(Number(v.toFixed(4))); } return ticks; } export const EconGraph: React.FC = ({ spec, width = 680, height = 480, className = "", }) => { const [hoverState, setHoverState] = useState<{ screenX: number; screenY: number; modelX: number; modelY: number; } | null>(null); const [compactLabels, setCompactLabels] = useState(false); const [showDrops, setShowDrops] = useState(true); const viewport = useViewport({ maxX: spec.xAxis.max, maxY: spec.yAxis.max, width, height, xStep: spec.xAxis.step, yStep: spec.yAxis.step, }); const xTicks = computeTicks(spec.xAxis.max, spec.xAxis.step); const yTicks = computeTicks(spec.yAxis.max, spec.yAxis.step); const plotLeft = viewport.padding.left; const plotRight = viewport.width - viewport.padding.right; const plotTop = viewport.padding.top; const plotBottom = viewport.height - viewport.padding.bottom; const handleMouseMove = (e: React.MouseEvent): void => { const rect = e.currentTarget.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return; const sx = ((e.clientX - rect.left) / rect.width) * viewport.width; const sy = ((e.clientY - rect.top) / rect.height) * viewport.height; if (sx >= plotLeft && sx <= plotRight && sy >= plotTop && sy <= plotBottom) { const modelX = viewport.toModelX(sx); const modelY = viewport.toModelY(sy); setHoverState({ screenX: sx, screenY: sy, modelX, modelY, }); } else { setHoverState(null); } }; const handleMouseLeave = (): void => { setHoverState(null); }; const xAxisLabel = spec.xAxis.unit ? `${spec.xAxis.label} (${spec.xAxis.unit})` : spec.xAxis.label; const yAxisLabel = spec.yAxis.unit ? `${spec.yAxis.label} (${spec.yAxis.unit})` : spec.yAxis.label; return (
{/* Top Header inside card: Title + Display Controls + Legend */}

{spec.title}

{spec.subtitle && (

{spec.subtitle}

)}
{/* Quick view controls to declutter complex charts */}
{spec.legends && spec.legends.length > 0 && (
{spec.legends.map((lg, i) => (
{lg.style === "dashed" && ( )} {lg.style === "area" && ( )} {lg.style === "point" && ( )} {(!lg.style || lg.style === "solid") && ( )} {lg.label}
))}
)}
{/* Grid lines */} {xTicks.map((x) => { if (x === 0) return null; const sx = viewport.toScreenX(x); return ( ); })} {yTicks.map((y) => { if (y === 0) return null; const sy = viewport.toScreenY(y); return ( ); })} {/* Dynamic Graphic Layers */} {/* Axes lines */} {/* X Axis */} {/* Y Axis */} {/* Ticks and numerical labels */} {/* X ticks */} {xTicks.map((x) => { const sx = viewport.toScreenX(x); return ( {x} ); })} {/* Y ticks */} {yTicks.map((y) => { const sy = viewport.toScreenY(y); return ( {y} ); })} {/* Axis Names */} {/* X axis name at bottom right */} {xAxisLabel} {/* Y axis name at top left */} {yAxisLabel} {/* Point Layer on top of axes with smart collision avoidance */} {/* Interactive hover tooltip & crosshairs */} {hoverState && ( )}
); };