@cdc/chart 4.26.7 → 4.26.10

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 (255) hide show
  1. package/CONFIG.md +502 -368
  2. package/dist/cdcchart-CYc-Kayd.es.js +22488 -0
  3. package/dist/cdcchart-DCKn8NmI.es.js +20695 -0
  4. package/dist/cdcchart-Dyui9nk_.es.js +45828 -0
  5. package/dist/cdcchart.js +4 -97406
  6. package/examples/__data__/vega-remote-data.json +6 -0
  7. package/examples/dendrogram.json +18 -0
  8. package/examples/feature/bar/bar-chart-race-annual-change.json +68 -0
  9. package/examples/feature/heatmap/average-age-categorical.json +2 -2
  10. package/examples/feature/heatmap/calendar-heatmap.json +2 -2
  11. package/examples/feature/heatmap/cell-values-bottom-axis.json +2 -2
  12. package/examples/feature/heatmap/sparse-aggregation.json +2 -2
  13. package/examples/feature/line/line-chart-race.json +50 -0
  14. package/examples/feature/sankey/sankey-example-data.json +40 -1441
  15. package/examples/feature/sankey/sankey_chart_data.csv +9 -20
  16. package/examples/network.json +31 -0
  17. package/examples/private/box-plot.json +573 -0
  18. package/examples/private/custom-color-issue.json +625 -0
  19. package/examples/private/custom-colors.json +522 -0
  20. package/examples/private/cutoff.json +10750 -0
  21. package/examples/private/overlay-legend.json +485 -0
  22. package/examples/private/pie-test.json +469 -0
  23. package/examples/vega-remote.json +22 -0
  24. package/package.json +11 -5
  25. package/src/CdcChartComponent.tsx +405 -208
  26. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  27. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  28. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  29. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  30. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  31. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  32. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  33. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  34. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  35. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  36. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  37. package/src/_stories/Chart.Network.stories.tsx +432 -0
  38. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  39. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  40. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  41. package/src/_stories/Chart.PieLabels.stories.tsx +4 -2
  42. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  43. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  44. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  45. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  46. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  47. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  48. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  49. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  50. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  51. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  52. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  53. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  54. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  55. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  56. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  57. package/src/_stories/_mock/editor-test-configs.json +20 -10
  58. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  59. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  60. package/src/_stories/_mock/heatmap.json +2 -2
  61. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  62. package/src/_stories/_mock/sankey_mock.json +23 -27
  63. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  64. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  65. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  66. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  67. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  68. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  69. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  70. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  71. package/src/components/Axis/BottomAxis.tsx +1 -1
  72. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  73. package/src/components/Axis/RightAxis.tsx +21 -13
  74. package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -6
  75. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -3
  76. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  77. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +29 -25
  78. package/src/components/BarChart/components/BarChart.Vertical.tsx +37 -26
  79. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  80. package/src/components/BarChart/components/context.tsx +6 -0
  81. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  82. package/src/components/BarChart/helpers/index.ts +8 -0
  83. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  84. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  85. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  86. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  87. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +104 -0
  88. package/src/components/BarChart/helpers/useBarChart.ts +60 -59
  89. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  90. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  91. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  92. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  93. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  94. package/src/components/BarChartRace/helpers.test.ts +114 -0
  95. package/src/components/BarChartRace/helpers.ts +185 -0
  96. package/src/components/BarChartRace/index.ts +3 -0
  97. package/src/components/BoxPlot/helpers/index.ts +11 -82
  98. package/src/components/Brush/BrushSelector.tsx +9 -23
  99. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  100. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  101. package/src/components/Dendrogram/dendrogram.css +113 -0
  102. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  103. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  104. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  105. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  106. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  107. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  108. package/src/components/Dendrogram/index.tsx +1 -0
  109. package/src/components/Dendrogram/types/index.ts +66 -0
  110. package/src/components/EditorPanel/EditorPanel.tsx +1005 -760
  111. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  112. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  113. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  114. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  115. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +88 -54
  116. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  117. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  118. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +233 -33
  119. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  120. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  121. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  122. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  123. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  124. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  125. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  126. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  127. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  128. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  129. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  130. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +2 -1
  131. package/src/components/EditorPanel/useEditorPermissions.test.tsx +62 -0
  132. package/src/components/EditorPanel/useEditorPermissions.ts +70 -24
  133. package/src/components/Forecasting/Forecasting.tsx +15 -6
  134. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  135. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  136. package/src/components/HeatMap/heatmap.css +101 -2
  137. package/src/components/HeatMap/helpers/layout.ts +80 -0
  138. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  139. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  140. package/src/components/Legend/Legend.Component.tsx +110 -90
  141. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  142. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -34
  143. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  144. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  145. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  146. package/src/components/LineChart/helpers.test.ts +126 -38
  147. package/src/components/LineChart/helpers.ts +9 -4
  148. package/src/components/LineChart/index.tsx +69 -24
  149. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  150. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  151. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  152. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  153. package/src/components/LineChartRace/helpers.test.ts +88 -0
  154. package/src/components/LineChartRace/helpers.ts +157 -0
  155. package/src/components/LineChartRace/index.ts +3 -0
  156. package/src/components/LineChartRace/line-chart-race.css +16 -0
  157. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  158. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  159. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  160. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  161. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  162. package/src/components/LinearChart.tsx +81 -51
  163. package/src/components/Network/components/Network.tsx +372 -0
  164. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  165. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  166. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  167. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  168. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  169. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  170. package/src/components/Network/index.tsx +3 -0
  171. package/src/components/Network/network.css +107 -0
  172. package/src/components/Network/types/index.ts +56 -0
  173. package/src/components/PairedBarChart.jsx +5 -10
  174. package/src/components/PairedBarChart.test.tsx +71 -0
  175. package/src/components/PieChart/PieChart.tsx +9 -8
  176. package/src/components/RacingChartRenderer.tsx +44 -0
  177. package/src/components/Regions/components/Regions.test.tsx +179 -0
  178. package/src/components/Regions/components/Regions.tsx +104 -69
  179. package/src/components/Regions/index.tsx +2 -3
  180. package/src/components/Sankey/components/Sankey.tsx +264 -437
  181. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  182. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  183. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  184. package/src/components/Sankey/sankey.scss +57 -116
  185. package/src/components/Sankey/types/index.ts +41 -4
  186. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -4
  187. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  188. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  189. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  190. package/src/components/raceTiming.ts +18 -0
  191. package/src/data/initial-state.js +77 -31
  192. package/src/helpers/applyChartTypeContract.ts +71 -0
  193. package/src/helpers/axisLabelFontSize.ts +2 -2
  194. package/src/helpers/boxPlotStats.ts +60 -0
  195. package/src/helpers/bumpChartTooltip.ts +2 -1
  196. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  197. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  198. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  199. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  200. package/src/helpers/createNewChartConfig.ts +56 -0
  201. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  202. package/src/helpers/forecastLegend.ts +88 -0
  203. package/src/helpers/getBoxPlotConfig.ts +18 -37
  204. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  205. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  206. package/src/helpers/getColorScale.ts +31 -22
  207. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  208. package/src/helpers/getMinMax.ts +9 -7
  209. package/src/helpers/getNewRuntime.ts +5 -2
  210. package/src/helpers/getStackedSeriesMax.ts +51 -0
  211. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  212. package/src/helpers/handleRankByValue.ts +15 -15
  213. package/src/helpers/seriesColumnSettings.ts +10 -0
  214. package/src/helpers/sizeHelpers.ts +4 -3
  215. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  216. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  217. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  218. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  219. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  220. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  221. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  222. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  223. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  224. package/src/helpers/tests/getColorScale.test.ts +226 -1
  225. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  226. package/src/helpers/tests/getMinMax.test.ts +41 -0
  227. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  228. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  229. package/src/helpers/tests/seriesColumnSettings.test.ts +23 -0
  230. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  231. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  232. package/src/helpers/tooltipHelpers.ts +16 -0
  233. package/src/hooks/tests/useTooltip.test.tsx +353 -1
  234. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  235. package/src/hooks/useReduceData.ts +4 -36
  236. package/src/hooks/useScales.ts +15 -0
  237. package/src/hooks/useTooltip.tsx +95 -64
  238. package/src/scss/main.scss +74 -2
  239. package/src/store/chart.reducer.ts +4 -3
  240. package/src/test/CdcChart.dataTable.test.tsx +490 -16
  241. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  242. package/src/test/CdcChart.finalization.test.tsx +143 -0
  243. package/src/test/CdcChart.network.test.tsx +271 -0
  244. package/src/types/ChartConfig.ts +91 -54
  245. package/src/types/ChartContext.ts +5 -0
  246. package/src/types/Horizon.ts +1 -1
  247. package/LICENSE +0 -201
  248. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  249. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  250. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  251. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  252. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  253. package/src/data/legacy-defaults.ts +0 -18
  254. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  255. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
@@ -1,25 +1,60 @@
1
1
  import React from 'react'
2
- import { render, screen, waitFor } from '@testing-library/react'
2
+ import { act, render, screen } from '@testing-library/react'
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest'
4
4
  import CdcChart from '../CdcChartComponent'
5
+ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
5
6
 
6
- const dataTableProps = vi.hoisted(() => {
7
+ vi.hoisted(() => {
7
8
  Object.defineProperty((globalThis as any).HTMLCanvasElement.prototype, 'getContext', {
8
9
  configurable: true,
9
10
  value: () => ({
10
11
  measureText: (text = '') => ({ width: String(text).length * 8 })
11
12
  })
12
13
  })
13
-
14
- return [] as any[]
15
14
  })
16
15
 
16
+ const chartRender = vi.hoisted(() => ({ onRender: undefined as undefined | ((config: any) => void) }))
17
+ const dataTableRender = vi.hoisted(() => ({ onRender: undefined as undefined | ((props: any) => void) }))
18
+ const chartResize = vi.hoisted(() => ({ resize: undefined as undefined | ((width: number) => void) }))
19
+
20
+ vi.mock('resize-observer-polyfill', () => ({
21
+ default: class {
22
+ constructor(private callback: ResizeObserverCallback) {}
23
+ observe(target: HTMLElement) {
24
+ if (target.classList.contains('cove-visualization')) {
25
+ chartResize.resize = width =>
26
+ this.callback(
27
+ [{ target, contentRect: { width, height: 360 } } as unknown as ResizeObserverEntry],
28
+ this as unknown as ResizeObserver
29
+ )
30
+ }
31
+ }
32
+ unobserve() {}
33
+ disconnect() {}
34
+ }
35
+ }))
36
+
37
+ const nextRender = (signal: { onRender?: (value: any) => void }, isReady: (value: any) => boolean = () => true) =>
38
+ new Promise<any>(resolve => {
39
+ signal.onRender = value => {
40
+ if (!isReady(value)) return
41
+ signal.onRender = undefined
42
+ resolve(value)
43
+ }
44
+ })
45
+
46
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
47
+ default: ({ display }) => React.createElement('span', { 'data-icon': display })
48
+ }))
49
+
50
+ vi.mock('@cdc/core/helpers/fetchRemoteData', () => ({ default: vi.fn() }))
51
+
17
52
  vi.mock('@cdc/core/components/DataTable', async () => {
18
53
  const React = await vi.importActual<typeof import('react')>('react')
19
54
 
20
55
  return {
21
56
  default: props => {
22
- dataTableProps.push(props)
57
+ dataTableRender.onRender?.(props)
23
58
  return React.createElement('div', { 'data-testid': 'data-table-probe' })
24
59
  }
25
60
  }
@@ -31,15 +66,118 @@ vi.mock('@visx/responsive/lib/components/ParentSize', () => ({
31
66
 
32
67
  vi.mock('../components/LinearChart', async () => {
33
68
  const React = await vi.importActual<typeof import('react')>('react')
69
+ const { default: ConfigContext } = await vi.importActual<typeof import('../ConfigContext')>('../ConfigContext')
34
70
 
35
71
  return {
36
- default: React.forwardRef(() => React.createElement('div', { 'data-testid': 'mock-linear-chart' }))
72
+ default: React.forwardRef(() => {
73
+ const { config } = React.useContext(ConfigContext)
74
+ chartRender.onRender?.(config)
75
+ return React.createElement('div', { 'data-testid': 'mock-linear-chart' })
76
+ })
37
77
  }
38
78
  })
39
79
 
40
- describe('CdcChart data table dataset wiring', () => {
80
+ describe('CdcChart config hydration and data table wiring', () => {
41
81
  beforeEach(() => {
42
- dataTableProps.length = 0
82
+ chartResize.resize = undefined
83
+ chartRender.onRender = undefined
84
+ dataTableRender.onRender = undefined
85
+ vi.mocked(fetchRemoteData).mockReset()
86
+ })
87
+
88
+ it('processes remote Vega data before rendering an imported chart', async () => {
89
+ vi.mocked(fetchRemoteData).mockResolvedValue({
90
+ data: { source: [{ category: 'Remote', value: 42 }] } as any,
91
+ dataMetadata: {}
92
+ })
93
+ const chartWithData = nextRender(chartRender, config => config.data?.length > 0)
94
+
95
+ render(
96
+ <CdcChart
97
+ config={
98
+ {
99
+ type: 'chart',
100
+ visualizationType: 'Bar',
101
+ dataUrl: '/vega-data.json',
102
+ data: [],
103
+ xAxis: { dataKey: 'category' },
104
+ series: [{ dataKey: 'value' }],
105
+ vegaConfig: {
106
+ data: [{ name: 'source', values: [{ category: 'Stale', value: 1 }] }],
107
+ marks: [{ type: 'rect', from: { data: 'source' } }],
108
+ scales: []
109
+ }
110
+ } as any
111
+ }
112
+ interactionLabel='remote-vega-chart-test'
113
+ />
114
+ )
115
+
116
+ expect((await chartWithData).data[0]).toMatchObject({ category: 'Remote', value: 42 })
117
+ })
118
+
119
+ it('loads palette configurations sequentially without mutating shared defaults', async () => {
120
+ const baseConfig = {
121
+ type: 'chart',
122
+ data: [{ category: 'A', value: 1 }],
123
+ xAxis: { dataKey: 'category' },
124
+ series: [{ dataKey: 'value' }]
125
+ }
126
+ const firstChart = nextRender(chartRender, config => config.general?.palette?.name === 'divergent_blue_cyan')
127
+ const first = render(
128
+ <CdcChart
129
+ config={{ ...baseConfig, visualizationType: 'Line', color: 'sequential-orange' } as any}
130
+ interactionLabel='first-palette-load'
131
+ />
132
+ )
133
+
134
+ const firstConfig = await firstChart
135
+ expect(firstConfig.general.palette).toMatchObject({
136
+ name: 'divergent_blue_cyan',
137
+ version: '2.0',
138
+ isReversed: false
139
+ })
140
+ expect(firstConfig.general.palette.backups).toBeUndefined()
141
+ expect(firstConfig.migrations?.paletteFallbackFrozen).toBeUndefined()
142
+ first.unmount()
143
+
144
+ const secondChart = nextRender(chartRender, config => config.general?.palette?.name === 'sequential_bluereverse')
145
+ render(
146
+ <CdcChart config={{ ...baseConfig, visualizationType: 'Bar' } as any} interactionLabel='second-palette-load' />
147
+ )
148
+
149
+ const secondConfig = await secondChart
150
+ expect(secondConfig.general.palette).toMatchObject({
151
+ name: 'sequential_bluereverse',
152
+ version: '2.0',
153
+ isReversed: true
154
+ })
155
+ expect(secondConfig.migrations?.paletteFallbackFrozen).toBeUndefined()
156
+ })
157
+
158
+ it.each([
159
+ ['the historical thickness for a versionless omission', {}, 0.35],
160
+ ['the current thickness for a current-version omission', { version: '4.26.8' }, 0.8],
161
+ ['an explicitly authored thickness', { barThickness: 0.8 }, 0.8]
162
+ ])('uses %s', async (_label, configOverrides, expectedBarThickness) => {
163
+ const chart = nextRender(chartRender, config => config.barThickness !== undefined)
164
+ render(
165
+ <CdcChart
166
+ config={
167
+ {
168
+ type: 'chart',
169
+ visualizationType: 'Bar',
170
+ data: [{ category: 'A', value: 1 }],
171
+ xAxis: { dataKey: 'category' },
172
+ series: [{ dataKey: 'value' }],
173
+ ...configOverrides
174
+ } as any
175
+ }
176
+ interactionLabel='chart-thickness-test'
177
+ />
178
+ )
179
+
180
+ expect((await chart).barThickness).toBe(expectedBarThickness)
43
181
  })
44
182
 
45
183
  it('passes the selected dashboard dataset metadata to DataTable', async () => {
@@ -51,6 +189,7 @@ describe('CdcChart data table dataset wiring', () => {
51
189
  runtimeDataUrl: '/wcms/vizdata/chart-runtime.json'
52
190
  }
53
191
 
192
+ const table = nextRender(dataTableRender, props => props.dataConfig?.runtimeDataUrl !== undefined)
54
193
  render(
55
194
  <CdcChart
56
195
  config={
@@ -77,14 +216,150 @@ describe('CdcChart data table dataset wiring', () => {
77
216
  />
78
217
  )
79
218
 
80
- await waitFor(() => expect(dataTableProps.length).toBeGreaterThan(0))
81
-
82
- expect(dataTableProps.at(-1).dataConfig).toMatchObject({
219
+ expect((await table).dataConfig).toMatchObject({
83
220
  runtimeDataUrl: '/wcms/vizdata/chart-runtime.json'
84
221
  })
85
222
  })
86
223
 
224
+ it('keeps the download area mounted when the data table is hidden', async () => {
225
+ const table = nextRender(dataTableRender)
226
+ render(
227
+ <CdcChart
228
+ config={
229
+ {
230
+ type: 'chart',
231
+ visualizationType: 'Bar',
232
+ title: 'Download-only Chart',
233
+ data: [{ category: 'A', value: 1 }],
234
+ xAxis: { dataKey: 'category' },
235
+ series: [{ dataKey: 'value' }],
236
+ table: {
237
+ show: false,
238
+ expanded: false,
239
+ download: true,
240
+ label: 'Data Table',
241
+ indexLabel: ''
242
+ }
243
+ } as any
244
+ }
245
+ interactionLabel='chart-download-only-test'
246
+ />
247
+ )
248
+
249
+ expect((await table).showTable).toBe(false)
250
+ })
251
+
252
+ it('renders the annotation table between chart subtext and the data-table area', async () => {
253
+ render(
254
+ <CdcChart
255
+ config={
256
+ {
257
+ type: 'chart',
258
+ visualizationType: 'Bar',
259
+ title: 'Annotated Chart',
260
+ description: 'Chart subtext',
261
+ dataKey: 'datasetA',
262
+ data: [{ category: 'A', value: 1 }],
263
+ xAxis: { dataKey: 'category' },
264
+ series: [{ dataKey: 'value' }],
265
+ annotations: [
266
+ {
267
+ text: 'Important annotation',
268
+ anchorMode: 'fixed',
269
+ x: 50,
270
+ y: 50,
271
+ dx: 20,
272
+ dy: -20
273
+ }
274
+ ],
275
+ general: {
276
+ annotationDropdownText: '',
277
+ showAnnotationDropdown: true
278
+ },
279
+ table: {
280
+ show: true,
281
+ expanded: false,
282
+ download: false,
283
+ label: 'Data Table',
284
+ indexLabel: '',
285
+ showDataTableLink: true
286
+ }
287
+ } as any
288
+ }
289
+ isDashboard={true}
290
+ interactionLabel='annotation-table-order-test'
291
+ />
292
+ )
293
+
294
+ const subtext = await screen.findByText('Chart subtext')
295
+ const annotations = await screen.findByRole('button', { name: 'Annotations' })
296
+ const tableLink = await screen.findByRole('link', { name: 'datasetA (Go to Table)' })
297
+ const dataTable = await screen.findByTestId('data-table-probe')
298
+ const bodyWrap = annotations.closest('.cove-visualization__body-wrap')
299
+ const annotationsSection = annotations.closest('.cove-visualization__body-annotations-section')
300
+ const footerSection = dataTable.closest('.cove-visualization__body-footer-section')
301
+
302
+ expect(subtext.compareDocumentPosition(annotations) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
303
+ expect(annotations.compareDocumentPosition(tableLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
304
+ expect(tableLink.compareDocumentPosition(dataTable) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
305
+ expect(annotationsSection?.parentElement).toBe(bodyWrap)
306
+ expect(footerSection?.parentElement).toBe(bodyWrap)
307
+ expect(annotationsSection?.nextElementSibling).toBe(footerSection)
308
+ })
309
+
310
+ it.each([
311
+ {
312
+ mobileAnnotationDisplay: undefined,
313
+ showAnnotationDropdown: undefined,
314
+ mobileVisible: true,
315
+ desktopVisible: false
316
+ },
317
+ { mobileAnnotationDisplay: 'symbol', showAnnotationDropdown: false, mobileVisible: true, desktopVisible: false },
318
+ { mobileAnnotationDisplay: 'text', showAnnotationDropdown: false, mobileVisible: false, desktopVisible: false },
319
+ { mobileAnnotationDisplay: 'text', showAnnotationDropdown: true, mobileVisible: true, desktopVisible: true },
320
+ { mobileAnnotationDisplay: 'symbol', showAnnotationDropdown: true, mobileVisible: true, desktopVisible: true }
321
+ ])(
322
+ 'mounts the annotation section only when visible across resizes: %j',
323
+ async ({ mobileAnnotationDisplay, showAnnotationDropdown, mobileVisible, desktopVisible }) => {
324
+ const { container } = render(
325
+ <CdcChart
326
+ config={
327
+ {
328
+ type: 'chart',
329
+ visualizationType: 'Bar',
330
+ data: [{ category: 'A', value: 1 }],
331
+ xAxis: { dataKey: 'category' },
332
+ series: [{ dataKey: 'value' }],
333
+ annotations: [{ text: 'Important annotation', anchorMode: 'fixed', x: 50, y: 50, dx: 20, dy: -20 }],
334
+ general: { annotationDropdownText: '', mobileAnnotationDisplay, showAnnotationDropdown },
335
+ table: { show: true, download: false }
336
+ } as any
337
+ }
338
+ interactionLabel='annotation-visibility-test'
339
+ />
340
+ )
341
+
342
+ await screen.findByTestId('data-table-probe')
343
+ for (const [width, visible] of [
344
+ [1300, desktopVisible],
345
+ [400, mobileVisible],
346
+ [1300, desktopVisible]
347
+ ] as const) {
348
+ await act(async () => chartResize.resize!(width))
349
+ const section = container.querySelector('.cove-visualization__body-annotations-section')
350
+ if (visible) {
351
+ expect(section).toBeInTheDocument()
352
+ expect(screen.getByRole('button', { name: 'Annotations' })).toBeVisible()
353
+ } else {
354
+ expect(section).not.toBeInTheDocument()
355
+ expect(screen.queryByRole('button', { name: 'Annotations' })).not.toBeInTheDocument()
356
+ }
357
+ }
358
+ }
359
+ )
360
+
87
361
  it('keeps chart footnotes visible when the data table is collapsed by default', async () => {
362
+ const table = nextRender(dataTableRender, props => props.expandDataTable === false)
88
363
  render(
89
364
  <CdcChart
90
365
  config={
@@ -112,15 +387,14 @@ describe('CdcChart data table dataset wiring', () => {
112
387
  />
113
388
  )
114
389
 
115
- await waitFor(() => {
116
- expect(dataTableProps.at(-1)?.expandDataTable).toBe(false)
117
- })
390
+ expect((await table).expandDataTable).toBe(false)
118
391
 
119
392
  expect(await screen.findByText('Legacy chart footnote')).toBeInTheDocument()
120
393
  expect(await screen.findByText('Structured chart footnote')).toBeInTheDocument()
121
394
  })
122
395
 
123
396
  it('passes custom category order to the chart-owned data table runtime data', async () => {
397
+ const table = nextRender(dataTableRender, props => props.runtimeData?.length === 4)
124
398
  render(
125
399
  <CdcChart
126
400
  config={
@@ -154,9 +428,51 @@ describe('CdcChart data table dataset wiring', () => {
154
428
  />
155
429
  )
156
430
 
157
- await waitFor(() => expect(dataTableProps.length).toBeGreaterThan(0))
431
+ expect((await table).runtimeData.map(row => row.category)).toEqual(['A', 'C', 'D', 'L'])
432
+ })
433
+
434
+ it('derives Box Plot table rows from prepared data before rendering the DataTable', async () => {
435
+ const formattedData = [
436
+ { Group: 'Group A', Score: '8' },
437
+ { Group: 'Group A', Score: '24' },
438
+ { Group: 'Group A', Score: '32' }
439
+ ]
440
+
441
+ const table = nextRender(dataTableRender, props => props.config?.boxplot?.plots?.length === 1)
442
+ render(
443
+ <CdcChart
444
+ config={
445
+ {
446
+ type: 'chart',
447
+ visualizationType: 'Box Plot',
448
+ title: 'Prepared Box Plot',
449
+ formattedData,
450
+ xAxis: { type: 'categorical', dataKey: 'Group' },
451
+ yAxis: { dataKey: 'Score' },
452
+ series: [{ dataKey: 'Score' }],
453
+ table: {
454
+ show: true,
455
+ expanded: true,
456
+ download: true,
457
+ label: 'Data Table',
458
+ indexLabel: ''
459
+ }
460
+ } as any
461
+ }
462
+ interactionLabel='chart-box-plot-table-test'
463
+ />
464
+ )
465
+
466
+ const tableProps = await table
467
+ expect(tableProps.config.boxplot.plots).toHaveLength(1)
158
468
 
159
- expect(dataTableProps.at(-1).runtimeData.map(row => row.category)).toEqual(['A', 'C', 'D', 'L'])
469
+ expect(tableProps.config.boxplot.categories).toEqual(['Group A'])
470
+ expect(tableProps.config.boxplot.plots[0]).toMatchObject({
471
+ columnCategory: 'Group A',
472
+ columnCount: 3,
473
+ columnMax: 32,
474
+ columnMin: 8
475
+ })
160
476
  })
161
477
 
162
478
  it('updates metadata-backed chart title and text when dataMetadata changes and data does not', async () => {
@@ -213,4 +529,162 @@ describe('CdcChart data table dataset wiring', () => {
213
529
  expect(container.textContent).toContain('Legend description June file')
214
530
  expect(screen.getAllByText('June file', { selector: 'strong' }).length).toBeGreaterThanOrEqual(2)
215
531
  })
532
+
533
+ it('parses inline markup in top y-axis titles', async () => {
534
+ const { container } = render(
535
+ <CdcChart
536
+ config={
537
+ {
538
+ type: 'chart',
539
+ visualizationType: 'Bar',
540
+ data: [{ category: 'A', value: 1 }],
541
+ xAxis: { dataKey: 'category' },
542
+ yAxis: {
543
+ titlePlacement: 'top',
544
+ label:
545
+ '<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><rect width="12" height="12" fill="#56b4e9"></rect></svg> Number of measles cases'
546
+ },
547
+ series: [{ dataKey: 'value' }],
548
+ table: {
549
+ show: false,
550
+ expanded: false,
551
+ label: 'Data Table',
552
+ indexLabel: ''
553
+ }
554
+ } as any
555
+ }
556
+ interactionLabel='chart-top-y-axis-title-test'
557
+ />
558
+ )
559
+
560
+ const topTitle = await screen.findByText('Number of measles cases')
561
+
562
+ expect(topTitle.closest('.y-axis-top-title')).toBeTruthy()
563
+ expect(container.querySelector('.y-axis-top-title svg rect')?.getAttribute('fill')).toBe('#56b4e9')
564
+ expect(container.querySelector('.y-axis-top-title')?.textContent).not.toContain('<svg')
565
+ })
566
+
567
+ it('renders a right-only top y-axis title above the chart and parses inline markup', async () => {
568
+ const { container } = render(
569
+ <CdcChart
570
+ config={
571
+ {
572
+ type: 'chart',
573
+ visualizationType: 'Combo',
574
+ data: [{ category: 'A', left: 1, right: 2 }],
575
+ xAxis: { dataKey: 'category' },
576
+ yAxis: {
577
+ titlePlacement: 'side',
578
+ label: 'Left title',
579
+ rightTitlePlacement: 'top',
580
+ rightLabel:
581
+ '<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><rect width="12" height="12" fill="#d55e00"></rect></svg> Right rate'
582
+ },
583
+ series: [
584
+ { dataKey: 'left', axis: 'Left', type: 'Bar' },
585
+ { dataKey: 'right', axis: 'Right', type: 'Line' }
586
+ ],
587
+ table: {
588
+ show: false,
589
+ expanded: false,
590
+ label: 'Data Table',
591
+ indexLabel: ''
592
+ }
593
+ } as any
594
+ }
595
+ interactionLabel='chart-top-right-y-axis-title-test'
596
+ />
597
+ )
598
+
599
+ const topTitle = await screen.findByText('Right rate')
600
+
601
+ expect(topTitle.closest('.y-axis-top-title--right')).toBeTruthy()
602
+ expect(container.querySelector('.y-axis-top-title--right svg rect')?.getAttribute('fill')).toBe('#d55e00')
603
+ expect(container.querySelector('.y-axis-top-title-row--split')).toBeFalsy()
604
+ })
605
+
606
+ it('processes markup variables in right top y-axis titles', async () => {
607
+ render(
608
+ <CdcChart
609
+ config={
610
+ {
611
+ type: 'chart',
612
+ visualizationType: 'Combo',
613
+ data: [{ category: 'A', left: 1, right: 2 }],
614
+ dataMetadata: { source: 'June file' },
615
+ enableMarkupVariables: true,
616
+ markupVariables: [
617
+ {
618
+ sourceType: 'metadata',
619
+ name: 'Source',
620
+ tag: '{{source}}',
621
+ metadataKey: 'source',
622
+ conditions: [],
623
+ addCommas: false
624
+ }
625
+ ],
626
+ xAxis: { dataKey: 'category' },
627
+ yAxis: {
628
+ titlePlacement: 'side',
629
+ label: 'Left title',
630
+ rightTitlePlacement: 'top',
631
+ rightLabel: 'Right {{source}} rate'
632
+ },
633
+ series: [
634
+ { dataKey: 'left', axis: 'Left', type: 'Bar' },
635
+ { dataKey: 'right', axis: 'Right', type: 'Line' }
636
+ ],
637
+ table: {
638
+ show: false,
639
+ expanded: false,
640
+ label: 'Data Table',
641
+ indexLabel: ''
642
+ }
643
+ } as any
644
+ }
645
+ interactionLabel='chart-top-right-y-axis-markup-variable-test'
646
+ />
647
+ )
648
+
649
+ expect(await screen.findByText('Right June file rate')).toBeInTheDocument()
650
+ })
651
+
652
+ it('renders left and right top y-axis titles in a shared split row', async () => {
653
+ const { container } = render(
654
+ <CdcChart
655
+ config={
656
+ {
657
+ type: 'chart',
658
+ visualizationType: 'Combo',
659
+ data: [{ category: 'A', left: 1, right: 2 }],
660
+ xAxis: { dataKey: 'category' },
661
+ yAxis: {
662
+ titlePlacement: 'top',
663
+ label: 'Left cases',
664
+ rightTitlePlacement: 'top',
665
+ rightLabel: 'Right rate'
666
+ },
667
+ series: [
668
+ { dataKey: 'left', axis: 'Left', type: 'Bar' },
669
+ { dataKey: 'right', axis: 'Right', type: 'Line' }
670
+ ],
671
+ table: {
672
+ show: false,
673
+ expanded: false,
674
+ label: 'Data Table',
675
+ indexLabel: ''
676
+ }
677
+ } as any
678
+ }
679
+ interactionLabel='chart-both-top-y-axis-title-test'
680
+ />
681
+ )
682
+
683
+ await screen.findByText('Left cases')
684
+ await screen.findByText('Right rate')
685
+
686
+ const titleRow = container.querySelector('.y-axis-top-title-row')
687
+ expect(titleRow?.classList.contains('y-axis-top-title-row--split')).toBe(true)
688
+ expect(titleRow?.querySelectorAll('.y-axis-top-title')).toHaveLength(2)
689
+ })
216
690
  })