@carto/ps-react-ui 6.1.1 → 6.2.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-CueYrfVr.js} +2 -2
- package/dist/actions-CueYrfVr.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/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/option-builders-DQivvvbb.js.map +1 -1
- 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/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/utils-C-eWwOhE.js.map +1 -1
- package/dist/widgets/actions.js +1 -1
- 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.map +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/pie.js +2 -2
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/scatterplot.js.map +1 -1
- 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/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/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
|
@@ -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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carto/ps-react-ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.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",
|
|
@@ -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
|
})
|
|
@@ -80,8 +80,16 @@ export interface CategoryUIProps {
|
|
|
80
80
|
* overflow row stays a static summary.
|
|
81
81
|
*/
|
|
82
82
|
onShowAll?: () => void
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Manual override for the top of the bar scale. With signed data it is a
|
|
85
|
+
* magnitude: the scale stays symmetric around the centred zero.
|
|
86
|
+
*/
|
|
84
87
|
maxOverride?: number
|
|
88
|
+
/**
|
|
89
|
+
* Manual override for the bottom of the bar scale, the negative mirror of
|
|
90
|
+
* `maxOverride`. Ignored unless negative.
|
|
91
|
+
*/
|
|
92
|
+
minOverride?: number
|
|
85
93
|
/**
|
|
86
94
|
* Visual density of the bar primitive. `'small'` (default) keeps the
|
|
87
95
|
* historical 4px-tall pill; `'medium'` switches to a 12px-tall track
|
|
@@ -163,6 +171,42 @@ function generateGroupedData(data: CategoryWidgetData): GroupedRow[] {
|
|
|
163
171
|
return order.map((k) => grouped.get(k)!)
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Bar domain `[lo, hi]`, always containing zero. Positive-only data keeps
|
|
176
|
+
* the zero at the left edge and negative-only at the right; mixed data is
|
|
177
|
+
* made symmetric so the zero sits in the middle of the track.
|
|
178
|
+
*/
|
|
179
|
+
function signedDomain(
|
|
180
|
+
rows: readonly GroupedRow[],
|
|
181
|
+
minOverride?: number,
|
|
182
|
+
maxOverride?: number,
|
|
183
|
+
): [lo: number, hi: number] {
|
|
184
|
+
let lo = 0
|
|
185
|
+
let hi = 0
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
for (const v of row.values) {
|
|
188
|
+
if (v < lo) lo = v
|
|
189
|
+
if (v > hi) hi = v
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (typeof minOverride === 'number' && minOverride < 0) lo = minOverride
|
|
193
|
+
if (typeof maxOverride === 'number' && maxOverride > 0) hi = maxOverride
|
|
194
|
+
if (lo < 0 && hi > 0) {
|
|
195
|
+
const m = Math.max(-lo, hi)
|
|
196
|
+
return [-m, m]
|
|
197
|
+
}
|
|
198
|
+
return [lo, hi]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The widget owns the sign: `formatter` only ever sees `|v|`, so a formatter
|
|
203
|
+
* that prefixes a unit reads `−$5`, not `$-5`, and one that emits its own
|
|
204
|
+
* `-` cannot double it. U+2212 is as wide as a tabular digit.
|
|
205
|
+
*/
|
|
206
|
+
function formatSigned(fmt: (n: number) => string, v: number): string {
|
|
207
|
+
return (v < 0 ? '\u2212' : '') + fmt(Math.abs(v))
|
|
208
|
+
}
|
|
209
|
+
|
|
166
210
|
/**
|
|
167
211
|
* Pure presentational component for the Category widget. Renders
|
|
168
212
|
* single-series rows (`data.length === 1`) as a compact grid or
|
|
@@ -189,13 +233,15 @@ export function CategoryUI({
|
|
|
189
233
|
labels,
|
|
190
234
|
onShowAll,
|
|
191
235
|
maxOverride,
|
|
236
|
+
minOverride,
|
|
192
237
|
size = 'small',
|
|
193
238
|
stacked = false,
|
|
194
239
|
hiddenSeries: hiddenSeriesProp,
|
|
195
240
|
onHiddenSeriesChange,
|
|
196
241
|
}: CategoryUIProps) {
|
|
197
242
|
const theme = useTheme()
|
|
198
|
-
const
|
|
243
|
+
const baseFmt = formatter ?? ((n: number) => String(n))
|
|
244
|
+
const fmt = (n: number) => formatSigned(baseFmt, n)
|
|
199
245
|
|
|
200
246
|
const hasSelection = !!selection && selection.length > 0
|
|
201
247
|
const selectionSet = useMemo(
|
|
@@ -250,14 +296,14 @@ export function CategoryUI({
|
|
|
250
296
|
|
|
251
297
|
const grouped = useMemo(() => generateGroupedData(data), [data])
|
|
252
298
|
|
|
253
|
-
const maxValue = useMemo(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
299
|
+
const [minValue, maxValue] = useMemo(
|
|
300
|
+
() => signedDomain(grouped, minOverride, maxOverride),
|
|
301
|
+
[grouped, minOverride, maxOverride],
|
|
302
|
+
)
|
|
303
|
+
// Where the zero sits on the track, handed to stacked rows so their zero
|
|
304
|
+
// matches the other rows'. Positive-only data keeps it at the left edge.
|
|
305
|
+
const zeroPct =
|
|
306
|
+
maxValue > minValue ? (-minValue / (maxValue - minValue)) * 100 : 0
|
|
261
307
|
|
|
262
308
|
// Three modes:
|
|
263
309
|
// * positive finite N (default `DEFAULT_MAX_ITEMS = 20` when prop
|
|
@@ -387,7 +433,11 @@ export function CategoryUI({
|
|
|
387
433
|
<Box sx={styles.root}>
|
|
388
434
|
<Box
|
|
389
435
|
ref={listRefCallback}
|
|
390
|
-
sx={
|
|
436
|
+
sx={{
|
|
437
|
+
...styles.list,
|
|
438
|
+
...(isMulti && !stacked && styles.listMulti),
|
|
439
|
+
...(scrollMode && styles.listScroll),
|
|
440
|
+
}}
|
|
391
441
|
style={
|
|
392
442
|
scrollMaxHeight !== undefined
|
|
393
443
|
? { maxHeight: scrollMaxHeight, overflowY: 'auto' }
|
|
@@ -437,6 +487,7 @@ export function CategoryUI({
|
|
|
437
487
|
selected={selected}
|
|
438
488
|
onToggle={toggle}
|
|
439
489
|
size={size}
|
|
490
|
+
zeroPct={zeroPct}
|
|
440
491
|
/>
|
|
441
492
|
)
|
|
442
493
|
}
|
|
@@ -448,6 +499,7 @@ export function CategoryUI({
|
|
|
448
499
|
values={visibleIdx.map((i) => row.values[i]!)}
|
|
449
500
|
colors={visibleIdx.map((i) => fill(i))}
|
|
450
501
|
maxValue={maxValue}
|
|
502
|
+
minValue={minValue}
|
|
451
503
|
formatter={fmt}
|
|
452
504
|
selected={selected}
|
|
453
505
|
onToggle={toggle}
|
|
@@ -462,6 +514,7 @@ export function CategoryUI({
|
|
|
462
514
|
displayName={displayName}
|
|
463
515
|
value={row.values[0] ?? 0}
|
|
464
516
|
maxValue={maxValue}
|
|
517
|
+
minValue={minValue}
|
|
465
518
|
color={fill(0)}
|
|
466
519
|
formatter={fmt}
|
|
467
520
|
selected={selected}
|
|
@@ -238,6 +238,73 @@ describe('<Category> bridge', () => {
|
|
|
238
238
|
expect(fills[0]!.style.width).toBe('25%')
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
+
it('keeps the zero centred when a transform hides every negative row', () => {
|
|
242
|
+
render(
|
|
243
|
+
<Provider
|
|
244
|
+
id='cat-bridge-auto-neg'
|
|
245
|
+
data={[
|
|
246
|
+
[
|
|
247
|
+
{ name: 'A', value: 1 },
|
|
248
|
+
{ name: 'B', value: -1 },
|
|
249
|
+
],
|
|
250
|
+
]}
|
|
251
|
+
>
|
|
252
|
+
<Category />
|
|
253
|
+
</Provider>,
|
|
254
|
+
)
|
|
255
|
+
act(() => {
|
|
256
|
+
getWidgetStore('cat-bridge-auto-neg').setState({
|
|
257
|
+
data: [[{ name: 'A', value: 1 }]],
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
const fills = getBarFills()
|
|
261
|
+
expect(fills).toHaveLength(1)
|
|
262
|
+
expect(fills[0]!.style.left).toBe('50%')
|
|
263
|
+
expect(fills[0]!.style.width).toBe('50%')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it.each([
|
|
267
|
+
[
|
|
268
|
+
'positive-only',
|
|
269
|
+
[50, 25],
|
|
270
|
+
[
|
|
271
|
+
['0%', '50%'],
|
|
272
|
+
['0%', '25%'],
|
|
273
|
+
],
|
|
274
|
+
],
|
|
275
|
+
[
|
|
276
|
+
'negative-only',
|
|
277
|
+
[-50, -25],
|
|
278
|
+
[
|
|
279
|
+
['50%', '50%'],
|
|
280
|
+
['75%', '25%'],
|
|
281
|
+
],
|
|
282
|
+
],
|
|
283
|
+
[
|
|
284
|
+
'mixed',
|
|
285
|
+
[50, -25],
|
|
286
|
+
[
|
|
287
|
+
['50%', '25%'],
|
|
288
|
+
['37.5%', '12.5%'],
|
|
289
|
+
],
|
|
290
|
+
],
|
|
291
|
+
])(
|
|
292
|
+
'applies ±100 overrides only on the side %s data reaches',
|
|
293
|
+
(_, values, expected) => {
|
|
294
|
+
render(
|
|
295
|
+
<Provider
|
|
296
|
+
id={`cat-bridge-side-${values.join('')}`}
|
|
297
|
+
data={[values.map((value, i) => ({ name: `R${i}`, value }))]}
|
|
298
|
+
>
|
|
299
|
+
<Category maxOverride={100} minOverride={-100} />
|
|
300
|
+
</Provider>,
|
|
301
|
+
)
|
|
302
|
+
expect(getBarFills().map((f) => [f.style.left, f.style.width])).toEqual(
|
|
303
|
+
expected,
|
|
304
|
+
)
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
|
|
241
308
|
it('consumer-supplied maxOverride wins over the rawData auto-fill', () => {
|
|
242
309
|
render(
|
|
243
310
|
<Provider id='cat-bridge-auto-3' data={PEAK_DATA}>
|
|
@@ -41,13 +41,19 @@ interface CategoryWidgetState extends WidgetState {
|
|
|
41
41
|
categoryHiddenSeries?: { key: string; indices: ReadonlySet<number> }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
/** `[min, max]` of every value, each clamped to include zero. */
|
|
45
|
+
function extentFromCategoryData(
|
|
46
|
+
d: CategoryWidgetData | undefined,
|
|
47
|
+
): [min: number, max: number] {
|
|
48
|
+
let min = 0
|
|
49
|
+
let max = 0
|
|
50
|
+
for (const series of d ?? []) {
|
|
51
|
+
for (const item of series) {
|
|
52
|
+
if (item.value < min) min = item.value
|
|
53
|
+
if (item.value > max) max = item.value
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
|
-
return
|
|
56
|
+
return [min, max]
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export interface CategoryProps {
|
|
@@ -83,6 +89,12 @@ export interface CategoryProps {
|
|
|
83
89
|
* regardless of the data.
|
|
84
90
|
*/
|
|
85
91
|
maxOverride?: number
|
|
92
|
+
/**
|
|
93
|
+
* Bottom of the bar scale for negative values, the mirror of
|
|
94
|
+
* `maxOverride`. Auto-filled from `rawData` the same way, so the zero
|
|
95
|
+
* does not move when the Searcher hides every negative row.
|
|
96
|
+
*/
|
|
97
|
+
minOverride?: number
|
|
86
98
|
/**
|
|
87
99
|
* Visual density of the bar primitive. `'small'` (default) keeps the
|
|
88
100
|
* historical 4px-tall pill; `'medium'` switches to a 12px-tall track
|
|
@@ -112,23 +124,27 @@ export function Category({
|
|
|
112
124
|
labels,
|
|
113
125
|
onShowAll,
|
|
114
126
|
maxOverride,
|
|
127
|
+
minOverride,
|
|
115
128
|
size,
|
|
116
129
|
stacked,
|
|
117
130
|
}: CategoryProps) {
|
|
118
131
|
const id = useWidgetId()
|
|
119
132
|
const slice = useWidgetShallow(id, categorySelector)
|
|
120
|
-
// Auto-fill
|
|
121
|
-
// across data transforms (e.g., the Searcher filtering
|
|
122
|
-
// Consumer's explicit value
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
// Auto-fill both ends of the scale from `rawData` so bar widths and the
|
|
134
|
+
// zero stay coherent across data transforms (e.g., the Searcher filtering
|
|
135
|
+
// rows in/out). Consumer's explicit value wins, but each end only applies
|
|
136
|
+
// when `rawData` reaches past zero on that side: a relative-data wiring can
|
|
137
|
+
// then pass `100` / `-100` unconditionally and positive-only data keeps its
|
|
138
|
+
// zero at the left edge, negative-only at the right. With no data on a side
|
|
139
|
+
// we pass nothing and CategoryUI falls back to the live `data`.
|
|
140
|
+
const [naturalMin, naturalMax] = useMemo(
|
|
141
|
+
() => extentFromCategoryData(slice.rawData),
|
|
128
142
|
[slice.rawData],
|
|
129
143
|
)
|
|
130
144
|
const effectiveMaxOverride =
|
|
131
|
-
|
|
145
|
+
naturalMax > 0 ? (maxOverride ?? naturalMax) : undefined
|
|
146
|
+
const effectiveMinOverride =
|
|
147
|
+
naturalMin < 0 ? (minOverride ?? naturalMin) : undefined
|
|
132
148
|
|
|
133
149
|
const seriesKey = categorySeriesKey(series)
|
|
134
150
|
const stored = useWidget<
|
|
@@ -156,6 +172,7 @@ export function Category({
|
|
|
156
172
|
labels={labels}
|
|
157
173
|
onShowAll={onShowAll}
|
|
158
174
|
maxOverride={effectiveMaxOverride}
|
|
175
|
+
minOverride={effectiveMinOverride}
|
|
159
176
|
size={size}
|
|
160
177
|
stacked={stacked}
|
|
161
178
|
hiddenSeries={hiddenSeries}
|
|
@@ -118,4 +118,22 @@ describe('<CategoryBarStacked>', () => {
|
|
|
118
118
|
const track = container.querySelector('[data-size]')
|
|
119
119
|
expect(track!.getAttribute('data-size')).toBe('medium')
|
|
120
120
|
})
|
|
121
|
+
|
|
122
|
+
it('diverges from zeroPct: positives stack rightwards, negatives leftwards', () => {
|
|
123
|
+
const { container } = render(
|
|
124
|
+
<CategoryBarStacked
|
|
125
|
+
values={[20, -10, 30, -20]}
|
|
126
|
+
colors={['a', 'b', 'c', 'd']}
|
|
127
|
+
maxValue={100}
|
|
128
|
+
zeroPct={50}
|
|
129
|
+
/>,
|
|
130
|
+
)
|
|
131
|
+
const segs = getSegments(container)
|
|
132
|
+
expect(segs.map((s) => [s.style.left, s.style.width])).toEqual([
|
|
133
|
+
['50%', '20%'],
|
|
134
|
+
['40%', '10%'],
|
|
135
|
+
['70%', '30%'],
|
|
136
|
+
['20%', '20%'],
|
|
137
|
+
])
|
|
138
|
+
})
|
|
121
139
|
})
|