import { IconButton } from '@mui/material' import { PercentOutlined } from '@mui/icons-material' import { useCallback, useEffect, useRef } from 'react' import { widgetStoreActions } from '../../stores/widget-store' import { useWidgetSelector } from '../../stores/use-widget-selector' import type { RelativeDataProps, RelativeDataState } from './types' import { actionButtonStyles } from '../shared/styles' import { Tooltip } from '../../../components' import { calculateTotal, toRelativeData } from './utils' import type { EchartWidgetData } from '../../../widgets/echart' export const RELATIVE_DATA_TOOL_ID = 'relative-data' export const RELATIVE_DATA_CONFIG_TOOL_ID = 'relative-data-config' /** * Widget action to toggle between relative (percentage) and absolute data display. * * Registers two transformation tools in the widget pipeline when mounted: * - A data tool that converts values to percentages (enabled/disabled via store) * - A config tool that is **always enabled** and reads `isRelative` from the store * to decide whether to apply the percentage formatter or restore the original one. * The config tool must always participate because the original formatter may have * been set via `setWidget` (not in the base config), so disabling the tool would * leave the percentage formatter stuck on the widget. * * @example * ```tsx * * ``` */ export function RelativeData({ id, order = 10, defaultIsRelative = false, labels, Icon, IconButtonProps, }: RelativeDataProps) { const percentFormatterRef = useRef<((value: number) => string) | undefined>( undefined, ) // Read isRelative from widget store root — single source of truth const { isRelative } = useWidgetSelector(id, (w) => ({ isRelative: (w as RelativeDataState | undefined)?.isRelative ?? defaultIsRelative, })) // Initialize store with default value on mount. // When defaultIsRelative=true, capture originals so toggling OFF can restore them. useEffect(() => { const current = widgetStoreActions.getWidget(id) if (current?.isRelative === undefined) { if (defaultIsRelative) { widgetStoreActions.setWidget(id, { isRelative: true, originalFormatter: current?.formatter, originalMax: (current as unknown as Record)?.max, }) } else { widgetStoreActions.setWidget(id, { isRelative: defaultIsRelative }) } } }, [id, defaultIsRelative]) // Register data tool once — fn has no closure deps useEffect(() => { widgetStoreActions.registerTool(id, { id: RELATIVE_DATA_TOOL_ID, order, enabled: defaultIsRelative, fn: (data) => { const echartData = data as EchartWidgetData const total = calculateTotal(echartData) return toRelativeData(echartData, total) }, }) return () => widgetStoreActions.unregisterTool(id, RELATIVE_DATA_TOOL_ID) }, [id, order, defaultIsRelative]) // Sync data tool enabled — lightweight, no re-registration useEffect(() => { widgetStoreActions.setToolEnabled(id, RELATIVE_DATA_TOOL_ID, isRelative) }, [id, isRelative]) // Register config tool — ALWAYS enabled. // Reads isRelative, originalFormatter, originalMax from the store at execution time: // - isRelative=true → applies percentage formatter and max=100 // - isRelative=false → restores original formatter/max from store useEffect(() => { widgetStoreActions.registerTool(id, { id: RELATIVE_DATA_CONFIG_TOOL_ID, type: 'config', order, enabled: true, fn: (currentConfig: unknown) => { const config = currentConfig as Record const widget = widgetStoreActions.getWidget(id) const hasSourceData = widget?.sourceData != null && !(Array.isArray(widget.sourceData) && widget.sourceData.length === 0) if (widget?.isRelative) { // Don't apply percentage formatter when there's no source data if (!hasSourceData) return config if (!percentFormatterRef.current) { const locale = widget?.locale percentFormatterRef.current = (value: number) => new Intl.NumberFormat(locale, { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1, }).format(value / 100) } return { ...config, formatter: percentFormatterRef.current, max: 100 } } // Not in relative mode — restore originals from store if captured. // Use `in` because originalFormatter may have been captured as undefined // (widget had no formatter before toggling relative on). percentFormatterRef.current = undefined if (widget != null && 'originalFormatter' in widget) { return { ...config, formatter: widget.originalFormatter, max: widget.originalMax, } } return config }, }) return () => { // Restore original formatter/max if unmounting while in relative mode const widget = widgetStoreActions.getWidget(id) if (widget?.isRelative && 'originalFormatter' in widget) { widgetStoreActions.setWidget(id, { formatter: widget.originalFormatter, max: widget.originalMax, }) } percentFormatterRef.current = undefined widgetStoreActions.unregisterTool(id, RELATIVE_DATA_CONFIG_TOOL_ID) } }, [id, order, defaultIsRelative]) const handleToggle = useCallback(() => { const newIsRelative = !isRelative percentFormatterRef.current = undefined if (newIsRelative) { // Capture current formatter/max in store before switching to relative const widget = widgetStoreActions.getWidget(id) widgetStoreActions.setWidget(id, { isRelative: true, originalFormatter: widget?.formatter, originalMax: (widget as unknown as Record)?.max, }) } else { widgetStoreActions.setWidget(id, { isRelative: false }) } }, [isRelative, id]) const tooltipLabel = isRelative ? (labels?.absolute ?? 'Show absolute values') : (labels?.relative ?? 'Show relative values') return ( {Icon ?? } ) }