@carto/ps-react-ui 6.3.0 → 6.5.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 (113) hide show
  1. package/dist/actions-D_PVFZaM.js +1272 -0
  2. package/dist/actions-D_PVFZaM.js.map +1 -0
  3. package/dist/{category-DULnoxvk.js → category-UIGvFN3D.js} +263 -246
  4. package/dist/category-UIGvFN3D.js.map +1 -0
  5. package/dist/chat.js +472 -470
  6. package/dist/chat.js.map +1 -1
  7. package/dist/{components-CTUpdQuj.js → components-DDJU4m0t.js} +8 -8
  8. package/dist/components-DDJU4m0t.js.map +1 -0
  9. package/dist/components.js +2 -2
  10. package/dist/{download-B4sykOFm.js → download-DIj1Iz0t.js} +2 -2
  11. package/dist/{download-B4sykOFm.js.map → download-DIj1Iz0t.js.map} +1 -1
  12. package/dist/{formula-DJS-JLpk.js → formula-CSHhAoyW.js} +4 -4
  13. package/dist/{formula-DJS-JLpk.js.map → formula-CSHhAoyW.js.map} +1 -1
  14. package/dist/{item-color-B7xkIpDZ.js → item-color-7o7pFBR2.js} +26 -18
  15. package/dist/item-color-7o7pFBR2.js.map +1 -0
  16. package/dist/markdown-B8lLR-NY.js +106 -0
  17. package/dist/markdown-B8lLR-NY.js.map +1 -0
  18. package/dist/scale-Bhx8HKEB.js +29 -0
  19. package/dist/scale-Bhx8HKEB.js.map +1 -0
  20. package/dist/{spread-CQwFIwuz.js → spread-BRfPPIep.js} +3 -3
  21. package/dist/{spread-CQwFIwuz.js.map → spread-BRfPPIep.js.map} +1 -1
  22. package/dist/style-DTLuIo84.js +87 -0
  23. package/dist/style-DTLuIo84.js.map +1 -0
  24. package/dist/{styles-BCEBm7xh.js → styles-yKcpfyNV.js} +5 -5
  25. package/dist/styles-yKcpfyNV.js.map +1 -0
  26. package/dist/{table-DEdW_fDj.js → table-Col_FRvb.js} +2 -2
  27. package/dist/{table-DEdW_fDj.js.map → table-Col_FRvb.js.map} +1 -1
  28. package/dist/theme/tailwind.css +373 -0
  29. package/dist/theme.js +123 -0
  30. package/dist/theme.js.map +1 -0
  31. package/dist/types/components/lasso-tool/styles.d.ts +4 -4
  32. package/dist/types/theme/create-carto-theme.d.ts +7 -0
  33. package/dist/types/theme/deep-merge.d.ts +6 -0
  34. package/dist/types/theme/generate-tailwind-css.d.ts +8 -0
  35. package/dist/types/theme/index.d.ts +2 -0
  36. package/dist/types/widgets/actions/index.d.ts +1 -1
  37. package/dist/types/widgets/actions/relative-data/index.d.ts +1 -0
  38. package/dist/types/widgets/actions/relative-data/labels.d.ts +3 -0
  39. package/dist/types/widgets/actions/relative-data/relative-data.d.ts +16 -2
  40. package/dist/types/widgets/actions/relative-data/scale.d.ts +26 -0
  41. package/dist/types/widgets/actions/relative-data/style.d.ts +12 -0
  42. package/dist/types/widgets/category/category.d.ts +2 -1
  43. package/dist/types/widgets/index.d.ts +1 -1
  44. package/dist/types/widgets/markdown/style.d.ts +74 -0
  45. package/dist/types/widgets/utils/spacing-px.d.ts +13 -0
  46. package/dist/{utils-C-eWwOhE.js → utils-Dw4y8tiq.js} +2 -2
  47. package/dist/{utils-C-eWwOhE.js.map → utils-Dw4y8tiq.js.map} +1 -1
  48. package/dist/widgets/actions.js +3 -3
  49. package/dist/widgets/bar.js +85 -84
  50. package/dist/widgets/bar.js.map +1 -1
  51. package/dist/widgets/category.js +1 -1
  52. package/dist/widgets/formula.js +1 -1
  53. package/dist/widgets/histogram.js +109 -108
  54. package/dist/widgets/histogram.js.map +1 -1
  55. package/dist/widgets/markdown.js +1 -1
  56. package/dist/widgets/pie.js +78 -78
  57. package/dist/widgets/pie.js.map +1 -1
  58. package/dist/widgets/scatterplot.js +121 -120
  59. package/dist/widgets/scatterplot.js.map +1 -1
  60. package/dist/widgets/spread.js +1 -1
  61. package/dist/widgets/table.js +1 -1
  62. package/dist/widgets/timeseries.js +91 -90
  63. package/dist/widgets/timeseries.js.map +1 -1
  64. package/dist/widgets/utils.js +1 -1
  65. package/dist/widgets.js +283 -279
  66. package/dist/widgets.js.map +1 -1
  67. package/dist/{zoom-toggle-MmLjMTEw.js → zoom-toggle-DqrV_0Ji.js} +2 -2
  68. package/dist/{zoom-toggle-MmLjMTEw.js.map → zoom-toggle-DqrV_0Ji.js.map} +1 -1
  69. package/package.json +8 -1
  70. package/src/chat/bubbles/styles.ts +2 -0
  71. package/src/components/lasso-tool/lasso-tool-inline.test.tsx +4 -4
  72. package/src/components/lasso-tool/lasso-tool-inline.tsx +6 -6
  73. package/src/components/lasso-tool/styles.ts +4 -4
  74. package/src/theme/create-carto-theme.test.ts +53 -0
  75. package/src/theme/create-carto-theme.ts +21 -0
  76. package/src/theme/deep-merge.test.ts +25 -0
  77. package/src/theme/deep-merge.ts +22 -0
  78. package/src/theme/generate-tailwind-css.test.ts +87 -0
  79. package/src/theme/generate-tailwind-css.ts +178 -0
  80. package/src/theme/index.ts +2 -0
  81. package/src/theme/tailwind-compile.test.ts +71 -0
  82. package/src/theme/tailwind.css +373 -0
  83. package/src/widgets/actions/index.ts +1 -0
  84. package/src/widgets/actions/relative-data/index.ts +1 -0
  85. package/src/widgets/actions/relative-data/labels.ts +6 -0
  86. package/src/widgets/actions/relative-data/relative-data.test.tsx +78 -1
  87. package/src/widgets/actions/relative-data/relative-data.tsx +137 -20
  88. package/src/widgets/actions/relative-data/scale.ts +48 -0
  89. package/src/widgets/actions/relative-data/style.ts +10 -0
  90. package/src/widgets/bar/options.test.ts +16 -0
  91. package/src/widgets/bar/options.ts +13 -8
  92. package/src/widgets/category/category.relative-data.test.tsx +51 -63
  93. package/src/widgets/category/category.tsx +25 -11
  94. package/src/widgets/histogram/options.ts +13 -8
  95. package/src/widgets/index.ts +1 -0
  96. package/src/widgets/markdown/style.ts +40 -0
  97. package/src/widgets/note/widget-note.tsx +4 -1
  98. package/src/widgets/pie/options.ts +5 -4
  99. package/src/widgets/scatterplot/options.test.ts +12 -0
  100. package/src/widgets/scatterplot/options.ts +25 -10
  101. package/src/widgets/timeseries/options.ts +13 -8
  102. package/src/widgets/utils/css-variables-theme.test.ts +32 -0
  103. package/src/widgets/utils/option-builders.ts +4 -6
  104. package/src/widgets/utils/spacing-px.test.ts +42 -0
  105. package/src/widgets/utils/spacing-px.ts +24 -0
  106. package/dist/actions-CkxKe8-J.js +0 -1160
  107. package/dist/actions-CkxKe8-J.js.map +0 -1
  108. package/dist/category-DULnoxvk.js.map +0 -1
  109. package/dist/components-CTUpdQuj.js.map +0 -1
  110. package/dist/item-color-B7xkIpDZ.js.map +0 -1
  111. package/dist/markdown-DmvLi3vx.js +0 -159
  112. package/dist/markdown-DmvLi3vx.js.map +0 -1
  113. package/dist/styles-BCEBm7xh.js.map +0 -1
@@ -12,6 +12,7 @@ import {
12
12
  import { ZOOM_LAYOUT } from '../actions/zoom-toggle'
13
13
  import type { OptionFactory } from '../echart'
14
14
  import { mergeOptions, resolveThemeColor } from '../utils'
15
+ import { spacingPx } from '../utils/spacing-px'
15
16
  import { createItemColorResolver } from '../utils/item-color'
16
17
  import { getHistogramBinValue } from './bins'
17
18
  import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
@@ -22,6 +23,7 @@ import type {
22
23
  HistogramBin,
23
24
  HistogramWidgetData,
24
25
  } from './types'
26
+ import { fixedAxisBounds } from '../actions/relative-data/scale'
25
27
 
26
28
  /**
27
29
  * Builds the **structural** ECharts option for a histogram widget — axes,
@@ -55,9 +57,9 @@ export function histogramOptions({
55
57
 
56
58
  return {
57
59
  grid: {
58
- left: parseInt(theme.spacing(1)),
59
- top: parseInt(theme.spacing(3)),
60
- right: parseInt(theme.spacing(1)),
60
+ left: spacingPx(theme, 1),
61
+ top: spacingPx(theme, 3),
62
+ right: spacingPx(theme, 1),
61
63
  // Default: no legend. Merger bumps this when there are >1 series.
62
64
  ...buildGridConfig(false, theme),
63
65
  containLabel: true,
@@ -66,7 +68,7 @@ export function histogramOptions({
66
68
  trigger: 'axis',
67
69
  backgroundColor: theme.palette.grey[900],
68
70
  borderWidth: 0,
69
- padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],
71
+ padding: [spacingPx(theme, 1), spacingPx(theme, 1)],
70
72
  textStyle: {
71
73
  color: theme.palette.common.white,
72
74
  fontSize: 11,
@@ -97,7 +99,7 @@ export function histogramOptions({
97
99
  axisTick: { show: false },
98
100
  axisLabel: {
99
101
  ...buildAxisLabelStyle(theme),
100
- padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
102
+ padding: [spacingPx(theme, 0.5), 0, 0, 0],
101
103
  margin: 0,
102
104
  // `interval: 0` makes every bin label a candidate so `hideOverlap`
103
105
  // can pack the maximum number that physically fit. The category-axis
@@ -125,7 +127,7 @@ export function histogramOptions({
125
127
  },
126
128
  axisLabel: {
127
129
  ...buildAxisLabelStyle(theme),
128
- margin: parseInt(theme.spacing(1)),
130
+ margin: spacingPx(theme, 1),
129
131
  show: true,
130
132
  showMaxLabel: true,
131
133
  showMinLabel: true,
@@ -216,6 +218,7 @@ export function createHistogramOptionFactory(
216
218
  typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)
217
219
  ? option.yAxis
218
220
  : {}
221
+ const fixedY = fixedAxisBounds(baseYAxis)
219
222
  const baseGrid =
220
223
  typeof option.grid === 'object' && !Array.isArray(option.grid)
221
224
  ? option.grid
@@ -354,12 +357,14 @@ export function createHistogramOptionFactory(
354
357
  ...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),
355
358
  yAxis: {
356
359
  ...baseYAxis,
360
+ // A config transform (RelativeData's `'total'` scale) may pin the
361
+ // ends; each applies only on the side the data reaches.
357
362
  min: (extent: { min: number }) => {
358
- niceMin = extent.min < 0 ? niceNum(extent.min) : 0
363
+ niceMin = extent.min < 0 ? (fixedY.min ?? niceNum(extent.min)) : 0
359
364
  return niceMin
360
365
  },
361
366
  max: (extent: { min: number; max: number }) => {
362
- niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)
367
+ niceMax = extent.max <= 0 ? 1 : (fixedY.max ?? niceNum(extent.max))
363
368
  return niceMax
364
369
  },
365
370
  axisLabel: {
@@ -155,6 +155,7 @@ export type {
155
155
  ZoomToggleProps,
156
156
  BrushToggleProps,
157
157
  RelativeDataProps,
158
+ RelativeScale,
158
159
  DownloadProps,
159
160
  DownloadItem,
160
161
  DownloadContext,
@@ -1,7 +1,47 @@
1
1
  import type { SxProps, Theme } from '@mui/material'
2
+ import type { CSSObject } from '@mui/material/styles'
3
+
4
+ /**
5
+ * Browser defaults that rendered Markdown relies on, restated with `revert`
6
+ * so a CSS reset in the app (Tailwind's preflight, for one) cannot strip
7
+ * them: list bullets and indent, link underline, blockquote margins, inline
8
+ * images, monospace fonts, `pre` margins, `sub`/`sup` placement. `revert`
9
+ * rolls back to the browser's own value, so pages without a reset render
10
+ * exactly as before.
11
+ *
12
+ * Wrapped in `:where()` so any element rule a consumer sets on top wins.
13
+ */
14
+ export const markdownElementDefaults = {
15
+ '& :where(ul, ol)': {
16
+ listStyle: 'revert',
17
+ margin: 'revert',
18
+ padding: 'revert',
19
+ },
20
+ '& :where(a)': { textDecoration: 'revert' },
21
+ '& :where(blockquote)': { margin: 'revert' },
22
+ '& :where(code, kbd, samp)': { fontFamily: 'revert' },
23
+ '& :where(img)': {
24
+ display: 'revert',
25
+ verticalAlign: 'revert',
26
+ maxWidth: 'revert',
27
+ height: 'revert',
28
+ },
29
+ '& :where(pre)': {
30
+ margin: 'revert',
31
+ fontFamily: 'revert',
32
+ fontSize: 'revert',
33
+ },
34
+ '& :where(sub, sup)': {
35
+ fontSize: 'revert',
36
+ lineHeight: 'revert',
37
+ position: 'revert',
38
+ verticalAlign: 'revert',
39
+ },
40
+ } satisfies CSSObject
2
41
 
3
42
  export const styles = {
4
43
  root: {
44
+ ...markdownElementDefaults,
5
45
  color: 'text.primary',
6
46
  '& a': { color: 'primary.main' },
7
47
  '& code': {
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
2
2
  import { Box, Link, Typography } from '@mui/material'
3
3
  import type { Components } from 'react-markdown'
4
4
  import { MarkdownContent } from '../markdown/markdown-content'
5
+ import { markdownElementDefaults } from '../markdown/style'
5
6
  import { observeResize } from '../echart/shared-resize-observer'
6
7
  import { DEFAULT_NOTE_LABELS, type NoteLabels } from './labels'
7
8
  import { styles } from './style'
@@ -107,7 +108,9 @@ function NoteBase({ lineClamp = 3, labels, children }: NoteProps) {
107
108
  function NoteMarkdown({ content, lineClamp = 3, labels }: NoteMarkdownProps) {
108
109
  return (
109
110
  <NoteShell lineClamp={lineClamp} labels={labels} dependency={content}>
110
- <MarkdownContent content={content} components={NOTE_COMPONENTS} />
111
+ <Box sx={markdownElementDefaults}>
112
+ <MarkdownContent content={content} components={NOTE_COMPONENTS} />
113
+ </Box>
111
114
  </NoteShell>
112
115
  )
113
116
  }
@@ -12,6 +12,7 @@ import {
12
12
  } from '../utils/option-builders'
13
13
  import type { OptionFactory, OptionFactoryContext } from '../echart'
14
14
  import { mergeOptions, resolveThemeColor } from '../utils'
15
+ import { spacingPx } from '../utils/spacing-px'
15
16
  import { createItemColorResolver } from '../utils/item-color'
16
17
  import type { WidgetSeries } from '../types'
17
18
  import type {
@@ -67,7 +68,7 @@ export function pieOptions({
67
68
  trigger: 'item',
68
69
  backgroundColor: theme.palette.grey[900],
69
70
  borderWidth: 0,
70
- padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],
71
+ padding: [spacingPx(theme, 1), spacingPx(theme, 1)],
71
72
  textStyle: {
72
73
  color: theme.palette.common.white,
73
74
  fontSize: 11,
@@ -466,7 +467,7 @@ function buildMultiSeriesBarFusion(
466
467
  grid: {
467
468
  ...baseGrid,
468
469
  ...buildGridConfig(true, theme),
469
- right: parseInt(theme.spacing(4)),
470
+ right: spacingPx(theme, 4),
470
471
  containLabel: true,
471
472
  },
472
473
  // Drop pie-specific structural keys that shouldn't render in the
@@ -511,7 +512,7 @@ function buildMultiSeriesBarFusion(
511
512
  axisLabel: {
512
513
  fontSize: theme.typography.overlineDelicate?.fontSize,
513
514
  fontFamily: theme.typography.overlineDelicate?.fontFamily,
514
- margin: parseInt(theme.spacing(1)),
515
+ margin: spacingPx(theme, 1),
515
516
  showMaxLabel: true,
516
517
  showMinLabel: true,
517
518
  formatter: (value: number) => {
@@ -526,7 +527,7 @@ function buildMultiSeriesBarFusion(
526
527
  axisLine: { show: false },
527
528
  axisTick: { show: false },
528
529
  axisLabel: {
529
- padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
530
+ padding: [spacingPx(theme, 0.5), 0, 0, 0],
530
531
  ...(labelFormatter && {
531
532
  formatter: (value: string | number) => String(labelFormatter(value)),
532
533
  }),
@@ -213,6 +213,18 @@ describe('createScatterplotOptionFactory', () => {
213
213
  expect(out.yAxis.min).toBe(0)
214
214
  })
215
215
 
216
+ it("pins the y ends a config transform set (RelativeData 'total')", () => {
217
+ const merge = createScatterplotOptionFactory({ theme })
218
+ const out = merge({ yAxis: { min: -100, max: 100 } }, [
219
+ [
220
+ [1, 47],
221
+ [2, 3],
222
+ ],
223
+ ]) as { yAxis: { min: number; max: number } }
224
+ expect(out.yAxis.max).toBe(100)
225
+ expect(out.yAxis.min).toBe(0)
226
+ })
227
+
216
228
  it('always emits itemStyle.color as a callback (passthrough when no selection)', () => {
217
229
  const merge = createScatterplotOptionFactory({ theme })
218
230
  const out = merge({}, [[[1, 2]]]) as {
@@ -12,6 +12,7 @@ import {
12
12
  import { ZOOM_LAYOUT } from '../actions/zoom-toggle'
13
13
  import type { OptionFactory } from '../echart'
14
14
  import { mergeOptions, resolveThemeColor } from '../utils'
15
+ import { spacingPx } from '../utils/spacing-px'
15
16
  import { createItemColorResolver } from '../utils/item-color'
16
17
  import { getScatterplotXY } from './points'
17
18
  import type {
@@ -21,6 +22,7 @@ import type {
21
22
  ScatterplotItem,
22
23
  ScatterplotWidgetData,
23
24
  } from './types'
25
+ import { fixedAxisBounds } from '../actions/relative-data/scale'
24
26
 
25
27
  /**
26
28
  * Builds the **structural** ECharts option for a scatterplot widget — both
@@ -44,9 +46,9 @@ export function scatterplotOptions({
44
46
  }: ScatterplotOptionsInput): ScatterplotEChartsOption {
45
47
  return {
46
48
  grid: {
47
- left: parseInt(theme.spacing(1)),
48
- top: parseInt(theme.spacing(3)),
49
- right: parseInt(theme.spacing(1)),
49
+ left: spacingPx(theme, 1),
50
+ top: spacingPx(theme, 3),
51
+ right: spacingPx(theme, 1),
50
52
  // Default: no legend. Merger bumps `bottom` when there are >1 series.
51
53
  ...buildGridConfig(false, theme),
52
54
  containLabel: true,
@@ -57,7 +59,7 @@ export function scatterplotOptions({
57
59
  trigger: 'item',
58
60
  backgroundColor: theme.palette.grey[900],
59
61
  borderWidth: 0,
60
- padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],
62
+ padding: [spacingPx(theme, 1), spacingPx(theme, 1)],
61
63
  textStyle: {
62
64
  color: theme.palette.common.white,
63
65
  fontSize: 11,
@@ -85,7 +87,7 @@ export function scatterplotOptions({
85
87
  axisTick: { show: false },
86
88
  axisLabel: {
87
89
  ...buildAxisLabelStyle(theme),
88
- margin: parseInt(theme.spacing(1)),
90
+ margin: spacingPx(theme, 1),
89
91
  hideOverlap: true,
90
92
  showMinLabel: true,
91
93
  showMaxLabel: true,
@@ -102,7 +104,7 @@ export function scatterplotOptions({
102
104
  axisTick: { show: false },
103
105
  axisLabel: {
104
106
  ...buildAxisLabelStyle(theme),
105
- margin: parseInt(theme.spacing(1)),
107
+ margin: spacingPx(theme, 1),
106
108
  hideOverlap: true,
107
109
  showMinLabel: true,
108
110
  showMaxLabel: true,
@@ -189,8 +191,12 @@ export function createScatterplotOptionFactory(
189
191
 
190
192
  const reactiveFormatter = ctx?.formatter
191
193
 
192
- const { niceMinX, niceMaxX, niceMinY, niceMaxY } =
193
- computeScatterBounds(seriesArr)
194
+ const { niceMinX, niceMaxX, niceMinY, niceMaxY } = computeScatterBounds(
195
+ seriesArr,
196
+ // A config transform (RelativeData's `'total'` scale) may pin the
197
+ // y ends; each applies only on the side the data reaches.
198
+ fixedAxisBounds(baseYAxis),
199
+ )
194
200
 
195
201
  // Dim non-selected points via `series.itemStyle.color`. The selection
196
202
  // key is `${seriesIndex}:${dataIndex}`; we read those off the params
@@ -410,6 +416,7 @@ function buildScatterTooltipFormatter(
410
416
 
411
417
  function computeScatterBounds(
412
418
  seriesArr: ScatterplotWidgetData<ScatterplotItem>,
419
+ fixedY: { min?: number; max?: number } = {},
413
420
  ): {
414
421
  niceMinX: number
415
422
  niceMaxX: number
@@ -439,8 +446,16 @@ function computeScatterBounds(
439
446
  // negative mins, and floor max=0 to 1 so the chart always has range.
440
447
  // Scatter can have free coordinates so we apply this per-axis.
441
448
  const niceMaxX = Number.isFinite(maxX) ? (maxX <= 0 ? 1 : niceNum(maxX)) : 1
442
- const niceMaxY = Number.isFinite(maxY) ? (maxY <= 0 ? 1 : niceNum(maxY)) : 1
449
+ const niceMaxY = Number.isFinite(maxY)
450
+ ? maxY <= 0
451
+ ? 1
452
+ : (fixedY.max ?? niceNum(maxY))
453
+ : 1
443
454
  const niceMinX = Number.isFinite(minX) ? (minX < 0 ? niceNum(minX) : 0) : 0
444
- const niceMinY = Number.isFinite(minY) ? (minY < 0 ? niceNum(minY) : 0) : 0
455
+ const niceMinY = Number.isFinite(minY)
456
+ ? minY < 0
457
+ ? (fixedY.min ?? niceNum(minY))
458
+ : 0
459
+ : 0
445
460
  return { niceMinX, niceMaxX, niceMinY, niceMaxY }
446
461
  }
@@ -12,6 +12,7 @@ import {
12
12
  import { ZOOM_LAYOUT } from '../actions/zoom-toggle'
13
13
  import type { OptionFactory } from '../echart'
14
14
  import { mergeOptions, resolveThemeColor } from '../utils'
15
+ import { spacingPx } from '../utils/spacing-px'
15
16
  import { createItemColorResolver } from '../utils/item-color'
16
17
  import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
17
18
  import type {
@@ -20,6 +21,7 @@ import type {
20
21
  TimeseriesOptionsInput,
21
22
  TimeseriesWidgetData,
22
23
  } from './types'
24
+ import { fixedAxisBounds } from '../actions/relative-data/scale'
23
25
 
24
26
  /**
25
27
  * Builds the **structural** ECharts option for a timeseries widget —
@@ -52,9 +54,9 @@ export function timeseriesOptions({
52
54
 
53
55
  return {
54
56
  grid: {
55
- left: parseInt(theme.spacing(1)),
56
- top: parseInt(theme.spacing(3)),
57
- right: parseInt(theme.spacing(1)),
57
+ left: spacingPx(theme, 1),
58
+ top: spacingPx(theme, 3),
59
+ right: spacingPx(theme, 1),
58
60
  // Default: no legend. Merger bumps this when there are >1 series.
59
61
  ...buildGridConfig(false, theme),
60
62
  containLabel: true,
@@ -63,7 +65,7 @@ export function timeseriesOptions({
63
65
  trigger: 'axis',
64
66
  backgroundColor: theme.palette.grey[900],
65
67
  borderWidth: 0,
66
- padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],
68
+ padding: [spacingPx(theme, 1), spacingPx(theme, 1)],
67
69
  textStyle: {
68
70
  color: theme.palette.common.white,
69
71
  fontSize: 11,
@@ -92,7 +94,7 @@ export function timeseriesOptions({
92
94
  axisTick: { show: false },
93
95
  axisLabel: {
94
96
  ...buildAxisLabelStyle(theme),
95
- padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
97
+ padding: [spacingPx(theme, 0.5), 0, 0, 0],
96
98
  margin: 0,
97
99
  hideOverlap: true,
98
100
  ...(labelFormatter && {
@@ -118,7 +120,7 @@ export function timeseriesOptions({
118
120
  },
119
121
  axisLabel: {
120
122
  ...buildAxisLabelStyle(theme),
121
- margin: parseInt(theme.spacing(1)),
123
+ margin: spacingPx(theme, 1),
122
124
  show: true,
123
125
  showMaxLabel: true,
124
126
  showMinLabel: true,
@@ -188,6 +190,7 @@ export function createTimeseriesOptionFactory(
188
190
  typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)
189
191
  ? option.yAxis
190
192
  : {}
193
+ const fixedY = fixedAxisBounds(baseYAxis)
191
194
  const baseGrid =
192
195
  typeof option.grid === 'object' && !Array.isArray(option.grid)
193
196
  ? option.grid
@@ -302,12 +305,14 @@ export function createTimeseriesOptionFactory(
302
305
  ...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),
303
306
  yAxis: {
304
307
  ...baseYAxis,
308
+ // A config transform (RelativeData's `'total'` scale) may pin the
309
+ // ends; each applies only on the side the data reaches.
305
310
  min: (extent: { min: number }) => {
306
- niceMin = extent.min < 0 ? niceNum(extent.min) : 0
311
+ niceMin = extent.min < 0 ? (fixedY.min ?? niceNum(extent.min)) : 0
307
312
  return niceMin
308
313
  },
309
314
  max: (extent: { min: number; max: number }) => {
310
- niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)
315
+ niceMax = extent.max <= 0 ? 1 : (fixedY.max ?? niceNum(extent.max))
311
316
  return niceMax
312
317
  },
313
318
  axisLabel: {
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createCartoTheme } from '../../theme/create-carto-theme'
3
+ import { barOptions } from '../bar/options'
4
+ import { histogramOptions } from '../histogram/options'
5
+ import { pieOptions } from '../pie/options'
6
+ import { scatterplotOptions } from '../scatterplot/options'
7
+ import { timeseriesOptions } from '../timeseries/options'
8
+
9
+ const theme = createCartoTheme()
10
+
11
+ function nanPaths(value: unknown, path = '$'): string[] {
12
+ if (typeof value === 'number') return Number.isNaN(value) ? [path] : []
13
+ if (Array.isArray(value))
14
+ return value.flatMap((v, i) => nanPaths(v, `${path}[${i}]`))
15
+ if (value && typeof value === 'object')
16
+ return Object.entries(value).flatMap(([k, v]) =>
17
+ nanPaths(v, `${path}.${k}`),
18
+ )
19
+ return []
20
+ }
21
+
22
+ describe('ECharts options with a cssVariables theme', () => {
23
+ it.each([
24
+ ['bar', () => barOptions({ theme })],
25
+ ['histogram', () => histogramOptions({ theme })],
26
+ ['pie', () => pieOptions({ theme })],
27
+ ['scatterplot', () => scatterplotOptions({ theme })],
28
+ ['timeseries', () => timeseriesOptions({ theme })],
29
+ ])('%s has no NaN in its options', (_name, build) => {
30
+ expect(nanPaths(build())).toEqual([])
31
+ })
32
+ })
@@ -4,6 +4,7 @@ import type {
4
4
  CallbackDataParams,
5
5
  TopLevelFormatterParams,
6
6
  } from 'echarts/types/dist/shared'
7
+ import { spacingPx } from './spacing-px'
7
8
 
8
9
  /**
9
10
  * Shared EChart configuration builders for chart widgets
@@ -61,8 +62,8 @@ export function buildLegendConfig({
61
62
  */
62
63
  export function buildGridConfig(hasLegend: boolean, theme: Theme) {
63
64
  return {
64
- ...(!hasLegend && { bottom: parseInt(theme.spacing(3)) }),
65
- ...(hasLegend && { bottom: parseInt(theme.spacing(7)) }),
65
+ ...(!hasLegend && { bottom: spacingPx(theme, 3) }),
66
+ ...(hasLegend && { bottom: spacingPx(theme, 7) }),
66
67
  }
67
68
  }
68
69
 
@@ -106,10 +107,7 @@ export function createTooltipPositioner(theme: Theme) {
106
107
  _rect: unknown,
107
108
  size: { contentSize: [number, number]; viewSize: [number, number] },
108
109
  ) {
109
- const position = { top: parseInt(theme.spacing(0.5)) } as Record<
110
- string,
111
- number
112
- >
110
+ const position = { top: spacingPx(theme, 0.5) } as Record<string, number>
113
111
 
114
112
  // Position tooltip left or right based on available space
115
113
  if (size.contentSize[0] < size.viewSize[0] - point[0]) {
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createTheme, responsiveFontSizes, type Theme } from '@mui/material'
3
+ import {
4
+ cartoThemeOptions,
5
+ theme as cartoTheme,
6
+ } from '@carto/meridian-ds/theme'
7
+ import { createCartoTheme } from '../../theme/create-carto-theme'
8
+ import { spacingPx } from './spacing-px'
9
+
10
+ const FACTORS = [0.5, 1, 3, 4, 7]
11
+
12
+ describe('spacingPx', () => {
13
+ it('matches the previous parseInt(theme.spacing(n)) for the current CARTO theme', () => {
14
+ const theme = createTheme(cartoTheme)
15
+ for (const factor of FACTORS) {
16
+ expect(spacingPx(theme, factor)).toBe(parseInt(theme.spacing(factor)))
17
+ }
18
+ })
19
+
20
+ it('resolves numbers for a CARTO theme with cssVariables', () => {
21
+ const theme = responsiveFontSizes(
22
+ createTheme({ ...cartoThemeOptions, cssVariables: true }),
23
+ )
24
+ expect(theme.spacing(1)).toContain('var(')
25
+ expect(FACTORS.map((f) => spacingPx(theme, f))).toEqual([4, 8, 24, 32, 56])
26
+ })
27
+
28
+ it('falls back to 8px per unit for a plain MUI theme with cssVariables', () => {
29
+ const theme = createTheme({ cssVariables: true }) as unknown as Theme
30
+ expect(FACTORS.map((f) => spacingPx(theme, f))).toEqual([4, 8, 24, 32, 56])
31
+ })
32
+
33
+ it('honours a numeric spacing override on createCartoTheme', () => {
34
+ const theme = createCartoTheme({ spacing: 4 })
35
+ expect(FACTORS.map((f) => spacingPx(theme, f))).toEqual([2, 4, 12, 16, 28])
36
+ })
37
+
38
+ it('honours a function spacing override under cssVariables', () => {
39
+ const theme = createCartoTheme({ spacing: (f: number) => f * 6 })
40
+ expect(spacingPx(theme, 1)).toBe(6)
41
+ })
42
+ })
@@ -0,0 +1,24 @@
1
+ import type { Theme } from '@mui/material'
2
+
3
+ /**
4
+ * Theme spacing as a pixel number, for APIs that need numbers (ECharts).
5
+ *
6
+ * `theme.spacing(n)` returns `"8px"` for a regular theme but
7
+ * `"var(--mui-spacing, 8px)"` / `"calc(...)"` once the theme uses
8
+ * `cssVariables`, which does not parse. Resolution order:
9
+ * 1. `parseFloat` of `theme.spacing(factor)` (regular themes);
10
+ * 2. the px unit MUI writes into `theme.spacing(1)` (`var(--mui-spacing, Npx)`),
11
+ * which honours a `spacing` override such as `createCartoTheme({ spacing: 4 })`;
12
+ * 3. meridian's numeric `spacingValue`, or MUI's default 8px.
13
+ */
14
+ export function spacingPx(theme: Theme, factor: number): number {
15
+ const parsed = parseFloat(theme.spacing(factor))
16
+ if (Number.isFinite(parsed)) return parsed
17
+ // `spacing(1)` is `var(--mui-spacing, Npx)`; larger factors are
18
+ // `calc(f * var(--mui-spacing, Npx))`, so read the unit from the former.
19
+ const fallback = /,\s*(-?[\d.]+)px\)\s*$/.exec(theme.spacing(1))
20
+ const px = fallback ? Number(fallback[1]) : NaN
21
+ if (Number.isFinite(px)) return px * factor
22
+ const unit = (theme.spacingValue as number | undefined) ?? 8
23
+ return unit * factor
24
+ }