@carto/ps-react-ui 6.1.1 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actions-BWlS5tpP.js → actions-CkxKe8-J.js} +57 -47
- package/dist/actions-CkxKe8-J.js.map +1 -0
- package/dist/category-DULnoxvk.js +933 -0
- package/dist/category-DULnoxvk.js.map +1 -0
- package/dist/chat.js +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/components-CTUpdQuj.js.map +1 -1
- package/dist/download-B4sykOFm.js.map +1 -1
- package/dist/{echart-CV8F50gn.js → echart-ysYFLa_T.js} +2 -2
- package/dist/echart-ysYFLa_T.js.map +1 -0
- package/dist/formula-DJS-JLpk.js.map +1 -1
- package/dist/{option-builders-DQivvvbb.js → item-color-B7xkIpDZ.js} +26 -9
- package/dist/item-color-B7xkIpDZ.js.map +1 -0
- package/dist/legend.js.map +1 -1
- package/dist/{markdown-Bn2nKCbn.js → markdown-DmvLi3vx.js} +2 -2
- package/dist/markdown-DmvLi3vx.js.map +1 -0
- package/dist/{markdown-content-kYNLOX0T.js → markdown-content-BpAH_V6S.js} +5 -4
- package/dist/markdown-content-BpAH_V6S.js.map +1 -0
- package/dist/range-BpU2zBmq.js.map +1 -1
- package/dist/smart-tooltip-Dc_rHhyI.js.map +1 -1
- package/dist/spread-CQwFIwuz.js.map +1 -1
- package/dist/stores-DcBOZlU3.js.map +1 -1
- package/dist/stores-mad-9yFn.js.map +1 -1
- package/dist/styles-BCEBm7xh.js.map +1 -1
- package/dist/table-DEdW_fDj.js.map +1 -1
- package/dist/types/widgets/bar/types.d.ts +6 -0
- package/dist/types/widgets/category/category-ui.d.ts +10 -2
- package/dist/types/widgets/category/category.d.ts +7 -1
- package/dist/types/widgets/category/components/category-bar-stacked.d.ts +8 -1
- package/dist/types/widgets/category/components/category-bar.d.ts +24 -2
- package/dist/types/widgets/category/components/category-row-multi.d.ts +3 -1
- package/dist/types/widgets/category/components/category-row-single.d.ts +3 -1
- package/dist/types/widgets/category/components/category-row-stacked.d.ts +10 -1
- package/dist/types/widgets/category/style.d.ts +33 -6
- package/dist/types/widgets/histogram/bins.d.ts +7 -0
- package/dist/types/widgets/histogram/download.d.ts +2 -2
- package/dist/types/widgets/histogram/index.d.ts +2 -1
- package/dist/types/widgets/histogram/options.d.ts +1 -1
- package/dist/types/widgets/histogram/transforms.d.ts +5 -4
- package/dist/types/widgets/histogram/types.d.ts +22 -5
- package/dist/types/widgets/index.d.ts +1 -1
- package/dist/types/widgets/pie/types.d.ts +6 -0
- package/dist/types/widgets/scatterplot/download.d.ts +2 -2
- package/dist/types/widgets/scatterplot/index.d.ts +2 -1
- package/dist/types/widgets/scatterplot/points.d.ts +7 -0
- package/dist/types/widgets/scatterplot/transforms.d.ts +4 -3
- package/dist/types/widgets/scatterplot/types.d.ts +21 -2
- package/dist/types/widgets/timeseries/types.d.ts +6 -0
- package/dist/types/widgets/utils/item-color.d.ts +26 -0
- package/dist/utils-C-eWwOhE.js.map +1 -1
- package/dist/widgets/actions.js +1 -1
- package/dist/widgets/bar.js +78 -70
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +1 -1
- package/dist/widgets/echart.js +1 -1
- package/dist/widgets/histogram.js +116 -85
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/pie.js +109 -90
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/scatterplot.js +131 -92
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/timeseries.js +82 -74
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets.js +5 -5
- package/dist/widgets.js.map +1 -1
- package/dist/zoom-toggle-MmLjMTEw.js.map +1 -1
- package/package.json +2 -2
- package/src/widgets/actions/fullscreen/fullscreen-store.test.tsx +77 -13
- package/src/widgets/actions/fullscreen/fullscreen.tsx +48 -5
- package/src/widgets/bar/options.test.ts +106 -0
- package/src/widgets/bar/options.ts +25 -14
- package/src/widgets/bar/types.ts +6 -0
- package/src/widgets/category/category-ui.test.tsx +139 -0
- package/src/widgets/category/category-ui.tsx +64 -11
- package/src/widgets/category/category.test.tsx +67 -0
- package/src/widgets/category/category.tsx +32 -15
- package/src/widgets/category/components/category-bar-stacked.test.tsx +18 -0
- package/src/widgets/category/components/category-bar-stacked.tsx +42 -29
- package/src/widgets/category/components/category-bar.test.tsx +66 -0
- package/src/widgets/category/components/category-bar.tsx +65 -9
- package/src/widgets/category/components/category-row-multi.tsx +21 -0
- package/src/widgets/category/components/category-row-single.tsx +4 -0
- package/src/widgets/category/components/category-row-stacked.tsx +24 -1
- package/src/widgets/category/style.ts +58 -14
- package/src/widgets/histogram/bins.ts +22 -0
- package/src/widgets/histogram/download.test.ts +10 -0
- package/src/widgets/histogram/download.ts +7 -3
- package/src/widgets/histogram/index.ts +3 -0
- package/src/widgets/histogram/options.test.ts +62 -0
- package/src/widgets/histogram/options.ts +38 -17
- package/src/widgets/histogram/transforms.test.ts +21 -7
- package/src/widgets/histogram/transforms.ts +26 -12
- package/src/widgets/histogram/types.ts +25 -5
- package/src/widgets/index.ts +6 -1
- package/src/widgets/pie/options.test.ts +91 -0
- package/src/widgets/pie/options.ts +50 -20
- package/src/widgets/pie/types.ts +6 -0
- package/src/widgets/scatterplot/download.test.ts +9 -0
- package/src/widgets/scatterplot/download.ts +5 -3
- package/src/widgets/scatterplot/index.ts +3 -0
- package/src/widgets/scatterplot/options.test.ts +78 -0
- package/src/widgets/scatterplot/options.ts +35 -11
- package/src/widgets/scatterplot/points.ts +35 -0
- package/src/widgets/scatterplot/transforms.test.ts +19 -0
- package/src/widgets/scatterplot/transforms.ts +23 -18
- package/src/widgets/scatterplot/types.ts +25 -2
- package/src/widgets/timeseries/options.test.ts +65 -0
- package/src/widgets/timeseries/options.ts +31 -14
- package/src/widgets/timeseries/types.ts +6 -0
- package/src/widgets/utils/item-color.test.ts +74 -0
- package/src/widgets/utils/item-color.ts +55 -0
- package/dist/actions-BWlS5tpP.js.map +0 -1
- package/dist/category-alzjKzvQ.js +0 -839
- package/dist/category-alzjKzvQ.js.map +0 -1
- package/dist/echart-CV8F50gn.js.map +0 -1
- package/dist/markdown-Bn2nKCbn.js.map +0 -1
- package/dist/markdown-content-kYNLOX0T.js.map +0 -1
- package/dist/option-builders-DQivvvbb.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zoom-toggle-MmLjMTEw.js","names":["Icon = ZoomInIcon","{\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps","initialEnabled = false","{ ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }","axes ? axes.includes('x') : true","axes ? axes.includes('y') : false","createAddZoom(theme, { axes: resolved })","useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])","{ initialEnabled }","() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }","useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])","{ ...styles.toggle, ...(enabled && styles.toggleActive) }","enabled && styles.toggleActive","<Icon fontSize='small' {...iconProps} />","<IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>","enabled ? 'active' : undefined","<Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>"],"sources":["../src/widgets/actions/zoom-toggle/transforms.ts","../src/widgets/actions/zoom-toggle/labels.ts","../src/widgets/actions/zoom-toggle/style.ts","../src/widgets/actions/zoom-toggle/zoom-toggle.tsx"],"sourcesContent":["import type { Theme } from '@mui/material'\n\n/**\n * v1's slider handle — 9×36 SVG with a white pill body, secondary-colored\n * stroke, and three grip lines. Inlined as a data URL so the chart can\n * render it without a network fetch.\n */\nconst SLIDER_HANDLE_ICON =\n 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIzNiIgdmlld0JveD0iMCAwIDkgMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iLTAuNSIgd2lkdGg9IjgiIGhlaWdodD0iMTgiIHJ4PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDI3KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMyAyMykiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDMgMTkpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAzIDE1KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4='\n\nconst SLIDER_HEIGHT = 32\nconst SLIDER_BOTTOM = 0\n\n/** Which chart axis (or axes) zoom applies to. */\nexport type ZoomAxis = 'x' | 'y'\n\nexport interface CreateAddZoomOptions {\n /**\n * Axes to enable zoom on. Default `['x']` (matches bar / histogram /\n * timeseries — horizontal pan/zoom on the category axis). Pass\n * `['x', 'y']` for a scatterplot-style 2D zoom (mouse-wheel scales both\n * axes; a horizontal slider sits at the bottom for x-range and a\n * vertical slider sits on the right for y-range).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Theme-bound zoom config transform: adds one inside `dataZoom`\n * (mouse-wheel + drag) per axis plus a styled slider per axis with the\n * v1 chrome (blue-tinted filler, secondary-color data preview, white\n * pill handles, CARTO typography). The x-slider is horizontal at the\n * bottom; the y-slider is vertical on the right.\n *\n * Returned function has stable identity for a given theme + axes\n * combination; pair with `useMemo` at the call site.\n *\n * **No `start` / `end`** are emitted: ECharts defaults to `0–100` on the\n * first `setOption` and then keeps the runtime slider state across\n * subsequent merges (no `replaceMerge` for `dataZoom`). Re-running the\n * transform on data/formatter/RelativeData updates therefore preserves the\n * user's dragged range instead of snapping it back.\n */\nexport function createAddZoom(\n theme: Theme,\n options: CreateAddZoomOptions = {},\n): (option: unknown) => unknown {\n const axes = options.axes ?? ['x']\n const includeX = axes.includes('x')\n const includeY = axes.includes('y')\n const sliderStyles = getZoomSliderStyles(theme)\n const baseSliderProps = {\n throttle: 0,\n showDetail: false,\n brushSelect: false,\n moveHandleSize: 8,\n handleSize: '100%',\n ...sliderStyles,\n }\n return (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n const dataZoom: object[] = []\n if (includeX) {\n dataZoom.push({ type: 'inside', xAxisIndex: [0], throttle: 0 })\n }\n if (includeY) {\n dataZoom.push({ type: 'inside', yAxisIndex: [0], throttle: 0 })\n }\n if (includeX) {\n dataZoom.push({\n type: 'slider',\n xAxisIndex: [0],\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n handleIcon: SLIDER_HANDLE_ICON,\n ...baseSliderProps,\n })\n }\n if (includeY) {\n // Vertical slider — `width` is its thickness, `right` pins it to\n // the right edge. We omit `handleIcon` so ECharts renders its\n // default vertical handle (the inline SVG is shaped for a\n // horizontal slider and looks wrong when rotated).\n dataZoom.push({\n type: 'slider',\n yAxisIndex: [0],\n width: SLIDER_HEIGHT,\n right: SLIDER_BOTTOM,\n ...baseSliderProps,\n })\n }\n return { ...cfg, dataZoom }\n }\n}\n\n/**\n * Back-compat untyped transform — produces the bare `dataZoom` config from\n * the original v2 implementation. Use {@link createAddZoom} when a theme is\n * available so the slider matches the v1 styling.\n *\n * `start` / `end` are intentionally omitted so ECharts preserves the user's\n * runtime range across subsequent `setOption` merges (see {@link createAddZoom}).\n */\nexport const addZoom = (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n return {\n ...cfg,\n dataZoom: [\n { type: 'inside' },\n {\n type: 'slider',\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n },\n ],\n }\n}\n\n/** Style block matching v1's `getEChartZoomSliderStyles`. */\nfunction getZoomSliderStyles(theme: Theme) {\n const secondary = theme.palette.secondary?.main ?? '#358BE7'\n return {\n fillerColor: 'rgba(53, 139, 231, 0.25)',\n borderColor: 'rgba(53, 139, 231, 0.3)',\n borderWidth: 0.5,\n backgroundColor: 'transparent',\n borderRadius: 4,\n dataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n selectedDataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n handleStyle: {\n color: theme.palette.common?.white ?? '#fff',\n borderColor: 'rgba(3, 111, 226, 0.08)',\n borderWidth: 1,\n shadowBlur: 3,\n shadowColor: 'rgba(0, 0, 0, 0.1)',\n shadowOffsetX: 0,\n shadowOffsetY: 1,\n },\n textStyle: {\n color: theme.palette.black?.[60] ?? theme.palette.text?.primary,\n fontSize: parseInt(\n (theme.typography.overlineDelicate?.fontSize as string | undefined) ??\n '11',\n ),\n fontFamily:\n theme.typography.overlineDelicate?.fontFamily ??\n theme.typography.caption?.fontFamily,\n },\n }\n}\n\n/**\n * Visual layout constants used by the data-fusion mergers (bar, histogram,\n * timeseries) when laying out the slider against the legend / grid.\n */\nexport const ZOOM_LAYOUT = {\n /** Slider height in px (matches `createAddZoom` output). */\n sliderHeight: SLIDER_HEIGHT,\n /** Vertical gap between chart grid and the slider. */\n sliderGap: 8,\n /** Slider `bottom` when a legend is shown — sits above the legend row. */\n sliderBottomWithLegend: 28,\n} as const\n","export interface ZoomToggleLabels {\n on: string\n off: string\n}\n\nexport const DEFAULT_ZOOM_TOGGLE_LABELS: ZoomToggleLabels = {\n on: 'Disable zoom',\n off: 'Enable zoom',\n}\n","import type { SxProps, Theme } from '@mui/material'\n\nexport const styles = {\n toggle: {\n p: 0.5,\n '& .MuiSvgIcon-root': { fontSize: 20 },\n },\n toggleActive: {\n background: (theme: Theme) => theme.palette.primary.relatedLight,\n },\n} satisfies Record<string, SxProps<Theme>>\n","import { useCallback, useMemo, type ComponentType } from 'react'\nimport { IconButton, useTheme, type SvgIconProps } from '@mui/material'\nimport { ZoomIn as ZoomInIcon } from '@mui/icons-material'\nimport { Tooltip } from '../../../components'\nimport {\n getEchartInstance,\n useSingleTransform,\n useWidgetId,\n} from '../../stores'\nimport { createAddZoom, type ZoomAxis } from './transforms'\nimport { DEFAULT_ZOOM_TOGGLE_LABELS, type ZoomToggleLabels } from './labels'\nimport { styles } from './style'\n\nconst ZOOM_DESCRIPTOR = {\n id: 'zoom-toggle',\n type: 'config' as const,\n order: 20,\n}\n\nexport interface ZoomToggleProps {\n initialEnabled?: boolean\n labels?: Partial<ZoomToggleLabels>\n icon?: ComponentType<SvgIconProps>\n iconProps?: SvgIconProps\n /**\n * Which axes the zoom controls. Default `['x']` — matches bar /\n * histogram / timeseries (horizontal pan on the category axis). Pass\n * `['x', 'y']` for 2D zoom on a scatterplot-style chart (mouse-wheel\n * scales both axes; horizontal slider at bottom + vertical slider on\n * the right).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Toggle a `dataZoom` slider + wheel-zoom on the chart. The transform is\n * theme-bound so the slider gets v1's CARTO chrome (blue-tinted filler,\n * secondary-color preview, white pill handles).\n *\n * `dataZoom` is intentionally NOT in `replaceMergeKeys` — ECharts merges\n * subsequent `setOption` calls so the user's dragged range survives\n * unrelated re-renders (data updates, formatter swaps, RelativeData toggles).\n *\n * On disable the transform is unregistered, but ECharts' merge semantics\n * would otherwise *keep* the live `dataZoom` component visible. To remove\n * the slider immediately we issue a one-shot\n * `setOption({}, { replaceMerge: ['dataZoom'] })` against the live chart\n * instance via {@link getEchartInstance} — this bypasses the pipeline so\n * unrelated transforms aren't disturbed.\n */\nexport function ZoomToggle({\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps) {\n const id = useWidgetId()\n const theme = useTheme()\n const _labels = { ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }\n // Reduce `axes` to two structural booleans before memoizing so the memo\n // depends on stable primitive values, not an array reference (callers\n // typically pass an inline `axes={['x', 'y']}` literal that changes\n // identity each render). Default — no `axes` prop — is x-axis only,\n // matching `createAddZoom`'s default.\n const includeX = axes ? axes.includes('x') : true\n const includeY = axes ? axes.includes('y') : false\n const transform = useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])\n const { enabled, toggle } = useSingleTransform(\n id,\n ZOOM_DESCRIPTOR,\n transform,\n { initialEnabled },\n )\n\n const handleToggle = useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])\n\n const tooltip = enabled ? _labels.on : _labels.off\n\n return (\n <Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"zoom-toggle-MmLjMTEw.js","names":["cfg","Icon = ZoomInIcon","{\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps","initialEnabled = false","{ ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }","axes ? axes.includes('x') : true","axes ? axes.includes('y') : false","createAddZoom(theme, { axes: resolved })","useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])","{ initialEnabled }","() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }","useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])","{ ...styles.toggle, ...(enabled && styles.toggleActive) }","enabled && styles.toggleActive","<Icon fontSize='small' {...iconProps} />","<IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>","enabled ? 'active' : undefined","<Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>","_c","ZoomInIcon","Tooltip","Tooltip$1","useWidgetId","getEchartInstance","useSingleTransform","SLIDER_HANDLE_ICON","SLIDER_HEIGHT","SLIDER_BOTTOM","createAddZoom","axes","options","includeX","includeY","baseSliderProps","getZoomSliderStyles","theme","option","cfg","dataZoom","addZoom","secondary","ZOOM_LAYOUT","DEFAULT_ZOOM_TOGGLE_LABELS","styles","ZOOM_DESCRIPTOR","ZoomToggle","$","c","t1","t2","t0","initialEnabled","Icon","ZoomIn","id","useTheme","t3","labels","_labels","t4","t5","t6","resolved","transform","t7","t8","enabled","toggle","handleToggle","tooltip","t9","t10","t11","t12","iconProps","t13","IconButton","t14"],"sources":["../src/widgets/actions/zoom-toggle/transforms.ts","../src/widgets/actions/zoom-toggle/labels.ts","../src/widgets/actions/zoom-toggle/style.ts","../src/widgets/actions/zoom-toggle/zoom-toggle.tsx"],"sourcesContent":["import type { Theme } from '@mui/material'\n\n/**\n * v1's slider handle — 9×36 SVG with a white pill body, secondary-colored\n * stroke, and three grip lines. Inlined as a data URL so the chart can\n * render it without a network fetch.\n */\nconst SLIDER_HANDLE_ICON =\n 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIzNiIgdmlld0JveD0iMCAwIDkgMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iLTAuNSIgd2lkdGg9IjgiIGhlaWdodD0iMTgiIHJ4PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDI3KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMyAyMykiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDMgMTkpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAzIDE1KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4='\n\nconst SLIDER_HEIGHT = 32\nconst SLIDER_BOTTOM = 0\n\n/** Which chart axis (or axes) zoom applies to. */\nexport type ZoomAxis = 'x' | 'y'\n\nexport interface CreateAddZoomOptions {\n /**\n * Axes to enable zoom on. Default `['x']` (matches bar / histogram /\n * timeseries — horizontal pan/zoom on the category axis). Pass\n * `['x', 'y']` for a scatterplot-style 2D zoom (mouse-wheel scales both\n * axes; a horizontal slider sits at the bottom for x-range and a\n * vertical slider sits on the right for y-range).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Theme-bound zoom config transform: adds one inside `dataZoom`\n * (mouse-wheel + drag) per axis plus a styled slider per axis with the\n * v1 chrome (blue-tinted filler, secondary-color data preview, white\n * pill handles, CARTO typography). The x-slider is horizontal at the\n * bottom; the y-slider is vertical on the right.\n *\n * Returned function has stable identity for a given theme + axes\n * combination; pair with `useMemo` at the call site.\n *\n * **No `start` / `end`** are emitted: ECharts defaults to `0–100` on the\n * first `setOption` and then keeps the runtime slider state across\n * subsequent merges (no `replaceMerge` for `dataZoom`). Re-running the\n * transform on data/formatter/RelativeData updates therefore preserves the\n * user's dragged range instead of snapping it back.\n */\nexport function createAddZoom(\n theme: Theme,\n options: CreateAddZoomOptions = {},\n): (option: unknown) => unknown {\n const axes = options.axes ?? ['x']\n const includeX = axes.includes('x')\n const includeY = axes.includes('y')\n const sliderStyles = getZoomSliderStyles(theme)\n const baseSliderProps = {\n throttle: 0,\n showDetail: false,\n brushSelect: false,\n moveHandleSize: 8,\n handleSize: '100%',\n ...sliderStyles,\n }\n return (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n const dataZoom: object[] = []\n if (includeX) {\n dataZoom.push({ type: 'inside', xAxisIndex: [0], throttle: 0 })\n }\n if (includeY) {\n dataZoom.push({ type: 'inside', yAxisIndex: [0], throttle: 0 })\n }\n if (includeX) {\n dataZoom.push({\n type: 'slider',\n xAxisIndex: [0],\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n handleIcon: SLIDER_HANDLE_ICON,\n ...baseSliderProps,\n })\n }\n if (includeY) {\n // Vertical slider — `width` is its thickness, `right` pins it to\n // the right edge. We omit `handleIcon` so ECharts renders its\n // default vertical handle (the inline SVG is shaped for a\n // horizontal slider and looks wrong when rotated).\n dataZoom.push({\n type: 'slider',\n yAxisIndex: [0],\n width: SLIDER_HEIGHT,\n right: SLIDER_BOTTOM,\n ...baseSliderProps,\n })\n }\n return { ...cfg, dataZoom }\n }\n}\n\n/**\n * Back-compat untyped transform — produces the bare `dataZoom` config from\n * the original v2 implementation. Use {@link createAddZoom} when a theme is\n * available so the slider matches the v1 styling.\n *\n * `start` / `end` are intentionally omitted so ECharts preserves the user's\n * runtime range across subsequent `setOption` merges (see {@link createAddZoom}).\n */\nexport const addZoom = (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n return {\n ...cfg,\n dataZoom: [\n { type: 'inside' },\n {\n type: 'slider',\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n },\n ],\n }\n}\n\n/** Style block matching v1's `getEChartZoomSliderStyles`. */\nfunction getZoomSliderStyles(theme: Theme) {\n const secondary = theme.palette.secondary?.main ?? '#358BE7'\n return {\n fillerColor: 'rgba(53, 139, 231, 0.25)',\n borderColor: 'rgba(53, 139, 231, 0.3)',\n borderWidth: 0.5,\n backgroundColor: 'transparent',\n borderRadius: 4,\n dataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n selectedDataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n handleStyle: {\n color: theme.palette.common?.white ?? '#fff',\n borderColor: 'rgba(3, 111, 226, 0.08)',\n borderWidth: 1,\n shadowBlur: 3,\n shadowColor: 'rgba(0, 0, 0, 0.1)',\n shadowOffsetX: 0,\n shadowOffsetY: 1,\n },\n textStyle: {\n color: theme.palette.black?.[60] ?? theme.palette.text?.primary,\n fontSize: parseInt(\n (theme.typography.overlineDelicate?.fontSize as string | undefined) ??\n '11',\n ),\n fontFamily:\n theme.typography.overlineDelicate?.fontFamily ??\n theme.typography.caption?.fontFamily,\n },\n }\n}\n\n/**\n * Visual layout constants used by the data-fusion mergers (bar, histogram,\n * timeseries) when laying out the slider against the legend / grid.\n */\nexport const ZOOM_LAYOUT = {\n /** Slider height in px (matches `createAddZoom` output). */\n sliderHeight: SLIDER_HEIGHT,\n /** Vertical gap between chart grid and the slider. */\n sliderGap: 8,\n /** Slider `bottom` when a legend is shown — sits above the legend row. */\n sliderBottomWithLegend: 28,\n} as const\n","export interface ZoomToggleLabels {\n on: string\n off: string\n}\n\nexport const DEFAULT_ZOOM_TOGGLE_LABELS: ZoomToggleLabels = {\n on: 'Disable zoom',\n off: 'Enable zoom',\n}\n","import type { SxProps, Theme } from '@mui/material'\n\nexport const styles = {\n toggle: {\n p: 0.5,\n '& .MuiSvgIcon-root': { fontSize: 20 },\n },\n toggleActive: {\n background: (theme: Theme) => theme.palette.primary.relatedLight,\n },\n} satisfies Record<string, SxProps<Theme>>\n","import { useCallback, useMemo, type ComponentType } from 'react'\nimport { IconButton, useTheme, type SvgIconProps } from '@mui/material'\nimport { ZoomIn as ZoomInIcon } from '@mui/icons-material'\nimport { Tooltip } from '../../../components'\nimport {\n getEchartInstance,\n useSingleTransform,\n useWidgetId,\n} from '../../stores'\nimport { createAddZoom, type ZoomAxis } from './transforms'\nimport { DEFAULT_ZOOM_TOGGLE_LABELS, type ZoomToggleLabels } from './labels'\nimport { styles } from './style'\n\nconst ZOOM_DESCRIPTOR = {\n id: 'zoom-toggle',\n type: 'config' as const,\n order: 20,\n}\n\nexport interface ZoomToggleProps {\n initialEnabled?: boolean\n labels?: Partial<ZoomToggleLabels>\n icon?: ComponentType<SvgIconProps>\n iconProps?: SvgIconProps\n /**\n * Which axes the zoom controls. Default `['x']` — matches bar /\n * histogram / timeseries (horizontal pan on the category axis). Pass\n * `['x', 'y']` for 2D zoom on a scatterplot-style chart (mouse-wheel\n * scales both axes; horizontal slider at bottom + vertical slider on\n * the right).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Toggle a `dataZoom` slider + wheel-zoom on the chart. The transform is\n * theme-bound so the slider gets v1's CARTO chrome (blue-tinted filler,\n * secondary-color preview, white pill handles).\n *\n * `dataZoom` is intentionally NOT in `replaceMergeKeys` — ECharts merges\n * subsequent `setOption` calls so the user's dragged range survives\n * unrelated re-renders (data updates, formatter swaps, RelativeData toggles).\n *\n * On disable the transform is unregistered, but ECharts' merge semantics\n * would otherwise *keep* the live `dataZoom` component visible. To remove\n * the slider immediately we issue a one-shot\n * `setOption({}, { replaceMerge: ['dataZoom'] })` against the live chart\n * instance via {@link getEchartInstance} — this bypasses the pipeline so\n * unrelated transforms aren't disturbed.\n */\nexport function ZoomToggle({\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps) {\n const id = useWidgetId()\n const theme = useTheme()\n const _labels = { ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }\n // Reduce `axes` to two structural booleans before memoizing so the memo\n // depends on stable primitive values, not an array reference (callers\n // typically pass an inline `axes={['x', 'y']}` literal that changes\n // identity each render). Default — no `axes` prop — is x-axis only,\n // matching `createAddZoom`'s default.\n const includeX = axes ? axes.includes('x') : true\n const includeY = axes ? axes.includes('y') : false\n const transform = useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])\n const { enabled, toggle } = useSingleTransform(\n id,\n ZOOM_DESCRIPTOR,\n transform,\n { initialEnabled },\n )\n\n const handleToggle = useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])\n\n const tooltip = enabled ? _labels.on : _labels.off\n\n return (\n <Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>\n )\n}\n"],"mappings":";;;;;;;;AAOA,IAAMyB,IACJ,8pBAEIC,IAAgB,IAChBC,IAAgB;AAgCtB,SAAgBC,EACd,GACA,IAAgC,CAAC,GACH;CAC9B,IAAMC,IAAOC,EAAQ,QAAQ,CAAC,GAAG,GAC3BC,IAAWF,EAAK,SAAS,GAAG,GAC5BG,IAAWH,EAAK,SAAS,GAAG,GAE5BI,IAAkB;EACtB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,gBAAgB;EAChB,YAAY;EACZ,GAPmBC,EAAoBC,CAOpC;CACL;CACA,QAAQ,MAA6B;EACnC,IAAsB,OAAOC,KAAW,aAApCA,GAA8C,OAAOA;EACzD,IAAMC,IAAMD,GACNE,IAAqB,CAAC;EA8B5B,OA7BIP,KACFO,EAAS,KAAK;GAAE,MAAM;GAAU,YAAY,CAAC,CAAC;GAAG,UAAU;EAAE,CAAC,GAE5DN,KACFM,EAAS,KAAK;GAAE,MAAM;GAAU,YAAY,CAAC,CAAC;GAAG,UAAU;EAAE,CAAC,GAE5DP,KACFO,EAAS,KAAK;GACZ,MAAM;GACN,YAAY,CAAC,CAAC;GACd,QAAQZ;GACR,QAAQC;GACR,YAAYF;GACZ,GAAGQ;EACL,CAAC,GAECD,KAKFM,EAAS,KAAK;GACZ,MAAM;GACN,YAAY,CAAC,CAAC;GACd,OAAOZ;GACP,OAAOC;GACP,GAAGM;EACL,CAAC,GAEI;GAAE,GAAGI;GAAK;EAAS;CAC5B;AACF;AAUA,IAAaE,KAAW,MACA,OAAOH,KAAW,aAApCA,IAAqDA,IAElD;CACL,GAAGpC;CACH,UAAU,CACR,EAAE,MAAM,SAAS,GACjB;EACE,MAAM;EACN,QAAQ0B;EACR,QAAQC;CACV,CACF;AACF;AAIF,SAASO,EAAoB,GAAc;CACzC,IAAMM,IAAYL,EAAM,QAAQ,WAAW,QAAQ;CACnD,OAAO;EACL,aAAa;EACb,aAAa;EACb,aAAa;EACb,iBAAiB;EACjB,cAAc;EACd,gBAAgB;GACd,WAAW,EAAE,SAAS,EAAE;GACxB,WAAW;IAAE,SAAS;IAAG,OAAOK;GAAU;EAC5C;EACA,wBAAwB;GACtB,WAAW,EAAE,SAAS,EAAE;GACxB,WAAW;IAAE,SAAS;IAAG,OAAOA;GAAU;EAC5C;EACA,aAAa;GACX,OAAOL,EAAM,QAAQ,QAAQ,SAAS;GACtC,aAAa;GACb,aAAa;GACb,YAAY;GACZ,aAAa;GACb,eAAe;GACf,eAAe;EACjB;EACA,WAAW;GACT,OAAOA,EAAM,QAAQ,QAAQ,OAAOA,EAAM,QAAQ,MAAM;GACxD,UAAU,SACPA,EAAM,WAAW,kBAAkB,YAClC,IACJ;GACA,YACEA,EAAM,WAAW,kBAAkB,cACnCA,EAAM,WAAW,SAAS;EAC9B;CACF;AACF;AAMA,IAAaM,IAAc;CAEzB,cAAcf;CAEd,WAAW;CAEX,wBAAwB;AAC1B,GCrKagB,IAA+C;CAC1D,IAAI;CACJ,KAAK;AACP,GCNaC,IAAS;CACpB,QAAQ;EACN,GAAG;EACH,sBAAsB,EAAE,UAAU,GAAG;CACvC;CACA,cAAc,EACZ,aAAa,MAAiBR,EAAM,QAAQ,QAAQ,aACtD;AACF,GCGMS,IAAkB;CACtB,IAAI;CACJ,MAAM;CACN,OAAO;AACT;AAiCA,SAAgBC,EAAA,GAMI;gBANO,EACzB,gBAAAG,GACA,WACA,MAAM/C,GACN,cACA,YALyBC,GACzBiD,IAAAH,MAAA,KAAA,KAAA7C,GAEMiD,IAAAH,MAAA,KAAA,IAAO9B,IAAPlB,GAIAqD,IAAKhC,EAAY,GACjBa,IAAQoB,EAAS,GACPnD;CAAoC,AAAA0C,EAAA,OAAAW,gBAApC,IAAA;EAAE,GAAGf;EAA4B,GAAGe;CAAO,GAAP,EAAA,KAAAA;CAApD,IAAMC,IAAUtD,GAMCC;0BAAO,IAAA,MAAAwB,EAAK,SAAS,GAAG,GAAxB,EAAA,KAAAA;CAAjB,IAAME,IAAW1B,GACAC;0BAAO,IAAAuB,IAAAA,EAAK,SAAS,GAAG,IAAI,IAA5B,EAAA,KAAAA;CAAjB,IAAMG,IAAW1B,GAKRC;CAFH,IAAAuC,EAAA,OAAAf,KAAAe,EAAA,OACAd,KAAAc,EAAA,OACiBX,GAAA;EAHrB,IAAM2B,IAAuB,CAAC;EAC1B/B,KAAU+B,EAAS,KAAK,GAAG,GAC3B9B,KAAU8B,EAAS,KAAK,GAAG,GACxB,IAAAlC,EAAcO,GAAO,EAAE,MAAM2B,EAAS,CAAC,GAF1C,EAAA,KAAA/B,GACA,EAAA,KAAAC,GACiB,EAAA,KAAAG;;CAJvB,IAAM4B,IAAYvD,GAUhBC;CAAE,AAAAqC,EAAA,QAAAK,iBAAF,IAAA,EAAE,kBAAe,GAAf,EAAA,MAAAA;CAJJ,IAAM,EAAE,YAAS,cAAW3B,EAC1B8B,GACAV,GACAmB,GACAtD,CACF,GAEiCC;CAC3B,AAAAoC,EAAA,QAAAoB,KAAApB,EAAA,QAGgBQ,KAAAR,EAAA,QAEpBqB,KAN+B,UAAM;EAMrC,AALID,KAGF3C,EAAkB+B,CAAE,CAAC,EAAE,UAAU,CAAC,GAAG,EAAE,cAAc,CAAC,UAAU,EAAE,CAAC,GAErEa,EAAO;CACT,GANM,EAAA,MAAAD,GAGgB,EAAA,MAAAZ,GAEpB,EAAA,MAAAa;CANF,IAAMC,IAAezD,GASf0D,IAAUH,IAAUR,EAAQ,KAAKA,EAAQ,KAS9BY,IAAAJ,IAAU,WAAW,KAAA,GACJK,IAAAL,KAAWvB,EAAO,cAA1C/B;CAAwBC,AAAAA,EAAAA,QAAAA,iBAAxB,IAAA;EAAE,GAAG8B,EAAO;EAAQ,GAAI9B;CAAgC,GAAhCA,EAAAA,MAAAA;CAE5BC,IAAAA;CAAC,AAAAgC,EAAA,QAAAM,KAAAN,EAAA,QAA0B4B,KAA3B,IAAA,kBAACtB,GAAD;EAAM,UAAS;EAAQ,GAAIsB;CAAY,CAAA,GAAtC,EAAA,MAAAtB,GAA0B,EAAA,MAAAsB;CAR7B3D,IAAAA;CAGgB,AAAA+B,EAAA,QAAAoB,KAAApB,EAAA,QACLsB,KAAAtB,EAAA,QAELlC,KAAAA,EAAAA,QAEJE,KAAAA,EAAAA,QAHWE,KAAAA,EAAAA,QAHCqD,KAFd,IAAA,kBAACO,GAAD;EACE,MAAK;EACL,cAAYP;EACZ,gBAAcH;EACd,SAASE;EACT,WAAWpD;EACX,IAAIJ;EAEJE,UAAAA;CACU,CAAA,GANI,EAAA,MAAAoD,GACL,EAAA,MAAAE,GAELxD,EAAAA,MAAAA,GAEJE,EAAAA,MAAAA,GAHWE,EAAAA,MAAAA,GAHC,EAAA,MAAAqD;CAHhBpD,IAAAA;CADF,OAEIF,EAAAA,QAAAA,KAAAA,EAAAA,QADcsD,KAAhB,IAAA,kBAACjD,GAAD;EAAS,OAAOiD;EACdtD,UAAAA;CAUO,CAAA,GAVPA,EAAAA,MAAAA,GADc,EAAA,MAAAsD,2BAAhBpD;AAaJ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carto/ps-react-ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "CARTO's Professional Service React Material library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@mui/icons-material": "9.4.0",
|
|
13
13
|
"@mui/material": "9.4.0",
|
|
14
14
|
"@mui/system": "9.4.0",
|
|
15
|
-
"@mui/x-date-pickers": "9.
|
|
15
|
+
"@mui/x-date-pickers": "9.14.0",
|
|
16
16
|
"@types/jscodeshift": "^17.3.0",
|
|
17
17
|
"echarts": "6.1.0",
|
|
18
18
|
"html2canvas": "1.4.1",
|
|
@@ -130,26 +130,90 @@ describe('<FullScreen.Slot> — the dialog runs on its own store', () => {
|
|
|
130
130
|
// Legend / dataZoom / brush live in the chart instance, not the store.
|
|
131
131
|
// The Slot moves them through the instance registry, so Widget.Echart
|
|
132
132
|
// doesn't need to know about fullscreen.
|
|
133
|
+
const fakeChart = (option: object, brushAreas?: unknown[]) =>
|
|
134
|
+
({
|
|
135
|
+
getOption: vi.fn(() => option),
|
|
136
|
+
setOption: vi.fn(),
|
|
137
|
+
dispatchAction: vi.fn(),
|
|
138
|
+
getModel: () => ({
|
|
139
|
+
getComponent: (type: string) =>
|
|
140
|
+
type === 'brush' ? { areas: brushAreas } : undefined,
|
|
141
|
+
}),
|
|
142
|
+
isDisposed: () => false,
|
|
143
|
+
}) as unknown as ECharts & {
|
|
144
|
+
setOption: ReturnType<typeof vi.fn>
|
|
145
|
+
dispatchAction: ReturnType<typeof vi.fn>
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
it('starts the dialog chart from the card chart and hands it back on close', async () => {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
const cardOption = {
|
|
150
|
+
series: [{ type: 'bar' }],
|
|
151
|
+
xAxis: [{ type: 'category' }],
|
|
152
|
+
legend: [{ selected: { a: true, b: false } }],
|
|
153
|
+
dataZoom: [{ start: 10, end: 60 }],
|
|
154
|
+
}
|
|
155
|
+
const card = fakeChart(cardOption)
|
|
156
|
+
const dialog = fakeChart({
|
|
157
|
+
legend: [{ selected: { a: false, b: true } }],
|
|
158
|
+
dataZoom: [{ start: 20, end: 80 }],
|
|
159
|
+
})
|
|
160
|
+
render(<Widget />)
|
|
161
|
+
act(() => setEchartInstance('w', card))
|
|
162
|
+
|
|
163
|
+
// The empty dialog chart takes the whole option, so the view state has
|
|
164
|
+
// the card's axes and series to land on.
|
|
165
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
166
|
+
act(() => setEchartInstance(dialogId, dialog))
|
|
167
|
+
expect(dialog.setOption).toHaveBeenCalledWith(cardOption)
|
|
168
|
+
|
|
169
|
+
await close()
|
|
170
|
+
expect(card.setOption).toHaveBeenCalledWith({
|
|
171
|
+
legend: [{ selected: { a: false, b: true } }],
|
|
172
|
+
dataZoom: [{ start: 20, end: 80 }],
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// The dialog often renders with its own styling (bigger axis labels,
|
|
177
|
+
// value labels on the series). Copying the whole option back would leave
|
|
178
|
+
// that styling on the card after closing.
|
|
179
|
+
it('hands back only the view state, not the dialog styling', async () => {
|
|
180
|
+
const card = fakeChart({ series: [{ type: 'bar' }] })
|
|
181
|
+
const dialog = fakeChart({
|
|
182
|
+
series: [{ type: 'bar', label: { show: true, rotate: 90 } }],
|
|
183
|
+
xAxis: [{ axisLabel: { fontSize: 13 } }],
|
|
184
|
+
dataZoom: [{ start: 0, end: 50, startValue: 3, endValue: 9 }],
|
|
185
|
+
})
|
|
186
|
+
render(<Widget />)
|
|
187
|
+
act(() => setEchartInstance('w', card))
|
|
188
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
189
|
+
act(() => setEchartInstance(dialogId, dialog))
|
|
190
|
+
|
|
191
|
+
await close()
|
|
192
|
+
expect(card.setOption).toHaveBeenCalledWith({
|
|
193
|
+
dataZoom: [{ start: 0, end: 50, startValue: 3, endValue: 9 }],
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// Brush areas aren't in getOption(); they travel through the brush action.
|
|
198
|
+
it('starts the dialog chart with the card brush areas and hands them back on close', async () => {
|
|
199
|
+
const cardAreas = [{ brushType: 'lineX', coordRange: [1, 3] }]
|
|
200
|
+
const dialogAreas = [{ brushType: 'lineX', coordRange: [4, 7] }]
|
|
201
|
+
const card = fakeChart({}, cardAreas)
|
|
202
|
+
const dialog = fakeChart({}, dialogAreas)
|
|
145
203
|
render(<Widget />)
|
|
146
204
|
act(() => setEchartInstance('w', card))
|
|
147
205
|
|
|
148
206
|
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
149
207
|
act(() => setEchartInstance(dialogId, dialog))
|
|
150
|
-
expect(dialog.
|
|
208
|
+
expect(dialog.dispatchAction).toHaveBeenCalledWith({
|
|
209
|
+
type: 'brush',
|
|
210
|
+
areas: cardAreas,
|
|
211
|
+
})
|
|
151
212
|
|
|
152
213
|
await close()
|
|
153
|
-
expect(card.
|
|
214
|
+
expect(card.dispatchAction).toHaveBeenCalledWith({
|
|
215
|
+
type: 'brush',
|
|
216
|
+
areas: dialogAreas,
|
|
217
|
+
})
|
|
154
218
|
})
|
|
155
219
|
})
|
|
@@ -312,15 +312,17 @@ function DialogStore({
|
|
|
312
312
|
const unsubscribe = subscribeEchartInstance(dialogId, (chart) => {
|
|
313
313
|
// Published from inside the chart's init, before its first
|
|
314
314
|
// setOption, so the dialog's own option merges on top of the copy.
|
|
315
|
+
// The empty chart takes the card's whole option: the legend, dataZoom
|
|
316
|
+
// and brush areas need the card's components (axes, brush) to land on.
|
|
315
317
|
const card = liveChart(sourceId)
|
|
316
|
-
if (chart && card) copyChartState(card, chart)
|
|
318
|
+
if (chart && card) copyChartState(card, chart, card.getOption())
|
|
317
319
|
dialogChart = chart
|
|
318
320
|
})
|
|
319
321
|
return () => {
|
|
320
322
|
unsubscribe()
|
|
321
323
|
const card = liveChart(sourceId)
|
|
322
324
|
if (card && dialogChart && !dialogChart.isDisposed()) {
|
|
323
|
-
copyChartState(dialogChart, card)
|
|
325
|
+
copyChartState(dialogChart, card, viewState(dialogChart))
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
328
|
}, [dialogId, sourceId])
|
|
@@ -340,10 +342,15 @@ function liveChart(id: string): ECharts | null {
|
|
|
340
342
|
/**
|
|
341
343
|
* Put on `chart` what `peer` is showing: the legend selection, the dataZoom
|
|
342
344
|
* window and any drawn brush areas, which live in the ECharts instance
|
|
343
|
-
* rather than the widget store.
|
|
345
|
+
* rather than the widget store. `option` is what of `peer`'s option to merge
|
|
346
|
+
* into `chart`.
|
|
344
347
|
*/
|
|
345
|
-
function copyChartState(
|
|
346
|
-
|
|
348
|
+
function copyChartState(
|
|
349
|
+
peer: ECharts,
|
|
350
|
+
chart: ECharts,
|
|
351
|
+
option: Parameters<ECharts['setOption']>[0],
|
|
352
|
+
): void {
|
|
353
|
+
chart.setOption(option)
|
|
347
354
|
// The pointer was over `peer`, not `chart`: drop the tooltip and axis
|
|
348
355
|
// pointer that came along, or they'd stay drawn until the next hover.
|
|
349
356
|
chart.dispatchAction({ type: 'hideTip' })
|
|
@@ -363,6 +370,42 @@ function copyChartState(peer: ECharts, chart: ECharts): void {
|
|
|
363
370
|
if (areas?.length) chart.dispatchAction({ type: 'brush', areas })
|
|
364
371
|
}
|
|
365
372
|
|
|
373
|
+
interface ViewStateOption {
|
|
374
|
+
// setOption takes an indexed option.
|
|
375
|
+
[key: string]: unknown
|
|
376
|
+
legend?: { selected?: Record<string, boolean> }[]
|
|
377
|
+
dataZoom?: Record<string, unknown>[]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const ZOOM_WINDOW_KEYS = ['start', 'end', 'startValue', 'endValue'] as const
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The part of `peer`'s option that is user view state. Only this goes back
|
|
384
|
+
* to the card on close: the dialog's full option also carries its own
|
|
385
|
+
* styling (value labels, bigger axis text), and merging it into the card
|
|
386
|
+
* would leave that styling behind once the dialog closes.
|
|
387
|
+
*/
|
|
388
|
+
function viewState(peer: ECharts): ViewStateOption {
|
|
389
|
+
const { legend, dataZoom } = peer.getOption() as ViewStateOption
|
|
390
|
+
const state: ViewStateOption = {}
|
|
391
|
+
if (legend?.some((l) => l.selected)) {
|
|
392
|
+
state.legend = legend.map((l) =>
|
|
393
|
+
l.selected ? { selected: l.selected } : {},
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
if (dataZoom?.length) {
|
|
397
|
+
state.dataZoom = dataZoom.map((zoom) =>
|
|
398
|
+
Object.fromEntries(
|
|
399
|
+
ZOOM_WINDOW_KEYS.filter((k) => zoom[k] !== undefined).map((k) => [
|
|
400
|
+
k,
|
|
401
|
+
zoom[k],
|
|
402
|
+
]),
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
return state
|
|
407
|
+
}
|
|
408
|
+
|
|
366
409
|
/**
|
|
367
410
|
* Merge a caller's `paperSx` over the default paper.
|
|
368
411
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import * as echarts from 'echarts'
|
|
2
3
|
import { createTheme, type Theme } from '@mui/material'
|
|
3
4
|
import { barOptions, createBarOptionFactory } from './options'
|
|
4
5
|
|
|
@@ -359,3 +360,108 @@ describe('createBarOptionFactory', () => {
|
|
|
359
360
|
expect(html).toContain('[ITEM:A]')
|
|
360
361
|
})
|
|
361
362
|
})
|
|
363
|
+
|
|
364
|
+
describe('createBarOptionFactory — per-item colour', () => {
|
|
365
|
+
interface Out {
|
|
366
|
+
series: { itemStyle: { color: (p: unknown) => string } }[]
|
|
367
|
+
}
|
|
368
|
+
const data = [
|
|
369
|
+
[
|
|
370
|
+
{ name: 'A', value: 1, color: '#00ff00' },
|
|
371
|
+
{ name: 'B', value: -2 },
|
|
372
|
+
],
|
|
373
|
+
]
|
|
374
|
+
const dim = (c: string) => echarts.color.modifyAlpha(c, 0.15)
|
|
375
|
+
|
|
376
|
+
it('datum colour wins over the series colour', () => {
|
|
377
|
+
const merge = createBarOptionFactory({
|
|
378
|
+
theme,
|
|
379
|
+
series: [{ name: 'S', color: '#0000ff' }],
|
|
380
|
+
})
|
|
381
|
+
const out = merge({}, data) as Out
|
|
382
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
383
|
+
expect(colorFn({ dataIndex: 0, color: '#0000ff', name: 'A' })).toBe(
|
|
384
|
+
'#00ff00',
|
|
385
|
+
)
|
|
386
|
+
expect(colorFn({ dataIndex: 1, color: '#0000ff', name: 'B' })).toBe(
|
|
387
|
+
'#0000ff',
|
|
388
|
+
)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('uses a string template itemStyle.color from optionsOverride', () => {
|
|
392
|
+
const factory = createBarOptionFactory({
|
|
393
|
+
theme,
|
|
394
|
+
series: [{ name: 'S', color: '#0000ff' }],
|
|
395
|
+
optionsOverride: {
|
|
396
|
+
series: [{ type: 'bar', itemStyle: { color: '#123456' } }],
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
const out = factory(factory(undefined, undefined), data) as Out
|
|
400
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
401
|
+
expect(colorFn({ dataIndex: 0, color: '#0000ff', name: 'A' })).toBe(
|
|
402
|
+
'#00ff00',
|
|
403
|
+
)
|
|
404
|
+
expect(colorFn({ dataIndex: 1, color: '#0000ff', name: 'B' })).toBe(
|
|
405
|
+
'#123456',
|
|
406
|
+
)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('calls a function template itemStyle.color with the ECharts params', () => {
|
|
410
|
+
const merge = createBarOptionFactory({ theme })
|
|
411
|
+
const out = merge(
|
|
412
|
+
{
|
|
413
|
+
series: [
|
|
414
|
+
{
|
|
415
|
+
type: 'bar',
|
|
416
|
+
itemStyle: {
|
|
417
|
+
color: (p: { dataIndex: number }) =>
|
|
418
|
+
p.dataIndex === 1 ? '#ff0000' : '#00ff00',
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
[
|
|
424
|
+
[
|
|
425
|
+
{ name: 'A', value: 1 },
|
|
426
|
+
{ name: 'B', value: -2 },
|
|
427
|
+
],
|
|
428
|
+
],
|
|
429
|
+
) as Out
|
|
430
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
431
|
+
expect(colorFn({ dataIndex: 0, color: '#abc', name: 'A' })).toBe('#00ff00')
|
|
432
|
+
expect(colorFn({ dataIndex: 1, color: '#abc', name: 'B' })).toBe('#ff0000')
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('resolves theme palette paths on the datum colour', () => {
|
|
436
|
+
const merge = createBarOptionFactory({ theme })
|
|
437
|
+
const out = merge({}, [[{ name: 'A', value: 1, color: 'primary.main' }]])
|
|
438
|
+
const colorFn = (out as Out).series[0]!.itemStyle.color
|
|
439
|
+
expect(colorFn({ dataIndex: 0, color: '#abc', name: 'A' })).toBe(
|
|
440
|
+
theme.palette.primary.main,
|
|
441
|
+
)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('dims both datum and template colours outside the selection', () => {
|
|
445
|
+
const merge = createBarOptionFactory({ theme, selection: ['Z'] })
|
|
446
|
+
const out = merge(
|
|
447
|
+
{ series: [{ type: 'bar', itemStyle: { color: '#123456' } }] },
|
|
448
|
+
data,
|
|
449
|
+
) as Out
|
|
450
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
451
|
+
expect(colorFn({ dataIndex: 0, color: '#abc', name: 'A' })).toBe(
|
|
452
|
+
dim('#00ff00'),
|
|
453
|
+
)
|
|
454
|
+
expect(colorFn({ dataIndex: 1, color: '#abc', name: 'B' })).toBe(
|
|
455
|
+
dim('#123456'),
|
|
456
|
+
)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('keeps the series / palette colour when no item colour is given', () => {
|
|
460
|
+
const merge = createBarOptionFactory({ theme, selection: ['Z'] })
|
|
461
|
+
const out = merge({}, [[{ name: 'A', value: 1 }]]) as Out
|
|
462
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
463
|
+
expect(colorFn({ dataIndex: 0, color: '#abcdef', name: 'A' })).toBe(
|
|
464
|
+
dim('#abcdef'),
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
})
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { ZOOM_LAYOUT } from '../actions/zoom-toggle'
|
|
13
13
|
import type { OptionFactory } from '../echart'
|
|
14
14
|
import { mergeOptions, resolveThemeColor } from '../utils'
|
|
15
|
+
import { createItemColorResolver } from '../utils/item-color'
|
|
15
16
|
import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
|
|
16
17
|
import type {
|
|
17
18
|
BarEChartsOption,
|
|
@@ -234,10 +235,12 @@ export function createBarOptionFactory(
|
|
|
234
235
|
: baseBottom
|
|
235
236
|
|
|
236
237
|
// When a selection is active, dim non-selected bars by routing the
|
|
237
|
-
//
|
|
238
|
+
// item's base colour through `modifyAlpha`. Per-row `itemStyle` on
|
|
238
239
|
// dataset object-rows is silently ignored when `series.encode` is in
|
|
239
240
|
// play — the callback approach is the standard ECharts pattern for
|
|
240
|
-
// per-data styling derived from a dataset.
|
|
241
|
+
// per-data styling derived from a dataset. The base colour is the
|
|
242
|
+
// datum's `color`, else the template's `itemStyle.color`, else the
|
|
243
|
+
// series colour / palette (`params.color`).
|
|
241
244
|
//
|
|
242
245
|
// We *always* emit `itemStyle.color` (a passthrough when nothing is
|
|
243
246
|
// selected), not conditionally — dropping the key between renders
|
|
@@ -245,22 +248,30 @@ export function createBarOptionFactory(
|
|
|
245
248
|
// and bars would stay dimmed forever after an external clear. Always
|
|
246
249
|
// emitting lets normal merge swap the callback in place, no
|
|
247
250
|
// `replaceMerge` and no entry-animation flash on selection on/off.
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
const makeDimItemStyle = (datums: readonly object[], template: unknown) => {
|
|
252
|
+
const baseColor = createItemColorResolver({
|
|
253
|
+
theme,
|
|
254
|
+
datums,
|
|
255
|
+
template,
|
|
256
|
+
fallback: (params) => params.color as string,
|
|
257
|
+
})
|
|
258
|
+
return {
|
|
259
|
+
color: (params: CallbackDataParams) => {
|
|
260
|
+
const base = baseColor(params)
|
|
261
|
+
if (!selectionSet) return base
|
|
262
|
+
const datum = params.value as { name?: string | number } | undefined
|
|
263
|
+
const name = datum?.name ?? params.name
|
|
264
|
+
return name != null && selectionSet.has(name)
|
|
265
|
+
? base
|
|
266
|
+
: echarts.color.modifyAlpha(base, 0.15)
|
|
267
|
+
},
|
|
268
|
+
}
|
|
258
269
|
}
|
|
259
270
|
|
|
260
271
|
return {
|
|
261
272
|
...option,
|
|
262
273
|
dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),
|
|
263
|
-
series: seriesArr.map((
|
|
274
|
+
series: seriesArr.map((s, i) => {
|
|
264
275
|
const template =
|
|
265
276
|
(seriesTemplates[i] as object | undefined) ??
|
|
266
277
|
(broadcastTemplate as object)
|
|
@@ -277,7 +288,7 @@ export function createBarOptionFactory(
|
|
|
277
288
|
encode: { x: 'name', y: 'value' },
|
|
278
289
|
barMaxWidth: 100,
|
|
279
290
|
emphasis: { focus: 'series' },
|
|
280
|
-
itemStyle:
|
|
291
|
+
itemStyle: makeDimItemStyle(s, template),
|
|
281
292
|
...(overrideColor ? { color: overrideColor } : {}),
|
|
282
293
|
}
|
|
283
294
|
}),
|
package/src/widgets/bar/types.ts
CHANGED
|
@@ -6,6 +6,12 @@ import type { WidgetSeries } from '../types'
|
|
|
6
6
|
export interface BarDatum {
|
|
7
7
|
name: string | number
|
|
8
8
|
value: number
|
|
9
|
+
/**
|
|
10
|
+
* Optional colour for this item (CSS colour or theme palette path such as
|
|
11
|
+
* `'primary.main'`). Wins over the template's `itemStyle.color` and the
|
|
12
|
+
* series colour; selection dimming still applies on top.
|
|
13
|
+
*/
|
|
14
|
+
color?: string
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
/** v2 bar widget data shape: array of series, each a flat list of `{name, value}`. */
|
|
@@ -925,4 +925,143 @@ describe('<CategoryUI>', () => {
|
|
|
925
925
|
fills.forEach((f) => expect(f.style.backgroundColor).toBe(DIM))
|
|
926
926
|
})
|
|
927
927
|
})
|
|
928
|
+
|
|
929
|
+
// ── Signed values ──────────────────────────────────────────────────
|
|
930
|
+
describe('negative values', () => {
|
|
931
|
+
const MIXED = [
|
|
932
|
+
[
|
|
933
|
+
{ name: 'Income', value: 0.8 },
|
|
934
|
+
{ name: 'Transit', value: -0.4 },
|
|
935
|
+
{ name: 'Size', value: 0 },
|
|
936
|
+
],
|
|
937
|
+
]
|
|
938
|
+
const fills = (c: HTMLElement) =>
|
|
939
|
+
Array.from(c.querySelectorAll<HTMLElement>('[data-bar-fill="true"]'))
|
|
940
|
+
const ticks = (c: HTMLElement) =>
|
|
941
|
+
Array.from(c.querySelectorAll<HTMLElement>('[data-zero-tick="true"]'))
|
|
942
|
+
|
|
943
|
+
it('centres the zero with a symmetric domain and keeps the series color', () => {
|
|
944
|
+
const { container } = render(<CategoryUI data={MIXED} />)
|
|
945
|
+
const [income, transit, size] = fills(container)
|
|
946
|
+
expect([income!.style.left, income!.style.width]).toEqual(['50%', '50%'])
|
|
947
|
+
expect([transit!.style.left, transit!.style.width]).toEqual([
|
|
948
|
+
'25%',
|
|
949
|
+
'25%',
|
|
950
|
+
])
|
|
951
|
+
expect(size!.style.width).toBe('0%')
|
|
952
|
+
expect(transit!.style.backgroundColor).toBe(income!.style.backgroundColor)
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
it('marks the zero with one tick per bar, none on "Others"', () => {
|
|
956
|
+
const { container } = render(<CategoryUI data={MIXED} maxItems={2} />)
|
|
957
|
+
expect(ticks(container).map((t) => t.style.left)).toEqual(['50%', '50%'])
|
|
958
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
it('draws one zero line per multi-series row, spanning every bar', () => {
|
|
962
|
+
const { container } = render(
|
|
963
|
+
<CategoryUI
|
|
964
|
+
data={[
|
|
965
|
+
[
|
|
966
|
+
{ name: 'A', value: 1 },
|
|
967
|
+
{ name: 'B', value: 2 },
|
|
968
|
+
],
|
|
969
|
+
[
|
|
970
|
+
{ name: 'A', value: -1 },
|
|
971
|
+
{ name: 'B', value: 0.5 },
|
|
972
|
+
],
|
|
973
|
+
[
|
|
974
|
+
{ name: 'A', value: 0 },
|
|
975
|
+
{ name: 'B', value: -2 },
|
|
976
|
+
],
|
|
977
|
+
]}
|
|
978
|
+
/>,
|
|
979
|
+
)
|
|
980
|
+
const t = ticks(container)
|
|
981
|
+
expect(t).toHaveLength(2)
|
|
982
|
+
expect(t[0]!.style.left).toBe('50%')
|
|
983
|
+
expect(t[0]!.parentElement!.style.gridRow).toBe('2 / span 3')
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
it.each([
|
|
987
|
+
['positive-only', [1, 2]],
|
|
988
|
+
['negative-only', [-1, -2]],
|
|
989
|
+
['all-zero', [0, 0]],
|
|
990
|
+
])('draws no zero tick for %s data', (_, values) => {
|
|
991
|
+
const { container } = render(
|
|
992
|
+
<CategoryUI
|
|
993
|
+
data={[values.map((value, i) => ({ name: `R${i}`, value }))]}
|
|
994
|
+
/>,
|
|
995
|
+
)
|
|
996
|
+
expect(ticks(container)).toHaveLength(0)
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
it('hangs negative-only bars from the right edge', () => {
|
|
1000
|
+
const { container } = render(
|
|
1001
|
+
<CategoryUI
|
|
1002
|
+
data={[
|
|
1003
|
+
[
|
|
1004
|
+
{ name: 'A', value: -2 },
|
|
1005
|
+
{ name: 'B', value: -1 },
|
|
1006
|
+
],
|
|
1007
|
+
]}
|
|
1008
|
+
/>,
|
|
1009
|
+
)
|
|
1010
|
+
const [a, b] = fills(container)
|
|
1011
|
+
expect([a!.style.left, a!.style.width]).toEqual(['0%', '100%'])
|
|
1012
|
+
expect([b!.style.left, b!.style.width]).toEqual(['50%', '50%'])
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
it('treats maxOverride as a magnitude for signed data', () => {
|
|
1016
|
+
const { container } = render(
|
|
1017
|
+
<CategoryUI data={MIXED} maxOverride={1.6} />,
|
|
1018
|
+
)
|
|
1019
|
+
const [income, transit] = fills(container)
|
|
1020
|
+
// domain [-1.6, 1.6]
|
|
1021
|
+
expect([income!.style.left, income!.style.width]).toEqual(['50%', '25%'])
|
|
1022
|
+
expect(transit!.style.width).toBe('12.5%')
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
it('minOverride pins the zero even when the data has no negatives', () => {
|
|
1026
|
+
const { container } = render(
|
|
1027
|
+
<CategoryUI data={[[{ name: 'A', value: 1 }]]} minOverride={-1} />,
|
|
1028
|
+
)
|
|
1029
|
+
expect(fills(container)[0]!.style.left).toBe('50%')
|
|
1030
|
+
expect(ticks(container)).toHaveLength(1)
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
it('prefixes U+2212 itself and hands the formatter |v|', () => {
|
|
1034
|
+
const formatter = vi.fn((n: number) => `$${n}`)
|
|
1035
|
+
render(
|
|
1036
|
+
<CategoryUI
|
|
1037
|
+
data={[
|
|
1038
|
+
[
|
|
1039
|
+
{ name: 'A', value: -0.5 },
|
|
1040
|
+
{ name: 'B', value: -0 },
|
|
1041
|
+
{ name: 'C', value: 0 },
|
|
1042
|
+
],
|
|
1043
|
+
]}
|
|
1044
|
+
formatter={formatter}
|
|
1045
|
+
/>,
|
|
1046
|
+
)
|
|
1047
|
+
expect(screen.getByText('\u2212$0.5')).toBeTruthy()
|
|
1048
|
+
expect(screen.getAllByText('$0')).toHaveLength(2)
|
|
1049
|
+
formatter.mock.calls.forEach(([n]) => expect(n).toBeGreaterThanOrEqual(0))
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
it('stacked mode diverges around the shared zero and shows the signed total', () => {
|
|
1053
|
+
const { container } = render(
|
|
1054
|
+
<CategoryUI
|
|
1055
|
+
stacked
|
|
1056
|
+
data={[[{ name: 'A', value: 3 }], [{ name: 'A', value: -1 }]]}
|
|
1057
|
+
/>,
|
|
1058
|
+
)
|
|
1059
|
+
const [pos, neg] = fills(container)
|
|
1060
|
+
// zero at 50%, longer side (3) fills its half
|
|
1061
|
+
expect([pos!.style.left, pos!.style.width]).toEqual(['50%', '50%'])
|
|
1062
|
+
expect(parseFloat(neg!.style.left)).toBeCloseTo(50 - 50 / 3)
|
|
1063
|
+
expect(screen.getByText('2')).toBeTruthy()
|
|
1064
|
+
expect(screen.getByText('Series 2: \u22121')).toBeTruthy()
|
|
1065
|
+
})
|
|
1066
|
+
})
|
|
928
1067
|
})
|