@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,50 @@
1
+ import React from 'react'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import ConfigContext from '../../../ConfigContext'
5
+ import AnnotationDropdown from '../components/AnnotationDropdown'
6
+
7
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
8
+ default: ({ display }: { display: string }) => <span data-icon={display} />
9
+ }))
10
+
11
+ describe('AnnotationDropdown', () => {
12
+ it('uses the shared toggle treatment and joins the expanded panel to its header', () => {
13
+ const annotation = { text: 'Important annotation' }
14
+ const context = {
15
+ config: {
16
+ annotations: [annotation],
17
+ general: {
18
+ annotationDropdownText: '',
19
+ mobileAnnotationDisplay: 'symbol',
20
+ showAnnotationDropdown: true
21
+ },
22
+ table: { height: 120, limitHeight: true }
23
+ },
24
+ currentViewport: 'lg',
25
+ visibleAnnotations: [annotation]
26
+ }
27
+
28
+ render(
29
+ <ConfigContext.Provider value={context as any}>
30
+ <AnnotationDropdown />
31
+ </ConfigContext.Provider>
32
+ )
33
+
34
+ const toggle = screen.getByRole('button', { name: 'Annotations' })
35
+ const section = toggle.closest('section')
36
+
37
+ expect(toggle).toHaveClass('data-table-heading', 'data-table-heading--toggle', 'annotation__dropdown-list')
38
+ expect(toggle).toHaveAttribute('aria-expanded', 'false')
39
+ expect(section).not.toHaveClass('my-4')
40
+ expect(section).not.toHaveClass('mt-4')
41
+ expect(section).not.toHaveClass('mb-4')
42
+
43
+ fireEvent.click(toggle)
44
+
45
+ const panel = section?.querySelector('.annotation-dropdown__panel') as HTMLElement
46
+ expect(toggle).toHaveAttribute('aria-expanded', 'true')
47
+ expect(toggle.nextElementSibling).toBe(panel)
48
+ expect(panel).toHaveStyle({ maxHeight: '120px', overflowY: 'auto' })
49
+ })
50
+ })
@@ -0,0 +1,203 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ getAnnotationAttachment,
4
+ getAnnotationLabelRect,
5
+ getFacingAnnotationSide,
6
+ getPreferredAnnotationSide,
7
+ MIN_ANNOTATION_LABEL_WIDTH_EM,
8
+ MIN_EVENT_LINE_LABEL_WIDTH_EM,
9
+ resolveAnnotationLayout,
10
+ resolveAnnotationRectangleDrag
11
+ } from '../helpers/resolveAnnotationLayout'
12
+
13
+ const base = {
14
+ endpointX: 200,
15
+ endpointY: 150,
16
+ dx: 100,
17
+ dy: 10,
18
+ labelWidth: 120,
19
+ labelHeight: 40,
20
+ plotWidth: 400,
21
+ plotHeight: 300,
22
+ fontSize: 16,
23
+ labelPosition: 'auto' as const
24
+ }
25
+
26
+ describe('resolveAnnotationLayout', () => {
27
+ it.each([
28
+ [100, 10, 'right'],
29
+ [-100, 10, 'left'],
30
+ [10, -100, 'above'],
31
+ [10, 100, 'below']
32
+ ] as const)('derives %s/%s as the %s preferred side', (dx, dy, side) => {
33
+ expect(getPreferredAnnotationSide(dx, dy)).toBe(side)
34
+ expect(resolveAnnotationLayout({ ...base, dx, dy }).side).toBe(side)
35
+ })
36
+
37
+ it('slides parallel to the preferred side without changing sides', () => {
38
+ const result = resolveAnnotationLayout({ ...base, endpointY: 5 })
39
+
40
+ expect(result.side).toBe('right')
41
+ expect(result.y).toBe(20)
42
+ expect(result.x).toBe(200)
43
+ })
44
+
45
+ it('keeps a short label on its preferred side near an edge when its measured width fits', () => {
46
+ const result = resolveAnnotationLayout({ ...base, endpointX: 320, labelWidth: 70 })
47
+
48
+ expect(result.side).toBe('right')
49
+ expect(result.constrainedWidth).toBeUndefined()
50
+ })
51
+
52
+ it('constrains a long label to the 6em minimum without changing sides', () => {
53
+ const result = resolveAnnotationLayout({ ...base, endpointX: 100, labelWidth: 180 })
54
+
55
+ expect(result.side).toBe('right')
56
+ expect(result.width).toBe(180)
57
+
58
+ const constrained = resolveAnnotationLayout({ ...base, endpointX: 304, labelWidth: 180 })
59
+ expect(constrained.side).toBe('right')
60
+ expect(constrained.width).toBe(96)
61
+ expect(constrained.constrainedWidth).toBe(96)
62
+ })
63
+
64
+ it('accepts the 4em event-line minimum', () => {
65
+ const result = resolveAnnotationLayout({
66
+ ...base,
67
+ endpointX: 336,
68
+ labelWidth: 180,
69
+ minWidthEm: MIN_EVENT_LINE_LABEL_WIDTH_EM,
70
+ labelPosition: 'right'
71
+ })
72
+
73
+ expect(result.width).toBe(64)
74
+ expect(result.constrainedWidth).toBe(64)
75
+ })
76
+
77
+ it('keeps a persisted automatic side even when the scaled offsets favor another side', () => {
78
+ const result = resolveAnnotationLayout({
79
+ ...base,
80
+ endpointX: 350,
81
+ endpointY: 30,
82
+ dx: 10,
83
+ dy: -200,
84
+ autoSide: 'right',
85
+ labelWidth: 140,
86
+ labelHeight: 60
87
+ })
88
+
89
+ expect(result.side).toBe('right')
90
+ })
91
+
92
+ it('keeps the preferred side for unavoidable overflow', () => {
93
+ const result = resolveAnnotationLayout({
94
+ ...base,
95
+ endpointX: 390,
96
+ endpointY: 150,
97
+ labelWidth: 240,
98
+ labelHeight: 310
99
+ })
100
+
101
+ expect(result.side).toBe('right')
102
+ expect(result.width).toBe(MIN_ANNOTATION_LABEL_WIDTH_EM * base.fontSize)
103
+ expect(result.constrainedWidth).toBe(MIN_ANNOTATION_LABEL_WIDTH_EM * base.fontSize)
104
+ })
105
+
106
+ it.each([
107
+ ['left', -100, 150, 96, 150],
108
+ ['right', 500, 150, 304, 150],
109
+ ['above', 200, -100, 200, 40],
110
+ ['below', 200, 500, 200, 260]
111
+ ] as const)('clamps %s placement to the same-side boundary', (autoSide, endpointX, endpointY, x, y) => {
112
+ const result = resolveAnnotationLayout({ ...base, endpointX, endpointY, autoSide })
113
+
114
+ expect(result.side).toBe(autoSide)
115
+ expect(result.x).toBe(x)
116
+ expect(result.y).toBe(y)
117
+ })
118
+
119
+ it.each(['left', 'right', 'above', 'below'] as const)(
120
+ 'keeps explicit %s placement even when it cannot fit',
121
+ labelPosition => {
122
+ const result = resolveAnnotationLayout({
123
+ ...base,
124
+ endpointX: 5,
125
+ endpointY: 5,
126
+ labelWidth: 300,
127
+ labelHeight: 200,
128
+ labelPosition
129
+ })
130
+
131
+ expect(result.side).toBe(labelPosition)
132
+ }
133
+ )
134
+ })
135
+
136
+ describe('rectangle-based annotation dragging', () => {
137
+ const rect = { left: 100, top: 60, width: 120, height: 40 }
138
+
139
+ it.each(['left', 'right', 'above', 'below'] as const)(
140
+ 'reconstructs the same rectangle from its %s attachment',
141
+ side => {
142
+ const attachment = getAnnotationAttachment(rect, side)
143
+
144
+ expect(getAnnotationLabelRect(attachment.x, attachment.y, rect.width, rect.height, side)).toEqual(rect)
145
+ }
146
+ )
147
+
148
+ it.each([
149
+ [20, 80, 'right'],
150
+ [300, 80, 'left'],
151
+ [160, 0, 'below'],
152
+ [160, 150, 'above']
153
+ ] as const)('chooses the edge facing a subject at %s/%s', (subjectX, subjectY, side) => {
154
+ expect(getFacingAnnotationSide(subjectX, subjectY, rect)).toBe(side)
155
+ })
156
+
157
+ it('normalizes distance by the rectangle dimensions when choosing the facing edge', () => {
158
+ const wideRect = { left: 0, top: 80, width: 200, height: 40 }
159
+
160
+ expect(getFacingAnnotationSide(160, 140, wideRect)).toBe('above')
161
+ })
162
+
163
+ it('changes only the attachment when the selected side changes', () => {
164
+ const right = resolveAnnotationRectangleDrag({
165
+ rect,
166
+ subjectX: 20,
167
+ subjectY: 80,
168
+ plotWidth: 400,
169
+ plotHeight: 300,
170
+ side: 'right'
171
+ })
172
+ const below = resolveAnnotationRectangleDrag({
173
+ rect,
174
+ subjectX: 160,
175
+ subjectY: 0,
176
+ plotWidth: 400,
177
+ plotHeight: 300,
178
+ side: 'below'
179
+ })
180
+
181
+ expect(right.rect).toEqual(rect)
182
+ expect(below.rect).toEqual(rect)
183
+ expect({ x: right.layout.x, y: right.layout.y }).toEqual({ x: 100, y: 80 })
184
+ expect({ x: below.layout.x, y: below.layout.y }).toEqual({ x: 160, y: 60 })
185
+ })
186
+
187
+ it('clamps the rectangle before deriving the persisted attachment', () => {
188
+ const result = resolveAnnotationRectangleDrag({
189
+ rect: { ...rect, left: 350, top: -20 },
190
+ subjectX: 200,
191
+ subjectY: 150,
192
+ plotWidth: 400,
193
+ plotHeight: 300,
194
+ side: 'left'
195
+ })
196
+
197
+ expect(result.rect).toEqual({ ...rect, left: 280, top: 0 })
198
+ expect(result.layout.x).toBe(400)
199
+ expect(result.layout.y).toBe(20)
200
+ expect(result.dx).toBe(200)
201
+ expect(result.dy).toBe(-130)
202
+ })
203
+ })
@@ -98,7 +98,7 @@ const BottomAxis: React.FC<BottomAxisProps> = ({
98
98
  config.runtime.xAxis.manual
99
99
  ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
100
100
  : config.runtime.xAxis.type === 'date'
101
- ? xAxisDataMapped
101
+ ? sortedUniqueXAxisData
102
102
  : // For date-time with small datasets (e.g., brush-filtered), use explicit tick values
103
103
  // to ensure each data point can have a tick. Otherwise, visx may generate too few.
104
104
  // Use uniqueXAxisDataMapped to handle cases where multiple series share x-axis values
@@ -9,7 +9,7 @@ import createBarElement from '@cdc/core/components/createBarElement'
9
9
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
10
10
  import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
11
11
 
12
- const CategoricalYAxis = ({ yScale, yMax, leftSize, xMax }) => {
12
+ const CategoricalYAxis = ({ yScale, yMax, leftSize, xMax, categories }) => {
13
13
  const { config } = useContext(ConfigContext)
14
14
 
15
15
  const { orientation } = config
@@ -22,7 +22,7 @@ const CategoricalYAxis = ({ yScale, yMax, leftSize, xMax }) => {
22
22
  }
23
23
  }
24
24
 
25
- const categories = config.yAxis?.categories
25
+ if (!categories?.length) return null
26
26
 
27
27
  // Get max from the yScale domain
28
28
  const max = yScale.domain()[1]
@@ -146,7 +146,7 @@ const CategoricalYAxis = ({ yScale, yMax, leftSize, xMax }) => {
146
146
  {/* White background spacing between stackes */}
147
147
  {!isLastIndex && <rect x={bar.x} y={bar.y} width={bar.width} height={1} fill={'#fff'}></rect>}
148
148
  {/* Right side Axis line */}
149
- <rect x={bar.x + bar.width} y={0} width={1} height={yMax} fill={'#000'}></rect>
149
+ {!config.yAxis.hideAxis && <rect x={bar.x + bar.width} y={0} width={1} height={yMax} fill='#000' />}
150
150
  </Group>
151
151
  </Group>
152
152
  )
@@ -6,6 +6,7 @@ import { Text } from '@visx/text'
6
6
  import { ScaleLinear } from 'd3-scale'
7
7
 
8
8
  import ConfigContext from '../../ConfigContext'
9
+ import { DEFAULT_TICK_LENGTH, TICK_LABEL_MARGIN_RIGHT } from './axis.constants'
9
10
 
10
11
  // Constants
11
12
  const HORIZONTAL_TICK_OFFSET_ADJUSTMENT = 5
@@ -15,6 +16,7 @@ type RightAxisProps = {
15
16
  yMax: number
16
17
  xMax: number
17
18
  yAxisWidth: number
19
+ rightAxisWidth: number
18
20
  tickValues?: number[]
19
21
  tickLabelFontSize: number
20
22
  axisLabelFontSize: number
@@ -30,12 +32,14 @@ export const RightAxis: React.FC<RightAxisProps> = ({
30
32
  yMax,
31
33
  xMax,
32
34
  yAxisWidth,
35
+ rightAxisWidth,
33
36
  tickValues,
34
37
  tickLabelFontSize,
35
38
  axisLabelFontSize
36
39
  }) => {
37
40
  const { config, formatNumber } = useContext(ConfigContext)
38
41
  const { runtime } = config
42
+ const showSideTitle = config.yAxis.rightTitlePlacement !== 'top'
39
43
 
40
44
  const horizontalTickOffset = (ticks: any[]) =>
41
45
  yMax / ticks.length / 2 - (yMax / ticks.length) * (1 - config.barThickness) + HORIZONTAL_TICK_OFFSET_ADJUSTMENT
@@ -44,8 +48,9 @@ export const RightAxis: React.FC<RightAxisProps> = ({
44
48
  <VisxAxisRight
45
49
  scale={yScaleRight}
46
50
  left={yAxisWidth + xMax}
47
- label={config.yAxis.rightLabel}
51
+ label={runtime.yAxis.rightLabel ?? config.yAxis.rightLabel}
48
52
  tickFormat={tick => formatNumber(tick, 'right')}
53
+ tickLength={DEFAULT_TICK_LENGTH}
49
54
  numTicks={runtime.yAxis.rightNumTicks || undefined}
50
55
  {...(tickValues ? { tickValues } : {})}
51
56
  labelOffset={45}
@@ -55,6 +60,7 @@ export const RightAxis: React.FC<RightAxisProps> = ({
55
60
  config.orientation === 'horizontal'
56
61
  ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
57
62
  : (props.axisFromPoint.y - props.axisToPoint.y) / 2
63
+ const sideTitleX = Math.max(0, rightAxisWidth - axisLabelFontSize)
58
64
 
59
65
  return (
60
66
  <Group className='right-axis'>
@@ -75,7 +81,7 @@ export const RightAxis: React.FC<RightAxisProps> = ({
75
81
 
76
82
  {!config.yAxis.rightHideLabel && (
77
83
  <Text
78
- x={tick.to.x}
84
+ x={tick.to.x + TICK_LABEL_MARGIN_RIGHT}
79
85
  y={tick.to.y + (runtime.horizontal ? horizontalTickOffset(props.ticks) : 0)}
80
86
  verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
81
87
  textAnchor='start'
@@ -90,17 +96,19 @@ export const RightAxis: React.FC<RightAxisProps> = ({
90
96
 
91
97
  {!config.yAxis.rightHideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
92
98
 
93
- <Text
94
- className='y-label'
95
- textAnchor='middle'
96
- verticalAnchor='start'
97
- transform={`translate(${config.yAxis.rightLabelOffsetSize || 0}, ${axisCenter}) rotate(-90)`}
98
- fontWeight='bold'
99
- fill={config.yAxis.rightAxisLabelColor}
100
- fontSize={axisLabelFontSize}
101
- >
102
- {props.label}
103
- </Text>
99
+ {showSideTitle && (
100
+ <Text
101
+ className='y-label'
102
+ textAnchor='middle'
103
+ verticalAnchor='start'
104
+ transform={`translate(${sideTitleX}, ${axisCenter}) rotate(-90)`}
105
+ fontWeight='bold'
106
+ fill={config.yAxis.rightAxisLabelColor}
107
+ fontSize={axisLabelFontSize}
108
+ >
109
+ {props.label}
110
+ </Text>
111
+ )}
104
112
  </Group>
105
113
  )
106
114
  }}
@@ -17,16 +17,18 @@ import createBarElement from '@cdc/core/components/createBarElement'
17
17
  import { getBarConfig, testZeroValue, getLollipopStemColor, getLollipopHeadColor } from '../helpers'
18
18
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
19
19
  import isNumber from '@cdc/core/helpers/isNumber'
20
+ import { getSeriesValueLabel } from '@cdc/core/helpers/getSeriesName'
20
21
 
21
22
  // Local context and types
22
23
  import BarChartContext, { BarChartContextValues } from './context'
23
24
  import { ChartContext } from '../../../types/ChartContext'
24
- import _ from 'lodash'
25
25
  import { getBarData } from '../helpers/getBarData'
26
26
  import { getHorizontalBarHeights } from '../helpers/getBarHeights'
27
27
  import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
28
+ import { getPortionPatternRenderData } from '../helpers/portionPattern'
28
29
  import { getChartPatternId } from '../../../helpers/getChartPatternId'
29
30
  import { buildSeriesTooltipListHtml } from '../../../helpers/tooltipHelpers'
31
+ import { BarPortionPatternOverlay } from './PortionPatternOverlay'
30
32
 
31
33
  const BarChartHorizontal = () => {
32
34
  const { xScale, yScale, yMax, seriesScale, barChart } = useContext<BarChartContextValues>(BarChartContext)
@@ -42,6 +44,7 @@ const BarChartHorizontal = () => {
42
44
  getHighlightedBarColorByValue,
43
45
  getHighlightedBarByValue,
44
46
  getAdditionalColumn,
47
+ formatTooltipValue,
45
48
  hoveredBar,
46
49
  onMouseLeaveBar,
47
50
  onMouseOverBar
@@ -49,7 +52,6 @@ const BarChartHorizontal = () => {
49
52
 
50
53
  const {
51
54
  transformedData: data,
52
- tableData,
53
55
  colorScale,
54
56
  seriesHighlight,
55
57
  config,
@@ -76,7 +78,7 @@ const BarChartHorizontal = () => {
76
78
  <defs>
77
79
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
78
80
  const patternId = getChartPatternId(key)
79
- const size = pattern.patternSize || 8
81
+ const size = pattern.patternSize || 10
80
82
 
81
83
  switch (pattern.shape) {
82
84
  case 'circles':
@@ -121,8 +123,9 @@ const BarChartHorizontal = () => {
121
123
  id={patternId}
122
124
  height={size}
123
125
  width={size}
124
- fill={pattern.color}
125
- strokeWidth={0.25}
126
+ fill='transparent'
127
+ stroke={pattern.color}
128
+ strokeWidth={0.75}
126
129
  />
127
130
  )
128
131
  default:
@@ -142,7 +145,7 @@ const BarChartHorizontal = () => {
142
145
  <Group>
143
146
  {renderPatternDefs()}
144
147
  <BarGroup
145
- data={config.preliminaryData?.some(pd => pd.value && pd.type === 'suppression') ? tableData : _data}
148
+ data={_data}
146
149
  keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys}
147
150
  height={yMax}
148
151
  x0={d => d[config.runtime.originalXAxis.dataKey]}
@@ -243,7 +246,8 @@ const BarChartHorizontal = () => {
243
246
  ? `${config.runtime.yAxis.label}: ${xAxisValue}`
244
247
  : xAxisValue
245
248
  const additionalColTooltip = getAdditionalColumn(bar.key, hoveredBar)
246
- const tooltipBody = `${config.runtime.seriesLabels[bar.key]}: ${yAxisValue}`
249
+ const tooltipValue = formatTooltipValue(bar.key, dataValue, yAxisValue, barGroup.index)
250
+ const tooltipBody = `${getSeriesValueLabel(bar.key, config)}: ${tooltipValue}`
247
251
  const tooltip = buildSeriesTooltipListHtml({
248
252
  config,
249
253
  colorScale,
@@ -262,7 +266,7 @@ const BarChartHorizontal = () => {
262
266
  : colorScale(bar.key)
263
267
  const hasDynamicCategory = config.series.find(s => s.dynamicCategory)
264
268
  if (!hasDynamicCategory && config.legend.colorCode) {
265
- barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor) // Color code by category
269
+ barColor = assignColorsToValues(barGroup.index, barColor) // Color code by category
266
270
  }
267
271
  const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
268
272
  const isTwoToneLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'two-tone'
@@ -340,6 +344,20 @@ const BarChartHorizontal = () => {
340
344
  allowNonSeriesFieldMatch: !config.series || config.series.length <= 1
341
345
  })
342
346
  const baseBackground = getBarBackgroundColor()
347
+ const portionPatternRenderData = getPortionPatternRenderData({
348
+ config,
349
+ orientation: 'horizontal',
350
+ bounds: {
351
+ x: barX,
352
+ y: barHeight * bar.index,
353
+ width: barWidth,
354
+ height: numbericBarHeight
355
+ },
356
+ patterns: config.legend?.patterns,
357
+ datum,
358
+ seriesKey: bar.key,
359
+ totalValue: bar.value
360
+ })
343
361
 
344
362
  return (
345
363
  <Group display={hideGroup} key={`${barGroup.index}--${index}`}>
@@ -401,6 +419,19 @@ const BarChartHorizontal = () => {
401
419
  }
402
420
  })}
403
421
 
422
+ {portionPatternRenderData && (
423
+ <BarPortionPatternOverlay
424
+ config={config}
425
+ index={newIndex}
426
+ renderData={portionPatternRenderData}
427
+ transformOrigin={`0 ${barY + barHeight}px`}
428
+ style={{
429
+ opacity: transparentBar ? 0.2 : 1,
430
+ display: displayBar ? 'block' : 'none'
431
+ }}
432
+ />
433
+ )}
434
+
404
435
  {(absentDataLabel || isSuppressed) && (
405
436
  <rect
406
437
  x={barX}
@@ -13,6 +13,7 @@ import BarChartContext, { type BarChartContextValues } from './context'
13
13
  import { type ChartContext } from '../../../types/ChartContext'
14
14
 
15
15
  import createBarElement from '@cdc/core/components/createBarElement'
16
+ import { getSeriesValueLabel } from '@cdc/core/helpers/getSeriesName'
16
17
  import { getHorizontalBarHeights } from '../helpers/getBarHeights'
17
18
  import { getPatternUrl as getPatternUrlForBar } from '../helpers/getPatternUrl'
18
19
  import { getChartPatternId } from '../../../helpers/getChartPatternId'
@@ -38,6 +39,7 @@ const BarChartStackedHorizontal = () => {
38
39
  barBorderWidth,
39
40
  displayNumbersOnBar,
40
41
  getAdditionalColumn,
42
+ formatTooltipValue,
41
43
  hoveredBar,
42
44
  isHorizontal,
43
45
  isLabelBelowBar,
@@ -59,7 +61,7 @@ const BarChartStackedHorizontal = () => {
59
61
  <defs>
60
62
  {Object.entries(config.legend.patterns).map(([key, pattern]) => {
61
63
  const patternId = getChartPatternId(key)
62
- const size = pattern.patternSize || 8
64
+ const size = pattern.patternSize || 10
63
65
 
64
66
  switch (pattern.shape) {
65
67
  case 'circles':
@@ -104,8 +106,9 @@ const BarChartStackedHorizontal = () => {
104
106
  id={patternId}
105
107
  height={size}
106
108
  width={size}
107
- fill={pattern.color}
108
- strokeWidth={0.25}
109
+ fill='transparent'
110
+ stroke={pattern.color}
111
+ strokeWidth={0.75}
109
112
  />
110
113
  )
111
114
  default:
@@ -161,7 +164,13 @@ const BarChartStackedHorizontal = () => {
161
164
  : yAxisValue
162
165
  const textWidth = getTextWidth(xAxisValue, `normal ${labelFontSize}px sans-serif`)
163
166
  const additionalColTooltip = getAdditionalColumn(bar.key, hoveredBar)
164
- const tooltipBody = `${config.runtime.seriesLabels[bar.key]}: ${xAxisValue}`
167
+ const tooltipValue = formatTooltipValue(
168
+ bar.key,
169
+ data[bar.index][config.runtime.originalXAxis.dataKey],
170
+ xAxisValue,
171
+ bar.index
172
+ )
173
+ const tooltipBody = `${getSeriesValueLabel(bar.key, config)}: ${tooltipValue}`
165
174
  const tooltip = buildSeriesTooltipListHtml({
166
175
  config,
167
176
  colorScale,
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getStackedVerticalBarThickness } from './BarChart.StackedVertical'
3
+
4
+ describe('getStackedVerticalBarThickness', () => {
5
+ it.each(['date', 'date-time'])('does not apply bar thickness twice for a %s axis', () => {
6
+ expect(
7
+ getStackedVerticalBarThickness({
8
+ isDateAxisType: true,
9
+ dateScaleWidth: 40,
10
+ xMax: 400,
11
+ barCount: 4,
12
+ barThickness: 0.8
13
+ })
14
+ ).toBe(40)
15
+ })
16
+
17
+ it('applies bar thickness to categorical bars', () => {
18
+ expect(
19
+ getStackedVerticalBarThickness({
20
+ isDateAxisType: false,
21
+ dateScaleWidth: 40,
22
+ xMax: 400,
23
+ barCount: 4,
24
+ barThickness: 0.8
25
+ })
26
+ ).toBe(80)
27
+ })
28
+ })