import { useCallback, useMemo, useRef } from 'react' import type { CallbackDataParams } from 'echarts/types/dist/shared' import type { EchartsEventHandler } from './echart-ui' export type SelectionKey = string | number export interface UseChartSelectionOptions { /** Controlled selection. `undefined` is treated the same as `[]`. */ selection?: readonly K[] /** Receives the next selection. When omitted the wiring is a no-op. */ onSelectionChange?: (next: readonly K[]) => void /** * Pull a selection key out of an ECharts `click` event. Return `null` to * skip (e.g. a click on empty plot area). For Bar/Pie/Timeseries this is * usually `(p) => p.name`; for Histogram/Scatterplot it's the dataIndex. */ keyFromClick: (params: CallbackDataParams) => K | null /** * Map a `brushSelected` payload's `selected[*]` entries (each carrying a * `seriesIndex` + `dataIndex[]`) into the full set of selection keys for * the brushed area. Called once per brush event. */ keysFromBrush: ( selected: readonly { seriesIndex: number; dataIndex: number[] }[], ) => K[] } export interface ChartSelectionWiring { /** * Pass straight to ``. Wires `click` and * `brushSelected` so the user's interactions reach `onSelectionChange`. */ onEvents: Record /** * The current selection — `null` when the consumer didn't pass one or it's * empty. Mergers should treat `null` as "no dimming" and a non-null array * as "render only these as fully opaque, dim the rest". */ selection: readonly K[] | null } /** * Translate ECharts' click + brush events into a consumer-owned selection * with the expected dashboard semantics: * * - **Click toggles** — clicking an item adds it to the selection if * absent, removes it if present. * - **Brush replaces** — finishing a brush stroke overwrites the selection * with whatever the brush covered (drag-to-empty clears). * * Wire the returned `onEvents` into `` and feed * `wiring.selection` to the widget's option factory so the merger can dim * non-selected rows at fusion time. */ export function useChartSelection({ selection, onSelectionChange, keyFromClick, keysFromBrush, }: UseChartSelectionOptions): ChartSelectionWiring { const handleClick = useCallback( (event: unknown) => { if (!onSelectionChange) return const params = event as CallbackDataParams const key = keyFromClick(params) if (key == null) return const current = selection ?? [] const idx = current.indexOf(key) const next = idx === -1 ? [...current, key] : [...current.slice(0, idx), ...current.slice(idx + 1)] onSelectionChange(next) }, [selection, onSelectionChange, keyFromClick], ) // ECharts fires `brushSelected` on every pointer-move while the user is // drawing a brush, but we only want to push to `onSelectionChange` once // the gesture ends. Cache the latest `brushSelected` payload here, then // commit on `brushEnd`. const latestBrushedRef = useRef< readonly { seriesIndex: number; dataIndex: number[] }[] >([]) const handleBrushSelected = useCallback((event: unknown) => { const params = event as { batch?: { selected?: { seriesIndex: number; dataIndex: number[] }[] }[] } latestBrushedRef.current = params.batch?.[0]?.selected ?? [] }, []) const handleBrushEnd = useCallback(() => { if (!onSelectionChange) return onSelectionChange(keysFromBrush(latestBrushedRef.current)) }, [onSelectionChange, keysFromBrush]) const onEvents = useMemo>( () => ({ click: handleClick, brushSelected: handleBrushSelected, brushEnd: handleBrushEnd, }), [handleClick, handleBrushSelected, handleBrushEnd], ) const wiringSelection = useMemo( () => (selection && selection.length > 0 ? selection : null), [selection], ) return { onEvents, selection: wiringSelection } }