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 ?? }
)
}