@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,13 +1,10 @@
1
1
  import capitalize from 'lodash/capitalize'
2
- import chain from 'lodash/chain'
3
- import filter from 'lodash/filter'
4
2
  import flatMap from 'lodash/flatMap'
5
3
  import map from 'lodash/map'
6
- import min from 'lodash/min'
7
4
  import round from 'lodash/round'
8
5
  import uniq from 'lodash/uniq'
6
+ import { calculateBoxPlotStats } from './boxPlotStats'
9
7
  import { ChartConfig } from '../types/ChartConfig'
10
- import * as d3 from 'd3-array'
11
8
 
12
9
  export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
13
10
  const combinedData = data
@@ -20,44 +17,28 @@ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
20
17
  try {
21
18
  if (!g) throw new Error('No groups resolved in box plots')
22
19
 
23
- // Start handle operations on combinedData
24
- const { count, sortedData } = chain(combinedData)
25
- // Filter by xAxis data key
26
- .filter(item => item[newConfig.xAxis.dataKey] === g)
27
- // perform multiple operations on the filtered data
28
- .thru(filteredData => ({
29
- count: filteredData.length,
30
- sortedData: map(filteredData, item => Number(item[seriesKey])).sort()
31
- }))
32
- // get the results from the chain
33
- .value()
20
+ const filteredData = combinedData.filter(item => item[newConfig.xAxis.dataKey] === g)
21
+ const stats = calculateBoxPlotStats(map(filteredData, item => item[seriesKey]))
34
22
 
35
- if (!sortedData) throw new Error('boxplots dont have data yet')
23
+ if (!stats) throw new Error('boxplots dont have data yet')
36
24
  if (!plots) throw new Error('boxplots dont have plots yet')
37
25
 
38
- const q1 = d3.quantile(sortedData, 0.25)
39
- const q3 = d3.quantile(sortedData, 0.75)
40
-
41
- const iqr = q3 - q1
42
- const lowerBounds = q1 - 1.5 * iqr
43
- const upperBounds = q3 + 1.5 * iqr
44
- const nonOutliers = sortedData.filter(value => value >= lowerBounds && value <= upperBounds)
45
26
  plots.push({
46
27
  columnCategory: g,
47
- columnMax: d3.max(nonOutliers),
48
- columnThirdQuartile: round(q3, newConfig.dataFormat.roundTo),
49
- columnMedian: Number(d3.median(sortedData)).toFixed(newConfig.dataFormat.roundTo),
50
- columnFirstQuartile: round(q1, newConfig.dataFormat.roundTo),
51
- columnMin: min(nonOutliers),
52
- columnCount: count,
53
- columnSd: Number(d3.deviation(sortedData)).toFixed(newConfig.dataFormat.roundTo),
54
- columnMean: Number(d3.mean(sortedData)).toFixed(newConfig.dataFormat.roundTo),
55
- columnIqr: round(iqr, newConfig.dataFormat.roundTo),
56
- values: sortedData,
57
- columnLowerBounds: lowerBounds,
58
- columnUpperBounds: upperBounds,
59
- columnOutliers: filter(sortedData, value => value < lowerBounds || value > upperBounds),
60
- columnNonOutliers: filter(sortedData, value => value >= lowerBounds && value <= upperBounds)
28
+ columnMax: stats.whiskerMax,
29
+ columnThirdQuartile: round(stats.q3, newConfig.dataFormat.roundTo),
30
+ columnMedian: Number(stats.median).toFixed(newConfig.dataFormat.roundTo),
31
+ columnFirstQuartile: round(stats.q1, newConfig.dataFormat.roundTo),
32
+ columnMin: stats.whiskerMin,
33
+ columnCount: stats.values.length,
34
+ columnSd: Number(stats.deviation).toFixed(newConfig.dataFormat.roundTo),
35
+ columnMean: Number(stats.mean).toFixed(newConfig.dataFormat.roundTo),
36
+ columnIqr: round(stats.iqr, newConfig.dataFormat.roundTo),
37
+ values: stats.values,
38
+ columnLowerBounds: stats.lowerBound,
39
+ columnUpperBounds: stats.upperBound,
40
+ columnOutliers: stats.outliers,
41
+ columnNonOutliers: stats.nonOutliers
61
42
  })
62
43
  } catch (e) {
63
44
  console.error('COVE: ', e.message) // eslint-disable-line
@@ -0,0 +1,29 @@
1
+ import { Palette } from '@cdc/core/types/Palette'
2
+
3
+ const paletteDefaultsByVisualizationType: Record<string, Palette> = {
4
+ Line: {
5
+ isReversed: false,
6
+ version: '2.1',
7
+ name: 'qualitative_standard'
8
+ },
9
+ 'Horizon Chart': {
10
+ isReversed: false,
11
+ version: '2.1',
12
+ name: 'sequential_blue'
13
+ },
14
+ HeatMap: {
15
+ isReversed: false,
16
+ version: '2.1',
17
+ name: 'sequential_blue'
18
+ },
19
+ Sankey: {
20
+ isReversed: true,
21
+ version: '2.1',
22
+ name: 'sequential_bluereverse'
23
+ }
24
+ }
25
+
26
+ export const getChartTypeDefaultPalette = (visualizationType?: string): Palette | undefined => {
27
+ const palette = visualizationType ? paletteDefaultsByVisualizationType[visualizationType] : undefined
28
+ return palette ? { ...palette } : undefined
29
+ }
@@ -0,0 +1,26 @@
1
+ import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
2
+ import { ChartConfig } from '../types/ChartConfig'
3
+ import { getV21ChartDistributionColors } from './getV21ChartDistributionColors'
4
+
5
+ export const getColorCodeCategoryColorMap = (
6
+ config: ChartConfig,
7
+ tableData: Record<string, unknown>[],
8
+ colorPalettes: Record<string, string[]>
9
+ ): Map<unknown, string> => {
10
+ const colorCode = config.legend?.colorCode
11
+ if (!colorCode || !Array.isArray(tableData)) return new Map()
12
+
13
+ let palette = getPaletteColors(config, colorPalettes)
14
+ if (!palette.length) return new Map()
15
+
16
+ const categoryColors = new Map<unknown, string>()
17
+
18
+ const categories = Array.from(new Set(tableData.map(row => row[colorCode])))
19
+ palette = getV21ChartDistributionColors(config, palette, categories.length) ?? palette
20
+
21
+ categories.forEach((category, index) => {
22
+ categoryColors.set(category, palette[index % palette.length])
23
+ })
24
+
25
+ return categoryColors
26
+ }
@@ -1,6 +1,6 @@
1
1
  import { twoColorPalette } from '@cdc/core/data/colorPalettes'
2
2
  import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
3
- import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
3
+ import { getColorPaletteMajorVersion } from '@cdc/core/helpers/getColorPaletteMajorVersion'
4
4
  import { scaleOrdinal } from '@visx/scale'
5
5
  import { ChartConfig } from '../types/ChartConfig'
6
6
  import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
@@ -10,7 +10,17 @@ import {
10
10
  divergentColorDistribution,
11
11
  colorblindColorDistribution
12
12
  } from '@cdc/core/helpers/palettes/colorDistributions'
13
+ import { isValidPaletteColor } from '@cdc/core/helpers/palettes/colorValidation'
13
14
  import { applySeriesColorAssignmentsToRange } from './colorAssignmentHelpers'
15
+ import { getV21ChartDistributionColors } from './getV21ChartDistributionColors'
16
+
17
+ const INVALID_CUSTOM_COLOR_FALLBACK = '#000000'
18
+
19
+ const normalizeCustomColors = (colors: unknown): string[] => {
20
+ if (!Array.isArray(colors) || colors.length === 0) return []
21
+
22
+ return colors.map(color => (isValidPaletteColor(color) ? color.trim() : INVALID_CUSTOM_COLOR_FALLBACK))
23
+ }
14
24
 
15
25
  export const getColorScale = (config: ChartConfig): ((value: string) => string) => {
16
26
  const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
@@ -19,7 +29,7 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
19
29
  const colorPalettes = filterChartColorPalettes(config)
20
30
 
21
31
  // Get the correct version of two-color palettes
22
- const version = getColorPaletteVersion(config)
32
+ const version = getColorPaletteMajorVersion(config)
23
33
  const versionKey = `v${version}`
24
34
  const versionedTwoColorPalette = twoColorPalette[versionKey] || twoColorPalette.v2
25
35
 
@@ -28,17 +38,24 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
28
38
  ? versionedTwoColorPalette
29
39
  : colorPalettes
30
40
 
31
- const allPalettes: Record<string, string[]> = { ...versionedTwoColorPalette, ...colorPalettes }
32
-
33
41
  // Migrate old palette name if needed
34
42
  const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
43
+ let namedPalette =
44
+ palettesSource[migratePaletteWithMap(migratedPaletteName, paletteMigrationMap, false)] ||
45
+ palettesSource[configPalette]
46
+
47
+ // Use a visible sentinel when saved palette config cannot resolve.
48
+ if (!namedPalette) {
49
+ console.warn(`Palette "${configPalette}" not found, falling back to black`)
50
+ namedPalette = [INVALID_CUSTOM_COLOR_FALLBACK]
51
+ }
35
52
 
36
53
  const domain = config.runtime.seriesLabelsAll
37
54
 
38
- const customColorsOrdered = config.general?.palette?.customColorsOrdered
55
+ const customColorsOrdered = normalizeCustomColors(config.general?.palette?.customColorsOrdered)
39
56
 
40
57
  // Check for customColorsOrdered first (direct 1-to-1 mapping, no distribution needed)
41
- if (Array.isArray(customColorsOrdered) && customColorsOrdered.length > 0) {
58
+ if (customColorsOrdered.length > 0) {
42
59
  const range = applySeriesColorAssignmentsToRange(config, domain, customColorsOrdered)
43
60
  return scaleOrdinal({
44
61
  domain,
@@ -47,26 +64,19 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
47
64
  })
48
65
  }
49
66
 
50
- const customColors = config.general?.palette?.customColors
51
- const isUsingCustomColors = Array.isArray(customColors) && customColors.length > 0
52
- let palette =
53
- (isUsingCustomColors ? customColors : undefined) ||
54
- palettesSource[migratePaletteWithMap(migratedPaletteName, paletteMigrationMap, false)] ||
55
- palettesSource[configPalette]
56
-
57
- // Fallback to a default palette if none found
58
- if (!palette) {
59
- console.warn(`Palette "${configPalette}" not found, falling back to default`)
60
- palette = Object.values(allPalettes)[0] || ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
61
- }
67
+ const customColors = normalizeCustomColors(config.general?.palette?.customColors)
68
+ const isUsingCustomColors = customColors.length > 0
69
+ let palette = isUsingCustomColors ? customColors : namedPalette
62
70
 
63
71
  let numberOfKeys = config.runtime.seriesKeys.length
64
72
 
65
73
  // Apply enhanced color distribution (same logic as pie charts)
66
- const paletteVersion = getColorPaletteVersion(config)
74
+ const paletteVersion = getColorPaletteMajorVersion(config)
75
+ const v21DistributionColors = getV21ChartDistributionColors(config, palette, numberOfKeys)
67
76
 
68
- // Skip enhanced distribution if using custom colors, not v2, too many keys, or wrong palette length
69
- if (isUsingCustomColors || paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
77
+ if (v21DistributionColors) {
78
+ palette = v21DistributionColors
79
+ } else if (isUsingCustomColors || paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
70
80
  // Use existing logic for v1 palettes and other cases
71
81
  while (numberOfKeys > palette.length) {
72
82
  palette = palette.concat(palette)
@@ -78,7 +88,6 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
78
88
  const isDivergent = configPalette && configPalette.includes('divergent')
79
89
  const isColorblindSafe =
80
90
  configPalette && (configPalette.includes('colorblindsafe') || configPalette.includes('qualitative_standard'))
81
-
82
91
  // Determine which distribution to use based on palette type
83
92
  let distributionMap = null
84
93
  if (isDivergent) {
@@ -0,0 +1,69 @@
1
+ import { Axis } from '@cdc/core/types/Axis'
2
+
3
+ export type DynamicYAxisCategory = {
4
+ label: string
5
+ upperBoundKey?: string
6
+ color?: string
7
+ }
8
+
9
+ export type DynamicYAxisCategoriesConfig = {
10
+ categories: DynamicYAxisCategory[]
11
+ }
12
+
13
+ type DynamicYAxisCategoriesParams = {
14
+ config?: DynamicYAxisCategoriesConfig
15
+ data?: Record<string, any>[]
16
+ }
17
+
18
+ type DynamicYAxisCategoriesResult = {
19
+ categories: NonNullable<Axis['categories']>
20
+ axisMax: number | null
21
+ } | null
22
+
23
+ const toFiniteNumber = value => {
24
+ const number = Number(value)
25
+ return Number.isFinite(number) ? number : null
26
+ }
27
+
28
+ export const getDynamicYAxisCategories = ({
29
+ config,
30
+ data = []
31
+ }: DynamicYAxisCategoriesParams): DynamicYAxisCategoriesResult => {
32
+ if (!config || !data.length || !config.categories?.length) return null
33
+
34
+ const currentRow = data.find(row =>
35
+ config.categories.every(
36
+ (category, index) =>
37
+ (index === config.categories.length - 1 && !category.upperBoundKey) ||
38
+ toFiniteNumber(row?.[category.upperBoundKey]) !== null
39
+ )
40
+ )
41
+ if (!currentRow) return null
42
+
43
+ const upperBounds = config.categories.map(category =>
44
+ category.upperBoundKey ? toFiniteNumber(currentRow[category.upperBoundKey]) : null
45
+ )
46
+ const lastCategory = config.categories[config.categories.length - 1]
47
+ const hasOpenEndedFinalCategory = !lastCategory.upperBoundKey
48
+ const axisMax = hasOpenEndedFinalCategory ? null : upperBounds[upperBounds.length - 1]
49
+ const resolvedUpperBounds = hasOpenEndedFinalCategory ? upperBounds.slice(0, -1) : upperBounds
50
+
51
+ if (resolvedUpperBounds.some(bound => bound === null)) return null
52
+ if (!hasOpenEndedFinalCategory && (axisMax === null || axisMax <= 0)) return null
53
+
54
+ const numericUpperBounds = resolvedUpperBounds as number[]
55
+ if (numericUpperBounds.some((bound, index) => index > 0 && bound <= numericUpperBounds[index - 1])) {
56
+ return null
57
+ }
58
+
59
+ const categories = config.categories.map((category, index) => ({
60
+ label: category.label,
61
+ height:
62
+ hasOpenEndedFinalCategory && index === config.categories.length - 1
63
+ ? ''
64
+ : String(numericUpperBounds[index] - (numericUpperBounds[index - 1] || 0)),
65
+ color: category.color || '#f1f1f1'
66
+ }))
67
+
68
+ return { categories, axisMax }
69
+ }
@@ -1,6 +1,7 @@
1
1
  import { ChartConfig } from '../types/ChartConfig'
2
2
  import { getCleanTopTickMax } from './getCleanTopTickMax'
3
3
  import { getAxisMaxOverride } from './getAxisMaxOverride'
4
+ import getStackedSeriesMax from './getStackedSeriesMax'
4
5
 
5
6
  type GetMinMaxProps = {
6
7
  /** config - standard chart config */
@@ -120,24 +121,25 @@ const getMinMax = ({
120
121
  let rightAxisSeriesItems = series.filter(s => s.axis === 'Right')
121
122
 
122
123
  const findMaxFromSeriesKeys = (data, seriesData, max, axis = 'left') => {
123
- let stackedBarMax = 0
124
- let axisSeriesKeys = seriesData.map(i => i.dataKey) || []
124
+ const stackedBarSeries =
125
+ config.visualizationSubType === 'stacked' && axis === 'left' ? seriesData.filter(s => s.type === 'Bar') : []
126
+ const stackedBarSeriesKeys = new Set(stackedBarSeries.map(s => s.dataKey))
127
+ let axisSeriesKeys = seriesData.map(i => i.dataKey).filter(key => !stackedBarSeriesKeys.has(key)) || []
125
128
 
126
129
  axisSeriesKeys.forEach(key => {
127
- let _seriesData = seriesData.find(s => s.dataKey === key)
128
130
  let _data = data.map(d => d[key])
129
131
  let seriesMax = Math.max.apply(null, _data)
130
- if (config.visualizationSubType === 'stacked' && axis === 'left' && _seriesData.type === 'Bar') {
131
- stackedBarMax += seriesMax
132
- }
133
132
  if (seriesMax > max) {
134
133
  max = seriesMax
135
134
  }
135
+ })
136
136
 
137
+ if (stackedBarSeries.length) {
138
+ const stackedBarMax = getStackedSeriesMax(data, stackedBarSeries)
137
139
  if (max < stackedBarMax) {
138
140
  max = stackedBarMax
139
141
  }
140
- })
142
+ }
141
143
  return max
142
144
  }
143
145
  leftMax = findMaxFromSeriesKeys(dataForMinMax, leftAxisSeriesItems, leftMax, 'left')
@@ -1,3 +1,5 @@
1
+ import { getSeriesName } from '@cdc/core/helpers/getSeriesName'
2
+
1
3
  export const getNewRuntime = (visualizationConfig, newFilteredData) => {
2
4
  const runtime = visualizationConfig.runtime ? { ...visualizationConfig.runtime } : {}
3
5
  runtime.series = []
@@ -23,8 +25,9 @@ export const getNewRuntime = (visualizationConfig, newFilteredData) => {
23
25
 
24
26
  runtime.seriesKeys = runtime.series
25
27
  ? runtime.series.map(series => {
26
- runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
27
- runtime.seriesLabelsAll.push(series.name || series.dataKey)
28
+ const seriesName = getSeriesName(series.dataKey, { series: [series] })
29
+ runtime.seriesLabels[series.dataKey] = seriesName
30
+ runtime.seriesLabelsAll.push(seriesName)
28
31
  return series.dataKey
29
32
  })
30
33
  : []
@@ -0,0 +1,51 @@
1
+ import isNumber from '@cdc/core/helpers/isNumber'
2
+
3
+ type SeriesKeyOrItem =
4
+ | string
5
+ | {
6
+ dataKey?: string
7
+ originalDataKey?: string
8
+ dynamicCategory?: boolean
9
+ }
10
+
11
+ const cleanValue = value => {
12
+ if (value === null || value === '') return ''
13
+ return typeof value === 'string' ? value.replace(/[,$]/g, '') : value
14
+ }
15
+
16
+ const getSeriesDataKey = (seriesKeyOrItem: SeriesKeyOrItem) => {
17
+ if (typeof seriesKeyOrItem === 'string') return seriesKeyOrItem
18
+ return seriesKeyOrItem.dynamicCategory && seriesKeyOrItem.originalDataKey
19
+ ? seriesKeyOrItem.originalDataKey
20
+ : seriesKeyOrItem.dataKey
21
+ }
22
+
23
+ const getStackedSeriesMax = (data: Object[] = [], seriesKeysOrSeriesItems: SeriesKeyOrItem[] = []) => {
24
+ let maxValue = Number.NEGATIVE_INFINITY
25
+ let hasValue = false
26
+
27
+ for (const row of data) {
28
+ let rowSum = 0
29
+ let hasRowValue = false
30
+
31
+ for (const seriesKeyOrItem of seriesKeysOrSeriesItems) {
32
+ const seriesKey = getSeriesDataKey(seriesKeyOrItem)
33
+ if (!seriesKey) continue
34
+
35
+ const value = cleanValue(row[seriesKey])
36
+ if (isNumber(value)) {
37
+ rowSum += Number(value)
38
+ hasRowValue = true
39
+ }
40
+ }
41
+
42
+ if (hasRowValue) {
43
+ maxValue = Math.max(maxValue, rowSum)
44
+ hasValue = true
45
+ }
46
+ }
47
+
48
+ return hasValue ? maxValue : 0
49
+ }
50
+
51
+ export default getStackedSeriesMax
@@ -0,0 +1,57 @@
1
+ import { type ChartConfig, type VisualizationType } from '../types/ChartConfig'
2
+ import {
3
+ chartV21ColorDistribution,
4
+ divergentColorDistribution,
5
+ qualitativeStandardColorDistribution
6
+ } from '@cdc/core/helpers/palettes/colorDistributions'
7
+
8
+ const V21_COLOR_DISTRIBUTION_CHART_TYPES = new Set<VisualizationType>([
9
+ 'Area Chart',
10
+ 'Bar',
11
+ 'Box Plot',
12
+ 'Bump Chart',
13
+ 'Combo',
14
+ 'Line',
15
+ 'Pie',
16
+ 'Radar',
17
+ 'Scatter Plot',
18
+ 'Spark Line'
19
+ ])
20
+
21
+ export const getV21ChartDistributionColors = (
22
+ config: ChartConfig,
23
+ palette: string[],
24
+ itemCount: number
25
+ ): string[] | undefined => {
26
+ const paletteName = config.general?.palette?.name || config.palette || ''
27
+ const hasCustomColors =
28
+ Boolean(config.general?.palette?.customColors?.length) ||
29
+ Boolean(config.general?.palette?.customColorsOrdered?.length)
30
+
31
+ if (
32
+ config.general?.palette?.version !== '2.1' ||
33
+ !V21_COLOR_DISTRIBUTION_CHART_TYPES.has(config.visualizationType) ||
34
+ hasCustomColors ||
35
+ palette.length !== 9
36
+ ) {
37
+ return undefined
38
+ }
39
+
40
+ let distributionMap: Record<number, number[]> | undefined
41
+ if (paletteName.includes('sequential')) {
42
+ distributionMap = chartV21ColorDistribution
43
+ } else if (paletteName.includes('divergent')) {
44
+ distributionMap = divergentColorDistribution
45
+ } else if (paletteName.includes('qualitative_standard')) {
46
+ distributionMap = qualitativeStandardColorDistribution
47
+ }
48
+
49
+ const distributionIndices = distributionMap?.[itemCount]
50
+ if (!distributionIndices) return undefined
51
+
52
+ const orientedIndices = paletteName.endsWith('reverse')
53
+ ? [...distributionIndices].reverse().map(index => palette.length - 1 - index)
54
+ : distributionIndices
55
+
56
+ return orientedIndices.map(index => palette[index])
57
+ }
@@ -1,15 +1,15 @@
1
- import { ChartConfig } from '../types/ChartConfig'
2
-
3
- const getNumericValue = number => {
4
- if (typeof number === 'string') return parseFloat(number.replace(/,/g, ''))
5
- return Number(number)
6
- }
7
-
8
- export const handleRankByValue = (data, passedConfig: ChartConfig) => {
9
- if (passedConfig.rankByValue) {
10
- const series = passedConfig.series[0].dataKey
11
- const sorted = data.sort((a, b) => getNumericValue(a[series]) - getNumericValue(b[series]))
12
- return passedConfig.rankByValue === 'asc' ? sorted : sorted.reverse()
13
- }
14
- return data
15
- }
1
+ import { ChartConfig } from '../types/ChartConfig'
2
+
3
+ const getNumericValue = number => {
4
+ if (typeof number === 'string') return parseFloat(number.replace(/,/g, ''))
5
+ return Number(number)
6
+ }
7
+
8
+ export const handleRankByValue = (data, passedConfig: ChartConfig) => {
9
+ if (passedConfig.rankByValue) {
10
+ const series = passedConfig.series[0].dataKey
11
+ const sorted = data.toSorted((a, b) => getNumericValue(a[series]) - getNumericValue(b[series]))
12
+ return passedConfig.rankByValue === 'asc' ? sorted : sorted.reverse()
13
+ }
14
+ return data
15
+ }
@@ -131,3 +131,13 @@ export const getSeriesColumnFormattingParams = (columnConfig?: Partial<Column>):
131
131
 
132
132
  return Object.keys(formattingParams).length ? formattingParams : undefined
133
133
  }
134
+
135
+ export const getAdditionalColumnFormattingParams = (columnConfig?: Partial<Column>): ColumnFormattingParams => {
136
+ const explicitFormattingParams = getSeriesColumnFormattingParams(columnConfig)
137
+
138
+ return {
139
+ addColPrefix: '',
140
+ addColSuffix: '',
141
+ ...(explicitFormattingParams || {})
142
+ }
143
+ }
@@ -1,10 +1,11 @@
1
1
  import { isMobileHeightViewport } from '@cdc/core/helpers/viewports'
2
- import { ChartConfig, ViewportSize } from '../types/ChartConfig'
2
+ import { ChartConfig } from '../types/ChartConfig'
3
3
  import { EDITOR_WIDTH } from '@cdc/core/helpers/constants'
4
+ import type { ViewPort } from '@cdc/core/types/ViewPort'
4
5
 
5
6
  export function getOrientation(
6
7
  { orientation, heights, visualizationType }: Pick<ChartConfig, 'orientation' | 'heights' | 'visualizationType'>,
7
- currentViewport: ViewportSize
8
+ currentViewport: ViewPort
8
9
  ): 'vertical' | 'horizontal' | 'mobileVertical' {
9
10
  const isForestPlot = visualizationType === 'Forest Plot'
10
11
  const useVertical = orientation === 'vertical' || isForestPlot
@@ -15,7 +16,7 @@ export function getOrientation(
15
16
  }
16
17
  export function calcInitialHeight(
17
18
  { heights, orientation, visualizationType }: Pick<ChartConfig, 'heights' | 'orientation' | 'visualizationType'>,
18
- currentViewport: ViewportSize
19
+ currentViewport: ViewPort
19
20
  ): number {
20
21
  // if no heights are provided assume config has not been loaded
21
22
  if (!heights) return 0
@@ -1,3 +1,4 @@
1
+ import { sortByNumericText } from '@cdc/core/helpers/sorting'
1
2
  import { scaleOrdinal } from '@visx/scale'
2
3
  import { getColorScale } from './getColorScale'
3
4
  import { ColorScale } from '../types/ChartContext'
@@ -105,18 +106,16 @@ export const applyTileOrder = (tileItems, orderType, customOrder, config) => {
105
106
 
106
107
  switch (orderType) {
107
108
  case 'asc':
108
- return [...tileItems].sort((a, b) => {
109
- const titleA = String(getTileDisplayTitle(a.mode, a.seriesKey, a.tileValue, a.key, config)).toLowerCase()
110
- const titleB = String(getTileDisplayTitle(b.mode, b.seriesKey, b.tileValue, b.key, config)).toLowerCase()
111
- return titleA.localeCompare(titleB)
112
- })
109
+ return sortByNumericText(tileItems, item =>
110
+ getTileDisplayTitle(item.mode, item.seriesKey, item.tileValue, item.key, config)
111
+ )
113
112
 
114
113
  case 'desc':
115
- return [...tileItems].sort((a, b) => {
116
- const titleA = String(getTileDisplayTitle(a.mode, a.seriesKey, a.tileValue, a.key, config)).toLowerCase()
117
- const titleB = String(getTileDisplayTitle(b.mode, b.seriesKey, b.tileValue, b.key, config)).toLowerCase()
118
- return titleB.localeCompare(titleA)
119
- })
114
+ return sortByNumericText(
115
+ tileItems,
116
+ item => getTileDisplayTitle(item.mode, item.seriesKey, item.tileValue, item.key, config),
117
+ 'desc'
118
+ )
120
119
 
121
120
  case 'custom':
122
121
  if (!customOrder || customOrder.length === 0) {
@@ -154,9 +153,7 @@ export const getTileKeys = (config, data) => {
154
153
  const tileColumn = config.smallMultiples.tileColumn
155
154
  if (!tileColumn) return []
156
155
 
157
- return Array.from(new Set(data.map(row => row[tileColumn])))
158
- .filter(val => val != null)
159
- .sort()
156
+ return sortByNumericText(Array.from(new Set(data.map(row => row[tileColumn]))).filter(val => val != null))
160
157
  }
161
158
  return []
162
159
  }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { applyChartTypeContract } from '../applyChartTypeContract'
3
+
4
+ const chart = (visualizationType: string) => ({
5
+ type: 'chart',
6
+ visualizationType,
7
+ orientation: 'vertical',
8
+ series: [{ dataKey: 'value' }],
9
+ xAxis: { type: 'categorical', dataKey: 'date' },
10
+ yAxis: {},
11
+ legend: {},
12
+ general: { paletteColorCount: 2 },
13
+ table: {}
14
+ })
15
+
16
+ describe('applyChartTypeContract', () => {
17
+ it.each(['Area Chart', 'Horizon Chart', 'Box Plot', 'Bump Chart', 'HeatMap', 'Forecasting'])(
18
+ 'does not mutate the input for %s transitions',
19
+ visualizationType => {
20
+ const input = chart(visualizationType)
21
+ const before = structuredClone(input)
22
+
23
+ const result = applyChartTypeContract(input as any)
24
+
25
+ expect(input).toEqual(before)
26
+ expect(result).not.toBe(input)
27
+ expect(result.xAxis).not.toBe(input.xAxis)
28
+ expect(result.series).not.toBe(input.series)
29
+ }
30
+ )
31
+
32
+ it('authors the stacked Area Chart subtype', () => {
33
+ const result = applyChartTypeContract(chart('Area Chart') as any)
34
+
35
+ expect(result.visualizationSubType).toBe('stacked')
36
+ })
37
+ })
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { calculateBoxPlotStats, getSortedBoxPlotValues } from '../boxPlotStats'
4
+
5
+ describe('boxPlotStats', () => {
6
+ it('normalizes numeric strings once for all box plot stats consumers', () => {
7
+ expect(getSortedBoxPlotValues(['1,000', '20', '', null, undefined, 'n/a', '3'])).toEqual([3, 20, 1000])
8
+ })
9
+
10
+ it('calculates quartiles, fences, whiskers, and outliers from sorted numeric values', () => {
11
+ const stats = calculateBoxPlotStats(['10', '20', '30', '40', '1,000'])
12
+
13
+ expect(stats).toMatchObject({
14
+ values: [10, 20, 30, 40, 1000],
15
+ q1: 20,
16
+ q3: 40,
17
+ median: 30,
18
+ iqr: 20,
19
+ lowerBound: -10,
20
+ upperBound: 70,
21
+ nonOutliers: [10, 20, 30, 40],
22
+ outliers: [1000],
23
+ whiskerMin: 10,
24
+ whiskerMax: 40
25
+ })
26
+ })
27
+
28
+ it('returns null when no finite values can be calculated', () => {
29
+ expect(calculateBoxPlotStats(['', null, undefined, 'n/a'])).toBeNull()
30
+ })
31
+ })