Files
controller-v2/client/src/components/HearingCompensationChart.tsx
T

187 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-07-14 14:30:55 +08:00
import { useEffect, useMemo, useRef } from "react";
import * as echarts from "echarts";
import {
buildHearingSplineSeries,
2026-09-09 18:26:26 +08:00
resolveHearingFreqLabels,
2026-07-14 14:30:55 +08:00
} 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<HTMLDivElement>(null);
const chartRef = useRef<echarts.ECharts | null>(null);
const leftSeries = useMemo(() => buildHearingSplineSeries(left), [left]);
const rightSeries = useMemo(() => buildHearingSplineSeries(right), [right]);
2026-09-09 18:26:26 +08:00
// 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);
2026-07-14 14:30:55 +08:00
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,
2026-09-09 18:26:26 +08:00
max: xMax,
2026-07-14 14:30:55 +08:00
interval: 1,
axisLabel: {
formatter: (value: number) => {
const idx = Math.round(value);
if (Math.abs(value - idx) > 1e-6) return "";
2026-09-09 18:26:26 +08:00
return freqLabels[idx] ?? "";
2026-07-14 14:30:55 +08:00
},
fontSize: 14,
color: "#999",
},
splitLine: {
show: true,
lineStyle: { type: "solid", color: "#3A4348" },
},
axisLine: {
lineStyle: { color: "#3A4348" },
},
},
yAxis: {
type: "value",
min: -80,
2026-09-09 18:26:26 +08:00
// 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,
2026-07-14 14:30:55 +08:00
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);
};
2026-09-09 18:26:26 +08:00
}, [leftSeries, rightSeries, leftLabel, rightLabel, freqLabels, xMax]);
2026-07-14 14:30:55 +08:00
useEffect(() => {
return () => {
chartRef.current?.dispose();
chartRef.current = null;
};
}, []);
return (
<div
ref={containerRef}
className={
className
2026-09-09 18:26:26 +08:00
?? "w-full h-[min(52vw,240px)] min-h-[180px] md:h-[320px] lg:h-[380px] rounded-lg bg-black/30"
2026-07-14 14:30:55 +08:00
}
aria-hidden
/>
);
}