import { useCallback, useMemo, type ComponentType } from 'react' import { IconButton, useTheme, type SvgIconProps } from '@mui/material' import { ZoomIn as ZoomInIcon } from '@mui/icons-material' import { Tooltip } from '../../../components' import { getEchartInstance, useSingleTransform, useWidgetId, } from '../../stores' import { createAddZoom, type ZoomAxis } from './transforms' import { DEFAULT_ZOOM_TOGGLE_LABELS, type ZoomToggleLabels } from './labels' import { styles } from './style' const ZOOM_DESCRIPTOR = { id: 'zoom-toggle', type: 'config' as const, order: 20, } export interface ZoomToggleProps { initialEnabled?: boolean labels?: Partial icon?: ComponentType iconProps?: SvgIconProps /** * Which axes the zoom controls. Default `['x']` — matches bar / * histogram / timeseries (horizontal pan on the category axis). Pass * `['x', 'y']` for 2D zoom on a scatterplot-style chart (mouse-wheel * scales both axes; horizontal slider at bottom + vertical slider on * the right). */ axes?: readonly ZoomAxis[] } /** * Toggle a `dataZoom` slider + wheel-zoom on the chart. The transform is * theme-bound so the slider gets v1's CARTO chrome (blue-tinted filler, * secondary-color preview, white pill handles). * * `dataZoom` is intentionally NOT in `replaceMergeKeys` — ECharts merges * subsequent `setOption` calls so the user's dragged range survives * unrelated re-renders (data updates, formatter swaps, RelativeData toggles). * * On disable the transform is unregistered, but ECharts' merge semantics * would otherwise *keep* the live `dataZoom` component visible. To remove * the slider immediately we issue a one-shot * `setOption({}, { replaceMerge: ['dataZoom'] })` against the live chart * instance via {@link getEchartInstance} — this bypasses the pipeline so * unrelated transforms aren't disturbed. */ export function ZoomToggle({ initialEnabled = false, labels, icon: Icon = ZoomInIcon, iconProps, axes, }: ZoomToggleProps) { const id = useWidgetId() const theme = useTheme() const _labels = { ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels } // Reduce `axes` to two structural booleans before memoizing so the memo // depends on stable primitive values, not an array reference (callers // typically pass an inline `axes={['x', 'y']}` literal that changes // identity each render). Default — no `axes` prop — is x-axis only, // matching `createAddZoom`'s default. const includeX = axes ? axes.includes('x') : true const includeY = axes ? axes.includes('y') : false const transform = useMemo(() => { const resolved: ZoomAxis[] = [] if (includeX) resolved.push('x') if (includeY) resolved.push('y') return createAddZoom(theme, { axes: resolved }) }, [theme, includeX, includeY]) const { enabled, toggle } = useSingleTransform( id, ZOOM_DESCRIPTOR, transform, { initialEnabled }, ) const handleToggle = useCallback(() => { if (enabled) { // About to disable — clear the dataZoom component on the live // instance so the slider disappears immediately. getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] }) } toggle() }, [enabled, toggle, id]) const tooltip = enabled ? _labels.on : _labels.off return ( ) }