import { useEffect, useMemo, useRef } from "react"; import * as echarts from "echarts"; import { buildHearingSplineSeries, resolveHearingFreqLabels, } from "@/lib/hearingCurve"; const LEFT_COLOR = "rgb(0, 255, 246)"; const RIGHT_COLOR = "#FF5A36"; type HearingCompensationChartProps = { left: number[]; right: number[]; leftLabel?: string; rightLabel?: string; className?: string; }; export default function HearingCompensationChart({ left, right, leftLabel = "Left", rightLabel = "Right", className, }: HearingCompensationChartProps) { const containerRef = useRef(null); const chartRef = useRef(null); const leftSeries = useMemo(() => buildHearingSplineSeries(left), [left]); const rightSeries = useMemo(() => buildHearingSplineSeries(right), [right]); // X axis adapts to the control-point count: 7-band (legacy) or 10-band (adds 3k/5k/6k). const pointCount = Math.max(left.length, right.length); const freqLabels = useMemo(() => resolveHearingFreqLabels(pointCount), [pointCount]); const xMax = Math.max(0, pointCount - 1); useEffect(() => { const el = containerRef.current; if (!el) return; const option: echarts.EChartsOption = { animation: false, legend: { show: true, top: 8, data: [ { name: leftLabel, icon: "circle", textStyle: { color: LEFT_COLOR }, itemStyle: { color: LEFT_COLOR, borderColor: LEFT_COLOR }, }, { name: rightLabel, icon: "circle", textStyle: { color: RIGHT_COLOR }, itemStyle: { color: RIGHT_COLOR, borderColor: RIGHT_COLOR }, }, ], textStyle: { fontSize: 13 }, }, grid: { left: 48, right: 36, top: 48, bottom: 28, }, graphic: [ { type: "text", right: 10, bottom: 6, style: { text: "Hz", fill: "#999", fontSize: 14, }, z: 100, }, ], xAxis: { type: "value", min: 0, max: xMax, interval: 1, axisLabel: { formatter: (value: number) => { const idx = Math.round(value); if (Math.abs(value - idx) > 1e-6) return ""; return freqLabels[idx] ?? ""; }, fontSize: 14, color: "#999", }, splitLine: { show: true, lineStyle: { type: "solid", color: "#3A4348" }, }, axisLine: { lineStyle: { color: "#3A4348" }, }, }, yAxis: { type: "value", min: -80, // Top raised to +20 dB: levels above 70 map to positive dB and were // clipped at the old max of 0, leaving gaps at the top of the curve. max: 20, interval: 10, name: "dB", nameLocation: "end", nameGap: 8, nameTextStyle: { color: "#999", fontSize: 14, align: "right", padding: [0, 8, 0, 0], }, axisLabel: { fontSize: 14, color: "#999", }, splitLine: { show: true, lineStyle: { type: "solid", color: "#3A4348" }, }, axisLine: { lineStyle: { color: "#3A4348" }, }, }, series: [ { name: leftLabel, type: "line", data: leftSeries, showSymbol: false, smooth: false, lineStyle: { color: LEFT_COLOR, width: 2 }, itemStyle: { color: LEFT_COLOR }, }, { name: rightLabel, type: "line", data: rightSeries, showSymbol: false, smooth: false, lineStyle: { color: RIGHT_COLOR, width: 2 }, itemStyle: { color: RIGHT_COLOR }, }, ], }; if (!chartRef.current) { chartRef.current = echarts.init(el, undefined, { renderer: "canvas" }); } chartRef.current.setOption(option, true); const onResize = () => chartRef.current?.resize(); const ro = new ResizeObserver(onResize); ro.observe(el); window.addEventListener("resize", onResize); return () => { ro.disconnect(); window.removeEventListener("resize", onResize); }; }, [leftSeries, rightSeries, leftLabel, rightLabel, freqLabels, xMax]); useEffect(() => { return () => { chartRef.current?.dispose(); chartRef.current = null; }; }, []); return (
); }