@cdc/chart 4.26.6 → 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 (276) hide show
  1. package/CONFIG.md +504 -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 -96498
  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/flueview.json +22051 -0
  22. package/examples/private/overlay-legend.json +485 -0
  23. package/examples/private/pie-test.json +469 -0
  24. package/examples/vega-remote.json +22 -0
  25. package/package.json +11 -13
  26. package/src/CdcChart.tsx +2 -2
  27. package/src/CdcChartComponent.tsx +444 -217
  28. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  29. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  30. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  31. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  32. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  33. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  34. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  35. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  36. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  37. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  38. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  39. package/src/_stories/Chart.Network.stories.tsx +432 -0
  40. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  41. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  42. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  43. package/src/_stories/Chart.PieLabels.stories.tsx +49 -0
  44. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  45. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  46. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  47. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  48. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  49. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  50. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  51. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  52. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  53. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  54. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  55. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  56. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  57. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  58. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  59. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  60. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  61. package/src/_stories/_mock/editor-test-configs.json +20 -10
  62. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  63. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  64. package/src/_stories/_mock/heatmap.json +2 -2
  65. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  66. package/src/_stories/_mock/sankey_mock.json +23 -27
  67. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  68. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  69. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  70. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  71. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  72. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  73. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  74. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  75. package/src/components/Axis/BottomAxis.tsx +1 -1
  76. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  77. package/src/components/Axis/RightAxis.tsx +21 -13
  78. package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -8
  79. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +13 -4
  80. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  81. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +30 -26
  82. package/src/components/BarChart/components/BarChart.Vertical.tsx +38 -28
  83. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  84. package/src/components/BarChart/components/context.tsx +6 -0
  85. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  86. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  87. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  88. package/src/components/BarChart/helpers/index.ts +8 -0
  89. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  90. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  91. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  92. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  93. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +202 -0
  94. package/src/components/BarChart/helpers/useBarChart.ts +60 -60
  95. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  96. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  97. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  98. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  99. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  100. package/src/components/BarChartRace/helpers.test.ts +114 -0
  101. package/src/components/BarChartRace/helpers.ts +185 -0
  102. package/src/components/BarChartRace/index.ts +3 -0
  103. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  104. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  105. package/src/components/BoxPlot/helpers/index.ts +13 -83
  106. package/src/components/Brush/BrushSelector.tsx +10 -24
  107. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  108. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  109. package/src/components/Dendrogram/dendrogram.css +113 -0
  110. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  111. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  112. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  113. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  114. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  115. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  116. package/src/components/Dendrogram/index.tsx +1 -0
  117. package/src/components/Dendrogram/types/index.ts +66 -0
  118. package/src/components/EditorPanel/EditorPanel.tsx +1111 -805
  119. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  120. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  121. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  122. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  123. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +104 -61
  124. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  125. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  126. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +236 -36
  127. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  128. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  129. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +21 -6
  130. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  131. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  132. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  133. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  134. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  135. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  136. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  137. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  138. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +5 -4
  139. package/src/components/EditorPanel/useEditorPermissions.test.tsx +94 -0
  140. package/src/components/EditorPanel/useEditorPermissions.ts +75 -25
  141. package/src/components/Forecasting/Forecasting.tsx +16 -7
  142. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  143. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  144. package/src/components/HeatMap/heatmap.css +101 -2
  145. package/src/components/HeatMap/helpers/layout.ts +80 -0
  146. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  147. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  148. package/src/components/Legend/Legend.Component.tsx +163 -148
  149. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  150. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  151. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -35
  152. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  153. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  154. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  155. package/src/components/LineChart/helpers.test.ts +126 -38
  156. package/src/components/LineChart/helpers.ts +9 -6
  157. package/src/components/LineChart/index.tsx +71 -27
  158. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  159. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  160. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  161. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  162. package/src/components/LineChartRace/helpers.test.ts +88 -0
  163. package/src/components/LineChartRace/helpers.ts +157 -0
  164. package/src/components/LineChartRace/index.ts +3 -0
  165. package/src/components/LineChartRace/line-chart-race.css +16 -0
  166. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  167. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  168. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  169. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  170. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  171. package/src/components/LinearChart.tsx +85 -54
  172. package/src/components/Network/components/Network.tsx +372 -0
  173. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  174. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  175. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  176. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  177. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  178. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  179. package/src/components/Network/index.tsx +3 -0
  180. package/src/components/Network/network.css +107 -0
  181. package/src/components/Network/types/index.ts +56 -0
  182. package/src/components/PairedBarChart.jsx +5 -10
  183. package/src/components/PairedBarChart.test.tsx +71 -0
  184. package/src/components/PieChart/PieChart.tsx +121 -13
  185. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  186. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  187. package/src/components/RacingChartRenderer.tsx +44 -0
  188. package/src/components/Regions/components/Regions.test.tsx +179 -0
  189. package/src/components/Regions/components/Regions.tsx +104 -69
  190. package/src/components/Regions/index.tsx +2 -3
  191. package/src/components/Sankey/components/Sankey.tsx +264 -437
  192. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  193. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  194. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  195. package/src/components/Sankey/sankey.scss +57 -116
  196. package/src/components/Sankey/types/index.ts +41 -4
  197. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -12
  198. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  199. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  200. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  201. package/src/components/raceTiming.ts +18 -0
  202. package/src/data/initial-state.js +79 -31
  203. package/src/helpers/applyChartTypeContract.ts +71 -0
  204. package/src/helpers/axisLabelFontSize.ts +2 -2
  205. package/src/helpers/boxPlotStats.ts +60 -0
  206. package/src/helpers/bumpChartTooltip.ts +2 -1
  207. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  208. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  209. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  210. package/src/helpers/categoryOrder.ts +60 -0
  211. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  212. package/src/helpers/createNewChartConfig.ts +56 -0
  213. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  214. package/src/helpers/forecastLegend.ts +88 -0
  215. package/src/helpers/getBoxPlotConfig.ts +26 -38
  216. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  217. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  218. package/src/helpers/getColorScale.ts +31 -22
  219. package/src/helpers/getComboChartConfig.ts +0 -1
  220. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  221. package/src/helpers/getExcludedData.ts +0 -1
  222. package/src/helpers/getHasBoundarySuppression.ts +4 -3
  223. package/src/helpers/getMinMax.ts +11 -8
  224. package/src/helpers/getNewRuntime.ts +4 -3
  225. package/src/helpers/getPiePercent.ts +3 -3
  226. package/src/helpers/getStackedSeriesMax.ts +51 -0
  227. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  228. package/src/helpers/handleRankByValue.ts +15 -15
  229. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  230. package/src/helpers/seriesColumnSettings.ts +31 -3
  231. package/src/helpers/sizeHelpers.ts +4 -5
  232. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  233. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  234. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  235. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  236. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  237. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  238. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  239. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  240. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  241. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  242. package/src/helpers/tests/getColorScale.test.ts +226 -1
  243. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  244. package/src/helpers/tests/getMinMax.test.ts +41 -0
  245. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  246. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  247. package/src/helpers/tests/seriesColumnSettings.test.ts +83 -0
  248. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  249. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  250. package/src/helpers/tooltipHelpers.ts +16 -0
  251. package/src/hooks/tests/useRightAxis.test.tsx +143 -2
  252. package/src/hooks/tests/useScales.test.tsx +3 -6
  253. package/src/hooks/tests/useTooltip.test.tsx +484 -0
  254. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  255. package/src/hooks/useReduceData.ts +4 -36
  256. package/src/hooks/useRightAxis.ts +19 -26
  257. package/src/hooks/useScales.ts +15 -0
  258. package/src/hooks/useTooltip.tsx +106 -71
  259. package/src/scss/main.scss +74 -2
  260. package/src/store/chart.reducer.ts +4 -4
  261. package/src/test/CdcChart.dataTable.test.tsx +582 -14
  262. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  263. package/src/test/CdcChart.finalization.test.tsx +143 -0
  264. package/src/test/CdcChart.network.test.tsx +271 -0
  265. package/src/types/ChartConfig.ts +91 -54
  266. package/src/types/ChartContext.ts +5 -0
  267. package/src/types/Horizon.ts +1 -1
  268. package/LICENSE +0 -201
  269. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  270. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  271. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  272. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  273. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  274. package/src/data/legacy-defaults.ts +0 -18
  275. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  276. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
@@ -0,0 +1,630 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { scaleBand, scaleLinear } from '@visx/scale'
5
+ import LineChart from '..'
6
+ import ConfigContext from '../../../ConfigContext'
7
+ import { handleLineType } from '../../../helpers/handleLineType'
8
+ import { createMockChartContext } from '../../LinearChart/tests/mockConfigContext'
9
+
10
+ const data = [
11
+ { Date: 'Jan', value: 10 },
12
+ { Date: 'Feb', value: 20 }
13
+ ]
14
+
15
+ const lineEffectData = [
16
+ { Date: 'Jan', value: 10, Coverage: '' },
17
+ { Date: 'Feb', value: 20, Coverage: 'Limited Coverage' },
18
+ { Date: 'Mar', value: 30, Coverage: '' }
19
+ ]
20
+
21
+ const getDynamicCategoryConfig = () => {
22
+ const series = ['Alpha', 'Beta'].map(dataKey => ({
23
+ dataKey,
24
+ originalDataKey: 'Value',
25
+ dynamicCategory: 'Category',
26
+ type: 'Line',
27
+ axis: 'Left',
28
+ lineType: 'curveLinear'
29
+ }))
30
+
31
+ return {
32
+ series,
33
+ runtime: {
34
+ ...createMockChartContext().config.runtime,
35
+ series,
36
+ seriesKeys: ['Alpha', 'Beta'],
37
+ lineSeriesKeys: ['Alpha', 'Beta'],
38
+ seriesLabels: { Alpha: 'Alpha', Beta: 'Beta' }
39
+ }
40
+ }
41
+ }
42
+
43
+ const dynamicCategoryColorScale = label => (label === 'Alpha' ? '#005ea8' : '#a83279')
44
+
45
+ const getPathXCoordinates = (path: Element | null) =>
46
+ Array.from(path?.getAttribute('d')?.matchAll(/[ML](-?\d+(?:\.\d+)?)/g) || [], match => Number(match[1]))
47
+
48
+ const renderLineChart = (configOverrides = {}, contextOverrides = {}, xScaleOverride?) => {
49
+ const series = [{ dataKey: 'value', type: 'Line', axis: 'Left', lineType: 'curveLinear' }]
50
+ const transformedData = (contextOverrides as any).transformedData ?? data
51
+ const context = createMockChartContext(
52
+ {
53
+ visualizationType: 'Line',
54
+ preliminaryData: [],
55
+ confidenceKeys: {},
56
+ visual: {
57
+ lineDatapointSymbol: 'circle',
58
+ maximumShapeAmount: 6
59
+ },
60
+ lineDatapointStyle: 'hover',
61
+ lineDatapointColor: 'Same as Line',
62
+ xAxis: {
63
+ ...createMockChartContext().config.xAxis,
64
+ type: 'categorical',
65
+ dataKey: 'Date'
66
+ },
67
+ series,
68
+ runtime: {
69
+ ...createMockChartContext().config.runtime,
70
+ series,
71
+ seriesKeys: ['value'],
72
+ lineSeriesKeys: ['value'],
73
+ seriesLabels: { value: 'Value' }
74
+ },
75
+ ...configOverrides
76
+ },
77
+ {
78
+ colorScale: () => '#005ea8',
79
+ tableData: data,
80
+ transformedData: data,
81
+ yAxisDomainData: data,
82
+ ...contextOverrides
83
+ } as any
84
+ )
85
+
86
+ const xScale = xScaleOverride || scaleBand({ domain: transformedData.map(row => row.Date), range: [0, 400] })
87
+ const yScale = scaleLinear({ domain: [0, 100], range: [300, 0] })
88
+
89
+ return render(
90
+ <ConfigContext.Provider value={context as any}>
91
+ <svg>
92
+ <LineChart
93
+ xScale={xScale}
94
+ yScale={yScale}
95
+ yAxisWidth={50}
96
+ getXAxisData={d => d.Date}
97
+ getYAxisData={(d, seriesKey) => Number(d[seriesKey])}
98
+ xMax={400}
99
+ yMax={300}
100
+ handleTooltipMouseOver={() => {}}
101
+ handleTooltipMouseOff={() => {}}
102
+ handleTooltipClick={() => {}}
103
+ tooltipData={undefined}
104
+ showTooltip={false}
105
+ seriesStyle={undefined as any}
106
+ svgRef={{ current: null }}
107
+ />
108
+ </svg>
109
+ </ConfigContext.Provider>
110
+ )
111
+ }
112
+
113
+ const renderLineChartWithPathMeasurements = (configOverrides, contextOverrides) => {
114
+ const pathPrototype = Object.getPrototypeOf(document.createElementNS('http://www.w3.org/2000/svg', 'path'))
115
+ const totalLengthDescriptor = Object.getOwnPropertyDescriptor(pathPrototype, 'getTotalLength')
116
+ const pointAtLengthDescriptor = Object.getOwnPropertyDescriptor(pathPrototype, 'getPointAtLength')
117
+
118
+ Object.defineProperty(pathPrototype, 'getTotalLength', { configurable: true, value: () => 200 })
119
+ Object.defineProperty(pathPrototype, 'getPointAtLength', {
120
+ configurable: true,
121
+ value: distance => ({
122
+ x: 66.66666666666667 + (distance / 100) * 133.33333333333334,
123
+ y: 270 - (distance / 100) * 30
124
+ })
125
+ })
126
+
127
+ try {
128
+ return renderLineChart(configOverrides, contextOverrides)
129
+ } finally {
130
+ if (totalLengthDescriptor) {
131
+ Object.defineProperty(pathPrototype, 'getTotalLength', totalLengthDescriptor)
132
+ } else {
133
+ delete pathPrototype.getTotalLength
134
+ }
135
+
136
+ if (pointAtLengthDescriptor) {
137
+ Object.defineProperty(pathPrototype, 'getPointAtLength', pointAtLengthDescriptor)
138
+ } else {
139
+ delete pathPrototype.getPointAtLength
140
+ }
141
+ }
142
+ }
143
+
144
+ describe('LineChart', () => {
145
+ it('does not render confidence interval areas when confidenceKeys is empty', () => {
146
+ const { container } = renderLineChart()
147
+
148
+ expect(container.querySelectorAll('path.visx-linepath')).toHaveLength(1)
149
+ expect(container.querySelectorAll('path.visx-area-closed')).toHaveLength(0)
150
+ })
151
+
152
+ it('starts a new effect segment at its matching row', () => {
153
+ const { container } = renderLineChartWithPathMeasurements(
154
+ {
155
+ preliminaryData: [
156
+ {
157
+ type: 'effect',
158
+ lineEffectScope: 'outgoing',
159
+ seriesKeys: ['value'],
160
+ label: 'Limited Coverage',
161
+ column: 'Coverage',
162
+ value: 'Limited Coverage',
163
+ style: 'dashed'
164
+ }
165
+ ]
166
+ },
167
+ {
168
+ tableData: lineEffectData,
169
+ transformedData: lineEffectData,
170
+ handleLineType: style => (style === 'dashed' ? '5 5' : '')
171
+ }
172
+ )
173
+ const dashedPath = container.querySelector('path[stroke-dasharray="5 5"]')
174
+
175
+ expect(dashedPath).not.toBeNull()
176
+ expect(dashedPath.getAttribute('d')).toMatch(/^M\s*200[, ]/)
177
+ })
178
+
179
+ it('does not render an effect segment for a matching final row', () => {
180
+ const dataWithFinalEffect = lineEffectData.map((row, index) => ({
181
+ ...row,
182
+ Coverage: index === lineEffectData.length - 1 ? 'Limited Coverage' : ''
183
+ }))
184
+ const { container } = renderLineChartWithPathMeasurements(
185
+ {
186
+ preliminaryData: [
187
+ {
188
+ type: 'effect',
189
+ lineEffectScope: 'outgoing',
190
+ seriesKeys: ['value'],
191
+ label: 'Limited Coverage',
192
+ column: 'Coverage',
193
+ value: 'Limited Coverage',
194
+ style: 'dashed'
195
+ }
196
+ ]
197
+ },
198
+ {
199
+ tableData: dataWithFinalEffect,
200
+ transformedData: dataWithFinalEffect,
201
+ handleLineType: style => (style === 'dashed' ? '5 5' : '')
202
+ }
203
+ )
204
+
205
+ expect(container.querySelector('path[stroke-dasharray="5 5"]')).toBeNull()
206
+ })
207
+
208
+ it.each([
209
+ ['first preliminary point', ['', 'Limited Coverage', 'Limited Coverage'], 0],
210
+ ['latest preliminary point', ['', '', 'Limited Coverage'], 1]
211
+ ])('preserves the incoming dashed segment for an existing rule at the %s', (_, coverage, segmentIndex) => {
212
+ const preliminaryData = lineEffectData.map((row, index) => ({ ...row, Coverage: coverage[index] }))
213
+ const { container } = renderLineChartWithPathMeasurements(
214
+ {
215
+ preliminaryData: [
216
+ {
217
+ type: 'effect',
218
+ seriesKeys: ['value'],
219
+ column: 'Coverage',
220
+ value: 'Limited Coverage',
221
+ style: 'dashed'
222
+ }
223
+ ]
224
+ },
225
+ {
226
+ tableData: preliminaryData,
227
+ transformedData: preliminaryData,
228
+ handleLineType: style => (style === 'dashed' ? '5 5' : '')
229
+ }
230
+ )
231
+ const incomingPath = container.querySelectorAll('path.visx-linepath')[segmentIndex]
232
+ const coordinates = getPathXCoordinates(incomingPath)
233
+
234
+ expect(incomingPath).toHaveAttribute('stroke-dasharray', '5 5')
235
+ expect(coordinates[0]).toBeCloseTo(segmentIndex === 0 ? 66.667 : 200, 2)
236
+ expect(coordinates.at(-1)).toBeGreaterThan(segmentIndex === 0 ? 198 : 332)
237
+ })
238
+
239
+ it('renders effect circles only for rows in the brushed data subset', () => {
240
+ const tableData = [
241
+ { Date: 'Jan', value: 10, Coverage: 'Limited Coverage' },
242
+ { Date: 'Feb', value: 20, Coverage: 'Limited Coverage' }
243
+ ]
244
+ const transformedData = [tableData[1]]
245
+
246
+ const { container } = renderLineChart(
247
+ {
248
+ preliminaryData: [
249
+ {
250
+ type: 'effect',
251
+ seriesKeys: ['value'],
252
+ label: 'Limited Coverage',
253
+ column: 'Coverage',
254
+ value: 'Limited Coverage',
255
+ style: 'Open Circles',
256
+ circleSize: 4
257
+ }
258
+ ]
259
+ },
260
+ { brushData: transformedData, tableData, transformedData, yAxisDomainData: tableData }
261
+ )
262
+
263
+ const effectCircles = Array.from(container.querySelectorAll('.line-chart-group > g > circle'))
264
+
265
+ expect(effectCircles).toHaveLength(1)
266
+ expect(Number.isFinite(Number(effectCircles[0].getAttribute('cx')))).toBe(true)
267
+ })
268
+
269
+ it('does not render suppression segments for rows outside the brushed data subset', () => {
270
+ const tableData = [
271
+ { Date: 'Jan', value: 'Suppressed' },
272
+ { Date: 'Feb', value: 20 },
273
+ { Date: 'Mar', value: 30 }
274
+ ]
275
+ const brushData = tableData.slice(1)
276
+
277
+ const { container } = renderLineChart(
278
+ {
279
+ preliminaryData: [
280
+ {
281
+ type: 'suppression',
282
+ seriesKeys: ['value'],
283
+ label: 'Suppressed',
284
+ column: 'value',
285
+ value: 'Suppressed',
286
+ style: 'dashed',
287
+ hideLineStyle: false
288
+ }
289
+ ]
290
+ },
291
+ {
292
+ brushData,
293
+ tableData,
294
+ transformedData: brushData,
295
+ yAxisDomainData: tableData,
296
+ handleLineType: style => (style === 'dashed' ? '5 5' : '')
297
+ }
298
+ )
299
+
300
+ expect(container.querySelectorAll('path[stroke-dasharray="5 5"]')).toHaveLength(0)
301
+ })
302
+
303
+ it('uses raw brushed rows to render selected suppression segments', () => {
304
+ const tableData = [
305
+ { Date: 'Jan', value: 10 },
306
+ { Date: 'Feb', value: 'Suppressed' },
307
+ { Date: 'Mar', value: 30 }
308
+ ]
309
+ const brushData = tableData.slice(1)
310
+ const transformedData = brushData.map(row => ({ ...row, value: row.value === 'Suppressed' ? '' : row.value }))
311
+
312
+ const { container } = renderLineChart(
313
+ {
314
+ preliminaryData: [
315
+ {
316
+ type: 'suppression',
317
+ seriesKeys: ['value'],
318
+ label: 'Suppressed',
319
+ column: 'value',
320
+ value: 'Suppressed',
321
+ style: 'dashed',
322
+ hideLineStyle: false
323
+ }
324
+ ]
325
+ },
326
+ {
327
+ brushData,
328
+ tableData,
329
+ transformedData,
330
+ yAxisDomainData: tableData,
331
+ handleLineType: style => (style === 'dashed' ? '5 5' : '')
332
+ }
333
+ )
334
+
335
+ const suppressionSegments = Array.from(container.querySelectorAll('path[stroke-dasharray="5 5"]'))
336
+
337
+ expect(suppressionSegments).toHaveLength(1)
338
+ expect(suppressionSegments[0].getAttribute('d')).not.toContain('NaN')
339
+ })
340
+
341
+ it('renders an effect marker only for its selected dynamic category in the brushed rows', () => {
342
+ const tableData = [
343
+ { Date: 'Jan', Category: 'Alpha', Value: 10, Coverage: 'Standard' },
344
+ { Date: 'Jan', Category: 'Beta', Value: 15, Coverage: 'Standard' },
345
+ { Date: 'Feb', Category: 'Alpha', Value: 20, Coverage: 'Limited Coverage' },
346
+ { Date: 'Feb', Category: 'Beta', Value: 25, Coverage: 'Limited Coverage' }
347
+ ]
348
+ const brushData = tableData.filter(row => row.Date === 'Feb')
349
+
350
+ const { container } = renderLineChart(
351
+ {
352
+ ...getDynamicCategoryConfig(),
353
+ preliminaryData: [
354
+ {
355
+ type: 'effect',
356
+ seriesKeys: ['Alpha'],
357
+ label: 'Limited Coverage',
358
+ column: 'Coverage',
359
+ value: 'Limited Coverage',
360
+ style: 'Open Circles',
361
+ circleSize: 4
362
+ }
363
+ ]
364
+ },
365
+ {
366
+ brushData,
367
+ colorScale: dynamicCategoryColorScale,
368
+ tableData,
369
+ transformedData: brushData,
370
+ yAxisDomainData: tableData
371
+ }
372
+ )
373
+
374
+ const effectCircles = Array.from(container.querySelectorAll('.line-chart-group > g > circle[r="4"]'))
375
+
376
+ expect(effectCircles).toHaveLength(1)
377
+ expect(effectCircles[0]).toHaveAttribute('stroke', '#005ea8')
378
+ expect(Number.isFinite(Number(effectCircles[0].getAttribute('cx')))).toBe(true)
379
+ })
380
+
381
+ it('renders a suppression segment only for its selected dynamic category in the brushed rows', () => {
382
+ const tableData = [
383
+ { Date: 'Jan', Category: 'Alpha', Value: 10 },
384
+ { Date: 'Jan', Category: 'Beta', Value: 15 },
385
+ { Date: 'Feb', Category: 'Alpha', Value: 'Suppressed' },
386
+ { Date: 'Feb', Category: 'Beta', Value: 'Suppressed' },
387
+ { Date: 'Mar', Category: 'Alpha', Value: 30 },
388
+ { Date: 'Mar', Category: 'Beta', Value: 35 },
389
+ { Date: 'Apr', Category: 'Alpha', Value: 40 },
390
+ { Date: 'Apr', Category: 'Beta', Value: 45 }
391
+ ]
392
+ const brushData = tableData.filter(row => row.Date !== 'Apr')
393
+ const transformedData = brushData.map(row => ({
394
+ ...row,
395
+ Value: row.Value === 'Suppressed' ? '' : row.Value
396
+ }))
397
+
398
+ const { container } = renderLineChart(
399
+ {
400
+ ...getDynamicCategoryConfig(),
401
+ preliminaryData: [
402
+ {
403
+ type: 'suppression',
404
+ seriesKeys: ['Alpha'],
405
+ label: 'Suppressed',
406
+ column: 'Alpha',
407
+ value: 'Suppressed',
408
+ style: 'dashed',
409
+ hideLineStyle: false
410
+ }
411
+ ]
412
+ },
413
+ {
414
+ brushData,
415
+ colorScale: dynamicCategoryColorScale,
416
+ tableData,
417
+ transformedData,
418
+ yAxisDomainData: tableData,
419
+ handleLineType: style => (style === 'dashed' ? '5 5' : '')
420
+ }
421
+ )
422
+
423
+ const suppressionSegments = Array.from(container.querySelectorAll('path[stroke-dasharray="5 5"]'))
424
+
425
+ expect(suppressionSegments).toHaveLength(1)
426
+ expect(suppressionSegments[0]).toHaveAttribute('stroke', '#005ea8')
427
+ expect(suppressionSegments[0].getAttribute('d')).not.toContain('NaN')
428
+ })
429
+
430
+ it.each([
431
+ ['Line', '0'],
432
+ ['dashed-sm', '5 5'],
433
+ ['dashed-md', '10 5'],
434
+ ['dashed-lg', '15 5']
435
+ ])('preserves the %s series style and weight during Line race playback', (type, dashPattern) => {
436
+ const series = [{ dataKey: 'value', type, weight: 7, axis: 'Left', lineType: 'curveLinear' }]
437
+ const { container } = renderLineChart(
438
+ {
439
+ series,
440
+ runtime: {
441
+ ...createMockChartContext().config.runtime,
442
+ series,
443
+ seriesKeys: ['value'],
444
+ lineSeriesKeys: ['value'],
445
+ seriesLabels: { value: 'Value' }
446
+ }
447
+ },
448
+ { handleLineType, lineRaceProgress: 0.4 }
449
+ )
450
+
451
+ const visiblePath = container.querySelector('[data-line-race-series-path]')
452
+ const revealPath = container.querySelector('[data-line-race-reveal-path]')
453
+
454
+ expect(visiblePath).toHaveAttribute('stroke-dasharray', dashPattern)
455
+ expect(visiblePath).toHaveAttribute('stroke-width', '7')
456
+ expect(visiblePath?.getAttribute('mask')).toMatch(/^url\(#line-race-reveal-[^)]+\)$/)
457
+ expect(visiblePath).not.toHaveAttribute('pathLength')
458
+ expect(revealPath).toHaveAttribute('stroke-dasharray', '0.4 1')
459
+ expect(revealPath).toHaveAttribute('pathLength', '1')
460
+ })
461
+
462
+ it.each([
463
+ [0, '0 1'],
464
+ [1, '1 1'],
465
+ [-0.5, '0 1'],
466
+ [1.5, '1 1'],
467
+ [Number.NaN, '0 1']
468
+ ])('normalizes Line race progress %s without changing authored styling', (progress, revealPattern) => {
469
+ const series = [{ dataKey: 'value', type: 'dashed-sm', weight: 4, axis: 'Left', lineType: 'curveLinear' }]
470
+ const { container } = renderLineChart(
471
+ {
472
+ series,
473
+ runtime: {
474
+ ...createMockChartContext().config.runtime,
475
+ series,
476
+ seriesKeys: ['value'],
477
+ lineSeriesKeys: ['value'],
478
+ seriesLabels: { value: 'Value' }
479
+ }
480
+ },
481
+ { handleLineType, lineRaceProgress: progress }
482
+ )
483
+
484
+ expect(container.querySelector('[data-line-race-series-path]')).toHaveAttribute('stroke-dasharray', '5 5')
485
+ expect(container.querySelector('[data-line-race-series-path]')).toHaveAttribute('stroke-width', '4')
486
+ expect(container.querySelector('[data-line-race-reveal-path]')).toHaveAttribute('stroke-dasharray', revealPattern)
487
+ })
488
+
489
+ it('uses independent reveal masks for differently styled Line race series', () => {
490
+ const raceData = [
491
+ { Date: 'Jan', alpha: 10, beta: 15 },
492
+ { Date: 'Feb', alpha: 20, beta: 25 }
493
+ ]
494
+ const series = [
495
+ { dataKey: 'alpha', type: 'Line', weight: 3, axis: 'Left', lineType: 'curveLinear' },
496
+ { dataKey: 'beta', type: 'dashed-md', weight: 8, axis: 'Left', lineType: 'curveLinear' }
497
+ ]
498
+ const { container } = renderLineChart(
499
+ {
500
+ series,
501
+ runtime: {
502
+ ...createMockChartContext().config.runtime,
503
+ series,
504
+ seriesKeys: ['alpha', 'beta'],
505
+ lineSeriesKeys: ['alpha', 'beta'],
506
+ seriesLabels: { alpha: 'Alpha', beta: 'Beta' }
507
+ }
508
+ },
509
+ {
510
+ handleLineType,
511
+ lineRaceProgress: 0.5,
512
+ tableData: raceData,
513
+ transformedData: raceData,
514
+ yAxisDomainData: raceData
515
+ }
516
+ )
517
+
518
+ const visiblePaths = Array.from(container.querySelectorAll('[data-line-race-series-path]'))
519
+ const maskReferences = visiblePaths.map(path => path.getAttribute('mask'))
520
+
521
+ expect(visiblePaths[0]).toHaveAttribute('stroke-dasharray', '0')
522
+ expect(visiblePaths[0]).toHaveAttribute('stroke-width', '3')
523
+ expect(visiblePaths[1]).toHaveAttribute('stroke-dasharray', '10 5')
524
+ expect(visiblePaths[1]).toHaveAttribute('stroke-width', '8')
525
+ expect(new Set(maskReferences).size).toBe(2)
526
+ maskReferences.forEach(reference => {
527
+ const maskId = reference?.match(/^url\(#(.+)\)$/)?.[1]
528
+ expect(maskId).toBeTruthy()
529
+ expect(container.querySelector(`mask[id="${maskId}"]`)).not.toBeNull()
530
+ })
531
+ })
532
+
533
+ it('applies one collision-safe reveal mask per dynamic-category Line race series', () => {
534
+ const raceData = [
535
+ { Date: 'Jan', Category: 'Alpha', Value: 10 },
536
+ { Date: 'Jan', Category: 'Beta', Value: 15 },
537
+ { Date: 'Feb', Category: 'Alpha', Value: 20 },
538
+ { Date: 'Feb', Category: 'Beta', Value: 25 }
539
+ ]
540
+ const dynamicConfig = getDynamicCategoryConfig()
541
+ const series = dynamicConfig.series.map(item => ({ ...item, type: 'dashed-lg', weight: 6 }))
542
+ const { container } = renderLineChart(
543
+ {
544
+ ...dynamicConfig,
545
+ series,
546
+ runtime: { ...dynamicConfig.runtime, series }
547
+ },
548
+ {
549
+ colorScale: dynamicCategoryColorScale,
550
+ handleLineType,
551
+ lineRaceProgress: 0.75,
552
+ tableData: raceData,
553
+ transformedData: raceData,
554
+ yAxisDomainData: raceData
555
+ }
556
+ )
557
+
558
+ const visiblePaths = Array.from(container.querySelectorAll('[data-line-race-series-path]'))
559
+ const maskReferences = visiblePaths.map(path => path.getAttribute('mask'))
560
+
561
+ expect(visiblePaths).toHaveLength(2)
562
+ visiblePaths.forEach(path => {
563
+ expect(path).toHaveAttribute('stroke-dasharray', '15 5')
564
+ expect(path).toHaveAttribute('stroke-width', '6')
565
+ })
566
+ expect(new Set(maskReferences).size).toBe(2)
567
+ })
568
+
569
+ it.each([
570
+ ['date band domain', 'date', scaleBand({ domain: ['2022-01-01', '2021-01-01', '2020-01-01'], range: [0, 400] })],
571
+ ['date-time reversed range', 'date-time', scaleLinear({ domain: [2020, 2022], range: [400, 0] })]
572
+ ])('orders newest-first Line race geometry by rendered position for a %s', (_strategy, axisType, xScale) => {
573
+ const raceData = [
574
+ { Date: axisType === 'date' ? '2020-01-01' : 2020, value: 10 },
575
+ { Date: axisType === 'date' ? '2021-01-01' : 2021, value: 20 },
576
+ { Date: axisType === 'date' ? '2022-01-01' : 2022, value: 30 }
577
+ ]
578
+ const sourceOrder = raceData.map(row => row.Date)
579
+ const { container } = renderLineChart(
580
+ { xAxis: { ...createMockChartContext().config.xAxis, type: axisType, dataKey: 'Date' } },
581
+ {
582
+ lineRaceProgress: 0.5,
583
+ tableData: raceData,
584
+ transformedData: raceData,
585
+ yAxisDomainData: raceData
586
+ },
587
+ xScale
588
+ )
589
+
590
+ const visiblePath = container.querySelector('[data-line-race-series-path]')
591
+ const revealPath = container.querySelector('[data-line-race-reveal-path]')
592
+ const xCoordinates = getPathXCoordinates(visiblePath)
593
+
594
+ expect(xCoordinates).toHaveLength(3)
595
+ expect(xCoordinates[0]).toBeLessThan(xCoordinates[1])
596
+ expect(xCoordinates[1]).toBeLessThan(xCoordinates[2])
597
+ expect(revealPath?.getAttribute('d')).toBe(visiblePath?.getAttribute('d'))
598
+ expect(raceData.map(row => row.Date)).toEqual(sourceOrder)
599
+ })
600
+
601
+ it('orders categorical Line race geometry by the rendered custom category order', () => {
602
+ const raceData = [
603
+ { Date: 'Jan', value: 10 },
604
+ { Date: 'Feb', value: 20 },
605
+ { Date: 'Mar', value: 30 }
606
+ ]
607
+ const sourceOrder = raceData.map(row => row.Date)
608
+ const customOrderScale = scaleBand({ domain: ['Mar', 'Jan', 'Feb'], range: [0, 400] })
609
+ const { container } = renderLineChart(
610
+ {},
611
+ {
612
+ lineRaceProgress: 0.5,
613
+ tableData: raceData,
614
+ transformedData: raceData,
615
+ yAxisDomainData: raceData
616
+ },
617
+ customOrderScale
618
+ )
619
+
620
+ const visiblePath = container.querySelector('[data-line-race-series-path]')
621
+ const revealPath = container.querySelector('[data-line-race-reveal-path]')
622
+ const xCoordinates = getPathXCoordinates(visiblePath)
623
+
624
+ expect(xCoordinates).toHaveLength(3)
625
+ expect(xCoordinates[0]).toBeLessThan(xCoordinates[1])
626
+ expect(xCoordinates[1]).toBeLessThan(xCoordinates[2])
627
+ expect(revealPath?.getAttribute('d')).toBe(visiblePath?.getAttribute('d'))
628
+ expect(raceData.map(row => row.Date)).toEqual(sourceOrder)
629
+ })
630
+ })