@cdc/chart 4.26.6 → 4.26.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CONFIG.md +504 -368
  2. package/dist/cdcchart-CYc-Kayd.es.js +22488 -0
  3. package/dist/cdcchart-DCKn8NmI.es.js +20695 -0
  4. package/dist/cdcchart-Dyui9nk_.es.js +45828 -0
  5. package/dist/cdcchart.js +4 -96498
  6. package/examples/__data__/vega-remote-data.json +6 -0
  7. package/examples/dendrogram.json +18 -0
  8. package/examples/feature/bar/bar-chart-race-annual-change.json +68 -0
  9. package/examples/feature/heatmap/average-age-categorical.json +2 -2
  10. package/examples/feature/heatmap/calendar-heatmap.json +2 -2
  11. package/examples/feature/heatmap/cell-values-bottom-axis.json +2 -2
  12. package/examples/feature/heatmap/sparse-aggregation.json +2 -2
  13. package/examples/feature/line/line-chart-race.json +50 -0
  14. package/examples/feature/sankey/sankey-example-data.json +40 -1441
  15. package/examples/feature/sankey/sankey_chart_data.csv +9 -20
  16. package/examples/network.json +31 -0
  17. package/examples/private/box-plot.json +573 -0
  18. package/examples/private/custom-color-issue.json +625 -0
  19. package/examples/private/custom-colors.json +522 -0
  20. package/examples/private/cutoff.json +10750 -0
  21. package/examples/private/flueview.json +22051 -0
  22. package/examples/private/overlay-legend.json +485 -0
  23. package/examples/private/pie-test.json +469 -0
  24. package/examples/vega-remote.json +22 -0
  25. package/package.json +11 -13
  26. package/src/CdcChart.tsx +2 -2
  27. package/src/CdcChartComponent.tsx +444 -217
  28. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  29. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  30. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  31. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  32. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  33. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  34. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  35. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  36. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  37. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  38. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  39. package/src/_stories/Chart.Network.stories.tsx +432 -0
  40. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  41. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  42. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  43. package/src/_stories/Chart.PieLabels.stories.tsx +49 -0
  44. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  45. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  46. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  47. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  48. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  49. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  50. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  51. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  52. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  53. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  54. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  55. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  56. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  57. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  58. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  59. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  60. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  61. package/src/_stories/_mock/editor-test-configs.json +20 -10
  62. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  63. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  64. package/src/_stories/_mock/heatmap.json +2 -2
  65. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  66. package/src/_stories/_mock/sankey_mock.json +23 -27
  67. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  68. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  69. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  70. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  71. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  72. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  73. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  74. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  75. package/src/components/Axis/BottomAxis.tsx +1 -1
  76. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  77. package/src/components/Axis/RightAxis.tsx +21 -13
  78. package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -8
  79. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +13 -4
  80. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  81. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +30 -26
  82. package/src/components/BarChart/components/BarChart.Vertical.tsx +38 -28
  83. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  84. package/src/components/BarChart/components/context.tsx +6 -0
  85. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  86. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  87. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  88. package/src/components/BarChart/helpers/index.ts +8 -0
  89. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  90. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  91. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  92. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  93. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +202 -0
  94. package/src/components/BarChart/helpers/useBarChart.ts +60 -60
  95. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  96. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  97. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  98. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  99. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  100. package/src/components/BarChartRace/helpers.test.ts +114 -0
  101. package/src/components/BarChartRace/helpers.ts +185 -0
  102. package/src/components/BarChartRace/index.ts +3 -0
  103. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  104. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  105. package/src/components/BoxPlot/helpers/index.ts +13 -83
  106. package/src/components/Brush/BrushSelector.tsx +10 -24
  107. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  108. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  109. package/src/components/Dendrogram/dendrogram.css +113 -0
  110. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  111. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  112. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  113. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  114. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  115. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  116. package/src/components/Dendrogram/index.tsx +1 -0
  117. package/src/components/Dendrogram/types/index.ts +66 -0
  118. package/src/components/EditorPanel/EditorPanel.tsx +1111 -805
  119. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  120. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  121. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  122. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  123. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +104 -61
  124. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  125. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  126. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +236 -36
  127. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  128. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  129. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +21 -6
  130. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  131. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  132. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  133. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  134. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  135. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  136. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  137. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  138. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +5 -4
  139. package/src/components/EditorPanel/useEditorPermissions.test.tsx +94 -0
  140. package/src/components/EditorPanel/useEditorPermissions.ts +75 -25
  141. package/src/components/Forecasting/Forecasting.tsx +16 -7
  142. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  143. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  144. package/src/components/HeatMap/heatmap.css +101 -2
  145. package/src/components/HeatMap/helpers/layout.ts +80 -0
  146. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  147. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  148. package/src/components/Legend/Legend.Component.tsx +163 -148
  149. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  150. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  151. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -35
  152. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  153. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  154. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  155. package/src/components/LineChart/helpers.test.ts +126 -38
  156. package/src/components/LineChart/helpers.ts +9 -6
  157. package/src/components/LineChart/index.tsx +71 -27
  158. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  159. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  160. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  161. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  162. package/src/components/LineChartRace/helpers.test.ts +88 -0
  163. package/src/components/LineChartRace/helpers.ts +157 -0
  164. package/src/components/LineChartRace/index.ts +3 -0
  165. package/src/components/LineChartRace/line-chart-race.css +16 -0
  166. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  167. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  168. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  169. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  170. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  171. package/src/components/LinearChart.tsx +85 -54
  172. package/src/components/Network/components/Network.tsx +372 -0
  173. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  174. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  175. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  176. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  177. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  178. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  179. package/src/components/Network/index.tsx +3 -0
  180. package/src/components/Network/network.css +107 -0
  181. package/src/components/Network/types/index.ts +56 -0
  182. package/src/components/PairedBarChart.jsx +5 -10
  183. package/src/components/PairedBarChart.test.tsx +71 -0
  184. package/src/components/PieChart/PieChart.tsx +121 -13
  185. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  186. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  187. package/src/components/RacingChartRenderer.tsx +44 -0
  188. package/src/components/Regions/components/Regions.test.tsx +179 -0
  189. package/src/components/Regions/components/Regions.tsx +104 -69
  190. package/src/components/Regions/index.tsx +2 -3
  191. package/src/components/Sankey/components/Sankey.tsx +264 -437
  192. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  193. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  194. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  195. package/src/components/Sankey/sankey.scss +57 -116
  196. package/src/components/Sankey/types/index.ts +41 -4
  197. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -12
  198. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  199. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  200. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  201. package/src/components/raceTiming.ts +18 -0
  202. package/src/data/initial-state.js +79 -31
  203. package/src/helpers/applyChartTypeContract.ts +71 -0
  204. package/src/helpers/axisLabelFontSize.ts +2 -2
  205. package/src/helpers/boxPlotStats.ts +60 -0
  206. package/src/helpers/bumpChartTooltip.ts +2 -1
  207. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  208. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  209. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  210. package/src/helpers/categoryOrder.ts +60 -0
  211. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  212. package/src/helpers/createNewChartConfig.ts +56 -0
  213. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  214. package/src/helpers/forecastLegend.ts +88 -0
  215. package/src/helpers/getBoxPlotConfig.ts +26 -38
  216. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  217. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  218. package/src/helpers/getColorScale.ts +31 -22
  219. package/src/helpers/getComboChartConfig.ts +0 -1
  220. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  221. package/src/helpers/getExcludedData.ts +0 -1
  222. package/src/helpers/getHasBoundarySuppression.ts +4 -3
  223. package/src/helpers/getMinMax.ts +11 -8
  224. package/src/helpers/getNewRuntime.ts +4 -3
  225. package/src/helpers/getPiePercent.ts +3 -3
  226. package/src/helpers/getStackedSeriesMax.ts +51 -0
  227. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  228. package/src/helpers/handleRankByValue.ts +15 -15
  229. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  230. package/src/helpers/seriesColumnSettings.ts +31 -3
  231. package/src/helpers/sizeHelpers.ts +4 -5
  232. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  233. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  234. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  235. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  236. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  237. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  238. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  239. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  240. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  241. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  242. package/src/helpers/tests/getColorScale.test.ts +226 -1
  243. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  244. package/src/helpers/tests/getMinMax.test.ts +41 -0
  245. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  246. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  247. package/src/helpers/tests/seriesColumnSettings.test.ts +83 -0
  248. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  249. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  250. package/src/helpers/tooltipHelpers.ts +16 -0
  251. package/src/hooks/tests/useRightAxis.test.tsx +143 -2
  252. package/src/hooks/tests/useScales.test.tsx +3 -6
  253. package/src/hooks/tests/useTooltip.test.tsx +484 -0
  254. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  255. package/src/hooks/useReduceData.ts +4 -36
  256. package/src/hooks/useRightAxis.ts +19 -26
  257. package/src/hooks/useScales.ts +15 -0
  258. package/src/hooks/useTooltip.tsx +106 -71
  259. package/src/scss/main.scss +74 -2
  260. package/src/store/chart.reducer.ts +4 -4
  261. package/src/test/CdcChart.dataTable.test.tsx +582 -14
  262. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  263. package/src/test/CdcChart.finalization.test.tsx +143 -0
  264. package/src/test/CdcChart.network.test.tsx +271 -0
  265. package/src/types/ChartConfig.ts +91 -54
  266. package/src/types/ChartContext.ts +5 -0
  267. package/src/types/Horizon.ts +1 -1
  268. package/LICENSE +0 -201
  269. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  270. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  271. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  272. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  273. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  274. package/src/data/legacy-defaults.ts +0 -18
  275. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  276. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
@@ -0,0 +1,71 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import ConfigContext from '../ConfigContext'
5
+ import PairedBarChart from './PairedBarChart'
6
+ import { createMockChartContext, createMockConfig } from './LinearChart/tests/mockConfigContext'
7
+
8
+ vi.mock('@cdc/core/helpers/cove/accessibility', () => ({
9
+ getContrastColor: () => '#000'
10
+ }))
11
+
12
+ vi.mock('@cdc/core/helpers/getTextWidth', () => ({
13
+ getTextWidth: () => 10
14
+ }))
15
+
16
+ const renderPairedBar = (casesLabel: string) => {
17
+ const baseConfig = createMockConfig()
18
+ const data = [{ Year: '2024', Cases: 10, Deaths: 2 }]
19
+ const config = createMockConfig({
20
+ visualizationType: 'Paired Bar',
21
+ orientation: 'horizontal',
22
+ barHasBorder: 'false',
23
+ barHeight: 25,
24
+ barSpace: 15,
25
+ xAxis: { ...baseConfig.xAxis, dataKey: 'Year', size: '50', type: 'categorical' },
26
+ yAxis: { ...baseConfig.yAxis, displayNumbersOnBar: false },
27
+ series: [
28
+ { dataKey: 'Cases', name: 'Reported Cases', type: 'Paired Bar', axis: 'Left', tooltip: true },
29
+ { dataKey: 'Deaths', name: 'Reported Deaths', type: 'Paired Bar', axis: 'Left', tooltip: true }
30
+ ] as any,
31
+ columns: {
32
+ Cases: { name: 'Cases', label: casesLabel },
33
+ Deaths: { name: 'Deaths', label: 'Death Rate' }
34
+ } as any,
35
+ runtime: {
36
+ ...baseConfig.runtime,
37
+ seriesLabels: { Cases: 'Reported Cases', Deaths: 'Reported Deaths' }
38
+ } as any
39
+ })
40
+ const context = createMockChartContext(config, {
41
+ transformedData: data,
42
+ colorScale: () => '#005ea8',
43
+ formatNumber: value => String(value)
44
+ })
45
+
46
+ return render(
47
+ <ConfigContext.Provider value={context}>
48
+ <PairedBarChart width={400} height={100} originalWidth={450} yAxisWidth={50} />
49
+ </ConfigContext.Provider>
50
+ )
51
+ }
52
+
53
+ describe('PairedBarChart tooltips', () => {
54
+ it('uses the customized series column label without rendering an undefined heading', () => {
55
+ const { container } = renderPairedBar('Case Rate')
56
+ const tooltip = container.querySelector('.bar.group-1')?.getAttribute('data-tooltip-html') || ''
57
+
58
+ expect(tooltip).toContain('Year: 2024')
59
+ expect(tooltip).toContain('Case Rate: 10')
60
+ expect(tooltip).not.toContain('Reported Cases')
61
+ expect(tooltip).not.toContain('undefined:')
62
+ })
63
+
64
+ it('falls back to the Series Name when the Label matches the data key', () => {
65
+ const { container } = renderPairedBar('Cases')
66
+ const tooltip = container.querySelector('.bar.group-1')?.getAttribute('data-tooltip-html') || ''
67
+
68
+ expect(tooltip).toContain('Reported Cases: 10')
69
+ expect(tooltip).not.toContain('undefined:')
70
+ })
71
+ })
@@ -4,11 +4,10 @@ import { animated, useTransition, to } from '@react-spring/web'
4
4
  // visx
5
5
  import { Pie } from '@visx/shape'
6
6
  import { Group } from '@visx/group'
7
- import { Text } from '@visx/text'
8
7
  import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
9
8
  import { colorPalettesChart as colorPalettes } from '@cdc/core/data/colorPalettes'
10
9
  import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
11
- import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
10
+ import { getColorPaletteMajorVersion } from '@cdc/core/helpers/getColorPaletteMajorVersion'
12
11
  import {
13
12
  v2ColorDistribution,
14
13
  divergentColorDistribution,
@@ -25,6 +24,9 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
25
24
  import { scaleOrdinal } from '@visx/scale'
26
25
  import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
27
26
  import { type TooltipDisplayData } from '../../helpers/tooltipHelpers'
27
+ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
28
+ import { getPieLabelPosition } from './helpers/labelPlacement'
29
+ import { getV21ChartDistributionColors } from '../../helpers/getV21ChartDistributionColors'
28
30
 
29
31
  type TooltipData = TooltipDisplayData
30
32
 
@@ -34,6 +36,37 @@ type PieChartProps = {
34
36
  interactionLabel?: string
35
37
  }
36
38
 
39
+ const ENHANCED_PIE_LABEL_FONT_SIZE = 13
40
+ const ENHANCED_PIE_LABEL_LINE_HEIGHT = 15
41
+ const ENHANCED_PIE_LABEL_HEIGHT = ENHANCED_PIE_LABEL_LINE_HEIGHT * 2
42
+ const ENHANCED_PIE_LABEL_FONT = `${ENHANCED_PIE_LABEL_FONT_SIZE}px sans-serif`
43
+ const ENHANCED_PIE_LABEL_MIN_ANGLE = 0.01
44
+ const LEGACY_PERCENT_LABEL_MIN_ANGLE = 0.1
45
+ const MIN_RADIUS_WITH_LABELS = 55
46
+ const MIN_LABEL_GUTTER = 96
47
+ const OUTSIDE_LABEL_OFFSET = 22
48
+ const LABEL_GUTTER_PADDING = OUTSIDE_LABEL_OFFSET + 16
49
+ const VERTICAL_LABEL_GUTTER = 44
50
+
51
+ const pieLabelTextWidthCache = new Map<string, number>()
52
+ const MAX_PIE_LABEL_TEXT_CACHE = 2000
53
+
54
+ const measurePieLabelText = (text: string) => {
55
+ const cached = pieLabelTextWidthCache.get(text)
56
+ if (cached !== undefined) return cached
57
+
58
+ const measured =
59
+ typeof document === 'undefined'
60
+ ? Math.ceil(text.length * ENHANCED_PIE_LABEL_FONT_SIZE * 0.6)
61
+ : getTextWidth(text, ENHANCED_PIE_LABEL_FONT) ?? Math.ceil(text.length * ENHANCED_PIE_LABEL_FONT_SIZE * 0.6)
62
+
63
+ // Avoid unbounded growth in long-lived sessions with many unique labels.
64
+ if (pieLabelTextWidthCache.size >= MAX_PIE_LABEL_TEXT_CACHE) pieLabelTextWidthCache.clear()
65
+
66
+ pieLabelTextWidthCache.set(text, measured)
67
+ return measured
68
+ }
69
+
37
70
  const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) => {
38
71
  const { interactionLabel = '' } = props
39
72
  const {
@@ -70,6 +103,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
70
103
  const pivotKey = dataNeedsPivot ? 'pivotColumn' : undefined
71
104
  const showPercentage = config.dataFormat.showPiePercent
72
105
  const labelForCalcArea = 'Calculated Area'
106
+ const showCategoryPercentageLabels = Boolean(config.labels)
73
107
 
74
108
  const _data = useMemo(() => {
75
109
  let baseData = []
@@ -124,7 +158,10 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
124
158
 
125
159
  // Helper function to determine enhanced distribution type and apply it
126
160
  const applyEnhancedColorDistribution = (config, palette, numberOfKeys) => {
127
- const version = getColorPaletteVersion(config)
161
+ const v21DistributionColors = getV21ChartDistributionColors(config, palette, numberOfKeys)
162
+ if (v21DistributionColors) return v21DistributionColors
163
+
164
+ const version = getColorPaletteMajorVersion(config)
128
165
  const configPalette = config.general?.palette?.name || config.palette
129
166
 
130
167
  // Skip enhanced distribution if not v2, too many keys, or wrong palette length
@@ -136,7 +173,6 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
136
173
  const isDivergent = configPalette && configPalette.includes('divergent')
137
174
  const isColorblindSafe =
138
175
  configPalette && (configPalette.includes('colorblindsafe') || configPalette.includes('qualitative_standard'))
139
-
140
176
  // Determine which distribution to use based on palette type
141
177
  let distributionMap = null
142
178
  if (isDivergent) {
@@ -217,6 +253,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
217
253
  config.xAxis.dataKey,
218
254
  config.general?.palette?.name,
219
255
  config.general?.palette?.isReversed,
256
+ config.general?.palette?.version,
220
257
  config.general?.palette?.customColors,
221
258
  config.general?.palette?.customColorsOrdered,
222
259
  config.palette
@@ -269,9 +306,25 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
269
306
 
270
307
  let roundedPercentage = percentageToDisplay.toFixed(roundTo) + '%'
271
308
  // add missing pie part
272
- if (arc.data[config.xAxis.dataKey] === labelForCalcArea && config.dataFormat.showPiePercent) {
309
+ if (arc.data[config.runtime.xAxis.dataKey] === labelForCalcArea && config.dataFormat.showPiePercent) {
273
310
  roundedPercentage = '**'
274
311
  }
312
+ let categoryLabel = ''
313
+ let labelPosition = { placement: 'inside' as const, x: 0, y: 0, textAnchor: 'middle' as const }
314
+
315
+ if (showCategoryPercentageLabels) {
316
+ categoryLabel = String(arc.data[config.runtime.xAxis.dataKey] ?? '')
317
+ const labelWidth = Math.max(measurePieLabelText(categoryLabel), measurePieLabelText(roundedPercentage))
318
+ labelPosition = getPieLabelPosition({
319
+ startAngle: arc.startAngle,
320
+ endAngle: arc.endAngle,
321
+ innerRadius: radius - donutThickness,
322
+ outerRadius: radius,
323
+ labelWidth,
324
+ labelHeight: ENHANCED_PIE_LABEL_HEIGHT,
325
+ isDonut: config.pieType === 'Donut'
326
+ })
327
+ }
275
328
 
276
329
  // Determine if this slice should be muted based on legend behavior
277
330
  const isHighlighted =
@@ -279,6 +332,8 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
279
332
  const shouldMute = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && !isHighlighted
280
333
  const sliceOpacity = shouldMute ? 0.3 : 1
281
334
  const textOpacity = shouldMute ? 0.3 : 1
335
+ const labelFill = labelPosition.placement === 'inside' ? textColor : '#000000'
336
+ const labelMinAngle = showCategoryPercentageLabels ? ENHANCED_PIE_LABEL_MIN_ANGLE : LEGACY_PERCENT_LABEL_MIN_ANGLE
282
337
 
283
338
  return (
284
339
  <Group key={key} className={`slice-${CSS.escape(String(key))}`}>
@@ -302,7 +357,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
302
357
  />
303
358
 
304
359
  {/* ── the percentage label */}
305
- {arc.endAngle - arc.startAngle > 0.1 && (
360
+ {arc.endAngle - arc.startAngle > labelMinAngle && !showCategoryPercentageLabels && (
306
361
  <animated.text
307
362
  transform={to([styles.startAngle, styles.endAngle], (start: number, end: number) => {
308
363
  const [x, y] = path.centroid({
@@ -321,6 +376,37 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
321
376
  {roundedPercentage}
322
377
  </animated.text>
323
378
  )}
379
+
380
+ {arc.endAngle - arc.startAngle > labelMinAngle && showCategoryPercentageLabels && (
381
+ <animated.text
382
+ className={`pie-label pie-label--${labelPosition.placement}`}
383
+ transform={to([styles.startAngle, styles.endAngle], (start: number, end: number) => {
384
+ const midAngle = start + (end - start) / 2
385
+ const innerRadius = radius - donutThickness
386
+ const outsideOffset = OUTSIDE_LABEL_OFFSET
387
+ const labelRadius =
388
+ labelPosition.placement === 'inside'
389
+ ? innerRadius + (radius - innerRadius) * 0.62
390
+ : radius + outsideOffset
391
+ const x = Math.sin(midAngle) * labelRadius
392
+ const y = -Math.cos(midAngle) * labelRadius
393
+ return `translate(${x},${y})`
394
+ })}
395
+ textAnchor={labelPosition.textAnchor}
396
+ pointerEvents='none'
397
+ fill={labelFill}
398
+ opacity={textOpacity}
399
+ style={{ fontSize: ENHANCED_PIE_LABEL_FONT_SIZE, fontFamily: 'sans-serif' }}
400
+ >
401
+ <title>{`${categoryLabel}: ${roundedPercentage}`}</title>
402
+ <tspan x={0} dy={-ENHANCED_PIE_LABEL_LINE_HEIGHT / 2}>
403
+ {categoryLabel}
404
+ </tspan>
405
+ <tspan x={0} dy={ENHANCED_PIE_LABEL_LINE_HEIGHT}>
406
+ {roundedPercentage}
407
+ </tspan>
408
+ </animated.text>
409
+ )}
324
410
  </Group>
325
411
  )
326
412
  })
@@ -334,11 +420,33 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
334
420
  }
335
421
 
336
422
  const height = config.heights.vertical
337
-
338
- const radius = Math.min(width, height) / 2
423
+ const maxPieLabelWidth = useMemo(() => {
424
+ if (!showCategoryPercentageLabels) return 0
425
+
426
+ const roundTo = Number(config.dataFormat.roundTo) || 0
427
+ const percentExample = `${(100).toFixed(roundTo)}%`
428
+
429
+ return _data.reduce((widestLabel, datum) => {
430
+ const categoryLabel = String(datum[config.runtime.xAxis.dataKey] ?? '')
431
+ return Math.max(widestLabel, measurePieLabelText(categoryLabel), measurePieLabelText(percentExample))
432
+ }, 0)
433
+ }, [_data, config.dataFormat.roundTo, config.runtime.xAxis.dataKey, showCategoryPercentageLabels])
434
+ const maxHorizontalLabelGutter = showCategoryPercentageLabels
435
+ ? Math.max(0, (width - MIN_RADIUS_WITH_LABELS * 2) / 2)
436
+ : 0
437
+ const labelGutter = showCategoryPercentageLabels
438
+ ? Math.min(Math.max(maxPieLabelWidth + LABEL_GUTTER_PADDING, MIN_LABEL_GUTTER), maxHorizontalLabelGutter)
439
+ : 0
440
+ const availableDiameter = showCategoryPercentageLabels
441
+ ? Math.min(width - labelGutter * 2, height - VERTICAL_LABEL_GUTTER * 2)
442
+ : Math.min(width, height)
443
+
444
+ const radius = showCategoryPercentageLabels ? Math.max(0, availableDiameter / 2) : Math.min(width, height) / 2
445
+ const svgWidth = showCategoryPercentageLabels ? width : radius * 2
446
+ const svgLeftOffset = Math.max((props.parentWidth - svgWidth) / 2, 0)
339
447
  const centerY = height / 2
340
- const centerX = props.parentWidth / 2
341
- const donutThickness = config.pieType === 'Donut' ? 75 : radius
448
+ const centerX = showCategoryPercentageLabels ? svgWidth / 2 : radius
449
+ const donutThickness = config.pieType === 'Donut' ? Math.min(75, radius * 0.65) : radius
342
450
 
343
451
  useEffect(() => {
344
452
  if (seriesHighlight.length > 0 && config.legend.behavior !== 'highlight') {
@@ -389,7 +497,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
389
497
  <>
390
498
  <ErrorBoundary component='PieChart'>
391
499
  <svg
392
- width={radius * 2}
500
+ width={svgWidth}
393
501
  height={height}
394
502
  className={getSvgClasses()}
395
503
  role='img'
@@ -400,7 +508,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
400
508
  handleChartMouseLeave()
401
509
  }}
402
510
  >
403
- <Group top={centerY} left={radius}>
511
+ <Group top={centerY} left={centerX}>
404
512
  {/* prettier-ignore */}
405
513
  <Pie
406
514
  data={filteredData || _data}
@@ -435,7 +543,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
435
543
  }) !important`}</style>
436
544
  <TooltipWithBounds
437
545
  className={'tooltip cove-visualization'}
438
- left={tooltipLeft + centerX - radius}
546
+ left={tooltipLeft + svgLeftOffset}
439
547
  top={tooltipTop}
440
548
  >
441
549
  <ul>
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getPieLabelPlacement, getPieLabelPosition } from './labelPlacement'
3
+
4
+ describe('pie label placement', () => {
5
+ it('keeps labels inside regular pie slices when the slice has enough room', () => {
6
+ expect(
7
+ getPieLabelPlacement({
8
+ startAngle: 0,
9
+ endAngle: Math.PI / 2,
10
+ innerRadius: 0,
11
+ outerRadius: 120,
12
+ labelWidth: 50,
13
+ labelHeight: 28,
14
+ isDonut: false
15
+ })
16
+ ).toBe('inside')
17
+ })
18
+
19
+ it('moves regular pie labels outside when they are too wide for the slice', () => {
20
+ expect(
21
+ getPieLabelPlacement({
22
+ startAngle: 0,
23
+ endAngle: Math.PI / 8,
24
+ innerRadius: 0,
25
+ outerRadius: 120,
26
+ labelWidth: 80,
27
+ labelHeight: 28,
28
+ isDonut: false
29
+ })
30
+ ).toBe('outside')
31
+ })
32
+
33
+ it('always places donut labels outside the partition', () => {
34
+ expect(
35
+ getPieLabelPlacement({
36
+ startAngle: 0,
37
+ endAngle: Math.PI,
38
+ innerRadius: 60,
39
+ outerRadius: 120,
40
+ labelWidth: 20,
41
+ labelHeight: 20,
42
+ isDonut: true
43
+ })
44
+ ).toBe('outside')
45
+ })
46
+
47
+ it('anchors outside labels by side of the pie', () => {
48
+ const rightSide = getPieLabelPosition({
49
+ startAngle: 0,
50
+ endAngle: Math.PI / 8,
51
+ innerRadius: 0,
52
+ outerRadius: 120,
53
+ labelWidth: 80,
54
+ labelHeight: 28,
55
+ isDonut: false
56
+ })
57
+
58
+ const leftSide = getPieLabelPosition({
59
+ startAngle: Math.PI,
60
+ endAngle: Math.PI + Math.PI / 8,
61
+ innerRadius: 0,
62
+ outerRadius: 120,
63
+ labelWidth: 80,
64
+ labelHeight: 28,
65
+ isDonut: false
66
+ })
67
+
68
+ expect(rightSide.placement).toBe('outside')
69
+ expect(rightSide.textAnchor).toBe('start')
70
+ expect(leftSide.placement).toBe('outside')
71
+ expect(leftSide.textAnchor).toBe('end')
72
+ })
73
+
74
+ it('uses the same angle orientation as the arc generator', () => {
75
+ const topLabel = getPieLabelPosition({
76
+ startAngle: 0,
77
+ endAngle: Math.PI / 12,
78
+ innerRadius: 0,
79
+ outerRadius: 120,
80
+ labelWidth: 80,
81
+ labelHeight: 28,
82
+ isDonut: false
83
+ })
84
+
85
+ expect(topLabel.placement).toBe('outside')
86
+ expect(topLabel.y).toBeLessThan(0)
87
+ })
88
+ })
@@ -0,0 +1,95 @@
1
+ export type PieLabelPlacement = 'inside' | 'outside'
2
+
3
+ export type PieLabelAnchor = 'start' | 'middle' | 'end'
4
+
5
+ export type PieLabelPosition = {
6
+ placement: PieLabelPlacement
7
+ x: number
8
+ y: number
9
+ textAnchor: PieLabelAnchor
10
+ }
11
+
12
+ type PieLabelPlacementOptions = {
13
+ startAngle: number
14
+ endAngle: number
15
+ innerRadius: number
16
+ outerRadius: number
17
+ labelWidth: number
18
+ labelHeight: number
19
+ isDonut: boolean
20
+ insidePadding?: number
21
+ }
22
+
23
+ type PieLabelPositionOptions = PieLabelPlacementOptions & {
24
+ outsideOffset?: number
25
+ }
26
+
27
+ const DEFAULT_INSIDE_PADDING = 12
28
+ const DEFAULT_OUTSIDE_OFFSET = 22
29
+
30
+ const getMidAngle = (startAngle: number, endAngle: number) => startAngle + (endAngle - startAngle) / 2
31
+ const getArcPoint = (angle: number, radius: number) => ({
32
+ x: Math.sin(angle) * radius,
33
+ y: -Math.cos(angle) * radius
34
+ })
35
+
36
+ export const getPieLabelPlacement = ({
37
+ startAngle,
38
+ endAngle,
39
+ innerRadius,
40
+ outerRadius,
41
+ labelWidth,
42
+ labelHeight,
43
+ isDonut,
44
+ insidePadding = DEFAULT_INSIDE_PADDING
45
+ }: PieLabelPlacementOptions): PieLabelPlacement => {
46
+ if (isDonut) return 'outside'
47
+
48
+ const angle = Math.abs(endAngle - startAngle)
49
+ const labelRadius = innerRadius + (outerRadius - innerRadius) * 0.62
50
+ const availableChord = 2 * labelRadius * Math.sin(angle / 2)
51
+ const availableRadialDepth = outerRadius - innerRadius
52
+
53
+ const hasEnoughArcWidth = labelWidth + insidePadding <= availableChord
54
+ const hasEnoughRadialDepth = labelHeight + insidePadding <= availableRadialDepth
55
+
56
+ return hasEnoughArcWidth && hasEnoughRadialDepth ? 'inside' : 'outside'
57
+ }
58
+
59
+ export const getPieLabelPosition = ({
60
+ startAngle,
61
+ endAngle,
62
+ innerRadius,
63
+ outerRadius,
64
+ labelWidth,
65
+ labelHeight,
66
+ isDonut,
67
+ insidePadding,
68
+ outsideOffset = DEFAULT_OUTSIDE_OFFSET
69
+ }: PieLabelPositionOptions): PieLabelPosition => {
70
+ const placement = getPieLabelPlacement({
71
+ startAngle,
72
+ endAngle,
73
+ innerRadius,
74
+ outerRadius,
75
+ labelWidth,
76
+ labelHeight,
77
+ isDonut,
78
+ insidePadding
79
+ })
80
+ const midAngle = getMidAngle(startAngle, endAngle)
81
+ const labelRadius =
82
+ placement === 'inside' ? innerRadius + (outerRadius - innerRadius) * 0.62 : outerRadius + outsideOffset
83
+ const { x, y } = getArcPoint(midAngle, labelRadius)
84
+
85
+ if (placement === 'inside') {
86
+ return { placement, x, y, textAnchor: 'middle' }
87
+ }
88
+
89
+ return {
90
+ placement,
91
+ x,
92
+ y,
93
+ textAnchor: x < 0 ? 'end' : 'start'
94
+ }
95
+ }
@@ -0,0 +1,44 @@
1
+ import { type ReactNode, type RefObject } from 'react'
2
+ import ParentSize from '@visx/responsive/lib/components/ParentSize'
3
+ import BarChartRace, { BarChartRaceFallback, type BarRaceEligibility } from './BarChartRace'
4
+ import LineChartRace, { LineChartRaceFallback, type LineRaceEligibility } from './LineChartRace'
5
+
6
+ type Props = {
7
+ barRace: BarRaceEligibility
8
+ family: 'Bar' | 'Line'
9
+ lineRace: LineRaceEligibility
10
+ parentRef: RefObject<HTMLDivElement>
11
+ renderTopYAxisTitles: () => ReactNode
12
+ svgRef: RefObject<SVGAElement>
13
+ }
14
+
15
+ const RacingChartRenderer = ({ barRace, family, lineRace, parentRef, renderTopYAxisTitles, svgRef }: Props) => {
16
+ const eligibility = family === 'Bar' ? barRace : lineRace
17
+
18
+ return (
19
+ <>
20
+ {(family === 'Line' || !eligibility.eligible) && renderTopYAxisTitles()}
21
+ <div ref={parentRef} style={{ width: '100%' }}>
22
+ <ParentSize>
23
+ {parent => {
24
+ if (family === 'Bar') {
25
+ return barRace.eligible ? (
26
+ <BarChartRace parentWidth={parent.width} race={barRace} />
27
+ ) : (
28
+ <BarChartRaceFallback ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
29
+ )
30
+ }
31
+
32
+ return lineRace.eligible ? (
33
+ <LineChartRace ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} race={lineRace} />
34
+ ) : (
35
+ <LineChartRaceFallback ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
36
+ )
37
+ }}
38
+ </ParentSize>
39
+ </div>
40
+ </>
41
+ )
42
+ }
43
+
44
+ export default RacingChartRenderer