import { useCallback } from 'react' import { useCanvas } from '../hooks/useCanvas' type Props = { values: number[] yMin: number yMax: number yTicks: number[] color: string tickLabel?: (v: number) => string } const M = { l: 40, r: 10, t: 8, b: 22 } /** Discrete spectrum as stems at harmonic numbers 1..N. */ export function StemPlot({ values, yMin, yMax, yTicks, color, tickLabel }: Props) { const draw = useCallback( (ctx: CanvasRenderingContext2D, w: number, h: number) => { const pw = w - M.l - M.r const ph = h - M.t - M.b const n = values.length const toY = (v: number) => M.t + ph - ((v - yMin) / (yMax - yMin)) * ph const toX = (k: number) => M.l + (pw * (k + 0.5)) / n const fmt = tickLabel ?? ((v: number) => String(v)) ctx.font = '11px system-ui, sans-serif' ctx.lineWidth = 1 ctx.strokeStyle = '#ddd' ctx.strokeRect(M.l + 0.5, M.t + 0.5, pw, ph) // horizontal grid + tick labels ctx.fillStyle = '#666' ctx.textAlign = 'right' ctx.textBaseline = 'middle' for (const v of yTicks) { const y = Math.round(toY(v)) + 0.5 ctx.strokeStyle = v === 0 ? '#bbb' : '#eee' ctx.beginPath() ctx.moveTo(M.l, y) ctx.lineTo(M.l + pw, y) ctx.stroke() ctx.fillText(fmt(v), M.l - 4, y) } // x labels (harmonic number) ctx.textAlign = 'center' ctx.textBaseline = 'top' const step = n > 16 ? 2 : 1 for (let k = 0; k < n; k += step) ctx.fillText(String(k + 1), toX(k), M.t + ph + 4) // stems const base = toY(Math.min(Math.max(0, yMin), yMax)) ctx.strokeStyle = color ctx.fillStyle = color ctx.lineWidth = 2 for (let k = 0; k < n; k++) { const x = toX(k) const y = toY(values[k]) ctx.beginPath() ctx.moveTo(x, base) ctx.lineTo(x, y) ctx.stroke() ctx.beginPath() ctx.arc(x, y, 3.5, 0, 2 * Math.PI) ctx.fill() } }, [values, yMin, yMax, yTicks, color, tickLabel], ) const ref = useCanvas(draw) return }