import { useEffect, type ComponentType } from 'react' import { IconButton, type SvgIconProps } from '@mui/material' import { HighlightAlt as HighlightAltIcon } from '@mui/icons-material' import { Tooltip } from '../../../components' import { useEchartInstance, useSingleTransform, useWidgetId, } from '../../stores' import { addBrush } from './transforms' import { DEFAULT_BRUSH_TOGGLE_LABELS, type BrushToggleLabels } from './labels' import { styles } from './style' const BRUSH_DESCRIPTOR = { id: 'brush-toggle', type: 'config' as const, order: 25, } export interface BrushToggleProps { initialEnabled?: boolean /** * ECharts brush type. `'lineX'` (default) selects a range on the * x-axis — appropriate for bar / histogram / timeseries. `'rect'` * selects a 2-D rectangle — use for scatterplot. Other ECharts * brush types (`'lineY'`, `'polygon'`) are also accepted. */ brushType?: 'lineX' | 'lineY' | 'rect' | 'polygon' /** * `'multiple'` (default) lets the user draw additional brushes on * top of existing ones. `'single'` replaces the previous brush on * each new draw. */ brushMode?: 'multiple' | 'single' /** * Optional view of the consumer-owned selection. When provided and * the array is empty (`[]`), any brush rectangles currently drawn on * the chart are wiped — keeps the UI in sync when the parent clears * the selection externally (e.g. via `Widget.SelectionSummary`'s * `onClear`). Pass the same array that drives `useChartSelection` / * the option factory's `selection` input. * * Leave `undefined` to opt out — the sync effect becomes a no-op. */ selection?: readonly unknown[] | null labels?: Partial icon?: ComponentType iconProps?: SvgIconProps } /** * Toggle ECharts' brush selection mode. Two-part wiring: * * 1. The config transform (`addBrush`) installs the `brush` * component on the option so brushed items get the * `outOfBrush` dim treatment. * 2. ECharts only enters "brush cursor" mode when something * dispatches `takeGlobalCursor({ key: 'brush', ... })`. This * component drives that dispatch via {@link useEchartInstance}, * reacting when the chart instance becomes available (the * `BrushToggle` mounts before `Widget.Echart` in tree order, so * on first mount the chart isn't ready yet). * * 3. ECharts clears the global cursor on every `setOption`, so we * re-dispatch on each `'finished'` event — required for * `brushMode: 'multiple'` to keep working across selection-driven * re-renders, RelativeData toggles, etc. * * Selection events (`brushSelected` / `brushEnd`) flow through the * existing `useChartSelection` wiring at the consumer level — no extra * plumbing here. */ export function BrushToggle({ initialEnabled = false, brushType = 'lineX', brushMode = 'multiple', selection, labels, icon: Icon = HighlightAltIcon, iconProps, }: BrushToggleProps) { const id = useWidgetId() const _labels = { ...DEFAULT_BRUSH_TOGGLE_LABELS, ...labels } const { enabled, toggle } = useSingleTransform( id, BRUSH_DESCRIPTOR, addBrush, { initialEnabled }, ) // Reactive read of the live instance — the effect below re-runs when // the chart arrives, departs, or re-inits, instead of polling. const chart = useEchartInstance(id) useEffect(() => { if (!chart) return undefined if (!enabled) { // Disabled: exit brush cursor and wipe any drawn rectangles. chart.dispatchAction({ type: 'takeGlobalCursor' }) chart.dispatchAction({ type: 'brush', areas: [] }) return undefined } const enterBrush = (): void => { chart.dispatchAction({ type: 'takeGlobalCursor', key: 'brush', brushOption: { brushType, brushMode }, }) } // Every `setOption` clears the global cursor. Re-dispatch after // each chart render so `brushMode: 'multiple'` stays usable across // selection-driven re-renders, RelativeData toggles, etc. const onFinished = (): void => enterBrush() chart.on('finished', onFinished) enterBrush() return () => { chart.off('finished', onFinished) } }, [chart, enabled, brushType, brushMode]) // Sync the drawn rectangles with the external selection. When the // parent flips `selection` to `[]` (e.g. SelectionSummary's "Clear" // button), wipe the brush areas so the chart UI matches the data. // Dispatching with an empty `areas` array when nothing is drawn is a // safe no-op on ECharts, so no extra "previous value" bookkeeping is // needed. Kept as a separate effect so a parent selection change // doesn't tear down / re-attach the `'finished'` listener above. useEffect(() => { if (!chart || !enabled) return if (selection?.length === 0) { chart.dispatchAction({ type: 'brush', areas: [] }) } }, [chart, enabled, selection]) const tooltip = enabled ? _labels.on : _labels.off return ( ) }