@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
@@ -9,7 +9,7 @@ vi.mock('@cdc/core/components/ui/Icon', () => ({
9
9
  vi.mock('@cdc/core/components/ui/Tooltip', () => {
10
10
  const Tooltip = ({ children }) => <>{children}</>
11
11
  Tooltip.Target = ({ children }) => <>{children}</>
12
- Tooltip.Content = ({ children }) => <>{children}</>
12
+ Tooltip.Content = ({ children }) => <span>{children}</span>
13
13
  return { default: Tooltip }
14
14
  })
15
15
 
@@ -33,7 +33,7 @@ import { createMockChartContext } from '../../LinearChart/tests/mockConfigContex
33
33
 
34
34
  beforeAll(() => {
35
35
  HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
36
- measureText: vi.fn(() => ({ width: 50 })),
36
+ measureText: vi.fn((text: string) => ({ width: text.length * 7 })),
37
37
  fillText: vi.fn(),
38
38
  fillRect: vi.fn(),
39
39
  clearRect: vi.fn()
@@ -88,8 +88,8 @@ const buildHeatMapContext = () => {
88
88
  },
89
89
  heatmap: {
90
90
  cellPadding: 1,
91
- rowLabelGap: 32,
92
- columnLabelGap: 56,
91
+ rowLabelGap: 0,
92
+ columnLabelGap: 15,
93
93
  colorBucketCount: 9,
94
94
  xAxisPosition: 'top',
95
95
  showCellValues: false
@@ -246,8 +246,8 @@ const buildSeriesModeHeatMapContext = () => {
246
246
  },
247
247
  heatmap: {
248
248
  cellPadding: 1,
249
- rowLabelGap: 32,
250
- columnLabelGap: 56,
249
+ rowLabelGap: 0,
250
+ columnLabelGap: 15,
251
251
  colorBucketCount: 9,
252
252
  xAxisPosition: 'top',
253
253
  showCellValues: false
@@ -436,13 +436,13 @@ const buildCategoricalAverageAgeHeatMapContext = () => {
436
436
  return context
437
437
  }
438
438
 
439
- const getTranslateY = (element: Element | null) => {
439
+ const getTranslateY = (element: Element | null | undefined) => {
440
440
  const transform = element?.getAttribute('transform') || ''
441
441
  const match = transform.match(/translate\([^,]+,\s*([^)]+)\)/)
442
442
  return match ? Number(match[1]) : 0
443
443
  }
444
444
 
445
- const getTranslateX = (element: Element | null) => {
445
+ const getTranslateX = (element: Element | null | undefined) => {
446
446
  const transform = element?.getAttribute('transform') || ''
447
447
  const match = transform.match(/translate\(([^,\s)]+)/)
448
448
  return match ? Number(match[1]) : 0
@@ -454,8 +454,8 @@ describe('HeatMap', () => {
454
454
  it('renders cells with tooltip metadata from additional columns', () => {
455
455
  const context = buildHeatMapContext()
456
456
  const rows = [
457
- { month: '2024-02-01', North: 5, South: 7, notes: 'Severe', population: 2500 },
458
- { month: '2024-01-01', North: 2, South: 4, notes: 'Low', population: 1000 }
457
+ { month: '2024-02-01', North: 5, South: 7, notes: 'Severe', population: 2500, rank: 20 },
458
+ { month: '2024-01-01', North: 2, South: 4, notes: 'Low', population: 1000, rank: 10 }
459
459
  ]
460
460
 
461
461
  context.config.data = rows
@@ -465,6 +465,7 @@ describe('HeatMap', () => {
465
465
  context.excludedData = rows
466
466
  context.rawData = rows
467
467
  context.tableData = rows
468
+ ;(context.config as any).dataFormat.suffix = '%'
468
469
  ;(context.config as any).columns.population = {
469
470
  label: 'Population',
470
471
  tooltips: true,
@@ -474,6 +475,13 @@ describe('HeatMap', () => {
474
475
  roundToPlace: 0,
475
476
  commas: true
476
477
  }
478
+ ;(context.config as any).columns.rank = {
479
+ label: 'Rank',
480
+ tooltips: true,
481
+ dataTable: true,
482
+ roundToPlace: 0,
483
+ commas: true
484
+ }
477
485
 
478
486
  const { container } = render(
479
487
  <ConfigContext.Provider value={context}>
@@ -492,6 +500,8 @@ describe('HeatMap', () => {
492
500
  expect(tooltipHtml).toContain('Value:')
493
501
  expect(tooltipHtml).toContain('Notes')
494
502
  expect(tooltipHtml).toContain('Population: $1,000 residents')
503
+ expect(tooltipHtml).toContain('Rank: 10')
504
+ expect(tooltipHtml).not.toContain('Rank: 10%')
495
505
  expect(tooltipHtml).not.toContain('<br/>')
496
506
  expect(cells[0]?.getAttribute('tabindex')).toBe('0')
497
507
  expect(cells[0]?.getAttribute('aria-label')).toContain('Month: Tooltip 2024-01-01')
@@ -669,16 +679,17 @@ describe('HeatMap', () => {
669
679
  const firstCellTooltipHtml = cells[0]?.getAttribute('data-tooltip-html') || ''
670
680
  const firstCellAriaLabel = cells[0]?.getAttribute('aria-label') || ''
671
681
  const firstCellX = Number(cells[0]?.getAttribute('x'))
682
+ const lastCell = cells[cells.length - 1]
683
+ const lastCellRight = Number(lastCell?.getAttribute('x')) + Number(lastCell?.getAttribute('width'))
672
684
  const xAxisTitle = Array.from(container.querySelectorAll('text')).find(
673
685
  text => text.textContent === 'Community Type'
674
686
  )
675
687
  const xAxisTitleX = Number(xAxisTitle?.getAttribute('x'))
676
688
 
677
689
  expect(cells).toHaveLength(9)
678
- expect(firstCellX).toBeGreaterThan(20)
679
- expect(firstCellX).toBeLessThan(90)
690
+ expect(firstCellX).toBe(0)
680
691
  expect(xAxisTitleX).toBeGreaterThan(firstCellX)
681
- expect(xAxisTitleX).toBeLessThan(180)
692
+ expect(xAxisTitleX).toBeLessThan(lastCellRight)
682
693
  expect(firstCellTooltipHtml).toContain('Community Type: Urban Core')
683
694
  expect(firstCellTooltipHtml).toContain('City: Atlanta')
684
695
  expect(firstCellTooltipHtml).toContain('Average age: 34')
@@ -733,10 +744,16 @@ describe('HeatMap', () => {
733
744
  expect(visibleSeriesTooltipHtml).toContain('Average age: 36')
734
745
  })
735
746
 
736
- it('applies y-axis tick rotation to row labels when configured', () => {
747
+ it('shares existing bottom space with rotated row labels instead of shrinking cells unnecessarily', () => {
748
+ const unrotatedContext = buildCategoricalAverageAgeHeatMapContext()
737
749
  const context = buildCategoricalAverageAgeHeatMapContext()
738
750
  ;(context.config as any).yAxis.tickRotation = 30
739
751
 
752
+ const { container: unrotatedContainer } = render(
753
+ <ConfigContext.Provider value={unrotatedContext}>
754
+ <HeatMap parentWidth={800} parentHeight={320} />
755
+ </ConfigContext.Provider>
756
+ )
740
757
  const { container } = render(
741
758
  <ConfigContext.Provider value={context}>
742
759
  <HeatMap parentWidth={800} parentHeight={320} />
@@ -744,8 +761,28 @@ describe('HeatMap', () => {
744
761
  )
745
762
 
746
763
  const rowLabel = Array.from(container.querySelectorAll('text')).find(text => text.textContent === 'Atlanta')
764
+ const unrotatedCell = unrotatedContainer.querySelector('.visx-heatmap-rect')
765
+ const rotatedCell = container.querySelector('.visx-heatmap-rect')
747
766
 
748
767
  expect(rowLabel?.getAttribute('transform')).toContain('rotate(-30')
768
+ expect(Number(rotatedCell?.getAttribute('height'))).toBe(Number(unrotatedCell?.getAttribute('height')))
769
+ })
770
+
771
+ it('keeps a visible grid when a long rotated row label needs more space than the chart height', () => {
772
+ const context = buildCategoricalAverageAgeHeatMapContext()
773
+ const longRowLabel = 'A'.repeat(500)
774
+ ;(context.config as any).yAxis.tickRotation = 90
775
+ ;(context.config as any).runtime.seriesLabels.Atlanta = longRowLabel
776
+
777
+ const { container } = render(
778
+ <ConfigContext.Provider value={context}>
779
+ <HeatMap parentWidth={800} parentHeight={320} />
780
+ </ConfigContext.Provider>
781
+ )
782
+
783
+ const firstCell = container.querySelector('.visx-heatmap-rect')
784
+
785
+ expect(Number(firstCell?.getAttribute('height'))).toBeGreaterThan(0)
749
786
  })
750
787
 
751
788
  it('renders top y-axis titles above the heatmap and reserves layout space', () => {
@@ -927,6 +964,28 @@ describe('HeatMap', () => {
927
964
  expect(columnLabel?.getAttribute('text-anchor')).toBe('start')
928
965
  })
929
966
 
967
+ it('samples x-axis labels without removing heatmap cells when a tick count is configured', () => {
968
+ const context = buildCategoricalAverageAgeHeatMapContext()
969
+ ;(context.config as any).xAxis.numTicks = 2
970
+
971
+ const { container } = render(
972
+ <ConfigContext.Provider value={context}>
973
+ <HeatMap parentWidth={800} parentHeight={320} />
974
+ </ConfigContext.Provider>
975
+ )
976
+
977
+ expect(container.querySelectorAll('.visx-heatmap-rect')).toHaveLength(9)
978
+ expect(screen.getByText('Urban Core')).toBeTruthy()
979
+ expect(screen.queryByText('Suburban')).toBeNull()
980
+ expect(screen.getByText('Rural')).toBeTruthy()
981
+
982
+ const plotLeft = getTranslateX(container.querySelector('.cdc-heatmap__plot'))
983
+ const lastCell = container.querySelectorAll('.visx-heatmap-rect')[8]
984
+ const lastCellRight = Number(lastCell?.getAttribute('x')) + Number(lastCell?.getAttribute('width'))
985
+
986
+ expect(800 - (plotLeft + lastCellRight)).toBeGreaterThanOrEqual(25)
987
+ })
988
+
930
989
  it('places the x-axis below the heatmap when configured', () => {
931
990
  const context = buildCategoricalAverageAgeHeatMapContext()
932
991
  ;(context.config as any).heatmap.xAxisPosition = 'bottom'
@@ -972,6 +1031,61 @@ describe('HeatMap', () => {
972
1031
  expect(columnLabel?.querySelector('tspan')?.getAttribute('dy')).toBe('0.355em')
973
1032
  })
974
1033
 
1034
+ it('uses larger cell text when it fits and hides values wider than the cell', () => {
1035
+ const context = buildHeatMapContext()
1036
+ ;(context.config as any).heatmap.showCellValues = true
1037
+ context.formatNumber = () => '1234567890'.repeat(20)
1038
+ const { container, rerender } = render(
1039
+ <ConfigContext.Provider value={context}>
1040
+ <HeatMap parentWidth={800} parentHeight={320} />
1041
+ </ConfigContext.Provider>
1042
+ )
1043
+ expect(container.querySelectorAll('.cdc-heatmap__cell-value')).toHaveLength(0)
1044
+ context.formatNumber = () => '12'
1045
+ rerender(
1046
+ <ConfigContext.Provider value={{ ...context }}>
1047
+ <HeatMap parentWidth={800} parentHeight={320} />
1048
+ </ConfigContext.Provider>
1049
+ )
1050
+ expect(container.querySelector('.cdc-heatmap__cell-value')?.getAttribute('font-size')).toBe('16')
1051
+ })
1052
+
1053
+ it('scales cell text with height below the cap and uses the new column gap fallback', () => {
1054
+ const context = buildHeatMapContext()
1055
+ const config = context.config as any
1056
+ config.heatmap.showCellValues = true
1057
+ delete config.heatmap.columnLabelGap
1058
+ config.hideXAxisLabel = true
1059
+ config.xAxis.hideLabel = true
1060
+ config.xAxis.hideTicks = true
1061
+ config.xAxis.size = 0
1062
+ const { container } = render(
1063
+ <ConfigContext.Provider value={context}>
1064
+ <HeatMap parentWidth={800} parentHeight={80} />
1065
+ </ConfigContext.Provider>
1066
+ )
1067
+ const cell = container.querySelector('.visx-heatmap-rect')
1068
+ const label = container.querySelector('.cdc-heatmap__cell-value')
1069
+ expect(Number(label?.getAttribute('font-size'))).toBeCloseTo(Number(cell?.getAttribute('height')) * 0.5)
1070
+ expect(Number(label?.getAttribute('font-size'))).toBeLessThan(16)
1071
+ expect(container.querySelector('.visx-axis-top text')?.getAttribute('y')).toBe('-20')
1072
+ })
1073
+
1074
+ it('keeps the bottom axis title close to the bottom edge', () => {
1075
+ const context = buildHeatMapContext()
1076
+ ;(context.config as any).heatmap.xAxisPosition = 'bottom'
1077
+ ;(context.config as any).xAxis.size = 0
1078
+ const { container } = render(
1079
+ <ConfigContext.Provider value={context}>
1080
+ <HeatMap parentWidth={800} parentHeight={320} />
1081
+ </ConfigContext.Provider>
1082
+ )
1083
+ const title = container.querySelector('.cdc-heatmap__axis-title')
1084
+ const baseline = getHeatMapPlotTop(container) + Number(title?.getAttribute('y'))
1085
+ expect(320 - baseline).toBeLessThanOrEqual(14)
1086
+ expect(320 - baseline).toBeGreaterThanOrEqual(8)
1087
+ })
1088
+
975
1089
  it('renders readable cell values when HeatMap cell labels are enabled', () => {
976
1090
  const context = buildCategoricalAverageAgeHeatMapContext()
977
1091
  ;(context.config as any).heatmap.showCellValues = true
@@ -988,9 +1102,10 @@ describe('HeatMap', () => {
988
1102
  expect(cellValues.every(label => ['#1c1d1f', '#fff'].includes(label.getAttribute('fill') || ''))).toBe(true)
989
1103
  })
990
1104
 
991
- it('uses the HeatMap row label gap setting to tune spacing between row labels and cells', () => {
1105
+ it('adds the row label gap before ticks while keeping the grid against the axis', () => {
992
1106
  const context = buildCategoricalAverageAgeHeatMapContext()
993
1107
  ;(context.config as any).heatmap.rowLabelGap = 18
1108
+ ;(context.config as any).xAxis.tickColor = '#123456'
994
1109
 
995
1110
  const { container } = render(
996
1111
  <ConfigContext.Provider value={context}>
@@ -999,8 +1114,207 @@ describe('HeatMap', () => {
999
1114
  )
1000
1115
 
1001
1116
  const firstCell = container.querySelector('.visx-heatmap-rect')
1117
+ const xAxisLine = container.querySelector('.cdc-heatmap__x-axis-line')
1118
+
1119
+ expect(Number(firstCell?.getAttribute('x'))).toBe(0)
1120
+ expect(Number(container.querySelector('.visx-axis-left text')?.getAttribute('x'))).toBe(-26)
1121
+ expect(xAxisLine?.getAttribute('x1')).toBe('0')
1122
+ expect(Number(xAxisLine?.getAttribute('x2'))).toBeGreaterThan(Number(firstCell?.getAttribute('x')))
1123
+ expect(xAxisLine?.getAttribute('stroke')).toBe('#123456')
1124
+ })
1125
+
1126
+ it('preserves a visible grid when the configured row label gap exceeds the plot width', () => {
1127
+ const context = buildCategoricalAverageAgeHeatMapContext()
1128
+ ;(context.config as any).heatmap.rowLabelGap = 10_000
1129
+
1130
+ const { container } = render(
1131
+ <ConfigContext.Provider value={context}>
1132
+ <HeatMap parentWidth={800} parentHeight={320} />
1133
+ </ConfigContext.Provider>
1134
+ )
1135
+
1136
+ const firstCell = container.querySelector('.visx-heatmap-rect')
1137
+
1138
+ expect(Number(firstCell?.getAttribute('width'))).toBeGreaterThan(0)
1139
+ })
1140
+
1141
+ it('uses rectangular cells to fill the available plot width', () => {
1142
+ const context = buildHeatMapContext()
1143
+
1144
+ const { container } = render(
1145
+ <ConfigContext.Provider value={context}>
1146
+ <HeatMap parentWidth={800} parentHeight={320} />
1147
+ </ConfigContext.Provider>
1148
+ )
1149
+
1150
+ const firstCell = container.querySelector('.visx-heatmap-rect')
1151
+ const cells = Array.from(container.querySelectorAll('.visx-heatmap-rect'))
1152
+ const lastCell = cells[cells.length - 1]
1153
+ const xAxisLine = container.querySelector('.cdc-heatmap__x-axis-line')
1154
+ const lastCellRight = Number(lastCell?.getAttribute('x')) + Number(lastCell?.getAttribute('width'))
1155
+
1156
+ expect(Number(firstCell?.getAttribute('width'))).toBeGreaterThan(Number(firstCell?.getAttribute('height')))
1157
+ expect(Math.abs(Number(xAxisLine?.getAttribute('x2')) - lastCellRight)).toBeLessThanOrEqual(2)
1158
+ })
1159
+
1160
+ it('keeps the row axis fixed and opens horizontal scrolling at the first column when enabled', () => {
1161
+ const context = buildHeatMapContext()
1162
+ ;(context.config as any).heatmap.horizontalScroll = true
1163
+ ;(context.config as any).heatmap.minColumnWidth = 220
1164
+
1165
+ const { container } = render(
1166
+ <ConfigContext.Provider value={context}>
1167
+ <HeatMap parentWidth={320} parentHeight={320} />
1168
+ </ConfigContext.Provider>
1169
+ )
1170
+
1171
+ const scrollArea = screen.getByRole('region', { name: /HeatMap chart.*Month.*scrollable columns/i })
1172
+ const scrollContent = scrollArea.querySelector('.cdc-heatmap__scroll-content')
1173
+ const fixedAxis = container.querySelector('.cdc-heatmap__fixed-axis-svg')
1174
+ const fixedXAxisTitle = container.querySelector('.cdc-heatmap__fixed-x-title')
1175
+ const firstCell = scrollArea.querySelector('.visx-heatmap-rect')
1176
+ const scrollInstructionsId = scrollArea.getAttribute('aria-describedby')
1177
+ const leftScrollCue = container.querySelector('.cdc-heatmap__scroll-cue--left')
1178
+ const rightScrollCue = container.querySelector('.cdc-heatmap__scroll-cue--right')
1179
+
1180
+ expect(scrollArea.scrollLeft).toBe(0)
1181
+ expect(Number(scrollContent?.getAttribute('width'))).toBeGreaterThan(320)
1182
+ expect(Number(firstCell?.getAttribute('width'))).toBeCloseTo(219, 1)
1183
+ expect(fixedAxis?.textContent).toContain('North')
1184
+ expect(fixedAxis?.getAttribute('aria-label')).toContain('Region row axis')
1185
+ expect(scrollArea.contains(fixedAxis)).toBe(false)
1186
+ expect(fixedXAxisTitle?.textContent).toContain('Month')
1187
+ expect(scrollContent?.textContent).not.toContain('Month')
1188
+ expect(scrollInstructionsId).toBeTruthy()
1189
+ expect(document.getElementById(scrollInstructionsId || '')?.textContent).toContain(
1190
+ 'Scroll horizontally to view additional columns.'
1191
+ )
1192
+ expect(leftScrollCue).toHaveAttribute('aria-hidden', 'true')
1193
+ expect(rightScrollCue).toHaveAttribute('aria-hidden', 'true')
1194
+
1195
+ Object.defineProperties(scrollArea, {
1196
+ clientWidth: { configurable: true, value: 200 },
1197
+ scrollWidth: { configurable: true, value: 600 }
1198
+ })
1199
+
1200
+ fireEvent.scroll(scrollArea)
1201
+ expect(leftScrollCue).not.toHaveClass('is-visible')
1202
+ expect(rightScrollCue).toHaveClass('is-visible')
1203
+
1204
+ scrollArea.scrollLeft = 200
1205
+ fireEvent.scroll(scrollArea)
1206
+ expect(leftScrollCue).toHaveClass('is-visible')
1207
+ expect(rightScrollCue).toHaveClass('is-visible')
1208
+
1209
+ scrollArea.scrollLeft = 400
1210
+ fireEvent.scroll(scrollArea)
1211
+ expect(leftScrollCue).toHaveClass('is-visible')
1212
+ expect(rightScrollCue).not.toHaveClass('is-visible')
1213
+ })
1214
+
1215
+ it('preserves the responsive non-scroll layout when horizontal scrolling is disabled', () => {
1216
+ const context = buildHeatMapContext()
1217
+ ;(context.config as any).heatmap.horizontalScroll = false
1218
+ ;(context.config as any).heatmap.minColumnWidth = 220
1219
+
1220
+ const { container } = render(
1221
+ <ConfigContext.Provider value={context}>
1222
+ <HeatMap parentWidth={320} parentHeight={320} />
1223
+ </ConfigContext.Provider>
1224
+ )
1225
+
1226
+ expect(screen.queryByRole('region', { name: /scrollable columns/i })).toBeNull()
1227
+ expect(container.querySelector('.cdc-heatmap__fixed-axis-svg')).toBeNull()
1228
+ expect(container.querySelector('.cdc-heatmap__scroll-cue')).toBeNull()
1229
+ expect(Number(container.querySelector('.cdc-heatmap__svg')?.getAttribute('width'))).toBe(320)
1230
+ })
1231
+
1232
+ it('does not add a scroll region when enabled columns already fit', () => {
1233
+ const context = buildHeatMapContext()
1234
+ ;(context.config as any).heatmap.horizontalScroll = true
1235
+ ;(context.config as any).heatmap.minColumnWidth = 44
1236
+
1237
+ const { container } = render(
1238
+ <ConfigContext.Provider value={context}>
1239
+ <HeatMap parentWidth={800} parentHeight={320} />
1240
+ </ConfigContext.Provider>
1241
+ )
1242
+
1243
+ expect(screen.queryByRole('region', { name: /scrollable columns/i })).toBeNull()
1244
+ expect(container.querySelector('.cdc-heatmap__scroll-cue')).toBeNull()
1245
+ })
1246
+
1247
+ it('caps authored minimum column widths at the supported maximum', () => {
1248
+ const context = buildHeatMapContext()
1249
+ ;(context.config as any).heatmap.horizontalScroll = true
1250
+ ;(context.config as any).heatmap.minColumnWidth = 10_000
1251
+
1252
+ render(
1253
+ <ConfigContext.Provider value={context}>
1254
+ <HeatMap parentWidth={320} parentHeight={320} />
1255
+ </ConfigContext.Provider>
1256
+ )
1257
+
1258
+ const scrollArea = screen.getByRole('region', { name: /scrollable columns/i })
1259
+ const firstCell = scrollArea.querySelector('.visx-heatmap-rect')
1260
+
1261
+ expect(Number(firstCell?.getAttribute('width'))).toBeCloseTo(399, 1)
1262
+ })
1263
+
1264
+ it('protects bottom rotated labels while preserving a positive row gap in scroll mode', () => {
1265
+ const context = buildHeatMapContext()
1266
+ ;(context.config as any).heatmap.horizontalScroll = true
1267
+ ;(context.config as any).heatmap.minColumnWidth = 60
1268
+ ;(context.config as any).heatmap.rowLabelGap = 18
1269
+ ;(context.config as any).heatmap.xAxisPosition = 'bottom'
1270
+ ;(context.config as any).xAxis.tickRotation = 45
1271
+
1272
+ const { container } = render(
1273
+ <ConfigContext.Provider value={context}>
1274
+ <HeatMap parentWidth={200} parentHeight={320} />
1275
+ </ConfigContext.Provider>
1276
+ )
1277
+
1278
+ const scrollArea = screen.getByRole('region', { name: /scrollable columns/i })
1279
+ const firstCell = scrollArea.querySelector('.visx-heatmap-rect')
1280
+ const firstTickLabel = scrollArea.querySelector('.visx-axis-bottom text')
1281
+ const xAxisLine = scrollArea.querySelector('.cdc-heatmap__x-axis-line')
1282
+ const fixedXAxisTitle = container.querySelector('.cdc-heatmap__fixed-x-title text')
1283
+ const fixedXAxisTitleSvg = container.querySelector('.cdc-heatmap__fixed-x-title')
1284
+
1285
+ expect(container.querySelector('.visx-axis-bottom')).toBeTruthy()
1286
+ expect(Number(firstCell?.getAttribute('x'))).toBeGreaterThan(18)
1287
+ expect(firstTickLabel?.getAttribute('transform')).toContain('rotate(-45')
1288
+ expect(xAxisLine?.getAttribute('x1')).toBe('0')
1289
+ expect(Number(fixedXAxisTitle?.getAttribute('x'))).toBeCloseTo(
1290
+ Number(fixedXAxisTitleSvg?.getAttribute('width')) / 2,
1291
+ 1
1292
+ )
1293
+ })
1294
+
1295
+ it('caps scrollable endpoint padding for extremely long first labels', () => {
1296
+ const context = buildHeatMapContext()
1297
+ ;(context.config as any).heatmap.horizontalScroll = true
1298
+ ;(context.config as any).heatmap.minColumnWidth = 60
1299
+ ;(context.config as any).heatmap.rowLabelGap = 18
1300
+ ;(context.config as any).heatmap.xAxisPosition = 'bottom'
1301
+ ;(context.config as any).xAxis.tickRotation = 45
1302
+ context.formatDate = () => 'X'.repeat(10_000)
1303
+
1304
+ render(
1305
+ <ConfigContext.Provider value={context}>
1306
+ <HeatMap parentWidth={200} parentHeight={320} />
1307
+ </ConfigContext.Provider>
1308
+ )
1309
+
1310
+ const scrollArea = screen.getByRole('region', { name: /scrollable columns/i })
1311
+ const firstCell = scrollArea.querySelector('.visx-heatmap-rect')
1312
+ const scrollContent = scrollArea.querySelector('.cdc-heatmap__scroll-content')
1313
+ const fixedXAxisTitle = document.querySelector('.cdc-heatmap__fixed-x-title')
1002
1314
 
1003
- expect(Number(firstCell?.getAttribute('x'))).toBeCloseTo(18, 1)
1315
+ expect(Number(firstCell?.getAttribute('x'))).toBeLessThanOrEqual(258)
1316
+ expect(Number(firstCell?.getAttribute('x'))).toBeLessThan(Number(fixedXAxisTitle?.getAttribute('width')))
1317
+ expect(Number(scrollContent?.getAttribute('width'))).toBeLessThan(500)
1004
1318
  })
1005
1319
 
1006
1320
  it('uses the HeatMap column label gap setting to tune spacing above the first row', () => {
@@ -1020,6 +1334,8 @@ describe('HeatMap', () => {
1020
1334
 
1021
1335
  it('shows HeatMap editor controls in their owning accordion sections', () => {
1022
1336
  const context = buildHeatMapContext()
1337
+ ;(context.config as any).xAxis.manual = true
1338
+ ;(context.config as any).heatmap.horizontalScroll = true
1023
1339
 
1024
1340
  render(
1025
1341
  <ConfigContext.Provider value={context}>
@@ -1042,6 +1358,7 @@ describe('HeatMap', () => {
1042
1358
  fireEvent.click(yAxisHeading)
1043
1359
  fireEvent.click(dateCategoryHeading)
1044
1360
  fireEvent.click(settingsHeading)
1361
+ expect(screen.getByText(/To adjust cell height, use Chart Height in the General panel\./)).toBeTruthy()
1045
1362
  fireEvent.click(dataSeriesHeading)
1046
1363
  fireEvent.click(legendHeading)
1047
1364
 
@@ -1052,6 +1369,9 @@ describe('HeatMap', () => {
1052
1369
  expect(screen.getAllByText('Hide Axis')).toHaveLength(2)
1053
1370
  expect(screen.getAllByText('Hide Ticks')).toHaveLength(2)
1054
1371
  expect(screen.getAllByText('Tick rotation (Degrees)').length).toBeGreaterThan(0)
1372
+ expect(screen.getByText('Number of ticks')).toBeTruthy()
1373
+ expect(screen.queryByText('Manual Ticks')).toBeNull()
1374
+ expect(screen.queryByText('Step count')).toBeNull()
1055
1375
  expect(screen.getByLabelText('Label Placement')).toBeTruthy()
1056
1376
  expect(screen.getByText('Add Data Series')).toBeTruthy()
1057
1377
  expect(
@@ -1061,6 +1381,9 @@ describe('HeatMap', () => {
1061
1381
  Boolean(xAxisPositionLabel.compareDocumentPosition(settingsHeading) & Node.DOCUMENT_POSITION_FOLLOWING)
1062
1382
  ).toBe(true)
1063
1383
  expect(screen.getByText('Show Cell Values')).toBeTruthy()
1384
+ expect(screen.getByText('Enable Horizontal Scrolling')).toBeTruthy()
1385
+ expect(screen.getByText('Minimum Column Width')).toBeTruthy()
1386
+ expect(screen.getByLabelText('Minimum Column Width')).toHaveAttribute('max', '400')
1064
1387
  expect(screen.getByText('Cell Padding')).toBeTruthy()
1065
1388
  expect(screen.getByText('Row Label Gap')).toBeTruthy()
1066
1389
  expect(screen.getByText('Column Label Gap')).toBeTruthy()
@@ -6,6 +6,7 @@ import {
6
6
  getHeatMapColorScale,
7
7
  getHeatMapDataGroupRanges
8
8
  } from '../helpers'
9
+ import { getHeatMapXAxisTickValues } from '../helpers/layout'
9
10
  import { type ChartConfig } from '../../../types/ChartConfig'
10
11
 
11
12
  describe('buildHeatMapData', () => {
@@ -165,3 +166,81 @@ describe('HeatMap data grouping', () => {
165
166
  ])
166
167
  })
167
168
  })
169
+
170
+ describe('HeatMap x-axis tick values', () => {
171
+ const domain = ['a', 'b', 'c', 'd', 'e']
172
+ const formattedLabels = Object.fromEntries(domain.map(value => [value, `Label ${value}`]))
173
+ const measureLabel = () => 50
174
+
175
+ it('samples an explicit tick count evenly while preserving the first and last values', () => {
176
+ expect(
177
+ getHeatMapXAxisTickValues({
178
+ domain,
179
+ formattedLabels,
180
+ gridWidth: 500,
181
+ rotationDegrees: 0,
182
+ requestedCount: 3,
183
+ measureLabel,
184
+ fontSize: 16
185
+ })
186
+ ).toEqual(['a', 'c', 'e'])
187
+ })
188
+
189
+ it('automatically reduces dense labels without changing the domain', () => {
190
+ const tickValues = getHeatMapXAxisTickValues({
191
+ domain,
192
+ formattedLabels,
193
+ gridWidth: 150,
194
+ rotationDegrees: 0,
195
+ measureLabel,
196
+ fontSize: 16
197
+ })
198
+
199
+ expect(tickValues).toEqual(['a', 'e'])
200
+ expect(domain).toEqual(['a', 'b', 'c', 'd', 'e'])
201
+ })
202
+
203
+ it('keeps every short categorical label when neighboring labels fit', () => {
204
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
205
+ const labelWidths = [23, 24, 27, 23, 29, 22, 17, 27, 26, 23, 27, 27]
206
+
207
+ expect(
208
+ getHeatMapXAxisTickValues({
209
+ domain: months,
210
+ formattedLabels: Object.fromEntries(months.map(month => [month, month])),
211
+ gridWidth: 468,
212
+ rotationDegrees: 0,
213
+ measureLabel: label => labelWidths[months.indexOf(label)],
214
+ fontSize: 16
215
+ })
216
+ ).toEqual(months)
217
+ })
218
+
219
+ it('allows more labels when rotation reduces their horizontal footprint', () => {
220
+ expect(
221
+ getHeatMapXAxisTickValues({
222
+ domain,
223
+ formattedLabels,
224
+ gridWidth: 150,
225
+ rotationDegrees: 90,
226
+ measureLabel,
227
+ fontSize: 16
228
+ })
229
+ ).toEqual(domain)
230
+ })
231
+
232
+ it('handles very large category domains without spreading every label into Math.max', () => {
233
+ const largeDomain = Array.from({ length: 150_000 }, (_, index) => String(index))
234
+ const tickValues = getHeatMapXAxisTickValues({
235
+ domain: largeDomain,
236
+ formattedLabels: {},
237
+ gridWidth: 100,
238
+ rotationDegrees: 0,
239
+ measureLabel: () => 1,
240
+ fontSize: 1
241
+ })
242
+
243
+ expect(tickValues[0]).toBe('0')
244
+ expect(tickValues[tickValues.length - 1]).toBe('149999')
245
+ })
246
+ })