import deepmerge from 'deepmerge' import type { EchartOptionsProps } from './types' import type { Theme } from '@mui/material' import { DEFAULT_STACK_GROUP } from './const' /** * Deep-merges two ECharts option objects with smart handling for `color`, `axisLabel`, and `series` keys. * * @param optionA - Base ECharts options. * @param optionB - Override ECharts options to merge on top. * @param customMergeFn - Optional function returning a custom merge strategy per key. * @returns The merged ECharts options object. * * @remarks * - `color` arrays are replaced entirely (not concatenated). * - `axisLabel` formatters are composed so the base formatter can suppress values by returning `''`. * - `series` arrays are merged element-by-element rather than concatenated. * * @example * ```tsx * const option = mergeEchartWidgetConfig(props.option, { * series: [{ barWidth: '60%', itemStyle: { borderRadius: [4, 4, 0, 0] } }], * tooltip: { trigger: 'axis' }, * }) * ``` */ export function mergeEchartWidgetConfig( optionA: T | undefined, optionB: T | undefined, customMergeFn?: ( key: string, ) => ((a: unknown, b: unknown) => unknown) | undefined, ): T { return deepmerge(optionA ?? {}, optionB ?? {}, { customMerge: (key) => { const externalMerge = customMergeFn?.(key) if (externalMerge) return externalMerge if (key === 'color') { return (_, b: T['color']) => b } if (key === 'axisLabel') { return ( a: Record | undefined, b: Record | undefined, ) => { const merged = { ...a, ...b } if ( typeof a?.formatter === 'function' && typeof b?.formatter === 'function' ) { const baseFormatter = a.formatter as (value: unknown) => unknown const customFormatter = b.formatter as (value: unknown) => unknown merged.formatter = (value: unknown) => { const baseResult = baseFormatter(value) if (baseResult === '') return '' return customFormatter(value) } } return merged } } if (key === 'series') { return (a: T['series'][], b: T['series'][]) => { const maxLength = Math.max(a?.length ?? 0, b?.length ?? 0) return Array.from({ length: maxLength }, (_, index) => { const aItem = a?.[index] ?? {} const bItem = b?.[index] ?? {} return deepmerge(aItem, bItem as object) }) } } }, }) as T } /** * Generates ECharts `dataZoom` configuration for interactive zoom and pan on chart axes. * * @param range - Initial visible range as `{ start, end }` percentages (0-100). Defaults to full range. * @param options - Zoom behavior options including axis sliders, inside zoom, and formatter. * @param theme - Optional MUI theme for styled slider handles and backgrounds. * @returns An object containing the `dataZoom` array configuration. */ export function getEChartZoomConfig( { start, end }: { start: number; end: number } = { start: 0, end: 100 }, { inside = true, xSlider = true, ySlider = false, showSliders = true, xAxisLabelFormatter, bottomOffset = 0, } = {} as { inside?: boolean xSlider?: boolean ySlider?: boolean showSliders?: boolean xAxisLabelFormatter?: (value: number) => string bottomOffset?: number }, theme?: Theme, ) { const zoom = true const sliderStyles = theme ? getEChartZoomSliderStyles(theme) : {} return { dataZoom: [ inside && { throttle: 0, type: 'inside', xAxisIndex: xSlider ? [0] : [], yAxisIndex: ySlider ? [0] : [], show: zoom, zoomLock: !zoom, start, end, }, inside && ySlider && { throttle: 0, type: 'inside', show: zoom, zoomLock: !zoom, start, end, orientation: 'vertical', }, xSlider && { throttle: 0, type: 'slider', xAxisIndex: [0], bottom: bottomOffset, height: parseInt(theme?.spacing?.(4) ?? '32'), show: zoom && showSliders, zoomLock: !zoom, start, end, labelFormatter: xAxisLabelFormatter, showDetail: false, ...sliderStyles, brushSelect: false, moveHandleSize: 8, handleSize: '100%', handleIcon: 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIzNiIgdmlld0JveD0iMCAwIDkgMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iLTAuNSIgd2lkdGg9IjgiIGhlaWdodD0iMTgiIHJ4PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDI3KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMyAyMykiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDMgMTkpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAzIDE1KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4=', }, ySlider && { throttle: 0, type: 'slider', left: parseInt(theme?.spacing?.(6) ?? '48'), width: parseInt(theme?.spacing?.(4) ?? '32'), yAxisIndex: [0], show: zoom && showSliders, zoomLock: !zoom, start, end, ...sliderStyles, brushSelect: false, moveHandleSize: 8, handleSize: '100%', handleIcon: 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDM2IDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iMC41IiB3aWR0aD0iOCIgaGVpZ2h0PSIxOCIgcng9IjQiIHRyYW5zZm9ybT0icm90YXRlKC05MCAwLjUgMC41KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDEzIDYpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09InJvdGF0ZSgtOTAgMTcgNikiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0icm90YXRlKC05MCAyMSA2KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4=', }, ].filter((d) => !!d), } } function getEChartZoomSliderStyles(theme: Theme) { return { fillerColor: 'rgba(53, 139, 231, 0.25)', borderColor: 'rgba(53, 139, 231, 0.3)', borderWidth: 0.5, backgroundColor: 'transparent', borderRadius: 4, dataBackground: { lineStyle: { opacity: 0, }, areaStyle: { opacity: 1, color: theme.palette.secondary.main, }, }, selectedDataBackground: { lineStyle: { opacity: 0, }, areaStyle: { opacity: 1, color: theme.palette.secondary.main, }, }, handleStyle: { color: theme.palette.common.white, borderColor: 'rgba(3, 111, 226, 0.08)', borderWidth: 1, shadowBlur: 3, shadowColor: 'rgba(0, 0, 0, 0.1)', shadowOffsetX: 0, shadowOffsetY: 1, }, textStyle: { color: theme.palette.black[60], fontSize: parseInt(theme.typography.overlineDelicate.fontSize as string), fontFamily: theme.typography.overlineDelicate.fontFamily, }, } as const } /** * Generates ECharts `brush` configuration for interactive data selection on charts. * * @param options - Brush options including type, mode, and target axis index. * @returns An object containing the `brush` configuration. */ export function getEChartBrushConfig( { brushType = 'lineX', brushMode = 'single', xAxisIndex = 0 } = {} as { brushType?: string brushMode?: string xAxisIndex?: number throttleType?: string throttleDelay?: number }, ) { return { brush: { toolbox: ['lineX', 'clear'], brushType, brushMode, xAxisIndex, }, } } /** * Returns an ECharts `stack` configuration to group series into a stacked layout. * * @param stackGroup - The stack group identifier. Defaults to the library's default group. * @returns An object with the `stack` property set to the group name. */ export function getEChartStackConfig(stackGroup: string = DEFAULT_STACK_GROUP) { return { stack: stackGroup } }