@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,6 +1,14 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
2
  import { getColorScale } from '../getColorScale'
3
3
  import { ChartConfig } from '../../types/ChartConfig'
4
+ import { colorPalettesChartV2 } from '@cdc/core/data/colorPalettes'
5
+ import {
6
+ colorblindColorDistribution,
7
+ chartV21ColorDistribution,
8
+ divergentColorDistribution,
9
+ qualitativeStandardColorDistribution,
10
+ v2ColorDistribution
11
+ } from '@cdc/core/helpers/palettes/colorDistributions'
4
12
 
5
13
  const buildConfig = (overrides: Partial<ChartConfig> = {}): ChartConfig =>
6
14
  ({
@@ -177,6 +185,63 @@ describe('getColorScale series color assignments', () => {
177
185
  expect(colorScale('Series C')).toBe('#303030')
178
186
  })
179
187
 
188
+ it('fills invalid customColorsOrdered entries with black', () => {
189
+ const colorScale = getColorScale(
190
+ buildConfig({
191
+ general: {
192
+ palette: {
193
+ name: 'qualitative_standard',
194
+ version: '2.0',
195
+ customColorsOrdered: ['', '#222222', 'whyte']
196
+ }
197
+ } as any
198
+ })
199
+ )
200
+
201
+ expect(colorScale('Series A')).toBe('#000000')
202
+ expect(colorScale('Series B')).toBe('#222222')
203
+ expect(colorScale('Series C')).toBe('#000000')
204
+ })
205
+
206
+ it('uses black when saved customColors are all invalid', () => {
207
+ const colorScale = getColorScale(
208
+ buildConfig({
209
+ general: {
210
+ palette: {
211
+ name: 'qualitative_standard',
212
+ version: '2.0',
213
+ customColors: ['', 'whyte']
214
+ }
215
+ } as any
216
+ })
217
+ )
218
+
219
+ expect(colorScale('Series A')).toBe('#000000')
220
+ expect(colorScale('Series B')).toBe('#000000')
221
+ expect(colorScale('Series C')).toBe('#000000')
222
+ })
223
+
224
+ it('uses black when the named palette cannot be resolved', () => {
225
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
226
+ const colorScale = getColorScale(
227
+ buildConfig({
228
+ general: {
229
+ palette: {
230
+ name: 'unknown_palette',
231
+ version: '2.0'
232
+ }
233
+ } as any
234
+ })
235
+ )
236
+
237
+ expect(warn).toHaveBeenCalledWith('Palette "unknown_palette" not found, falling back to black')
238
+ expect(colorScale('Series A')).toBe('#000000')
239
+ expect(colorScale('Series B')).toBe('#000000')
240
+ expect(colorScale('Series C')).toBe('#000000')
241
+
242
+ warn.mockRestore()
243
+ })
244
+
180
245
  it('falls back to the named palette when customColors is empty', () => {
181
246
  const colorScale = getColorScale(
182
247
  buildConfig({
@@ -195,3 +260,163 @@ describe('getColorScale series color assignments', () => {
195
260
  expect(colorScale('Series C')).toBe('#5A8E3F')
196
261
  })
197
262
  })
263
+
264
+ const buildDistributionConfig = (
265
+ numberOfSeries: number,
266
+ paletteName = 'qualitative_standard',
267
+ paletteVersion: '2.0' | '2.1' = '2.0',
268
+ overrides: Partial<ChartConfig> = {}
269
+ ) => {
270
+ const seriesKeys = Array.from({ length: numberOfSeries }, (_, index) => `series_${index + 1}`)
271
+ const seriesLabelsAll = seriesKeys.map((_, index) => `Series ${index + 1}`)
272
+
273
+ return buildConfig({
274
+ general: {
275
+ palette: {
276
+ name: paletteName,
277
+ version: paletteVersion
278
+ }
279
+ } as any,
280
+ series: seriesKeys.map((dataKey, index) => ({ dataKey, name: seriesLabelsAll[index], type: 'Line' })) as any,
281
+ runtime: {
282
+ seriesKeys,
283
+ seriesLabels: Object.fromEntries(seriesKeys.map((key, index) => [key, seriesLabelsAll[index]])),
284
+ seriesLabelsAll
285
+ } as any,
286
+ ...overrides
287
+ })
288
+ }
289
+
290
+ const getScaleColors = (config: ChartConfig) => {
291
+ const scale = getColorScale(config)
292
+ return config.runtime.seriesLabelsAll.map(label => scale(label))
293
+ }
294
+
295
+ describe('getColorScale distribution profiles', () => {
296
+ it.each(Object.keys(qualitativeStandardColorDistribution).map(Number))(
297
+ 'uses released colorblind colors for %i series with palette version 2.0',
298
+ count => {
299
+ const expected = colorblindColorDistribution[count].map(index => colorPalettesChartV2.qualitative_standard[index])
300
+
301
+ expect(getScaleColors(buildDistributionConfig(count))).toEqual(expected)
302
+ }
303
+ )
304
+
305
+ it.each(Object.keys(qualitativeStandardColorDistribution).map(Number))(
306
+ 'uses improved colorblind colors for %i series with palette version 2.1',
307
+ count => {
308
+ const expected = qualitativeStandardColorDistribution[count].map(
309
+ index => colorPalettesChartV2.qualitative_standard[index]
310
+ )
311
+
312
+ expect(getScaleColors(buildDistributionConfig(count, 'qualitative_standard', '2.1'))).toEqual(expected)
313
+ }
314
+ )
315
+
316
+ it('uses the V2 forward colors in reverse order for a reversed palette', () => {
317
+ const count = 5
318
+ const expected = qualitativeStandardColorDistribution[count]
319
+ .map(index => colorPalettesChartV2.qualitative_standard[index])
320
+ .reverse()
321
+
322
+ expect(getScaleColors(buildDistributionConfig(count, 'qualitative_standardreverse', '2.1'))).toEqual(expected)
323
+ })
324
+
325
+ it('uses the chart 2.1 distribution for a two-series sequential palette', () => {
326
+ const expected = chartV21ColorDistribution[2].map(index => colorPalettesChartV2.sequential_blue[index])
327
+
328
+ expect(getScaleColors(buildDistributionConfig(2, 'sequential_blue', '2.1'))).toEqual(expected)
329
+ })
330
+
331
+ it('preserves the released sequential distribution for palette version 2.0', () => {
332
+ const expected = v2ColorDistribution[2].map(index => colorPalettesChartV2.sequential_blue[index])
333
+
334
+ expect(getScaleColors(buildDistributionConfig(2, 'sequential_blue', '2.0'))).toEqual(expected)
335
+ })
336
+
337
+ it('uses the 2.1 sequential colors in reverse order for a reversed palette', () => {
338
+ const expected = chartV21ColorDistribution[2].map(index => colorPalettesChartV2.sequential_blue[index]).reverse()
339
+
340
+ expect(
341
+ getScaleColors(
342
+ buildDistributionConfig(2, 'sequential_bluereverse', '2.1', {
343
+ general: {
344
+ palette: {
345
+ name: 'sequential_bluereverse',
346
+ version: '2.1',
347
+ isReversed: true
348
+ }
349
+ } as any
350
+ })
351
+ )
352
+ ).toEqual(expected)
353
+ })
354
+
355
+ it('does not change divergent colors between palette versions', () => {
356
+ const count = 5
357
+ const expected = divergentColorDistribution[count].map(index => colorPalettesChartV2.divergent_blue_orange[index])
358
+
359
+ expect(getScaleColors(buildDistributionConfig(count, 'divergent_blue_orange', '2.0'))).toEqual(expected)
360
+ expect(getScaleColors(buildDistributionConfig(count, 'divergent_blue_orange', '2.1'))).toEqual(expected)
361
+ })
362
+
363
+ it('bypasses distribution profiles for non-empty custom colors', () => {
364
+ const customColors = Array.from({ length: 9 }, (_, index) => `#00000${index}`)
365
+ const config = buildDistributionConfig(5, 'qualitative_standard', '2.1', {
366
+ general: {
367
+ palette: {
368
+ name: 'qualitative_standard',
369
+ version: '2.1',
370
+ customColors
371
+ }
372
+ } as any
373
+ })
374
+
375
+ expect(getScaleColors(config)).toEqual(customColors.slice(0, 5))
376
+ })
377
+
378
+ it('treats empty custom color arrays as inactive', () => {
379
+ const config = buildDistributionConfig(3, 'qualitative_standard', '2.1', {
380
+ general: {
381
+ palette: {
382
+ name: 'qualitative_standard',
383
+ version: '2.1',
384
+ customColors: [],
385
+ customColorsOrdered: []
386
+ }
387
+ } as any
388
+ })
389
+ const expected = qualitativeStandardColorDistribution[3].map(
390
+ index => colorPalettesChartV2.qualitative_standard[index]
391
+ )
392
+
393
+ expect(getScaleColors(config)).toEqual(expected)
394
+ })
395
+
396
+ it('retains slice and repeat behavior outside the supported item range', () => {
397
+ const expected = [...colorPalettesChartV2.qualitative_standard, colorPalettesChartV2.qualitative_standard[0]]
398
+
399
+ expect(getScaleColors(buildDistributionConfig(10, 'qualitative_standard', '2.1'))).toEqual(expected)
400
+ })
401
+
402
+ it('does not apply the 2.1 distribution to an unsupported chart type', () => {
403
+ const config = buildDistributionConfig(3, 'qualitative_standard', '2.1', {
404
+ visualizationType: 'Horizon Chart'
405
+ })
406
+ const expected = colorblindColorDistribution[3].map(index => colorPalettesChartV2.qualitative_standard[index])
407
+
408
+ expect(getScaleColors(config)).toEqual(expected)
409
+ })
410
+
411
+ it('applies the 2.1 distribution to a color-coded bar chart', () => {
412
+ const config = buildDistributionConfig(3, 'qualitative_standard', '2.1', {
413
+ visualizationType: 'Bar',
414
+ legend: { colorCode: 'category' } as any
415
+ })
416
+ const expected = qualitativeStandardColorDistribution[3].map(
417
+ index => colorPalettesChartV2.qualitative_standard[index]
418
+ )
419
+
420
+ expect(getScaleColors(config)).toEqual(expected)
421
+ })
422
+ })
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getDynamicYAxisCategories } from '../getDynamicYAxisCategories'
3
+
4
+ const config = {
5
+ categories: [
6
+ { label: 'Very Low', upperBoundKey: 'very_low_max' },
7
+ { label: 'Low', upperBoundKey: 'low_max' },
8
+ { label: 'Moderate', upperBoundKey: 'moderate_max' },
9
+ { label: 'High', upperBoundKey: 'high_max' },
10
+ { label: 'Very High', upperBoundKey: 'very_high_max' }
11
+ ]
12
+ }
13
+
14
+ describe('getDynamicYAxisCategories', () => {
15
+ it('converts cumulative upper bounds into category heights and uses configured colors', () => {
16
+ config.categories = config.categories.map((category, index) => ({ ...category, color: `color-${index}` }))
17
+ const result = getDynamicYAxisCategories({
18
+ config,
19
+ data: [{ very_low_max: 12.5, low_max: 16, moderate_max: 18, high_max: 21, very_high_max: 35 }]
20
+ })
21
+
22
+ expect(result).toEqual({
23
+ axisMax: 35,
24
+ categories: [
25
+ { label: 'Very Low', height: '12.5', color: 'color-0' },
26
+ { label: 'Low', height: '3.5', color: 'color-1' },
27
+ { label: 'Moderate', height: '2', color: 'color-2' },
28
+ { label: 'High', height: '3', color: 'color-3' },
29
+ { label: 'Very High', height: '14', color: 'color-4' }
30
+ ]
31
+ })
32
+ })
33
+
34
+ it('returns null for missing, invalid, or non-increasing current-row data', () => {
35
+ expect(getDynamicYAxisCategories({ config, data: [{ very_low_max: 12 }] })).toBeNull()
36
+ expect(
37
+ getDynamicYAxisCategories({
38
+ config,
39
+ data: [{ very_low_max: 12, low_max: 10, moderate_max: 18, high_max: 21, very_high_max: 35 }]
40
+ })
41
+ ).toBeNull()
42
+ })
43
+
44
+ it('returns null while filtered chart data is temporarily empty', () => {
45
+ expect(getDynamicYAxisCategories({ config, data: [] })).toBeNull()
46
+ })
47
+
48
+ it('leaves the final category open-ended when it has no upper-bound key', () => {
49
+ const categories = config.categories.map((category, index) =>
50
+ index === config.categories.length - 1 ? { ...category, upperBoundKey: undefined } : category
51
+ )
52
+
53
+ const result = getDynamicYAxisCategories({
54
+ config: { categories },
55
+ data: [{ very_low_max: 12.5, low_max: 16, moderate_max: 18, high_max: 21 }]
56
+ })
57
+
58
+ expect(result?.axisMax).toBeNull()
59
+ expect(result?.categories.at(-1)).toEqual({ label: 'Very High', height: '', color: 'color-4' })
60
+ })
61
+ })
@@ -155,6 +155,47 @@ describe('getMinMax automatic max strategy', () => {
155
155
  expect(result.rightMax).toBe(89)
156
156
  })
157
157
 
158
+ it('uses stacked row totals for combo left-axis bar series and keeps right-axis line max independent', () => {
159
+ const barSeriesA = { dataKey: 'DoseOne', type: 'Bar', axis: 'Left', tooltip: true }
160
+ const barSeriesB = { dataKey: 'DoseTwo', type: 'Bar', axis: 'Left', tooltip: true }
161
+ const rightLineSeries = { dataKey: 'Rate', type: 'Line', axis: 'Right', tooltip: true }
162
+ const data = [
163
+ { DoseOne: 520, DoseTwo: 80, Rate: 900 },
164
+ { DoseOne: 0, DoseTwo: 600, Rate: 725 }
165
+ ]
166
+ const config = createConfig({
167
+ visualizationType: 'Combo',
168
+ visualizationSubType: 'stacked',
169
+ series: [barSeriesA, barSeriesB, rightLineSeries],
170
+ runtime: {
171
+ ...createConfig().runtime,
172
+ yAxis: {
173
+ ...createConfig().runtime.yAxis,
174
+ min: '',
175
+ max: ''
176
+ },
177
+ series: [barSeriesA, barSeriesB, rightLineSeries],
178
+ seriesKeys: ['DoseOne', 'DoseTwo', 'Rate'],
179
+ barSeriesKeys: ['DoseOne', 'DoseTwo'],
180
+ lineSeriesKeys: ['Rate']
181
+ } as any
182
+ })
183
+
184
+ const result = getMinMax({
185
+ config,
186
+ minValue: 0,
187
+ maxValue: 900,
188
+ existPositiveValue: true,
189
+ data,
190
+ tableData: data,
191
+ isAllLine: false
192
+ })
193
+
194
+ expect(result.leftMax).toBe(600)
195
+ expect(result.leftMax).not.toBe(1120)
196
+ expect(result.rightMax).toBe(900)
197
+ })
198
+
158
199
  it('forces a zero min for line charts when boundary suppression is present', () => {
159
200
  const config = createConfig({
160
201
  visualizationType: 'Line',
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import getStackedSeriesMax from '../getStackedSeriesMax'
3
+
4
+ describe('getStackedSeriesMax', () => {
5
+ it('uses the tallest row sum instead of summing independent series maxima', () => {
6
+ const data = [
7
+ { apples: 400, oranges: 10 },
8
+ { apples: 50, oranges: 300 }
9
+ ]
10
+
11
+ expect(getStackedSeriesMax(data, ['apples', 'oranges'])).toBe(410)
12
+ })
13
+
14
+ it('accepts series objects and dynamic category original data keys', () => {
15
+ const data = [
16
+ { apples: 125, oranges: 75, dynamicFruit: 999 },
17
+ { apples: 10, oranges: 50, dynamicFruit: 999 }
18
+ ]
19
+ const series = [
20
+ { dataKey: 'apples' },
21
+ { dataKey: 'dynamicFruit', originalDataKey: 'oranges', dynamicCategory: true }
22
+ ]
23
+
24
+ expect(getStackedSeriesMax(data, series)).toBe(200)
25
+ })
26
+ })
@@ -1,37 +1,55 @@
1
- import { handleRankByValue } from '../handleRankByValue'
2
- import { ChartConfig } from '../../types/ChartConfig'
3
-
4
- describe('handleRankByValue', () => {
5
- it('should sort the data in ascending order when rankByValue is "asc"', () => {
6
- const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
7
- const config: ChartConfig = {
8
- rankByValue: 'asc',
9
- series: [{ dataKey: 'value' }]
10
- }
11
-
12
- const result = handleRankByValue(data, config)
13
- expect(result).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }])
14
- })
15
-
16
- it('should sort the data in descending order when rankByValue is "desc"', () => {
17
- const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
18
- const config: ChartConfig = {
19
- rankByValue: 'desc',
20
- series: [{ dataKey: 'value' }]
21
- }
22
-
23
- const result = handleRankByValue(data, config)
24
- expect(result).toEqual([{ value: 3 }, { value: 2 }, { value: 1 }])
25
- })
26
-
27
- it('should handle numeric strings correctly', () => {
28
- const data = [{ value: '3' }, { value: '1' }, { value: '2' }]
29
- const config: ChartConfig = {
30
- rankByValue: 'asc',
31
- series: [{ dataKey: 'value' }]
32
- }
33
-
34
- const result = handleRankByValue(data, config)
35
- expect(result).toEqual([{ value: '1' }, { value: '2' }, { value: '3' }])
36
- })
37
- })
1
+ import { handleRankByValue } from '../handleRankByValue'
2
+ import { ChartConfig } from '../../types/ChartConfig'
3
+
4
+ describe('handleRankByValue', () => {
5
+ it('should sort the data in ascending order when rankByValue is "asc"', () => {
6
+ const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
7
+ const originalData = [...data]
8
+ const config: ChartConfig = {
9
+ rankByValue: 'asc',
10
+ series: [{ dataKey: 'value' }]
11
+ }
12
+
13
+ const result = handleRankByValue(data, config)
14
+ expect(result).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }])
15
+ expect(result).not.toBe(data)
16
+ expect(data).toEqual(originalData)
17
+ })
18
+
19
+ it('should sort the data in descending order when rankByValue is "desc"', () => {
20
+ const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
21
+ const originalData = [...data]
22
+ const config: ChartConfig = {
23
+ rankByValue: 'desc',
24
+ series: [{ dataKey: 'value' }]
25
+ }
26
+
27
+ const result = handleRankByValue(data, config)
28
+ expect(result).toEqual([{ value: 3 }, { value: 2 }, { value: 1 }])
29
+ expect(result).not.toBe(data)
30
+ expect(data).toEqual(originalData)
31
+ })
32
+
33
+ it('should handle numeric strings correctly', () => {
34
+ const data = [{ value: '3' }, { value: '1' }, { value: '2' }]
35
+ const config: ChartConfig = {
36
+ rankByValue: 'asc',
37
+ series: [{ dataKey: 'value' }]
38
+ }
39
+
40
+ const result = handleRankByValue(data, config)
41
+ expect(result).toEqual([{ value: '1' }, { value: '2' }, { value: '3' }])
42
+ })
43
+
44
+ it('should return the original data reference when rankByValue is not set', () => {
45
+ const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
46
+ const config: ChartConfig = {
47
+ rankByValue: undefined,
48
+ series: [{ dataKey: 'value' }]
49
+ }
50
+
51
+ const result = handleRankByValue(data, config)
52
+ expect(result).toBe(data)
53
+ expect(result).toEqual([{ value: 3 }, { value: 1 }, { value: 2 }])
54
+ })
55
+ })
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createDefaultSeriesColumnConfig,
3
3
  findColumnConfigByName,
4
+ getAdditionalColumnFormattingParams,
4
5
  getSeriesColumnConfig,
5
6
  getSeriesColumnFormattingParams,
6
7
  getSeriesOwnedColumnNames,
@@ -155,4 +156,26 @@ describe('seriesColumnSettings', () => {
155
156
  it('returns undefined when no formatting overrides were explicitly configured', () => {
156
157
  expect(getSeriesColumnFormattingParams({ label: 'Cases' })).toBeUndefined()
157
158
  })
159
+
160
+ it('blocks global prefix and suffix fallback for additional columns', () => {
161
+ expect(getAdditionalColumnFormattingParams({ label: 'Count' })).toEqual({
162
+ addColPrefix: '',
163
+ addColSuffix: ''
164
+ })
165
+ })
166
+
167
+ it('preserves explicit additional column formatting while blocking missing affixes', () => {
168
+ expect(
169
+ getAdditionalColumnFormattingParams({
170
+ prefix: '$',
171
+ roundToPlace: 0,
172
+ commas: false
173
+ })
174
+ ).toEqual({
175
+ addColPrefix: '$',
176
+ addColSuffix: '',
177
+ addColRoundTo: 0,
178
+ addColCommas: false
179
+ })
180
+ })
158
181
  })
@@ -1,6 +1,6 @@
1
1
  import { scaleOrdinal } from '@visx/scale'
2
2
  import { describe, expect, it } from 'vitest'
3
- import { createTileColorScale } from '../smallMultiplesHelpers'
3
+ import { applyTileOrder, createTileColorScale, getTileKeys } from '../smallMultiplesHelpers'
4
4
  import { ChartConfig } from '../../types/ChartConfig'
5
5
 
6
6
  const buildConfig = (overrides: Partial<ChartConfig> = {}): ChartConfig =>
@@ -97,3 +97,39 @@ describe('smallMultiplesHelpers createTileColorScale', () => {
97
97
  expect(tileScale('Series B')).toBe('#aa0000')
98
98
  })
99
99
  })
100
+
101
+ describe('smallMultiplesHelpers tile ordering', () => {
102
+ it('sorts by-column tile keys in numeric-aware order', () => {
103
+ const config = buildConfig({
104
+ smallMultiples: {
105
+ mode: 'by-column',
106
+ tileColumn: 'Week'
107
+ }
108
+ })
109
+ const data = [{ Week: '10' }, { Week: '2' }, { Week: '1' }, { Week: '20' }, { Week: '3' }, { Week: null }]
110
+
111
+ expect(getTileKeys(config, data)).toEqual(['1', '2', '3', '10', '20'])
112
+ })
113
+
114
+ it('uses numeric-aware order when sorting tile display titles', () => {
115
+ const config = buildConfig({
116
+ smallMultiples: {
117
+ mode: 'by-column',
118
+ tileColumn: 'Week',
119
+ tileTitles: {
120
+ '1': 'Week 1',
121
+ '2': 'Week 2',
122
+ '10': 'Week 10'
123
+ }
124
+ }
125
+ })
126
+ const tileItems = [
127
+ { mode: 'by-column', tileValue: '10', key: '10' },
128
+ { mode: 'by-column', tileValue: '2', key: '2' },
129
+ { mode: 'by-column', tileValue: '1', key: '1' }
130
+ ]
131
+
132
+ expect(applyTileOrder(tileItems, 'asc', [], config).map(item => item.tileValue)).toEqual(['1', '2', '10'])
133
+ expect(applyTileOrder(tileItems, 'desc', [], config).map(item => item.tileValue)).toEqual(['10', '2', '1'])
134
+ })
135
+ })
@@ -4,6 +4,7 @@ import {
4
4
  getTooltipMarkerColor,
5
5
  getTooltipMarkerShape,
6
6
  shouldUseTooltipLegendMarkers,
7
+ shouldShowTooltipSeriesRow,
7
8
  tooltipHasMarkerColumn,
8
9
  tooltipShouldUseMarkerColumn,
9
10
  type TooltipRow
@@ -148,4 +149,83 @@ describe('tooltipHelpers', () => {
148
149
  expect(ambiguousMarkers).not.toContain('tooltip-body--marker-layout')
149
150
  expect(ambiguousMarkers).not.toContain('tooltip-marker-slot')
150
151
  })
152
+
153
+ it('shows normal tooltip series rows by default when isolate has no legend selection', () => {
154
+ expect(
155
+ shouldShowTooltipSeriesRow({
156
+ legendBehavior: 'isolate',
157
+ seriesHighlight: [],
158
+ seriesKey: 'cases'
159
+ })
160
+ ).toBe(true)
161
+ expect(
162
+ shouldShowTooltipSeriesRow({
163
+ seriesKey: 'deaths'
164
+ })
165
+ ).toBe(true)
166
+ })
167
+
168
+ it('limits normal tooltip series rows to one isolated legend selection', () => {
169
+ expect(
170
+ shouldShowTooltipSeriesRow({
171
+ legendBehavior: 'isolate',
172
+ seriesHighlight: ['cases'],
173
+ seriesKey: 'cases'
174
+ })
175
+ ).toBe(true)
176
+ expect(
177
+ shouldShowTooltipSeriesRow({
178
+ legendBehavior: 'isolate',
179
+ seriesHighlight: ['cases'],
180
+ seriesKey: 'deaths'
181
+ })
182
+ ).toBe(false)
183
+ })
184
+
185
+ it('limits normal tooltip series rows to multiple isolated legend selections', () => {
186
+ expect(
187
+ shouldShowTooltipSeriesRow({
188
+ legendBehavior: 'isolate',
189
+ seriesHighlight: ['cases', 'hospitalizations'],
190
+ seriesKey: 'hospitalizations'
191
+ })
192
+ ).toBe(true)
193
+ expect(
194
+ shouldShowTooltipSeriesRow({
195
+ legendBehavior: 'isolate',
196
+ seriesHighlight: ['cases', 'hospitalizations'],
197
+ seriesKey: 'deaths'
198
+ })
199
+ ).toBe(false)
200
+ })
201
+
202
+ it('keeps all normal tooltip series rows visible in highlight mode with selected series', () => {
203
+ expect(
204
+ shouldShowTooltipSeriesRow({
205
+ legendBehavior: 'highlight',
206
+ seriesHighlight: ['cases'],
207
+ seriesKey: 'deaths'
208
+ })
209
+ ).toBe(true)
210
+ })
211
+
212
+ it('can filter normal tooltip rows while preserving headings and additional columns', () => {
213
+ const rows: TooltipRow[] = [
214
+ { key: 'Date', value: '2024-01-01', kind: 'heading' },
215
+ { key: 'Cases', value: '10', kind: 'series', seriesKey: 'cases' },
216
+ { key: 'Deaths', value: '2', kind: 'series', seriesKey: 'deaths' },
217
+ { key: 'Source', value: 'CDC', kind: 'extra' }
218
+ ]
219
+ const filteredRows = rows.filter(
220
+ row =>
221
+ row.kind !== 'series' ||
222
+ shouldShowTooltipSeriesRow({
223
+ legendBehavior: 'isolate',
224
+ seriesHighlight: ['cases'],
225
+ seriesKey: row.seriesKey
226
+ })
227
+ )
228
+
229
+ expect(filteredRows.map(row => row.key)).toEqual(['Date', 'Cases', 'Source'])
230
+ })
151
231
  })
@@ -84,6 +84,22 @@ export const getTooltipSeriesMarker = (
84
84
 
85
85
  export const buildTooltipRow = (row: TooltipRow): TooltipRow => row
86
86
 
87
+ export const shouldShowTooltipSeriesRow = ({
88
+ legendBehavior,
89
+ seriesHighlight = [],
90
+ seriesKey
91
+ }: {
92
+ legendBehavior?: string
93
+ seriesHighlight?: string[]
94
+ seriesKey?: string
95
+ }): boolean => {
96
+ if (legendBehavior === 'isolate' && seriesHighlight.length > 0 && seriesKey) {
97
+ return seriesHighlight.includes(seriesKey)
98
+ }
99
+
100
+ return true
101
+ }
102
+
87
103
  const getMarkerSignature = ({
88
104
  markerColor,
89
105
  markerShape = 'circle'