@carto/ps-react-ui 6.1.0 → 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.
Files changed (72) hide show
  1. package/dist/{actions-BWlS5tpP.js → actions-CueYrfVr.js} +2 -2
  2. package/dist/actions-CueYrfVr.js.map +1 -0
  3. package/dist/category-DULnoxvk.js +933 -0
  4. package/dist/category-DULnoxvk.js.map +1 -0
  5. package/dist/chat.js +1 -1
  6. package/dist/chat.js.map +1 -1
  7. package/dist/components-CTUpdQuj.js.map +1 -1
  8. package/dist/download-B4sykOFm.js.map +1 -1
  9. package/dist/{echart-CV8F50gn.js → echart-ysYFLa_T.js} +2 -2
  10. package/dist/echart-ysYFLa_T.js.map +1 -0
  11. package/dist/formula-DJS-JLpk.js.map +1 -1
  12. package/dist/legend.js.map +1 -1
  13. package/dist/{markdown-Bn2nKCbn.js → markdown-DmvLi3vx.js} +2 -2
  14. package/dist/markdown-DmvLi3vx.js.map +1 -0
  15. package/dist/{markdown-content-kYNLOX0T.js → markdown-content-BpAH_V6S.js} +5 -4
  16. package/dist/markdown-content-BpAH_V6S.js.map +1 -0
  17. package/dist/option-builders-DQivvvbb.js.map +1 -1
  18. package/dist/range-BpU2zBmq.js.map +1 -1
  19. package/dist/smart-tooltip-Dc_rHhyI.js.map +1 -1
  20. package/dist/spread-CQwFIwuz.js.map +1 -1
  21. package/dist/stores-DcBOZlU3.js.map +1 -1
  22. package/dist/stores-mad-9yFn.js.map +1 -1
  23. package/dist/styles-BCEBm7xh.js.map +1 -1
  24. package/dist/table-DEdW_fDj.js.map +1 -1
  25. package/dist/types/widgets/category/category-ui.d.ts +10 -2
  26. package/dist/types/widgets/category/category.d.ts +7 -1
  27. package/dist/types/widgets/category/components/category-bar-stacked.d.ts +8 -1
  28. package/dist/types/widgets/category/components/category-bar.d.ts +24 -2
  29. package/dist/types/widgets/category/components/category-row-multi.d.ts +3 -1
  30. package/dist/types/widgets/category/components/category-row-single.d.ts +3 -1
  31. package/dist/types/widgets/category/components/category-row-stacked.d.ts +10 -1
  32. package/dist/types/widgets/category/style.d.ts +33 -6
  33. package/dist/types/widgets/wrapper/style.d.ts +31 -2
  34. package/dist/types/widgets/wrapper/widget-actions.d.ts +2 -2
  35. package/dist/types/widgets/wrapper/widget-wrapper.d.ts +2 -2
  36. package/dist/utils-C-eWwOhE.js.map +1 -1
  37. package/dist/widgets/actions.js +1 -1
  38. package/dist/widgets/bar.js.map +1 -1
  39. package/dist/widgets/category.js +1 -1
  40. package/dist/widgets/echart.js +1 -1
  41. package/dist/widgets/histogram.js.map +1 -1
  42. package/dist/widgets/markdown.js +2 -2
  43. package/dist/widgets/pie.js +2 -2
  44. package/dist/widgets/pie.js.map +1 -1
  45. package/dist/widgets/scatterplot.js.map +1 -1
  46. package/dist/widgets/timeseries.js.map +1 -1
  47. package/dist/widgets.js +344 -297
  48. package/dist/widgets.js.map +1 -1
  49. package/dist/zoom-toggle-MmLjMTEw.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/widgets/category/category-ui.test.tsx +139 -0
  52. package/src/widgets/category/category-ui.tsx +64 -11
  53. package/src/widgets/category/category.test.tsx +67 -0
  54. package/src/widgets/category/category.tsx +32 -15
  55. package/src/widgets/category/components/category-bar-stacked.test.tsx +18 -0
  56. package/src/widgets/category/components/category-bar-stacked.tsx +42 -29
  57. package/src/widgets/category/components/category-bar.test.tsx +66 -0
  58. package/src/widgets/category/components/category-bar.tsx +65 -9
  59. package/src/widgets/category/components/category-row-multi.tsx +21 -0
  60. package/src/widgets/category/components/category-row-single.tsx +4 -0
  61. package/src/widgets/category/components/category-row-stacked.tsx +24 -1
  62. package/src/widgets/category/style.ts +58 -14
  63. package/src/widgets/wrapper/style.ts +38 -2
  64. package/src/widgets/wrapper/widget-actions.tsx +2 -2
  65. package/src/widgets/wrapper/widget-wrapper.test.tsx +32 -2
  66. package/src/widgets/wrapper/widget-wrapper.tsx +62 -36
  67. package/dist/actions-BWlS5tpP.js.map +0 -1
  68. package/dist/category-alzjKzvQ.js +0 -839
  69. package/dist/category-alzjKzvQ.js.map +0 -1
  70. package/dist/echart-CV8F50gn.js.map +0 -1
  71. package/dist/markdown-Bn2nKCbn.js.map +0 -1
  72. package/dist/markdown-content-kYNLOX0T.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"pie.js","names":["<Skeleton variant='circular' sx={styles.ring} />","<Box sx={styles.hole} />","<Box sx={styles.donut}>\n <Skeleton variant='circular' sx={styles.ring} />\n <Box sx={styles.hole} />\n <Box sx={styles.label}>\n <Skeleton width={56} height={18} />\n <Skeleton width={36} height={8} />\n </Box>\n </Box>","<Box sx={styles.root}>\n <Box sx={styles.donut}>\n <Skeleton variant='circular' sx={styles.ring} />\n <Box sx={styles.hole} />\n <Box sx={styles.label}>\n <Skeleton width={56} height={18} />\n <Skeleton width={36} height={8} />\n </Box>\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>","(i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n )"],"sources":["../../src/widgets/pie/options.ts","../../src/widgets/pie/skeleton.tsx","../../src/widgets/pie/download.ts"],"sourcesContent":["import type { Theme } from '@mui/material'\nimport type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildGridConfig,\n buildLegendConfig,\n buildSeriesLabelConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../utils/option-builders'\nimport type { OptionFactory, OptionFactoryContext } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport type { WidgetSeries } from '../types'\nimport type {\n PieEChartsOption,\n PieOptionFactoryInput,\n PieOptionsInput,\n PieWidgetData,\n} from './types'\n\nconst DEFAULT_RADIUS: readonly [string, string] = ['58%', '74%']\n\n/**\n * Builds the **structural** ECharts option for a pie / donut widget.\n * Mirrors v1's pie look-and-feel: themed legend (`buildLegendConfig`),\n * themed tooltip via `createTooltipPositioner` / `createTooltipFormatter`,\n * `qualitative.bold` color palette, slice borders against the paper\n * background, hover-emphasis disabled, and a centered rich-text label.\n *\n * Layered like Bar / Histogram:\n * - **Theme-aware bits** live here on a series template (slice border,\n * center-label rich style sizes, emphasis-disabled).\n * - **Reactive bits** (tooltip value formatter, center-label text,\n * selection-driven dim) are re-emitted by\n * {@link createPieOptionFactory} at fusion time so RelativeData's\n * percent override flows through.\n */\nexport function pieOptions({\n theme,\n formatter,\n labelFormatter,\n}: PieOptionsInput): PieEChartsOption {\n return {\n legend: {\n ...buildLegendConfig({ hasLegend: true, labelFormatter }),\n // Bottom-left, same as every other widget — `buildLegendConfig`'s\n // `left: 0` is kept deliberately. The wider `itemGap` gives chips\n // breathing room in the typical 3–6 category case.\n type: 'scroll',\n itemGap: 16,\n // The legend is presentational only here — the pie has its own\n // click-to-select affordance on the slices themselves, so legend\n // clicks toggling series visibility would be a second, redundant/\n // conflicting interaction model. Hover-highlight is disabled per\n // series via `legendHoverLink` (ECharts has no legend-level knob\n // for it).\n selectedMode: false,\n },\n tooltip: {\n // Pie uses item-trigger (no axis); the rest of the styling mirrors\n // bar/histogram so the dashboard reads consistently — dark\n // grey[900] bg, white caption-font text, padded, with the shared\n // overflow-aware positioner.\n trigger: 'item',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n position: createTooltipPositioner(theme),\n formatter: buildPieTooltipFormatter(formatter, labelFormatter),\n },\n // V1 used `qualitative.bold` only (no `secondary.main` prefix) because\n // pie slices are categorical — each slice deserves a distinct palette\n // entry from the start, not a primary highlight.\n color: Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n // Pie template. The merger spreads this into each per-data series it\n // emits — borderColor / borderWidth / emphasis-disabled / label rich\n // sizes all survive through.\n series: [\n {\n type: 'pie',\n colorBy: 'data',\n avoidLabelOverlap: true,\n selectedOffset: 0,\n legendHoverLink: false,\n emphasis: { disabled: true },\n itemStyle: {\n borderColor: theme.palette.background.paper,\n borderWidth: 1,\n },\n label: {\n show: true,\n position: 'center',\n rich: {\n b: { fontSize: 16, fontWeight: 'normal', lineHeight: 20 },\n c: { fontSize: 28, fontWeight: 'bold', lineHeight: 27 },\n },\n },\n },\n ],\n }\n}\n\n/**\n * Returns the pie widget's {@link OptionFactory} — one closure that owns\n * BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link pieOptions}, optionally merging the\n * consumer-supplied `optionsOverride` on top. Called once by Provider\n * to seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`PieWidgetData`) into the option via the dataset API. Single-series\n * → donut (one dataset, `series[i].encode = { itemName: 'name', value: 'value' }`).\n * Multi-series → horizontal-bar fallback (mirrors v1 pie). Reactive\n * formatters from `ctx` drive the tooltip + center label at fusion\n * time so RelativeData's percent formatter flows through without a\n * structural rebuild.\n */\nexport function createPieOptionFactory(\n options: PieOptionFactoryInput,\n): OptionFactory {\n const { theme, formatter, labelFormatter, optionsOverride, series } = options\n const radius = options.radius ?? DEFAULT_RADIUS\n const selection = options.selection\n // An explicit `[]` (deselected down to zero via chart clicks) is treated\n // the same as no selection: the pie has no bar/histogram-style \"nothing\n // selected\" affordance, so dimming every slice would just look broken —\n // it renders fully active, same as the no-interaction state.\n const selectionSet =\n selection === undefined || selection === null || selection.length === 0\n ? null\n : new Set<string | number>(selection)\n return (option, data, ctx) => {\n if (option == null) {\n const structural = pieOptions({ theme, formatter, labelFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as PieWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n if (seriesArr.length > 1) {\n return buildMultiSeriesBarFusion(\n option,\n seriesArr,\n theme,\n series,\n ctx,\n selectionSet,\n )\n }\n return buildSingleSeriesPieFusion(\n option,\n seriesArr,\n radius,\n series,\n ctx,\n selectionSet,\n )\n }\n}\n\n/**\n * Single-series donut fusion. Spreads the structural pie series template\n * into per-data series with center/radius/encoding, wires the reactive\n * center-label / tooltip formatters and the per-data palette-aware\n * selection-dim callback.\n */\nfunction buildSingleSeriesPieFusion(\n option: EChartsOption,\n seriesArr: PieWidgetData,\n radius: readonly [string, string],\n series: readonly WidgetSeries[] | undefined,\n ctx: OptionFactoryContext | undefined,\n selectionSet: Set<string | number> | null,\n): EChartsOption {\n const seriesTemplates = Array.isArray(option.series) ? option.series : []\n const broadcastTemplate = seriesTemplates[0] ?? {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n const formatter = ctx?.formatter\n const labelFormatter = ctx?.labelFormatter\n\n // Pie wants `colorBy: 'data'` — each slice draws from the option's\n // `color` palette by data index. But installing an `itemStyle.color`\n // callback disables ECharts' automatic per-data cycling, and\n // `params.color` collapses to the series color (so every slice goes\n // the same color). We re-implement the per-data palette resolution\n // here so the multicolor look survives selection-driven dimming.\n const palette = Array.isArray(option.color)\n ? (option.color as readonly (string | undefined)[])\n : []\n const resolvePaletteColor = (params: CallbackDataParams): string => {\n const swatch =\n palette.length > 0\n ? palette[params.dataIndex % palette.length]\n : undefined\n return swatch ?? (params.color as string)\n }\n\n // Always emit `itemStyle.color` (passthrough when nothing is selected),\n // not conditionally — same anti-stale-callback rationale as bar /\n // histogram. The slice border + width come from the structural\n // template's `itemStyle` and survive via the spread below.\n //\n // `selectionSet` is the *included* set, same direct semantics as\n // bar/histogram/timeseries (no ADR-014 exclusion inversion). Dim a slice\n // when its name is NOT in the set. `selectionSet` is never a non-null\n // empty Set — the collapse above (`selection.length === 0 → null`) rules\n // that out — so there's no \"dim everything\" case here, unlike a widget\n // that gives `[]` its own \"selected down to nothing\" meaning.\n const colorFn = (params: CallbackDataParams): string => {\n const base = resolvePaletteColor(params)\n if (!selectionSet) return base\n const datum = params.value as { name?: string | number } | undefined\n const name = datum?.name ?? params.name\n return name != null && selectionSet.has(name)\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n }\n\n // ECharts anchors EVERY slice's `position: 'center'` label at the same\n // point, so at most one of them can ever be legible — the donut design\n // leans into that (one KPI inside the ring) and picks a single slice\n // below. A consumer that moves the labels off the centre gives each\n // slice its own anchor (`'outside'` with leader lines is what the PDF\n // export uses, `'inside'` / `'insideTop'` … likewise), and there the\n // single-label rule is simply wrong: every slice must render its own\n // label. Read the position off the structural template, which already\n // carries the consumer's `optionsOverride`.\n const templateLabelPosition = (\n broadcastTemplate as { label?: { position?: unknown } }\n ).label?.position\n const isCenterLabel =\n templateLabelPosition === undefined || templateLabelPosition === 'center'\n\n // Which slice renders the centre value+name: the greatest value among\n // the *included* slices (all of them when there's no active\n // selection), computed locally — no query round-trip. Only that one\n // slice's formatter call returns text; every other slice returns ''\n // so exactly one label ever renders at the shared centre anchor. Only\n // `typeof value === 'number'` participates — if every included slice's\n // value is non-numeric, `centerMaxName` stays `undefined` and no slice\n // matches it in `labelTextFormatter` below, so the chart renders with no\n // centre label at all (graceful no-op, not a crash). Skipped entirely\n // for off-centre labels, where every slice labels itself.\n const centerData = seriesArr[0] ?? []\n let centerMaxName: string | number | undefined\n let centerMaxValue = -Infinity\n for (const d of centerData) {\n if (selectionSet && !selectionSet.has(d.name)) continue\n if (typeof d.value === 'number' && d.value > centerMaxValue) {\n centerMaxValue = d.value\n centerMaxName = d.name\n }\n }\n\n // Slice label formatter — lives in the merger because it reads\n // reactive `formatter` / `labelFormatter` from ctx. The rich tags\n // `{c|…}` and `{b|…}` reference the rich styles baked into the\n // structural label template.\n const labelTextFormatter = (params: CallbackDataParams): string => {\n const { name } = params\n if (name == null) return ''\n if (isCenterLabel && name !== centerMaxName) return ''\n const encodeIndex = params.encode?.value?.[0]\n if (encodeIndex === undefined) return ''\n const value = (Object.values(params.data ?? {}) as unknown[]).at(\n encodeIndex,\n )\n // `value` here is the resolved dataset cell — for pie data it's a\n // `string | number` primitive. Anything else is a misuse we won't\n // dress up with `String(...)` (would render \"[object Object]\").\n const formattedValue =\n typeof value === 'number'\n ? formatter\n ? formatter(value)\n : String(value)\n : typeof value === 'string'\n ? value\n : ''\n const formattedName = labelFormatter\n ? String(labelFormatter(name ?? ''))\n : String(name ?? '')\n // The blank line is what separates the big centre KPI from its\n // caption. Off-centre labels sit next to their own slice and are\n // laid out against their neighbours, so the spacer only inflates\n // each label's bounding box — enough, with a hidden `{b|…}` block,\n // to make ECharts drop colliding labels altogether.\n const separator = isCenterLabel ? '\\n\\n' : '\\n'\n return `{c|${formattedValue}}${separator}{b|${formattedName}}`\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n series: seriesArr.map((_, i) => {\n const template =\n (seriesTemplates[i] as object | undefined) ??\n (broadcastTemplate as object)\n const templateObj = typeof template === 'object' ? template : {}\n const templateLabel = (templateObj as { label?: object }).label ?? {}\n const templateItemStyle =\n (templateObj as { itemStyle?: object }).itemStyle ?? {}\n return {\n ...templateObj,\n type: 'pie' as const,\n legendHoverLink: false,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n radius: [...radius],\n // Lift the donut up so the bottom-anchored legend has the\n // vertical real estate to wrap to multiple rows for long\n // category lists — at exactly `50%` the bottom slices crowd\n // the chips and `type: 'plain'` legends can't expand upward\n // without overlapping the donut.\n center: ['50%', '38%'] as [string, string],\n encode: { itemName: 'name', value: 'value' },\n label: {\n ...templateLabel,\n formatter: labelTextFormatter,\n },\n itemStyle: {\n ...templateItemStyle,\n color: colorFn,\n },\n }\n }),\n // Legend always shows for pie (mirrors v1) — slice names drive the\n // entries, so even a single donut benefits from a category list.\n //\n // Exclusion is conveyed purely via `colorFn`/`labelTextFormatter`\n // dimming above, never via the legend's `selected` map: for a pie\n // series, ECharts treats `legend.selected[name] = false` as \"remove\n // this data point,\" which recomputes every remaining slice's angle —\n // the excluded slice disappears instead of rendering dimmed. Forcing\n // every name to `true` on every render also overrides ECharts' own\n // internal toggle state from a native legend click (which flips a\n // name to `false` and hides it before our `legendselectchanged`\n // handler gets a chance to react), so a legend click never leaves a\n // slice structurally removed — only the color/label dimming reflects\n // exclusion, and it stays in sync with the slice itself since both\n // read `selectionSet` on the same render.\n legend: {\n ...baseLegend,\n show: true,\n selectedMode: false,\n selected: Object.fromEntries(\n (seriesArr[0] ?? []).map((d) => [String(d.name), true]),\n ),\n },\n tooltip: {\n ...baseTooltip,\n formatter: buildPieTooltipFormatter(formatter, labelFormatter),\n },\n }\n}\n\n/**\n * Multi-series horizontal-bar fusion. Mirrors v1 pie's \"pie data with\n * >1 series collapses into a horizontal bar chart\" behavior — side-by-\n * side donuts don't read well when you're comparing the same categories\n * across cohorts, so swap to a value-on-x / category-on-y bar layout.\n *\n * Pie data shape is identical to bar's (`{ name, value }[][]`), so the\n * same dataset feeds either layout without transformation.\n */\nfunction buildMultiSeriesBarFusion(\n option: EChartsOption,\n seriesArr: PieWidgetData,\n theme: Theme,\n series: readonly WidgetSeries[] | undefined,\n ctx: OptionFactoryContext | undefined,\n selectionSet: Set<string | number> | null,\n): EChartsOption {\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const formatter = ctx?.formatter\n const labelFormatter = ctx?.labelFormatter\n\n const { niceMinVal, niceMaxVal } = computeNiceBounds(seriesArr)\n\n // Per-series palette — multi-series bars take one color per series\n // (not per-data, unlike donut slices). Same anti-stale-callback rule:\n // always emit the callback so a transition to/from a selection\n // replaces the previous closure cleanly.\n const palette = Array.isArray(option.color)\n ? (option.color as readonly (string | undefined)[])\n : []\n const barColorFn = (params: CallbackDataParams): string => {\n const seriesIdx = params.seriesIndex ?? 0\n const seriesSwatch =\n palette.length > 0 ? palette[seriesIdx % palette.length] : undefined\n const base = (seriesSwatch ?? params.color) as string\n if (!selectionSet) return base\n const datum = params.value as { name?: string | number } | undefined\n const name = datum?.name ?? params.name\n return name != null && selectionSet.has(name)\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n grid: {\n ...baseGrid,\n ...buildGridConfig(true, theme),\n right: parseInt(theme.spacing(4)),\n containLabel: true,\n },\n // Drop pie-specific structural keys that shouldn't render in the\n // bar fallback. ECharts ignores undefined keys, so this is a clean\n // override.\n series: seriesArr.map((_, i) => {\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n datasetIndex: i,\n type: 'bar' as const,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n barMaxWidth: 100,\n legendHoverLink: false,\n emphasis: { focus: 'series' },\n ...buildSeriesLabelConfig(formatter, 'x'),\n itemStyle: { color: barColorFn },\n ...(overrideColor ? { color: overrideColor } : {}),\n }\n }),\n xAxis: {\n type: 'value',\n // Closures over the pre-computed nice bounds — ECharts calls them\n // once per render to resolve the axis extents.\n min: () => niceMinVal,\n max: () => niceMaxVal,\n axisLine: { show: false },\n axisTick: { show: false },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n // Value labels render BELOW the axis line (default placement),\n // NOT inside the plot. Bar/histogram use `inside: true` +\n // `verticalAlign: 'bottom'` for their *vertical* y-axis where the\n // axis is the left wall — labels inside read like grid annotations.\n // A horizontal bar chart's value axis is the x-axis, and `inside`\n // there pushes the max label *behind* the bar (it visually\n // disappears once a bar reaches the right edge). Use the default\n // below-the-axis placement so the rounded extents stay readable.\n axisLabel: {\n fontSize: theme.typography.overlineDelicate?.fontSize,\n fontFamily: theme.typography.overlineDelicate?.fontFamily,\n margin: parseInt(theme.spacing(1)),\n showMaxLabel: true,\n showMinLabel: true,\n formatter: (value: number) => {\n if (value !== niceMaxVal && value !== niceMinVal) return ''\n if (value === 0) return ''\n return formatter ? formatter(value) : String(value)\n },\n },\n },\n yAxis: {\n type: 'category',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],\n ...(labelFormatter && {\n formatter: (value: string | number) => String(labelFormatter(value)),\n }),\n },\n },\n // Mirrors the donut branch's `selected` reset above — `selectedMode:\n // false` blocks the native legend click that would otherwise undo an\n // ECharts-internal toggle, so without forcing every entry back to\n // `true` here a prior native toggle (from before `selectedMode` took\n // effect, or a stale `keepAlive` instance) would stay hidden forever\n // with no way to restore it.\n legend: {\n ...baseLegend,\n show: true,\n selectedMode: false,\n selected: Object.fromEntries(\n seriesArr.map((_, i) => [series?.[i]?.name ?? `Series ${i + 1}`, true]),\n ),\n },\n tooltip: {\n ...baseTooltip,\n trigger: 'axis',\n formatter: buildHorizontalBarTooltipFormatter(formatter, labelFormatter),\n },\n } as EChartsOption\n}\n\n/**\n * Tooltip formatter for the horizontal-bar fallback. Reads the value by\n * `encode.x` dimension index (v1 parity) so it stays robust to\n * downstream changes that rename dataset columns.\n */\nfunction buildHorizontalBarTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: string | number) => string | number) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const encodeIndex = item.encode?.x?.at(0)\n const dimName =\n encodeIndex !== undefined ? item.dimensionNames?.[encodeIndex] : undefined\n const row =\n item.value && typeof item.value === 'object' && !Array.isArray(item.value)\n ? (item.value as Record<string, string | number>)\n : undefined\n const raw = dimName && row ? row[dimName] : undefined\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const name = labelFormatter\n ? String(labelFormatter(item.name ?? ''))\n : String(item.name ?? '')\n return { name, seriesName, marker, value: formattedValue }\n })\n}\n\n/**\n * Min/max bounds over every datum's `.value` across all series. Used by\n * the horizontal-bar fallback's x-axis label formatter so we only render\n * labels at the rounded extents (matches v1 + bar / histogram).\n */\nfunction computeNiceBounds(seriesArr: PieWidgetData): {\n niceMinVal: number\n niceMaxVal: number\n} {\n let min = 0\n let max = -Infinity\n for (const series of seriesArr) {\n for (const d of series) {\n if (typeof d?.value !== 'number' || !Number.isFinite(d.value)) continue\n if (d.value < min) min = d.value\n if (d.value > max) max = d.value\n }\n }\n return {\n niceMinVal: min < 0 ? niceNum(min) : 0,\n niceMaxVal: max <= 0 ? 1 : niceNum(max),\n }\n}\n\n/**\n * Tooltip formatter for pie slices. `item.value` is the dataset row\n * object (e.g. `{ name: 'A', value: 10 }`); we pull the value by\n * `encode.value`'s dimension index, formatted via the reactive `formatter`\n * if present. Slice name passes through `labelFormatter`.\n */\nfunction buildPieTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: string | number) => string | number) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const encodeIndex = item.encode?.value?.at(0) ?? 1\n const values =\n item.value && typeof item.value === 'object' && !Array.isArray(item.value)\n ? (Object.values(item.value) as (string | number)[])\n : []\n const raw = values[encodeIndex]\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const name = labelFormatter\n ? String(labelFormatter(item.name ?? ''))\n : String(item.name ?? '')\n return { name, seriesName, marker, value: formattedValue }\n })\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst OUTER_SIZE = 160\nconst HOLE_SIZE = 96\n\nconst styles = {\n root: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 200,\n py: 1,\n gap: ({ spacing }) => spacing(2),\n },\n donut: {\n position: 'relative',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n },\n ring: {\n width: OUTER_SIZE,\n height: OUTER_SIZE,\n },\n hole: {\n position: 'absolute',\n zIndex: 1,\n width: HOLE_SIZE,\n height: HOLE_SIZE,\n borderRadius: '50%',\n bgcolor: 'background.paper',\n },\n label: {\n position: 'absolute',\n zIndex: 2,\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: ({ spacing }) => spacing(0.5),\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\nexport interface PieSkeletonProps {\n count?: number\n}\n\n/**\n * Loading state for the Pie widget. Mirrors the donut silhouette — a ring\n * (gray circle with a background-coloured hole punched out), a stacked\n * value/name stub in the centre, and a centered legend stub below — so the\n * skeleton reads as \"a donut chart\" rather than a solid disc.\n *\n * Single donut only: multi-series pie loads as a horizontal bar chart, so\n * rendering one ring stays honest regardless of `count`.\n */\nexport function PieSkeleton({ count }: PieSkeletonProps) {\n // `count` is accepted for API compatibility but intentionally unused: the\n // skeleton always renders a single donut, since multi-series pie loads as a\n // horizontal bar chart rather than multiple donuts.\n void count\n return (\n <Box sx={styles.root}>\n <Box sx={styles.donut}>\n <Skeleton variant='circular' sx={styles.ring} />\n <Box sx={styles.hole} />\n <Box sx={styles.label}>\n <Skeleton width={56} height={18} />\n <Skeleton width={36} height={8} />\n </Box>\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildCsvDownloadItem,\n withPngItem,\n type DownloadItem,\n type WidgetDownloadArgs,\n} from '../actions/download'\nimport type { PieWidgetData } from './types'\n\n/**\n * Download menu items for the Pie widget. Always includes a CSV item with\n * `series, name, value` columns (one row per slice; series labels come from\n * `seriesNames` if provided, else `series_<n>`). When `getCaptureEl` is\n * given, prepends a PNG item that rasterises the captured element via\n * `html2canvas`.\n */\nexport function createPieDownloadConfig(\n args: WidgetDownloadArgs<PieWidgetData> & {\n seriesNames?: readonly string[]\n },\n): DownloadItem[] {\n return withPngItem(\n args,\n buildCsvDownloadItem({\n filename: args.filename,\n getRows: (ctx) => {\n const data = args.getData(ctx)\n const rows: unknown[][] = [['series', 'name', 'value']]\n for (const [i, series] of data.entries()) {\n const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`\n for (const slice of series) {\n rows.push([seriesName, slice.name, slice.value])\n }\n }\n return rows\n },\n }),\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pie.js","names":["<Skeleton variant='circular' sx={styles.ring} />","<Box sx={styles.hole} />","<Box sx={styles.donut}>\n <Skeleton variant='circular' sx={styles.ring} />\n <Box sx={styles.hole} />\n <Box sx={styles.label}>\n <Skeleton width={56} height={18} />\n <Skeleton width={36} height={8} />\n </Box>\n </Box>","<Box sx={styles.root}>\n <Box sx={styles.donut}>\n <Skeleton variant='circular' sx={styles.ring} />\n <Box sx={styles.hole} />\n <Box sx={styles.label}>\n <Skeleton width={56} height={18} />\n <Skeleton width={36} height={8} />\n </Box>\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>","(i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n )","_c","buildCsvDownloadItem","mergeOptions","resolveThemeColor","createTooltipFormatter","buildSeriesLabelConfig","buildGridConfig","createTooltipPositioner","buildLegendConfig","niceNum","echarts","DEFAULT_RADIUS","pieOptions","theme","buildPieTooltipFormatter","formatter","labelFormatter","createPieOptionFactory","options","radius","selection","selectionSet","option","structural","optionsOverride","seriesArr","data","buildMultiSeriesBarFusion","series","ctx","buildSingleSeriesPieFusion","seriesTemplates","broadcastTemplate","baseTooltip","baseLegend","palette","resolvePaletteColor","params","colorFn","base","name","templateLabelPosition","isCenterLabel","centerData","centerMaxName","centerMaxValue","d","labelTextFormatter","encodeIndex","value","formattedValue","formattedName","s","template","i","templateObj","templateLabel","templateItemStyle","baseGrid","computeNiceBounds","barColorFn","seriesIdx","overrideColor","niceMinVal","niceMaxVal","buildHorizontalBarTooltipFormatter","item","dimName","row","raw","marker","seriesName","min","max","OUTER_SIZE","HOLE_SIZE","styles","spacing","PieSkeleton","$","c","t1","t2","Skeleton","Box","t3","t4","_temp","createPieDownloadConfig","withPngItem","args","rows","slice"],"sources":["../../src/widgets/pie/options.ts","../../src/widgets/pie/skeleton.tsx","../../src/widgets/pie/download.ts"],"sourcesContent":["import type { Theme } from '@mui/material'\nimport type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildGridConfig,\n buildLegendConfig,\n buildSeriesLabelConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../utils/option-builders'\nimport type { OptionFactory, OptionFactoryContext } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport type { WidgetSeries } from '../types'\nimport type {\n PieEChartsOption,\n PieOptionFactoryInput,\n PieOptionsInput,\n PieWidgetData,\n} from './types'\n\nconst DEFAULT_RADIUS: readonly [string, string] = ['58%', '74%']\n\n/**\n * Builds the **structural** ECharts option for a pie / donut widget.\n * Mirrors v1's pie look-and-feel: themed legend (`buildLegendConfig`),\n * themed tooltip via `createTooltipPositioner` / `createTooltipFormatter`,\n * `qualitative.bold` color palette, slice borders against the paper\n * background, hover-emphasis disabled, and a centered rich-text label.\n *\n * Layered like Bar / Histogram:\n * - **Theme-aware bits** live here on a series template (slice border,\n * center-label rich style sizes, emphasis-disabled).\n * - **Reactive bits** (tooltip value formatter, center-label text,\n * selection-driven dim) are re-emitted by\n * {@link createPieOptionFactory} at fusion time so RelativeData's\n * percent override flows through.\n */\nexport function pieOptions({\n theme,\n formatter,\n labelFormatter,\n}: PieOptionsInput): PieEChartsOption {\n return {\n legend: {\n ...buildLegendConfig({ hasLegend: true, labelFormatter }),\n // Bottom-left, same as every other widget — `buildLegendConfig`'s\n // `left: 0` is kept deliberately. The wider `itemGap` gives chips\n // breathing room in the typical 3–6 category case.\n type: 'scroll',\n itemGap: 16,\n // The legend is presentational only here — the pie has its own\n // click-to-select affordance on the slices themselves, so legend\n // clicks toggling series visibility would be a second, redundant/\n // conflicting interaction model. Hover-highlight is disabled per\n // series via `legendHoverLink` (ECharts has no legend-level knob\n // for it).\n selectedMode: false,\n },\n tooltip: {\n // Pie uses item-trigger (no axis); the rest of the styling mirrors\n // bar/histogram so the dashboard reads consistently — dark\n // grey[900] bg, white caption-font text, padded, with the shared\n // overflow-aware positioner.\n trigger: 'item',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n position: createTooltipPositioner(theme),\n formatter: buildPieTooltipFormatter(formatter, labelFormatter),\n },\n // V1 used `qualitative.bold` only (no `secondary.main` prefix) because\n // pie slices are categorical — each slice deserves a distinct palette\n // entry from the start, not a primary highlight.\n color: Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n // Pie template. The merger spreads this into each per-data series it\n // emits — borderColor / borderWidth / emphasis-disabled / label rich\n // sizes all survive through.\n series: [\n {\n type: 'pie',\n colorBy: 'data',\n avoidLabelOverlap: true,\n selectedOffset: 0,\n legendHoverLink: false,\n emphasis: { disabled: true },\n itemStyle: {\n borderColor: theme.palette.background.paper,\n borderWidth: 1,\n },\n label: {\n show: true,\n position: 'center',\n rich: {\n b: { fontSize: 16, fontWeight: 'normal', lineHeight: 20 },\n c: { fontSize: 28, fontWeight: 'bold', lineHeight: 27 },\n },\n },\n },\n ],\n }\n}\n\n/**\n * Returns the pie widget's {@link OptionFactory} — one closure that owns\n * BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link pieOptions}, optionally merging the\n * consumer-supplied `optionsOverride` on top. Called once by Provider\n * to seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`PieWidgetData`) into the option via the dataset API. Single-series\n * → donut (one dataset, `series[i].encode = { itemName: 'name', value: 'value' }`).\n * Multi-series → horizontal-bar fallback (mirrors v1 pie). Reactive\n * formatters from `ctx` drive the tooltip + center label at fusion\n * time so RelativeData's percent formatter flows through without a\n * structural rebuild.\n */\nexport function createPieOptionFactory(\n options: PieOptionFactoryInput,\n): OptionFactory {\n const { theme, formatter, labelFormatter, optionsOverride, series } = options\n const radius = options.radius ?? DEFAULT_RADIUS\n const selection = options.selection\n // An explicit `[]` (deselected down to zero via chart clicks) is treated\n // the same as no selection: the pie has no bar/histogram-style \"nothing\n // selected\" affordance, so dimming every slice would just look broken —\n // it renders fully active, same as the no-interaction state.\n const selectionSet =\n selection === undefined || selection === null || selection.length === 0\n ? null\n : new Set<string | number>(selection)\n return (option, data, ctx) => {\n if (option == null) {\n const structural = pieOptions({ theme, formatter, labelFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as PieWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n if (seriesArr.length > 1) {\n return buildMultiSeriesBarFusion(\n option,\n seriesArr,\n theme,\n series,\n ctx,\n selectionSet,\n )\n }\n return buildSingleSeriesPieFusion(\n option,\n seriesArr,\n radius,\n series,\n ctx,\n selectionSet,\n )\n }\n}\n\n/**\n * Single-series donut fusion. Spreads the structural pie series template\n * into per-data series with center/radius/encoding, wires the reactive\n * center-label / tooltip formatters and the per-data palette-aware\n * selection-dim callback.\n */\nfunction buildSingleSeriesPieFusion(\n option: EChartsOption,\n seriesArr: PieWidgetData,\n radius: readonly [string, string],\n series: readonly WidgetSeries[] | undefined,\n ctx: OptionFactoryContext | undefined,\n selectionSet: Set<string | number> | null,\n): EChartsOption {\n const seriesTemplates = Array.isArray(option.series) ? option.series : []\n const broadcastTemplate = seriesTemplates[0] ?? {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n const formatter = ctx?.formatter\n const labelFormatter = ctx?.labelFormatter\n\n // Pie wants `colorBy: 'data'` — each slice draws from the option's\n // `color` palette by data index. But installing an `itemStyle.color`\n // callback disables ECharts' automatic per-data cycling, and\n // `params.color` collapses to the series color (so every slice goes\n // the same color). We re-implement the per-data palette resolution\n // here so the multicolor look survives selection-driven dimming.\n const palette = Array.isArray(option.color)\n ? (option.color as readonly (string | undefined)[])\n : []\n const resolvePaletteColor = (params: CallbackDataParams): string => {\n const swatch =\n palette.length > 0\n ? palette[params.dataIndex % palette.length]\n : undefined\n return swatch ?? (params.color as string)\n }\n\n // Always emit `itemStyle.color` (passthrough when nothing is selected),\n // not conditionally — same anti-stale-callback rationale as bar /\n // histogram. The slice border + width come from the structural\n // template's `itemStyle` and survive via the spread below.\n //\n // `selectionSet` is the *included* set, same direct semantics as\n // bar/histogram/timeseries (no ADR-014 exclusion inversion). Dim a slice\n // when its name is NOT in the set. `selectionSet` is never a non-null\n // empty Set — the collapse above (`selection.length === 0 → null`) rules\n // that out — so there's no \"dim everything\" case here, unlike a widget\n // that gives `[]` its own \"selected down to nothing\" meaning.\n const colorFn = (params: CallbackDataParams): string => {\n const base = resolvePaletteColor(params)\n if (!selectionSet) return base\n const datum = params.value as { name?: string | number } | undefined\n const name = datum?.name ?? params.name\n return name != null && selectionSet.has(name)\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n }\n\n // ECharts anchors EVERY slice's `position: 'center'` label at the same\n // point, so at most one of them can ever be legible — the donut design\n // leans into that (one KPI inside the ring) and picks a single slice\n // below. A consumer that moves the labels off the centre gives each\n // slice its own anchor (`'outside'` with leader lines is what the PDF\n // export uses, `'inside'` / `'insideTop'` … likewise), and there the\n // single-label rule is simply wrong: every slice must render its own\n // label. Read the position off the structural template, which already\n // carries the consumer's `optionsOverride`.\n const templateLabelPosition = (\n broadcastTemplate as { label?: { position?: unknown } }\n ).label?.position\n const isCenterLabel =\n templateLabelPosition === undefined || templateLabelPosition === 'center'\n\n // Which slice renders the centre value+name: the greatest value among\n // the *included* slices (all of them when there's no active\n // selection), computed locally — no query round-trip. Only that one\n // slice's formatter call returns text; every other slice returns ''\n // so exactly one label ever renders at the shared centre anchor. Only\n // `typeof value === 'number'` participates — if every included slice's\n // value is non-numeric, `centerMaxName` stays `undefined` and no slice\n // matches it in `labelTextFormatter` below, so the chart renders with no\n // centre label at all (graceful no-op, not a crash). Skipped entirely\n // for off-centre labels, where every slice labels itself.\n const centerData = seriesArr[0] ?? []\n let centerMaxName: string | number | undefined\n let centerMaxValue = -Infinity\n for (const d of centerData) {\n if (selectionSet && !selectionSet.has(d.name)) continue\n if (typeof d.value === 'number' && d.value > centerMaxValue) {\n centerMaxValue = d.value\n centerMaxName = d.name\n }\n }\n\n // Slice label formatter — lives in the merger because it reads\n // reactive `formatter` / `labelFormatter` from ctx. The rich tags\n // `{c|…}` and `{b|…}` reference the rich styles baked into the\n // structural label template.\n const labelTextFormatter = (params: CallbackDataParams): string => {\n const { name } = params\n if (name == null) return ''\n if (isCenterLabel && name !== centerMaxName) return ''\n const encodeIndex = params.encode?.value?.[0]\n if (encodeIndex === undefined) return ''\n const value = (Object.values(params.data ?? {}) as unknown[]).at(\n encodeIndex,\n )\n // `value` here is the resolved dataset cell — for pie data it's a\n // `string | number` primitive. Anything else is a misuse we won't\n // dress up with `String(...)` (would render \"[object Object]\").\n const formattedValue =\n typeof value === 'number'\n ? formatter\n ? formatter(value)\n : String(value)\n : typeof value === 'string'\n ? value\n : ''\n const formattedName = labelFormatter\n ? String(labelFormatter(name ?? ''))\n : String(name ?? '')\n // The blank line is what separates the big centre KPI from its\n // caption. Off-centre labels sit next to their own slice and are\n // laid out against their neighbours, so the spacer only inflates\n // each label's bounding box — enough, with a hidden `{b|…}` block,\n // to make ECharts drop colliding labels altogether.\n const separator = isCenterLabel ? '\\n\\n' : '\\n'\n return `{c|${formattedValue}}${separator}{b|${formattedName}}`\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n series: seriesArr.map((_, i) => {\n const template =\n (seriesTemplates[i] as object | undefined) ??\n (broadcastTemplate as object)\n const templateObj = typeof template === 'object' ? template : {}\n const templateLabel = (templateObj as { label?: object }).label ?? {}\n const templateItemStyle =\n (templateObj as { itemStyle?: object }).itemStyle ?? {}\n return {\n ...templateObj,\n type: 'pie' as const,\n legendHoverLink: false,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n radius: [...radius],\n // Lift the donut up so the bottom-anchored legend has the\n // vertical real estate to wrap to multiple rows for long\n // category lists — at exactly `50%` the bottom slices crowd\n // the chips and `type: 'plain'` legends can't expand upward\n // without overlapping the donut.\n center: ['50%', '38%'] as [string, string],\n encode: { itemName: 'name', value: 'value' },\n label: {\n ...templateLabel,\n formatter: labelTextFormatter,\n },\n itemStyle: {\n ...templateItemStyle,\n color: colorFn,\n },\n }\n }),\n // Legend always shows for pie (mirrors v1) — slice names drive the\n // entries, so even a single donut benefits from a category list.\n //\n // Exclusion is conveyed purely via `colorFn`/`labelTextFormatter`\n // dimming above, never via the legend's `selected` map: for a pie\n // series, ECharts treats `legend.selected[name] = false` as \"remove\n // this data point,\" which recomputes every remaining slice's angle —\n // the excluded slice disappears instead of rendering dimmed. Forcing\n // every name to `true` on every render also overrides ECharts' own\n // internal toggle state from a native legend click (which flips a\n // name to `false` and hides it before our `legendselectchanged`\n // handler gets a chance to react), so a legend click never leaves a\n // slice structurally removed — only the color/label dimming reflects\n // exclusion, and it stays in sync with the slice itself since both\n // read `selectionSet` on the same render.\n legend: {\n ...baseLegend,\n show: true,\n selectedMode: false,\n selected: Object.fromEntries(\n (seriesArr[0] ?? []).map((d) => [String(d.name), true]),\n ),\n },\n tooltip: {\n ...baseTooltip,\n formatter: buildPieTooltipFormatter(formatter, labelFormatter),\n },\n }\n}\n\n/**\n * Multi-series horizontal-bar fusion. Mirrors v1 pie's \"pie data with\n * >1 series collapses into a horizontal bar chart\" behavior — side-by-\n * side donuts don't read well when you're comparing the same categories\n * across cohorts, so swap to a value-on-x / category-on-y bar layout.\n *\n * Pie data shape is identical to bar's (`{ name, value }[][]`), so the\n * same dataset feeds either layout without transformation.\n */\nfunction buildMultiSeriesBarFusion(\n option: EChartsOption,\n seriesArr: PieWidgetData,\n theme: Theme,\n series: readonly WidgetSeries[] | undefined,\n ctx: OptionFactoryContext | undefined,\n selectionSet: Set<string | number> | null,\n): EChartsOption {\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const formatter = ctx?.formatter\n const labelFormatter = ctx?.labelFormatter\n\n const { niceMinVal, niceMaxVal } = computeNiceBounds(seriesArr)\n\n // Per-series palette — multi-series bars take one color per series\n // (not per-data, unlike donut slices). Same anti-stale-callback rule:\n // always emit the callback so a transition to/from a selection\n // replaces the previous closure cleanly.\n const palette = Array.isArray(option.color)\n ? (option.color as readonly (string | undefined)[])\n : []\n const barColorFn = (params: CallbackDataParams): string => {\n const seriesIdx = params.seriesIndex ?? 0\n const seriesSwatch =\n palette.length > 0 ? palette[seriesIdx % palette.length] : undefined\n const base = (seriesSwatch ?? params.color) as string\n if (!selectionSet) return base\n const datum = params.value as { name?: string | number } | undefined\n const name = datum?.name ?? params.name\n return name != null && selectionSet.has(name)\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n grid: {\n ...baseGrid,\n ...buildGridConfig(true, theme),\n right: parseInt(theme.spacing(4)),\n containLabel: true,\n },\n // Drop pie-specific structural keys that shouldn't render in the\n // bar fallback. ECharts ignores undefined keys, so this is a clean\n // override.\n series: seriesArr.map((_, i) => {\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n datasetIndex: i,\n type: 'bar' as const,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n barMaxWidth: 100,\n legendHoverLink: false,\n emphasis: { focus: 'series' },\n ...buildSeriesLabelConfig(formatter, 'x'),\n itemStyle: { color: barColorFn },\n ...(overrideColor ? { color: overrideColor } : {}),\n }\n }),\n xAxis: {\n type: 'value',\n // Closures over the pre-computed nice bounds — ECharts calls them\n // once per render to resolve the axis extents.\n min: () => niceMinVal,\n max: () => niceMaxVal,\n axisLine: { show: false },\n axisTick: { show: false },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n // Value labels render BELOW the axis line (default placement),\n // NOT inside the plot. Bar/histogram use `inside: true` +\n // `verticalAlign: 'bottom'` for their *vertical* y-axis where the\n // axis is the left wall — labels inside read like grid annotations.\n // A horizontal bar chart's value axis is the x-axis, and `inside`\n // there pushes the max label *behind* the bar (it visually\n // disappears once a bar reaches the right edge). Use the default\n // below-the-axis placement so the rounded extents stay readable.\n axisLabel: {\n fontSize: theme.typography.overlineDelicate?.fontSize,\n fontFamily: theme.typography.overlineDelicate?.fontFamily,\n margin: parseInt(theme.spacing(1)),\n showMaxLabel: true,\n showMinLabel: true,\n formatter: (value: number) => {\n if (value !== niceMaxVal && value !== niceMinVal) return ''\n if (value === 0) return ''\n return formatter ? formatter(value) : String(value)\n },\n },\n },\n yAxis: {\n type: 'category',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],\n ...(labelFormatter && {\n formatter: (value: string | number) => String(labelFormatter(value)),\n }),\n },\n },\n // Mirrors the donut branch's `selected` reset above — `selectedMode:\n // false` blocks the native legend click that would otherwise undo an\n // ECharts-internal toggle, so without forcing every entry back to\n // `true` here a prior native toggle (from before `selectedMode` took\n // effect, or a stale `keepAlive` instance) would stay hidden forever\n // with no way to restore it.\n legend: {\n ...baseLegend,\n show: true,\n selectedMode: false,\n selected: Object.fromEntries(\n seriesArr.map((_, i) => [series?.[i]?.name ?? `Series ${i + 1}`, true]),\n ),\n },\n tooltip: {\n ...baseTooltip,\n trigger: 'axis',\n formatter: buildHorizontalBarTooltipFormatter(formatter, labelFormatter),\n },\n } as EChartsOption\n}\n\n/**\n * Tooltip formatter for the horizontal-bar fallback. Reads the value by\n * `encode.x` dimension index (v1 parity) so it stays robust to\n * downstream changes that rename dataset columns.\n */\nfunction buildHorizontalBarTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: string | number) => string | number) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const encodeIndex = item.encode?.x?.at(0)\n const dimName =\n encodeIndex !== undefined ? item.dimensionNames?.[encodeIndex] : undefined\n const row =\n item.value && typeof item.value === 'object' && !Array.isArray(item.value)\n ? (item.value as Record<string, string | number>)\n : undefined\n const raw = dimName && row ? row[dimName] : undefined\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const name = labelFormatter\n ? String(labelFormatter(item.name ?? ''))\n : String(item.name ?? '')\n return { name, seriesName, marker, value: formattedValue }\n })\n}\n\n/**\n * Min/max bounds over every datum's `.value` across all series. Used by\n * the horizontal-bar fallback's x-axis label formatter so we only render\n * labels at the rounded extents (matches v1 + bar / histogram).\n */\nfunction computeNiceBounds(seriesArr: PieWidgetData): {\n niceMinVal: number\n niceMaxVal: number\n} {\n let min = 0\n let max = -Infinity\n for (const series of seriesArr) {\n for (const d of series) {\n if (typeof d?.value !== 'number' || !Number.isFinite(d.value)) continue\n if (d.value < min) min = d.value\n if (d.value > max) max = d.value\n }\n }\n return {\n niceMinVal: min < 0 ? niceNum(min) : 0,\n niceMaxVal: max <= 0 ? 1 : niceNum(max),\n }\n}\n\n/**\n * Tooltip formatter for pie slices. `item.value` is the dataset row\n * object (e.g. `{ name: 'A', value: 10 }`); we pull the value by\n * `encode.value`'s dimension index, formatted via the reactive `formatter`\n * if present. Slice name passes through `labelFormatter`.\n */\nfunction buildPieTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: string | number) => string | number) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const encodeIndex = item.encode?.value?.at(0) ?? 1\n const values =\n item.value && typeof item.value === 'object' && !Array.isArray(item.value)\n ? (Object.values(item.value) as (string | number)[])\n : []\n const raw = values[encodeIndex]\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const name = labelFormatter\n ? String(labelFormatter(item.name ?? ''))\n : String(item.name ?? '')\n return { name, seriesName, marker, value: formattedValue }\n })\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst OUTER_SIZE = 160\nconst HOLE_SIZE = 96\n\nconst styles = {\n root: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 200,\n py: 1,\n gap: ({ spacing }) => spacing(2),\n },\n donut: {\n position: 'relative',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n },\n ring: {\n width: OUTER_SIZE,\n height: OUTER_SIZE,\n },\n hole: {\n position: 'absolute',\n zIndex: 1,\n width: HOLE_SIZE,\n height: HOLE_SIZE,\n borderRadius: '50%',\n bgcolor: 'background.paper',\n },\n label: {\n position: 'absolute',\n zIndex: 2,\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: ({ spacing }) => spacing(0.5),\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\nexport interface PieSkeletonProps {\n count?: number\n}\n\n/**\n * Loading state for the Pie widget. Mirrors the donut silhouette — a ring\n * (gray circle with a background-coloured hole punched out), a stacked\n * value/name stub in the centre, and a centered legend stub below — so the\n * skeleton reads as \"a donut chart\" rather than a solid disc.\n *\n * Single donut only: multi-series pie loads as a horizontal bar chart, so\n * rendering one ring stays honest regardless of `count`.\n */\nexport function PieSkeleton({ count }: PieSkeletonProps) {\n // `count` is accepted for API compatibility but intentionally unused: the\n // skeleton always renders a single donut, since multi-series pie loads as a\n // horizontal bar chart rather than multiple donuts.\n void count\n return (\n <Box sx={styles.root}>\n <Box sx={styles.donut}>\n <Skeleton variant='circular' sx={styles.ring} />\n <Box sx={styles.hole} />\n <Box sx={styles.label}>\n <Skeleton width={56} height={18} />\n <Skeleton width={36} height={8} />\n </Box>\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildCsvDownloadItem,\n withPngItem,\n type DownloadItem,\n type WidgetDownloadArgs,\n} from '../actions/download'\nimport type { PieWidgetData } from './types'\n\n/**\n * Download menu items for the Pie widget. Always includes a CSV item with\n * `series, name, value` columns (one row per slice; series labels come from\n * `seriesNames` if provided, else `series_<n>`). When `getCaptureEl` is\n * given, prepends a PNG item that rasterises the captured element via\n * `html2canvas`.\n */\nexport function createPieDownloadConfig(\n args: WidgetDownloadArgs<PieWidgetData> & {\n seriesNames?: readonly string[]\n },\n): DownloadItem[] {\n return withPngItem(\n args,\n buildCsvDownloadItem({\n filename: args.filename,\n getRows: (ctx) => {\n const data = args.getData(ctx)\n const rows: unknown[][] = [['series', 'name', 'value']]\n for (const [i, series] of data.entries()) {\n const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`\n for (const slice of series) {\n rows.push([seriesName, slice.name, slice.value])\n }\n }\n return rows\n },\n }),\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"scatterplot.js","names":["{ count = 24 }: ScatterplotSkeletonProps","count = 24","Box","styles.grid","dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))","styles.container","(_, i) => {\n const top = 10 + ((i * 37) % 80)\n const left = 5 + ((i * 53) % 90)\n const size = 8 + ((i * 7) % 6)\n return { top: `${top}%`, left: `${left}%`, size }\n }","(d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n )","<Box sx={styles.grid}>\n {dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))}\n </Box>","<Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>","(i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n )","<Box sx={styles.container}>\n <Box sx={styles.grid}>\n {dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))}\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>","i"],"sources":["../../src/widgets/scatterplot/options.ts","../../src/widgets/scatterplot/skeleton.tsx","../../src/widgets/scatterplot/download.ts","../../src/widgets/scatterplot/transforms.ts"],"sourcesContent":["import type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildAxisLabelStyle,\n buildGridConfig,\n buildLegendConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../utils/option-builders'\nimport { ZOOM_LAYOUT } from '../actions/zoom-toggle'\nimport type { OptionFactory } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport type {\n ScatterplotEChartsOption,\n ScatterplotOptionFactoryInput,\n ScatterplotOptionsInput,\n ScatterplotWidgetData,\n} from './types'\n\n/**\n * Builds the **structural** ECharts option for a scatterplot widget — both\n * axes `type: 'value'` (not category, unlike Bar/Histogram), grid, tooltip\n * triggered per-item rather than per-axis. Intentionally data-agnostic: no\n * series, no dataset, no `legend.show` (those depend on data and are added\n * by {@link createScatterplotOptionFactory}).\n *\n * Styling parity with v1: dark themed tooltip via `createTooltipFormatter`\n * + `createTooltipPositioner`, `buildGridConfig`-based grid, polished\n * axisLine/Tick/splitLine, `overlineDelicate` axis labels, structural\n * legend wired via `buildLegendConfig` (toggled by the merger), and the\n * CARTO `qualitative.bold + secondary` palette — same pattern bar /\n * histogram / pie already use. {@link createScatterplotOptionFactory}\n * wraps this builder in its structural-phase branch.\n */\nexport function scatterplotOptions({\n theme,\n xFormatter,\n yFormatter,\n}: ScatterplotOptionsInput): ScatterplotEChartsOption {\n return {\n grid: {\n left: parseInt(theme.spacing(1)),\n top: parseInt(theme.spacing(3)),\n right: parseInt(theme.spacing(1)),\n // Default: no legend. Merger bumps `bottom` when there are >1 series.\n ...buildGridConfig(false, theme),\n containLabel: true,\n },\n tooltip: {\n // Per-point trigger — different from Bar's 'axis' trigger because\n // points don't share an x-coordinate.\n trigger: 'item',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n position: createTooltipPositioner(theme),\n formatter: buildScatterTooltipFormatter(xFormatter, yFormatter),\n },\n // Legend styling baked here; `show` is toggled by the merger based on\n // series count.\n legend: {\n ...buildLegendConfig({ hasLegend: false, labelFormatter: undefined }),\n },\n axisPointer: { lineStyle: { color: theme.palette.grey[400] } },\n color: [\n theme.palette.secondary.main,\n ...Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n ],\n xAxis: {\n type: 'value',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n margin: parseInt(theme.spacing(1)),\n hideOverlap: true,\n showMinLabel: true,\n showMaxLabel: true,\n ...(xFormatter && { formatter: xFormatter }),\n },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n },\n yAxis: {\n type: 'value',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n margin: parseInt(theme.spacing(1)),\n hideOverlap: true,\n showMinLabel: true,\n showMaxLabel: true,\n ...(yFormatter && { formatter: yFormatter }),\n },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n },\n } as ScatterplotEChartsOption\n}\n\n/**\n * Returns the scatterplot widget's {@link OptionFactory} — one closure\n * that owns BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link scatterplotOptions}, optionally merging\n * the consumer-supplied `optionsOverride`. Called once by Provider to\n * seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`ScatterplotWidgetData`) into the option via the dataset API. Each\n * series's `[x, y]` tuples land in `dataset[i].source` as 2-column\n * rows; the series uses positional encoding (`encode: { x: 0, y: 1 }`)\n * and `type: 'scatter'`. niceMin/niceMax are computed over both axes\n * so the chart frames data on rounded extents and numeric jitters\n * don't shift gridlines per render. Reactive `ctx.formatter` (driven\n * by RelativeData) re-derives the y-axis label and tooltip at fusion\n * time; `xFormatter` stays baked at structural-build time (relative\n * is a values-axis concept; x is coordinate-space).\n *\n * When `ZoomToggle` installs a `dataZoom` slider, grid bottom is\n * extended and the slider is positioned above the legend row (if any) —\n * same layout dance bar / histogram / timeseries do.\n */\nexport function createScatterplotOptionFactory(\n options: ScatterplotOptionFactoryInput,\n): OptionFactory {\n const { theme, xFormatter, yFormatter, optionsOverride } = options\n const series = options.series\n const symbolSize = options.symbolSize ?? 8\n const selection = options.selection\n const selectionSet =\n selection && selection.length > 0 ? new Set<string>(selection) : null\n return (option, data, ctx) => {\n if (option == null) {\n const structural = scatterplotOptions({ theme, xFormatter, yFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as ScatterplotWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n const hasLegend = seriesArr.length > 1\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseXAxis =\n typeof option.xAxis === 'object' && !Array.isArray(option.xAxis)\n ? option.xAxis\n : {}\n const baseYAxis =\n typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)\n ? option.yAxis\n : {}\n\n const reactiveFormatter = ctx?.formatter\n\n const { niceMinX, niceMaxX, niceMinY, niceMaxY } =\n computeScatterBounds(seriesArr)\n\n // Dim non-selected points via `series.itemStyle.color`. The selection\n // key is `${seriesIndex}:${dataIndex}`; we read those off the params\n // ECharts hands to the callback per-data.\n //\n // We *always* emit `itemStyle.color` (a passthrough when nothing is\n // selected), not conditionally — dropping the key between renders\n // would let ECharts' default merge keep the previous callback alive\n // and items would stay dimmed forever after an external clear.\n // Keeping the key always-present means a plain setOption merge swaps\n // the callback in place, no `replaceMerge` and no entry-animation\n // flash on selection on/off.\n const makeDimColor =\n (seriesIdx: number) => (params: CallbackDataParams) => {\n const base = params.color as string\n if (!selectionSet) return base\n const key = `${seriesIdx}:${params.dataIndex}`\n return selectionSet.has(key)\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n }\n\n // Zoom slider layout: when ZoomToggle has installed `dataZoom`,\n // reserve grid space and lift any x-slider above the legend (if\n // present). Scatter supports 2D zoom via `axes: ['x', 'y']` — in\n // that case there's also a vertical y-slider on the right edge, so\n // we reserve `grid.right` separately. Inside-only dataZoom entries\n // (no `type: 'slider'`) take no grid space.\n const dataZoomLayout = layoutDataZoomForScatter(option.dataZoom, hasLegend)\n const hasXSlider = dataZoomLayout?.hasXSlider ?? false\n const hasYSlider = dataZoomLayout?.hasYSlider ?? false\n const fallbackBottom =\n typeof baseGrid.bottom === 'number' ? baseGrid.bottom : 24\n const baseBottom = hasLegend ? 56 : fallbackBottom\n const gridBottom = hasXSlider\n ? baseBottom + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : baseBottom\n const fallbackRight =\n typeof baseGrid.right === 'number' ? baseGrid.right : 8\n const gridRight = hasYSlider\n ? fallbackRight + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : fallbackRight\n\n return {\n ...option,\n // ECharts dataset.source wants a mutable [number, number][] shape; we\n // hold readonly tuples internally, so cast at the boundary.\n dataset: seriesArr.map((s) => ({ source: s as unknown as number[][] })),\n series: seriesArr.map((_, i) => {\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n type: 'scatter' as const,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n encode: { x: 0, y: 1 },\n symbolSize,\n emphasis: { focus: 'series' },\n itemStyle: { color: makeDimColor(i) },\n ...(overrideColor ? { color: overrideColor } : {}),\n }\n }),\n legend: { ...baseLegend, show: hasLegend },\n grid: { ...baseGrid, bottom: gridBottom, right: gridRight },\n ...(dataZoomLayout ? { dataZoom: dataZoomLayout.entries } : {}),\n xAxis: {\n ...baseXAxis,\n min: niceMinX,\n max: niceMaxX,\n } as EChartsOption['xAxis'],\n yAxis: {\n ...baseYAxis,\n min: niceMinY,\n max: niceMaxY,\n axisLabel: {\n ...((baseYAxis as { axisLabel?: object }).axisLabel ?? {}),\n // Re-derive the y-axis formatter at fusion time so RelativeData's\n // percent formatter (written to `state.formatter`) flows through\n // without rebuilding the structural option. Falls back to the\n // structural `yFormatter` already baked in `baseYAxis.axisLabel`\n // (which `String(value)` if neither is set).\n ...(reactiveFormatter ? { formatter: reactiveFormatter } : {}),\n },\n } as EChartsOption['yAxis'],\n tooltip: {\n ...baseTooltip,\n // Rebuild the tooltip formatter so the live y-axis formatter is\n // applied to the y-coordinate in the (x, y) label. xFormatter is\n // structural — relative is a values-axis concept, so xFormatter\n // doesn't change under RelativeData.\n formatter: buildReactiveScatterTooltipFormatter(\n (baseTooltip as { formatter?: unknown }).formatter,\n reactiveFormatter,\n xFormatter,\n ),\n },\n } as EChartsOption\n }\n}\n\n/**\n * Lay out the `dataZoom` array for the scatter chart:\n * - Detect whether any horizontal (x-axis) slider is present — if so\n * lift it above the legend row when a legend is shown.\n * - Detect whether any vertical (y-axis) slider is present — the\n * caller reserves `grid.right` so the slider doesn't overlap the\n * plot area.\n *\n * Returns `null` when there's no `dataZoom` so callers can skip the\n * layout adjustment entirely. An entry is considered an \"x-slider\" if\n * it has `xAxisIndex` set (or no axis index — defaults to x in ECharts).\n * A \"y-slider\" has `yAxisIndex` set.\n */\nfunction layoutDataZoomForScatter(\n dataZoom: unknown,\n hasLegend: boolean,\n): { entries: unknown[]; hasXSlider: boolean; hasYSlider: boolean } | null {\n if (!Array.isArray(dataZoom) || dataZoom.length === 0) return null\n let hasXSlider = false\n let hasYSlider = false\n const entries = dataZoom.map((entry: unknown) => {\n if (entry == null || typeof entry !== 'object') return entry\n const dz = entry as {\n type?: string\n xAxisIndex?: unknown\n yAxisIndex?: unknown\n bottom?: number\n }\n if (dz.type !== 'slider') return dz\n const targetsY = dz.yAxisIndex !== undefined\n if (targetsY) {\n hasYSlider = true\n return dz\n }\n // Either explicit x or defaulted (ECharts defaults sliders to xAxis\n // when no axis index is provided).\n hasXSlider = true\n if (hasLegend) {\n return { ...dz, bottom: ZOOM_LAYOUT.sliderBottomWithLegend }\n }\n return dz\n })\n return { entries, hasXSlider, hasYSlider }\n}\n\n/**\n * If a reactive (store-driven) y formatter is provided, re-build the\n * scatter tooltip formatter using it. Otherwise, return the structural\n * formatter unchanged so the original `xFormatter` / `yFormatter`\n * baked into `scatterplotOptions` still applies. The structural\n * formatter has stable identity per `scatterplotOptions` call, so this\n * path doesn't churn ECharts on every render.\n */\nfunction buildReactiveScatterTooltipFormatter(\n structuralFormatter: unknown,\n reactiveYFormatter: ((value: number) => string) | undefined,\n xFormatter: ((value: number) => string) | undefined,\n) {\n if (!reactiveYFormatter) return structuralFormatter\n return createTooltipFormatter((item) => {\n const value = item.value as readonly [number, number] | undefined\n const x = value?.[0]\n const y = value?.[1]\n const formattedX =\n typeof x === 'number' ? (xFormatter ? xFormatter(x) : String(x)) : ''\n const formattedY =\n typeof y === 'number' ? reactiveYFormatter(y) : String(y ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n return {\n name: `(${formattedX}, ${formattedY})`,\n seriesName,\n marker,\n value: '',\n }\n })\n}\n\nfunction buildScatterTooltipFormatter(\n xFormatter: ((value: number) => string) | undefined,\n yFormatter: ((value: number) => string) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const value = item.value as readonly [number, number] | undefined\n const x = value?.[0]\n const y = value?.[1]\n const formattedX =\n typeof x === 'number' ? (xFormatter ? xFormatter(x) : String(x)) : ''\n const formattedY =\n typeof y === 'number' ? (yFormatter ? yFormatter(y) : String(y)) : ''\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n return {\n name: `(${formattedX}, ${formattedY})`,\n seriesName,\n marker,\n value: '',\n }\n })\n}\n\nfunction computeScatterBounds(seriesArr: ScatterplotWidgetData): {\n niceMinX: number\n niceMaxX: number\n niceMinY: number\n niceMaxY: number\n} {\n let minX = Infinity\n let maxX = -Infinity\n let minY = Infinity\n let maxY = -Infinity\n for (const series of seriesArr) {\n for (const point of series) {\n const x = point?.[0]\n const y = point?.[1]\n if (typeof x === 'number' && Number.isFinite(x)) {\n if (x < minX) minX = x\n if (x > maxX) maxX = x\n }\n if (typeof y === 'number' && Number.isFinite(y)) {\n if (y < minY) minY = y\n if (y > maxY) maxY = y\n }\n }\n }\n // Mirror bar's `computeNiceBounds`: clamp min to 0 when data is\n // non-negative (gridline reads cleanly from zero), apply `niceNum` to\n // negative mins, and floor max=0 to 1 so the chart always has range.\n // Scatter can have free coordinates so we apply this per-axis.\n const niceMaxX = Number.isFinite(maxX) ? (maxX <= 0 ? 1 : niceNum(maxX)) : 1\n const niceMaxY = Number.isFinite(maxY) ? (maxY <= 0 ? 1 : niceNum(maxY)) : 1\n const niceMinX = Number.isFinite(minX) ? (minX < 0 ? niceNum(minX) : 0) : 0\n const niceMinY = Number.isFinite(minY) ? (minY < 0 ? niceNum(minY) : 0) : 0\n return { niceMinX, niceMaxX, niceMinY, niceMaxY }\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst styles = {\n container: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n flexDirection: 'column',\n gap: ({ spacing }) => spacing(1),\n height: ({ spacing }) => spacing(38),\n },\n grid: {\n position: 'relative',\n flex: '1 1 auto',\n width: '100%',\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n height: ({ spacing }) => spacing(5),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\n// `sx` callback that needs runtime args — extracted at module scope so the\n// `styles` object can satisfy `Record<string, SxProps<Theme>>` cleanly\n// (function `sx`-with-args isn't assignable to plain `SxProps<Theme>`).\nconst dotSx = (top: string, left: string, size: number): SxProps<Theme> => ({\n position: 'absolute',\n top,\n left,\n width: size,\n height: size,\n borderRadius: '50%',\n})\n\nexport interface ScatterplotSkeletonProps {\n /** Number of dots to render. */\n count?: number\n}\n\n/**\n * Loading state for the Scatterplot widget. Mirrors a scatter chart's\n * silhouette — a deterministic spread of small circular dots in the plot\n * area plus a legend stub — so the skeleton reads as \"a scatter chart\"\n * rather than a generic list. Matches bar/histogram/pie skeleton structure\n * (grid stub + legend stub in a flex column).\n */\nexport function ScatterplotSkeleton({ count = 24 }: ScatterplotSkeletonProps) {\n // Deterministic pseudo-scatter positions so the skeleton doesn't flicker.\n const dots = Array.from({ length: count }, (_, i) => {\n const top = 10 + ((i * 37) % 80)\n const left = 5 + ((i * 53) % 90)\n const size = 8 + ((i * 7) % 6)\n return { top: `${top}%`, left: `${left}%`, size }\n })\n return (\n <Box sx={styles.container}>\n <Box sx={styles.grid}>\n {dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))}\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildCsvDownloadItem,\n withPngItem,\n type DownloadItem,\n type WidgetDownloadArgs,\n} from '../actions/download'\nimport type { ScatterplotWidgetData } from './types'\n\n/**\n * Download menu items for the Scatterplot widget. Always includes a CSV item\n * with `series, x, y` columns (one row per point). When `getCaptureEl` is\n * given, prepends a PNG item that rasterises the captured element via\n * `html2canvas`.\n */\nexport function createScatterplotDownloadConfig(\n args: WidgetDownloadArgs<ScatterplotWidgetData> & {\n seriesNames?: readonly string[]\n },\n): DownloadItem[] {\n return withPngItem(\n args,\n buildCsvDownloadItem({\n filename: args.filename,\n getRows: (ctx) => {\n const data = args.getData(ctx)\n const rows: unknown[][] = [['series', 'x', 'y']]\n for (const [i, series] of data.entries()) {\n const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`\n for (const [x, y] of series) {\n rows.push([seriesName, x, y])\n }\n }\n return rows\n },\n }),\n )\n}\n","/**\n * Scatterplot-specific `RelativeData` transform. Scatter data is\n * `[number, number][]` — each series is a list of `[x, y]` tuples.\n * Relative is a values-axis concept, so this rewrites `y` to its\n * share of the series's total `y` (0–100) and leaves `x` raw — x is\n * coordinate space, not part of the cohort total.\n *\n * Pass to `<Widget.RelativeData transform={toRelativeScatterplotData} />`.\n *\n * The denominator is the sum of |y| across the series so mixed-sign\n * y values produce sane signed shares-of-magnitude. A series whose\n * total y-magnitude is zero (all-zero or empty input) is returned\n * unchanged so a stalled or empty data set doesn't show misleading 0%\n * values. Inputs whose shape isn't `[number, number][]` fall through\n * untouched.\n */\nexport const toRelativeScatterplotData = (input: unknown): unknown => {\n if (!Array.isArray(input)) return input\n return input.map((series: unknown): unknown => {\n if (!isXyTupleArray(series)) return series\n const total = series.reduce((acc, [, y]) => acc + Math.abs(y), 0)\n if (total <= 0) return series\n return series.map(([x, y]) => [x, (y / total) * 100] as [number, number])\n })\n}\n\nfunction isXyTupleArray(v: unknown): v is [number, number][] {\n if (!Array.isArray(v)) return false\n return v.every(\n (item) =>\n Array.isArray(item) &&\n item.length === 2 &&\n typeof item[0] === 'number' &&\n Number.isFinite(item[0]) &&\n typeof item[1] === 'number' &&\n Number.isFinite(item[1]),\n )\n}\n\n/**\n * `LockSelection` filter for Scatterplot. Data is `[number, number][][]` —\n * points have no `name`, so selection (and locking) is keyed on a synthetic\n * `${seriesIndex}:${dataIndex}` string (same format `useChartSelection`'s\n * `keyFromClick`/`keysFromBrush` produce for this widget). Unlike Histogram,\n * a point's position carries no external meaning (no `ticks` to desync from),\n * so non-locked points are removed outright rather than zeroed.\n */\nexport function filterScatterplotByLockedPoints(\n input: unknown,\n lockedItems: readonly (string | number)[],\n): unknown {\n if (lockedItems.length === 0) return input\n if (!Array.isArray(input)) return input\n const locked = new Set<string>(lockedItems.map(String))\n return input.map((series: unknown, seriesIndex: number): unknown => {\n if (!isXyTupleArray(series)) return series\n return series.filter((_, dataIndex) =>\n locked.has(`${seriesIndex}:${dataIndex}`),\n )\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scatterplot.js","names":["{ count = 24 }: ScatterplotSkeletonProps","count = 24","Box","styles.grid","dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))","styles.container","(_, i) => {\n const top = 10 + ((i * 37) % 80)\n const left = 5 + ((i * 53) % 90)\n const size = 8 + ((i * 7) % 6)\n return { top: `${top}%`, left: `${left}%`, size }\n }","(d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n )","<Box sx={styles.grid}>\n {dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))}\n </Box>","<Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>","(i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n )","<Box sx={styles.container}>\n <Box sx={styles.grid}>\n {dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))}\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>","i","_c","ZOOM_LAYOUT","buildCsvDownloadItem","withPngItem","resolveThemeColor","buildGridConfig","createTooltipPositioner","buildLegendConfig","niceNum","buildAxisLabelStyle","echarts","scatterplotOptions","theme","buildScatterTooltipFormatter","xFormatter","yFormatter","createScatterplotOptionFactory","options","series","symbolSize","selection","selectionSet","option","structural","optionsOverride","mergeOptions","seriesArr","data","hasLegend","baseLegend","baseGrid","baseTooltip","baseXAxis","baseYAxis","reactiveFormatter","ctx","computeScatterBounds","makeDimColor","base","params","key","seriesIdx","dataZoomLayout","layoutDataZoomForScatter","hasXSlider","hasYSlider","fallbackBottom","baseBottom","gridBottom","fallbackRight","gridRight","s","overrideColor","i","niceMinX","niceMaxX","niceMinY","niceMaxY","buildReactiveScatterTooltipFormatter","dataZoom","entry","dz","reactiveYFormatter","createTooltipFormatter","value","item","x","y","formattedX","formattedY","marker","seriesName","structuralFormatter","minX","maxX","minY","maxY","point","styles","spacing","dotSx","size","ScatterplotSkeleton","$","c","t1","t0","count","T0","T1","t2","t3","t4","dots","_temp","Box","_temp2","t5","t6","_temp3","t7","Skeleton","i_1","d","i_0","top","left","createScatterplotDownloadConfig","args","rows","toRelativeScatterplotData","input","isXyTupleArray","total","acc","v","filterScatterplotByLockedPoints","lockedItems","locked","seriesIndex","dataIndex"],"sources":["../../src/widgets/scatterplot/options.ts","../../src/widgets/scatterplot/skeleton.tsx","../../src/widgets/scatterplot/download.ts","../../src/widgets/scatterplot/transforms.ts"],"sourcesContent":["import type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildAxisLabelStyle,\n buildGridConfig,\n buildLegendConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../utils/option-builders'\nimport { ZOOM_LAYOUT } from '../actions/zoom-toggle'\nimport type { OptionFactory } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport type {\n ScatterplotEChartsOption,\n ScatterplotOptionFactoryInput,\n ScatterplotOptionsInput,\n ScatterplotWidgetData,\n} from './types'\n\n/**\n * Builds the **structural** ECharts option for a scatterplot widget — both\n * axes `type: 'value'` (not category, unlike Bar/Histogram), grid, tooltip\n * triggered per-item rather than per-axis. Intentionally data-agnostic: no\n * series, no dataset, no `legend.show` (those depend on data and are added\n * by {@link createScatterplotOptionFactory}).\n *\n * Styling parity with v1: dark themed tooltip via `createTooltipFormatter`\n * + `createTooltipPositioner`, `buildGridConfig`-based grid, polished\n * axisLine/Tick/splitLine, `overlineDelicate` axis labels, structural\n * legend wired via `buildLegendConfig` (toggled by the merger), and the\n * CARTO `qualitative.bold + secondary` palette — same pattern bar /\n * histogram / pie already use. {@link createScatterplotOptionFactory}\n * wraps this builder in its structural-phase branch.\n */\nexport function scatterplotOptions({\n theme,\n xFormatter,\n yFormatter,\n}: ScatterplotOptionsInput): ScatterplotEChartsOption {\n return {\n grid: {\n left: parseInt(theme.spacing(1)),\n top: parseInt(theme.spacing(3)),\n right: parseInt(theme.spacing(1)),\n // Default: no legend. Merger bumps `bottom` when there are >1 series.\n ...buildGridConfig(false, theme),\n containLabel: true,\n },\n tooltip: {\n // Per-point trigger — different from Bar's 'axis' trigger because\n // points don't share an x-coordinate.\n trigger: 'item',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n position: createTooltipPositioner(theme),\n formatter: buildScatterTooltipFormatter(xFormatter, yFormatter),\n },\n // Legend styling baked here; `show` is toggled by the merger based on\n // series count.\n legend: {\n ...buildLegendConfig({ hasLegend: false, labelFormatter: undefined }),\n },\n axisPointer: { lineStyle: { color: theme.palette.grey[400] } },\n color: [\n theme.palette.secondary.main,\n ...Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n ],\n xAxis: {\n type: 'value',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n margin: parseInt(theme.spacing(1)),\n hideOverlap: true,\n showMinLabel: true,\n showMaxLabel: true,\n ...(xFormatter && { formatter: xFormatter }),\n },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n },\n yAxis: {\n type: 'value',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n margin: parseInt(theme.spacing(1)),\n hideOverlap: true,\n showMinLabel: true,\n showMaxLabel: true,\n ...(yFormatter && { formatter: yFormatter }),\n },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n },\n } as ScatterplotEChartsOption\n}\n\n/**\n * Returns the scatterplot widget's {@link OptionFactory} — one closure\n * that owns BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link scatterplotOptions}, optionally merging\n * the consumer-supplied `optionsOverride`. Called once by Provider to\n * seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`ScatterplotWidgetData`) into the option via the dataset API. Each\n * series's `[x, y]` tuples land in `dataset[i].source` as 2-column\n * rows; the series uses positional encoding (`encode: { x: 0, y: 1 }`)\n * and `type: 'scatter'`. niceMin/niceMax are computed over both axes\n * so the chart frames data on rounded extents and numeric jitters\n * don't shift gridlines per render. Reactive `ctx.formatter` (driven\n * by RelativeData) re-derives the y-axis label and tooltip at fusion\n * time; `xFormatter` stays baked at structural-build time (relative\n * is a values-axis concept; x is coordinate-space).\n *\n * When `ZoomToggle` installs a `dataZoom` slider, grid bottom is\n * extended and the slider is positioned above the legend row (if any) —\n * same layout dance bar / histogram / timeseries do.\n */\nexport function createScatterplotOptionFactory(\n options: ScatterplotOptionFactoryInput,\n): OptionFactory {\n const { theme, xFormatter, yFormatter, optionsOverride } = options\n const series = options.series\n const symbolSize = options.symbolSize ?? 8\n const selection = options.selection\n const selectionSet =\n selection && selection.length > 0 ? new Set<string>(selection) : null\n return (option, data, ctx) => {\n if (option == null) {\n const structural = scatterplotOptions({ theme, xFormatter, yFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as ScatterplotWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n const hasLegend = seriesArr.length > 1\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseXAxis =\n typeof option.xAxis === 'object' && !Array.isArray(option.xAxis)\n ? option.xAxis\n : {}\n const baseYAxis =\n typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)\n ? option.yAxis\n : {}\n\n const reactiveFormatter = ctx?.formatter\n\n const { niceMinX, niceMaxX, niceMinY, niceMaxY } =\n computeScatterBounds(seriesArr)\n\n // Dim non-selected points via `series.itemStyle.color`. The selection\n // key is `${seriesIndex}:${dataIndex}`; we read those off the params\n // ECharts hands to the callback per-data.\n //\n // We *always* emit `itemStyle.color` (a passthrough when nothing is\n // selected), not conditionally — dropping the key between renders\n // would let ECharts' default merge keep the previous callback alive\n // and items would stay dimmed forever after an external clear.\n // Keeping the key always-present means a plain setOption merge swaps\n // the callback in place, no `replaceMerge` and no entry-animation\n // flash on selection on/off.\n const makeDimColor =\n (seriesIdx: number) => (params: CallbackDataParams) => {\n const base = params.color as string\n if (!selectionSet) return base\n const key = `${seriesIdx}:${params.dataIndex}`\n return selectionSet.has(key)\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n }\n\n // Zoom slider layout: when ZoomToggle has installed `dataZoom`,\n // reserve grid space and lift any x-slider above the legend (if\n // present). Scatter supports 2D zoom via `axes: ['x', 'y']` — in\n // that case there's also a vertical y-slider on the right edge, so\n // we reserve `grid.right` separately. Inside-only dataZoom entries\n // (no `type: 'slider'`) take no grid space.\n const dataZoomLayout = layoutDataZoomForScatter(option.dataZoom, hasLegend)\n const hasXSlider = dataZoomLayout?.hasXSlider ?? false\n const hasYSlider = dataZoomLayout?.hasYSlider ?? false\n const fallbackBottom =\n typeof baseGrid.bottom === 'number' ? baseGrid.bottom : 24\n const baseBottom = hasLegend ? 56 : fallbackBottom\n const gridBottom = hasXSlider\n ? baseBottom + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : baseBottom\n const fallbackRight =\n typeof baseGrid.right === 'number' ? baseGrid.right : 8\n const gridRight = hasYSlider\n ? fallbackRight + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : fallbackRight\n\n return {\n ...option,\n // ECharts dataset.source wants a mutable [number, number][] shape; we\n // hold readonly tuples internally, so cast at the boundary.\n dataset: seriesArr.map((s) => ({ source: s as unknown as number[][] })),\n series: seriesArr.map((_, i) => {\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n type: 'scatter' as const,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n encode: { x: 0, y: 1 },\n symbolSize,\n emphasis: { focus: 'series' },\n itemStyle: { color: makeDimColor(i) },\n ...(overrideColor ? { color: overrideColor } : {}),\n }\n }),\n legend: { ...baseLegend, show: hasLegend },\n grid: { ...baseGrid, bottom: gridBottom, right: gridRight },\n ...(dataZoomLayout ? { dataZoom: dataZoomLayout.entries } : {}),\n xAxis: {\n ...baseXAxis,\n min: niceMinX,\n max: niceMaxX,\n } as EChartsOption['xAxis'],\n yAxis: {\n ...baseYAxis,\n min: niceMinY,\n max: niceMaxY,\n axisLabel: {\n ...((baseYAxis as { axisLabel?: object }).axisLabel ?? {}),\n // Re-derive the y-axis formatter at fusion time so RelativeData's\n // percent formatter (written to `state.formatter`) flows through\n // without rebuilding the structural option. Falls back to the\n // structural `yFormatter` already baked in `baseYAxis.axisLabel`\n // (which `String(value)` if neither is set).\n ...(reactiveFormatter ? { formatter: reactiveFormatter } : {}),\n },\n } as EChartsOption['yAxis'],\n tooltip: {\n ...baseTooltip,\n // Rebuild the tooltip formatter so the live y-axis formatter is\n // applied to the y-coordinate in the (x, y) label. xFormatter is\n // structural — relative is a values-axis concept, so xFormatter\n // doesn't change under RelativeData.\n formatter: buildReactiveScatterTooltipFormatter(\n (baseTooltip as { formatter?: unknown }).formatter,\n reactiveFormatter,\n xFormatter,\n ),\n },\n } as EChartsOption\n }\n}\n\n/**\n * Lay out the `dataZoom` array for the scatter chart:\n * - Detect whether any horizontal (x-axis) slider is present — if so\n * lift it above the legend row when a legend is shown.\n * - Detect whether any vertical (y-axis) slider is present — the\n * caller reserves `grid.right` so the slider doesn't overlap the\n * plot area.\n *\n * Returns `null` when there's no `dataZoom` so callers can skip the\n * layout adjustment entirely. An entry is considered an \"x-slider\" if\n * it has `xAxisIndex` set (or no axis index — defaults to x in ECharts).\n * A \"y-slider\" has `yAxisIndex` set.\n */\nfunction layoutDataZoomForScatter(\n dataZoom: unknown,\n hasLegend: boolean,\n): { entries: unknown[]; hasXSlider: boolean; hasYSlider: boolean } | null {\n if (!Array.isArray(dataZoom) || dataZoom.length === 0) return null\n let hasXSlider = false\n let hasYSlider = false\n const entries = dataZoom.map((entry: unknown) => {\n if (entry == null || typeof entry !== 'object') return entry\n const dz = entry as {\n type?: string\n xAxisIndex?: unknown\n yAxisIndex?: unknown\n bottom?: number\n }\n if (dz.type !== 'slider') return dz\n const targetsY = dz.yAxisIndex !== undefined\n if (targetsY) {\n hasYSlider = true\n return dz\n }\n // Either explicit x or defaulted (ECharts defaults sliders to xAxis\n // when no axis index is provided).\n hasXSlider = true\n if (hasLegend) {\n return { ...dz, bottom: ZOOM_LAYOUT.sliderBottomWithLegend }\n }\n return dz\n })\n return { entries, hasXSlider, hasYSlider }\n}\n\n/**\n * If a reactive (store-driven) y formatter is provided, re-build the\n * scatter tooltip formatter using it. Otherwise, return the structural\n * formatter unchanged so the original `xFormatter` / `yFormatter`\n * baked into `scatterplotOptions` still applies. The structural\n * formatter has stable identity per `scatterplotOptions` call, so this\n * path doesn't churn ECharts on every render.\n */\nfunction buildReactiveScatterTooltipFormatter(\n structuralFormatter: unknown,\n reactiveYFormatter: ((value: number) => string) | undefined,\n xFormatter: ((value: number) => string) | undefined,\n) {\n if (!reactiveYFormatter) return structuralFormatter\n return createTooltipFormatter((item) => {\n const value = item.value as readonly [number, number] | undefined\n const x = value?.[0]\n const y = value?.[1]\n const formattedX =\n typeof x === 'number' ? (xFormatter ? xFormatter(x) : String(x)) : ''\n const formattedY =\n typeof y === 'number' ? reactiveYFormatter(y) : String(y ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n return {\n name: `(${formattedX}, ${formattedY})`,\n seriesName,\n marker,\n value: '',\n }\n })\n}\n\nfunction buildScatterTooltipFormatter(\n xFormatter: ((value: number) => string) | undefined,\n yFormatter: ((value: number) => string) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const value = item.value as readonly [number, number] | undefined\n const x = value?.[0]\n const y = value?.[1]\n const formattedX =\n typeof x === 'number' ? (xFormatter ? xFormatter(x) : String(x)) : ''\n const formattedY =\n typeof y === 'number' ? (yFormatter ? yFormatter(y) : String(y)) : ''\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n return {\n name: `(${formattedX}, ${formattedY})`,\n seriesName,\n marker,\n value: '',\n }\n })\n}\n\nfunction computeScatterBounds(seriesArr: ScatterplotWidgetData): {\n niceMinX: number\n niceMaxX: number\n niceMinY: number\n niceMaxY: number\n} {\n let minX = Infinity\n let maxX = -Infinity\n let minY = Infinity\n let maxY = -Infinity\n for (const series of seriesArr) {\n for (const point of series) {\n const x = point?.[0]\n const y = point?.[1]\n if (typeof x === 'number' && Number.isFinite(x)) {\n if (x < minX) minX = x\n if (x > maxX) maxX = x\n }\n if (typeof y === 'number' && Number.isFinite(y)) {\n if (y < minY) minY = y\n if (y > maxY) maxY = y\n }\n }\n }\n // Mirror bar's `computeNiceBounds`: clamp min to 0 when data is\n // non-negative (gridline reads cleanly from zero), apply `niceNum` to\n // negative mins, and floor max=0 to 1 so the chart always has range.\n // Scatter can have free coordinates so we apply this per-axis.\n const niceMaxX = Number.isFinite(maxX) ? (maxX <= 0 ? 1 : niceNum(maxX)) : 1\n const niceMaxY = Number.isFinite(maxY) ? (maxY <= 0 ? 1 : niceNum(maxY)) : 1\n const niceMinX = Number.isFinite(minX) ? (minX < 0 ? niceNum(minX) : 0) : 0\n const niceMinY = Number.isFinite(minY) ? (minY < 0 ? niceNum(minY) : 0) : 0\n return { niceMinX, niceMaxX, niceMinY, niceMaxY }\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst styles = {\n container: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n flexDirection: 'column',\n gap: ({ spacing }) => spacing(1),\n height: ({ spacing }) => spacing(38),\n },\n grid: {\n position: 'relative',\n flex: '1 1 auto',\n width: '100%',\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n height: ({ spacing }) => spacing(5),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\n// `sx` callback that needs runtime args — extracted at module scope so the\n// `styles` object can satisfy `Record<string, SxProps<Theme>>` cleanly\n// (function `sx`-with-args isn't assignable to plain `SxProps<Theme>`).\nconst dotSx = (top: string, left: string, size: number): SxProps<Theme> => ({\n position: 'absolute',\n top,\n left,\n width: size,\n height: size,\n borderRadius: '50%',\n})\n\nexport interface ScatterplotSkeletonProps {\n /** Number of dots to render. */\n count?: number\n}\n\n/**\n * Loading state for the Scatterplot widget. Mirrors a scatter chart's\n * silhouette — a deterministic spread of small circular dots in the plot\n * area plus a legend stub — so the skeleton reads as \"a scatter chart\"\n * rather than a generic list. Matches bar/histogram/pie skeleton structure\n * (grid stub + legend stub in a flex column).\n */\nexport function ScatterplotSkeleton({ count = 24 }: ScatterplotSkeletonProps) {\n // Deterministic pseudo-scatter positions so the skeleton doesn't flicker.\n const dots = Array.from({ length: count }, (_, i) => {\n const top = 10 + ((i * 37) % 80)\n const left = 5 + ((i * 53) % 90)\n const size = 8 + ((i * 7) % 6)\n return { top: `${top}%`, left: `${left}%`, size }\n })\n return (\n <Box sx={styles.container}>\n <Box sx={styles.grid}>\n {dots.map((d, i) => (\n <Skeleton\n key={`dot-${i}`}\n variant='circular'\n sx={dotSx(d.top, d.left, d.size)}\n />\n ))}\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildCsvDownloadItem,\n withPngItem,\n type DownloadItem,\n type WidgetDownloadArgs,\n} from '../actions/download'\nimport type { ScatterplotWidgetData } from './types'\n\n/**\n * Download menu items for the Scatterplot widget. Always includes a CSV item\n * with `series, x, y` columns (one row per point). When `getCaptureEl` is\n * given, prepends a PNG item that rasterises the captured element via\n * `html2canvas`.\n */\nexport function createScatterplotDownloadConfig(\n args: WidgetDownloadArgs<ScatterplotWidgetData> & {\n seriesNames?: readonly string[]\n },\n): DownloadItem[] {\n return withPngItem(\n args,\n buildCsvDownloadItem({\n filename: args.filename,\n getRows: (ctx) => {\n const data = args.getData(ctx)\n const rows: unknown[][] = [['series', 'x', 'y']]\n for (const [i, series] of data.entries()) {\n const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`\n for (const [x, y] of series) {\n rows.push([seriesName, x, y])\n }\n }\n return rows\n },\n }),\n )\n}\n","/**\n * Scatterplot-specific `RelativeData` transform. Scatter data is\n * `[number, number][]` — each series is a list of `[x, y]` tuples.\n * Relative is a values-axis concept, so this rewrites `y` to its\n * share of the series's total `y` (0–100) and leaves `x` raw — x is\n * coordinate space, not part of the cohort total.\n *\n * Pass to `<Widget.RelativeData transform={toRelativeScatterplotData} />`.\n *\n * The denominator is the sum of |y| across the series so mixed-sign\n * y values produce sane signed shares-of-magnitude. A series whose\n * total y-magnitude is zero (all-zero or empty input) is returned\n * unchanged so a stalled or empty data set doesn't show misleading 0%\n * values. Inputs whose shape isn't `[number, number][]` fall through\n * untouched.\n */\nexport const toRelativeScatterplotData = (input: unknown): unknown => {\n if (!Array.isArray(input)) return input\n return input.map((series: unknown): unknown => {\n if (!isXyTupleArray(series)) return series\n const total = series.reduce((acc, [, y]) => acc + Math.abs(y), 0)\n if (total <= 0) return series\n return series.map(([x, y]) => [x, (y / total) * 100] as [number, number])\n })\n}\n\nfunction isXyTupleArray(v: unknown): v is [number, number][] {\n if (!Array.isArray(v)) return false\n return v.every(\n (item) =>\n Array.isArray(item) &&\n item.length === 2 &&\n typeof item[0] === 'number' &&\n Number.isFinite(item[0]) &&\n typeof item[1] === 'number' &&\n Number.isFinite(item[1]),\n )\n}\n\n/**\n * `LockSelection` filter for Scatterplot. Data is `[number, number][][]` —\n * points have no `name`, so selection (and locking) is keyed on a synthetic\n * `${seriesIndex}:${dataIndex}` string (same format `useChartSelection`'s\n * `keyFromClick`/`keysFromBrush` produce for this widget). Unlike Histogram,\n * a point's position carries no external meaning (no `ticks` to desync from),\n * so non-locked points are removed outright rather than zeroed.\n */\nexport function filterScatterplotByLockedPoints(\n input: unknown,\n lockedItems: readonly (string | number)[],\n): unknown {\n if (lockedItems.length === 0) return input\n if (!Array.isArray(input)) return input\n const locked = new Set<string>(lockedItems.map(String))\n return input.map((series: unknown, seriesIndex: number): unknown => {\n if (!isXyTupleArray(series)) return series\n return series.filter((_, dataIndex) =>\n locked.has(`${seriesIndex}:${dataIndex}`),\n )\n })\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"timeseries.js","names":["<Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>","<Box sx={styles.container}>\n <Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>","(i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n )"],"sources":["../../src/widgets/timeseries/options.ts","../../src/widgets/timeseries/skeleton.tsx","../../src/widgets/timeseries/download.ts","../../src/widgets/timeseries/transforms.ts"],"sourcesContent":["import type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildAxisLabelStyle,\n buildGridConfig,\n buildLegendConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../utils/option-builders'\nimport { ZOOM_LAYOUT } from '../actions/zoom-toggle'\nimport type { OptionFactory } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport { positionDataZoomForLegend } from '../utils/data-zoom-layout'\nimport type {\n TimeseriesEChartsOption,\n TimeseriesOptionFactoryInput,\n TimeseriesOptionsInput,\n TimeseriesWidgetData,\n} from './types'\n\n/**\n * Builds the **structural** ECharts option for a timeseries widget —\n * time x-axis, value y-axis, themed tooltip, themed legend, CARTO color\n * palette. Mirrors {@link import('../bar/options').barOptions} so all\n * four ECharts widgets share v1 look-and-feel.\n *\n * Intentional deviations from bar (timeseries-specific):\n * - **X-axis is `type: 'time'`** (not 'category'). ECharts handles\n * uneven sample spacing and zoom-level-aware label formatting.\n * `labelFormatter` is wrapped so the consumer sees a `Date`, not\n * a numeric timestamp.\n * - **Tooltip body reads `{ name, value }` rows**, same as bar, but\n * the `name` may arrive as `Date | number | string`. The\n * `labelFormatter` receives a `Date` regardless.\n *\n * Intentionally data-agnostic: no series, no dataset, no `legend.show`\n * (those depend on data and are added by the option factory's merge\n * phase via {@link createTimeseriesOptionFactory}).\n */\nexport function timeseriesOptions({\n theme,\n formatter,\n labelFormatter,\n}: TimeseriesOptionsInput): TimeseriesEChartsOption {\n // Closure shared between yAxis min/max callbacks and the label formatter,\n // so only the rounded extents are labelled (matches v1 + bar).\n let niceMin = 0\n let niceMax = 1\n\n return {\n grid: {\n left: parseInt(theme.spacing(1)),\n top: parseInt(theme.spacing(3)),\n right: parseInt(theme.spacing(1)),\n // Default: no legend. Merger bumps this when there are >1 series.\n ...buildGridConfig(false, theme),\n containLabel: true,\n },\n tooltip: {\n trigger: 'axis',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n axisPointer: { type: 'line' },\n position: createTooltipPositioner(theme),\n formatter: buildTimeseriesTooltipFormatter(formatter, labelFormatter),\n },\n // Legend styling baked here; `show` is toggled by the merger based on\n // series count.\n legend: {\n ...buildLegendConfig({ hasLegend: false }),\n },\n axisPointer: { lineStyle: { color: theme.palette.grey[400] } },\n color: [\n theme.palette.secondary.main,\n ...Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n ],\n xAxis: {\n type: 'time',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],\n margin: 0,\n hideOverlap: true,\n ...(labelFormatter && {\n formatter: (value: number) => labelFormatter(new Date(value)),\n }),\n },\n },\n yAxis: {\n type: 'value',\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLine: { show: false },\n axisTick: { show: false },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n margin: parseInt(theme.spacing(1)),\n show: true,\n showMaxLabel: true,\n showMinLabel: true,\n verticalAlign: 'bottom',\n inside: true,\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return formatter ? formatter(value) : String(value)\n },\n },\n },\n } as TimeseriesEChartsOption\n}\n\n/**\n * Returns the timeseries widget's {@link OptionFactory} — one closure\n * that owns BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link timeseriesOptions}, optionally merging\n * the consumer-supplied `optionsOverride`. Called once by Provider to\n * seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`TimeseriesWidgetData`) into the option via the dataset API: one\n * dataset per series, each series referencing its dataset by index,\n * encoded by `name` (x — time) and `value` (y). Mirrors {@link import('../bar/options').createBarOptionFactory}:\n * series-template merge for `addStack`, reactive formatters from\n * `ctx`, `niceNum`-rounded y-axis bounds at fusion time, and\n * `positionDataZoomForLegend` layout for ZoomToggle sliders.\n */\nexport function createTimeseriesOptionFactory(\n options: TimeseriesOptionFactoryInput,\n): OptionFactory {\n const { theme, formatter, labelFormatter, optionsOverride } = options\n const series = options.series\n const smooth = options.smooth ?? true\n const area = options.area ?? false\n const selection = options.selection\n const selectionSet =\n selection && selection.length > 0\n ? new Set<string | number>(selection)\n : null\n // `name` may be Date | number | string. Normalize to the same type the\n // selection is keyed on (Date → ms) so Set lookups match.\n const normalizeName = (n: Date | number | string): string | number =>\n n instanceof Date ? n.getTime() : n\n return (option, data, ctx) => {\n if (option == null) {\n const structural = timeseriesOptions({ theme, formatter, labelFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as TimeseriesWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n const hasLegend = seriesArr.length > 1\n const seriesTemplates = Array.isArray(option.series) ? option.series : []\n const broadcastTemplate = seriesTemplates[0] ?? {}\n const baseYAxis =\n typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)\n ? option.yAxis\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n\n // Reactive (live store) formatter from ctx — distinct from the\n // closure-time `formatter` captured for the structural-build branch\n // above. RelativeData can install a percent formatter on the store\n // after the factory was constructed; the merge phase reads `ctx` to\n // pick that up. `labelFormatter` (Date → string) is structural-only —\n // not relativizable — so the merge branch reads the closure-time value.\n const liveFormatter = ctx?.formatter\n\n // Closure shared between the yAxis min/max callbacks and the label\n // formatter, so only the rounded extents are labelled (matches v1 +\n // bar). Delegating the extent to ECharts (rather than precomputing\n // scalars from the raw data) keeps stacked lines inside the plot: when\n // StackToggle marks the series, ECharts feeds the *post-stack* extent\n // to these callbacks, so `niceNum` rounds the stacked total.\n let niceMin = 0\n let niceMax = 1\n\n // Zoom slider layout: when ZoomToggle has installed `dataZoom`, push the\n // slider above the legend (if any) and reserve room in the grid below.\n const dataZoomLayout = positionDataZoomForLegend(option.dataZoom, hasLegend)\n const fallbackBottom =\n typeof baseGrid.bottom === 'number' ? baseGrid.bottom : 24\n const baseBottom = hasLegend ? 56 : fallbackBottom\n const gridBottom = dataZoomLayout\n ? baseBottom + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : baseBottom\n\n // Dim non-selected points via `series.itemStyle.color`. Per-row\n // `itemStyle` on dataset object-rows is silently ignored when\n // `series.encode` is in play.\n //\n // We *always* emit `itemStyle.color` (a passthrough when nothing is\n // selected), not conditionally — dropping the key between renders\n // would let ECharts' default merge keep the previous callback alive\n // and points would stay dimmed forever after an external clear.\n // Always emitting lets normal merge swap the callback in place, no\n // `replaceMerge` and no entry-animation flash on selection on/off.\n const dimItemStyle = {\n color: (params: CallbackDataParams) => {\n const datum = params.value as\n { name?: Date | number | string } | undefined\n const raw = datum?.name\n const base = params.color as string\n if (!selectionSet || raw == null) return base\n return selectionSet.has(normalizeName(raw))\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n },\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n series: seriesArr.map((_, i) => {\n const template =\n (seriesTemplates[i] as object | undefined) ??\n (broadcastTemplate as object)\n // For line series, set BOTH `series[i].color` (legend swatch +\n // markers) AND `series[i].lineStyle.color` (the line itself) so\n // the override paints everywhere a series has a colour slot.\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n ...(typeof template === 'object' ? template : {}),\n type: 'line' as const,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n encode: { x: 'name', y: 'value' },\n smooth,\n // When a selection is active, surface markers so the per-point\n // color callback has something to dim — a continuous line would\n // hide the visual selection feedback.\n showSymbol: selectionSet != null,\n ...(area ? { areaStyle: {} } : {}),\n emphasis: { focus: 'series' },\n itemStyle: dimItemStyle,\n ...(overrideColor\n ? { color: overrideColor, lineStyle: { color: overrideColor } }\n : {}),\n }\n }),\n legend: { ...baseLegend, show: hasLegend },\n grid: { ...baseGrid, bottom: gridBottom },\n ...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),\n yAxis: {\n ...baseYAxis,\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLabel: {\n ...((baseYAxis as { axisLabel?: object }).axisLabel ?? {}),\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return liveFormatter ? liveFormatter(value) : String(value)\n },\n },\n } as EChartsOption['yAxis'],\n tooltip: {\n ...baseTooltip,\n formatter: buildTimeseriesTooltipFormatter(\n liveFormatter,\n labelFormatter,\n ),\n },\n } as EChartsOption\n }\n}\n\n/**\n * Tooltip formatter for the timeseries `{ name, value }` row shape.\n * `name` arrives as `Date | number | string` (the time-axis stores the\n * raw value the consumer supplied). The consumer's `labelFormatter`\n * expects a `Date`, so we coerce non-Date values via `new Date(...)`\n * before invoking it.\n */\nfunction buildTimeseriesTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: Date) => string) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const row = item.value as\n { name?: Date | number | string; value?: number } | undefined\n const raw = row?.value\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const rawName = row?.name ?? item.name\n const name =\n labelFormatter && rawName != null\n ? labelFormatter(rawName instanceof Date ? rawName : new Date(rawName))\n : (rawName ?? '')\n return { name: String(name), seriesName, marker, value: formattedValue }\n })\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst styles = {\n container: {\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'space-between',\n gap: ({ spacing }) => spacing(1),\n height: ({ spacing }) => spacing(38),\n },\n graph: {\n position: 'relative',\n flex: '1 1 auto',\n width: '100%',\n },\n segmentA: {\n position: 'absolute',\n left: 0,\n right: '70%',\n top: '60%',\n height: 4,\n },\n segmentB: {\n position: 'absolute',\n left: '25%',\n right: '40%',\n top: '35%',\n height: 4,\n },\n segmentC: {\n position: 'absolute',\n left: '55%',\n right: 0,\n top: '50%',\n height: 4,\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n height: ({ spacing }) => spacing(5),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\n/**\n * Loading state for the Timeseries widget. Mirrors a line chart's\n * silhouette — three thin horizontal segments at staggered offsets to\n * suggest a moving line, plus a 2-dot legend stub. Sibling-consistent\n * with Bar / Histogram / Scatter skeletons (column-flex container with\n * legend strip below).\n */\nexport function TimeseriesSkeleton() {\n return (\n <Box sx={styles.container}>\n <Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildCsvDownloadItem,\n withPngItem,\n type DownloadItem,\n type WidgetDownloadArgs,\n} from '../actions/download'\nimport type { TimeseriesWidgetData } from './types'\n\n/**\n * Download menu items for the Timeseries widget. Always includes a CSV item\n * with `time, series_1, series_2, …` columns (one row per unique time across\n * all series; ISO-8601 strings for `Date`/numeric times). When\n * `getCaptureEl` is given, prepends a PNG item that rasterises the captured\n * element via `html2canvas`.\n */\nexport function createTimeseriesDownloadConfig(\n args: WidgetDownloadArgs<TimeseriesWidgetData> & {\n seriesNames?: readonly string[]\n },\n): DownloadItem[] {\n return withPngItem(\n args,\n buildCsvDownloadItem({\n filename: args.filename,\n getRows: (ctx) => {\n const data = args.getData(ctx)\n const seriesCount = data.length\n\n // Collect every unique time, preserving insertion order.\n const timeKeys: (Date | number | string)[] = []\n const seenKeys = new Set<string>()\n for (const series of data) {\n for (const point of series) {\n const key = String(point.name)\n if (!seenKeys.has(key)) {\n seenKeys.add(key)\n timeKeys.push(point.name)\n }\n }\n }\n\n // Build a quick lookup per series for O(rows × series) emit.\n const lookups = data.map(\n (series) => new Map(series.map((p) => [String(p.name), p.value])),\n )\n\n const header: unknown[] = ['time']\n for (let i = 0; i < seriesCount; i++) {\n header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)\n }\n const rows: unknown[][] = [header]\n for (const key of timeKeys) {\n const row: unknown[] = [formatTime(key)]\n const lookupKey = String(key)\n for (const lookup of lookups) row.push(lookup.get(lookupKey) ?? '')\n rows.push(row)\n }\n\n return rows\n },\n }),\n )\n}\n\nfunction formatTime(v: Date | number | string): string {\n if (v instanceof Date) return v.toISOString()\n if (typeof v === 'number') return new Date(v).toISOString()\n return v\n}\n","/**\n * `LockSelection` filter for Timeseries. Data is `{name, value}[][]` like\n * Bar/Pie/Category, but `name` may arrive as `Date | number | string` (the\n * time axis stores a `Date`). `lockedItems` is snapshotted from `selection`,\n * whose keys are already normalized to `Date → ms` (see `options.ts`'s\n * `normalizeName`) — so a raw `Date` name must be normalized the same way\n * before the membership check, or every point would compare a `Date` object\n * against a `number` and silently fail to match, filtering everything out.\n */\nexport function filterTimeseriesByLockedItems(\n input: unknown,\n lockedItems: readonly (string | number)[],\n): unknown {\n if (lockedItems.length === 0) return input\n if (!Array.isArray(input)) return input\n const locked = new Set<string | number>(lockedItems)\n return input.map((series: unknown): unknown => {\n if (!Array.isArray(series)) return series\n return series.filter((item: unknown) => {\n if (item == null || typeof item !== 'object' || !('name' in item)) {\n return false\n }\n const raw = (item as { name: unknown }).name\n const name = raw instanceof Date ? raw.getTime() : raw\n return (\n (typeof name === 'string' || typeof name === 'number') &&\n locked.has(name)\n )\n })\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"timeseries.js","names":["<Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>","<Box sx={styles.container}>\n <Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>","(i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n )","_c","ZOOM_LAYOUT","buildCsvDownloadItem","withPngItem","positionDataZoomForLegend","mergeOptions","resolveThemeColor","createTooltipFormatter","buildGridConfig","createTooltipPositioner","buildLegendConfig","niceNum","buildAxisLabelStyle","echarts","timeseriesOptions","niceMin","niceMax","theme","buildTimeseriesTooltipFormatter","formatter","labelFormatter","value","extent","createTimeseriesOptionFactory","options","series","smooth","area","selection","selectionSet","normalizeName","n","option","structural","optionsOverride","seriesArr","data","hasLegend","seriesTemplates","broadcastTemplate","baseYAxis","baseGrid","baseTooltip","baseLegend","liveFormatter","ctx","dataZoomLayout","fallbackBottom","baseBottom","gridBottom","dimItemStyle","raw","params","base","s","template","i","overrideColor","row","item","formattedValue","marker","seriesName","rawName","name","styles","spacing","TimeseriesSkeleton","$","c","t0","Box","Skeleton","t1","_temp","createTimeseriesDownloadConfig","args","seriesCount","timeKeys","seenKeys","point","key","lookups","p","header","rows","formatTime","lookupKey","lookup","v","filterTimeseriesByLockedItems","lockedItems","input","locked"],"sources":["../../src/widgets/timeseries/options.ts","../../src/widgets/timeseries/skeleton.tsx","../../src/widgets/timeseries/download.ts","../../src/widgets/timeseries/transforms.ts"],"sourcesContent":["import type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildAxisLabelStyle,\n buildGridConfig,\n buildLegendConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../utils/option-builders'\nimport { ZOOM_LAYOUT } from '../actions/zoom-toggle'\nimport type { OptionFactory } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport { positionDataZoomForLegend } from '../utils/data-zoom-layout'\nimport type {\n TimeseriesEChartsOption,\n TimeseriesOptionFactoryInput,\n TimeseriesOptionsInput,\n TimeseriesWidgetData,\n} from './types'\n\n/**\n * Builds the **structural** ECharts option for a timeseries widget —\n * time x-axis, value y-axis, themed tooltip, themed legend, CARTO color\n * palette. Mirrors {@link import('../bar/options').barOptions} so all\n * four ECharts widgets share v1 look-and-feel.\n *\n * Intentional deviations from bar (timeseries-specific):\n * - **X-axis is `type: 'time'`** (not 'category'). ECharts handles\n * uneven sample spacing and zoom-level-aware label formatting.\n * `labelFormatter` is wrapped so the consumer sees a `Date`, not\n * a numeric timestamp.\n * - **Tooltip body reads `{ name, value }` rows**, same as bar, but\n * the `name` may arrive as `Date | number | string`. The\n * `labelFormatter` receives a `Date` regardless.\n *\n * Intentionally data-agnostic: no series, no dataset, no `legend.show`\n * (those depend on data and are added by the option factory's merge\n * phase via {@link createTimeseriesOptionFactory}).\n */\nexport function timeseriesOptions({\n theme,\n formatter,\n labelFormatter,\n}: TimeseriesOptionsInput): TimeseriesEChartsOption {\n // Closure shared between yAxis min/max callbacks and the label formatter,\n // so only the rounded extents are labelled (matches v1 + bar).\n let niceMin = 0\n let niceMax = 1\n\n return {\n grid: {\n left: parseInt(theme.spacing(1)),\n top: parseInt(theme.spacing(3)),\n right: parseInt(theme.spacing(1)),\n // Default: no legend. Merger bumps this when there are >1 series.\n ...buildGridConfig(false, theme),\n containLabel: true,\n },\n tooltip: {\n trigger: 'axis',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n axisPointer: { type: 'line' },\n position: createTooltipPositioner(theme),\n formatter: buildTimeseriesTooltipFormatter(formatter, labelFormatter),\n },\n // Legend styling baked here; `show` is toggled by the merger based on\n // series count.\n legend: {\n ...buildLegendConfig({ hasLegend: false }),\n },\n axisPointer: { lineStyle: { color: theme.palette.grey[400] } },\n color: [\n theme.palette.secondary.main,\n ...Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n ],\n xAxis: {\n type: 'time',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],\n margin: 0,\n hideOverlap: true,\n ...(labelFormatter && {\n formatter: (value: number) => labelFormatter(new Date(value)),\n }),\n },\n },\n yAxis: {\n type: 'value',\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLine: { show: false },\n axisTick: { show: false },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n axisLabel: {\n ...buildAxisLabelStyle(theme),\n margin: parseInt(theme.spacing(1)),\n show: true,\n showMaxLabel: true,\n showMinLabel: true,\n verticalAlign: 'bottom',\n inside: true,\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return formatter ? formatter(value) : String(value)\n },\n },\n },\n } as TimeseriesEChartsOption\n}\n\n/**\n * Returns the timeseries widget's {@link OptionFactory} — one closure\n * that owns BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link timeseriesOptions}, optionally merging\n * the consumer-supplied `optionsOverride`. Called once by Provider to\n * seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`TimeseriesWidgetData`) into the option via the dataset API: one\n * dataset per series, each series referencing its dataset by index,\n * encoded by `name` (x — time) and `value` (y). Mirrors {@link import('../bar/options').createBarOptionFactory}:\n * series-template merge for `addStack`, reactive formatters from\n * `ctx`, `niceNum`-rounded y-axis bounds at fusion time, and\n * `positionDataZoomForLegend` layout for ZoomToggle sliders.\n */\nexport function createTimeseriesOptionFactory(\n options: TimeseriesOptionFactoryInput,\n): OptionFactory {\n const { theme, formatter, labelFormatter, optionsOverride } = options\n const series = options.series\n const smooth = options.smooth ?? true\n const area = options.area ?? false\n const selection = options.selection\n const selectionSet =\n selection && selection.length > 0\n ? new Set<string | number>(selection)\n : null\n // `name` may be Date | number | string. Normalize to the same type the\n // selection is keyed on (Date → ms) so Set lookups match.\n const normalizeName = (n: Date | number | string): string | number =>\n n instanceof Date ? n.getTime() : n\n return (option, data, ctx) => {\n if (option == null) {\n const structural = timeseriesOptions({ theme, formatter, labelFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as TimeseriesWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n const hasLegend = seriesArr.length > 1\n const seriesTemplates = Array.isArray(option.series) ? option.series : []\n const broadcastTemplate = seriesTemplates[0] ?? {}\n const baseYAxis =\n typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)\n ? option.yAxis\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n\n // Reactive (live store) formatter from ctx — distinct from the\n // closure-time `formatter` captured for the structural-build branch\n // above. RelativeData can install a percent formatter on the store\n // after the factory was constructed; the merge phase reads `ctx` to\n // pick that up. `labelFormatter` (Date → string) is structural-only —\n // not relativizable — so the merge branch reads the closure-time value.\n const liveFormatter = ctx?.formatter\n\n // Closure shared between the yAxis min/max callbacks and the label\n // formatter, so only the rounded extents are labelled (matches v1 +\n // bar). Delegating the extent to ECharts (rather than precomputing\n // scalars from the raw data) keeps stacked lines inside the plot: when\n // StackToggle marks the series, ECharts feeds the *post-stack* extent\n // to these callbacks, so `niceNum` rounds the stacked total.\n let niceMin = 0\n let niceMax = 1\n\n // Zoom slider layout: when ZoomToggle has installed `dataZoom`, push the\n // slider above the legend (if any) and reserve room in the grid below.\n const dataZoomLayout = positionDataZoomForLegend(option.dataZoom, hasLegend)\n const fallbackBottom =\n typeof baseGrid.bottom === 'number' ? baseGrid.bottom : 24\n const baseBottom = hasLegend ? 56 : fallbackBottom\n const gridBottom = dataZoomLayout\n ? baseBottom + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : baseBottom\n\n // Dim non-selected points via `series.itemStyle.color`. Per-row\n // `itemStyle` on dataset object-rows is silently ignored when\n // `series.encode` is in play.\n //\n // We *always* emit `itemStyle.color` (a passthrough when nothing is\n // selected), not conditionally — dropping the key between renders\n // would let ECharts' default merge keep the previous callback alive\n // and points would stay dimmed forever after an external clear.\n // Always emitting lets normal merge swap the callback in place, no\n // `replaceMerge` and no entry-animation flash on selection on/off.\n const dimItemStyle = {\n color: (params: CallbackDataParams) => {\n const datum = params.value as\n { name?: Date | number | string } | undefined\n const raw = datum?.name\n const base = params.color as string\n if (!selectionSet || raw == null) return base\n return selectionSet.has(normalizeName(raw))\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n },\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n series: seriesArr.map((_, i) => {\n const template =\n (seriesTemplates[i] as object | undefined) ??\n (broadcastTemplate as object)\n // For line series, set BOTH `series[i].color` (legend swatch +\n // markers) AND `series[i].lineStyle.color` (the line itself) so\n // the override paints everywhere a series has a colour slot.\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n ...(typeof template === 'object' ? template : {}),\n type: 'line' as const,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n encode: { x: 'name', y: 'value' },\n smooth,\n // When a selection is active, surface markers so the per-point\n // color callback has something to dim — a continuous line would\n // hide the visual selection feedback.\n showSymbol: selectionSet != null,\n ...(area ? { areaStyle: {} } : {}),\n emphasis: { focus: 'series' },\n itemStyle: dimItemStyle,\n ...(overrideColor\n ? { color: overrideColor, lineStyle: { color: overrideColor } }\n : {}),\n }\n }),\n legend: { ...baseLegend, show: hasLegend },\n grid: { ...baseGrid, bottom: gridBottom },\n ...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),\n yAxis: {\n ...baseYAxis,\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLabel: {\n ...((baseYAxis as { axisLabel?: object }).axisLabel ?? {}),\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return liveFormatter ? liveFormatter(value) : String(value)\n },\n },\n } as EChartsOption['yAxis'],\n tooltip: {\n ...baseTooltip,\n formatter: buildTimeseriesTooltipFormatter(\n liveFormatter,\n labelFormatter,\n ),\n },\n } as EChartsOption\n }\n}\n\n/**\n * Tooltip formatter for the timeseries `{ name, value }` row shape.\n * `name` arrives as `Date | number | string` (the time-axis stores the\n * raw value the consumer supplied). The consumer's `labelFormatter`\n * expects a `Date`, so we coerce non-Date values via `new Date(...)`\n * before invoking it.\n */\nfunction buildTimeseriesTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: Date) => string) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const row = item.value as\n { name?: Date | number | string; value?: number } | undefined\n const raw = row?.value\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const rawName = row?.name ?? item.name\n const name =\n labelFormatter && rawName != null\n ? labelFormatter(rawName instanceof Date ? rawName : new Date(rawName))\n : (rawName ?? '')\n return { name: String(name), seriesName, marker, value: formattedValue }\n })\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst styles = {\n container: {\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'space-between',\n gap: ({ spacing }) => spacing(1),\n height: ({ spacing }) => spacing(38),\n },\n graph: {\n position: 'relative',\n flex: '1 1 auto',\n width: '100%',\n },\n segmentA: {\n position: 'absolute',\n left: 0,\n right: '70%',\n top: '60%',\n height: 4,\n },\n segmentB: {\n position: 'absolute',\n left: '25%',\n right: '40%',\n top: '35%',\n height: 4,\n },\n segmentC: {\n position: 'absolute',\n left: '55%',\n right: 0,\n top: '50%',\n height: 4,\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n height: ({ spacing }) => spacing(5),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\n/**\n * Loading state for the Timeseries widget. Mirrors a line chart's\n * silhouette — three thin horizontal segments at staggered offsets to\n * suggest a moving line, plus a 2-dot legend stub. Sibling-consistent\n * with Bar / Histogram / Scatter skeletons (column-flex container with\n * legend strip below).\n */\nexport function TimeseriesSkeleton() {\n return (\n <Box sx={styles.container}>\n <Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildCsvDownloadItem,\n withPngItem,\n type DownloadItem,\n type WidgetDownloadArgs,\n} from '../actions/download'\nimport type { TimeseriesWidgetData } from './types'\n\n/**\n * Download menu items for the Timeseries widget. Always includes a CSV item\n * with `time, series_1, series_2, …` columns (one row per unique time across\n * all series; ISO-8601 strings for `Date`/numeric times). When\n * `getCaptureEl` is given, prepends a PNG item that rasterises the captured\n * element via `html2canvas`.\n */\nexport function createTimeseriesDownloadConfig(\n args: WidgetDownloadArgs<TimeseriesWidgetData> & {\n seriesNames?: readonly string[]\n },\n): DownloadItem[] {\n return withPngItem(\n args,\n buildCsvDownloadItem({\n filename: args.filename,\n getRows: (ctx) => {\n const data = args.getData(ctx)\n const seriesCount = data.length\n\n // Collect every unique time, preserving insertion order.\n const timeKeys: (Date | number | string)[] = []\n const seenKeys = new Set<string>()\n for (const series of data) {\n for (const point of series) {\n const key = String(point.name)\n if (!seenKeys.has(key)) {\n seenKeys.add(key)\n timeKeys.push(point.name)\n }\n }\n }\n\n // Build a quick lookup per series for O(rows × series) emit.\n const lookups = data.map(\n (series) => new Map(series.map((p) => [String(p.name), p.value])),\n )\n\n const header: unknown[] = ['time']\n for (let i = 0; i < seriesCount; i++) {\n header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)\n }\n const rows: unknown[][] = [header]\n for (const key of timeKeys) {\n const row: unknown[] = [formatTime(key)]\n const lookupKey = String(key)\n for (const lookup of lookups) row.push(lookup.get(lookupKey) ?? '')\n rows.push(row)\n }\n\n return rows\n },\n }),\n )\n}\n\nfunction formatTime(v: Date | number | string): string {\n if (v instanceof Date) return v.toISOString()\n if (typeof v === 'number') return new Date(v).toISOString()\n return v\n}\n","/**\n * `LockSelection` filter for Timeseries. Data is `{name, value}[][]` like\n * Bar/Pie/Category, but `name` may arrive as `Date | number | string` (the\n * time axis stores a `Date`). `lockedItems` is snapshotted from `selection`,\n * whose keys are already normalized to `Date → ms` (see `options.ts`'s\n * `normalizeName`) — so a raw `Date` name must be normalized the same way\n * before the membership check, or every point would compare a `Date` object\n * against a `number` and silently fail to match, filtering everything out.\n */\nexport function filterTimeseriesByLockedItems(\n input: unknown,\n lockedItems: readonly (string | number)[],\n): unknown {\n if (lockedItems.length === 0) return input\n if (!Array.isArray(input)) return input\n const locked = new Set<string | number>(lockedItems)\n return input.map((series: unknown): unknown => {\n if (!Array.isArray(series)) return series\n return series.filter((item: unknown) => {\n if (item == null || typeof item !== 'object' || !('name' in item)) {\n return false\n }\n const raw = (item as { name: unknown }).name\n const name = raw instanceof Date ? raw.getTime() : raw\n return (\n (typeof name === 'string' || typeof name === 'number') &&\n locked.has(name)\n )\n })\n })\n}\n"],"mappings":"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"}