@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,243 @@
1
+ import { type Column } from '@cdc/core/types/Column'
2
+
3
+ import { getChartPatternId } from '../../../helpers/getChartPatternId'
4
+ import { type ChartConfig, type LegendPattern } from '../../../types/ChartConfig'
5
+
6
+ type PortionPatternSupportConfig = Pick<
7
+ ChartConfig,
8
+ 'visualizationType' | 'visualizationSubType' | 'isLollipopChart' | 'yAxis'
9
+ >
10
+
11
+ export type PortionPatternOverlay = {
12
+ patternUrl: string
13
+ ratio: number
14
+ placement: 'start' | 'end'
15
+ patternColor?: string
16
+ }
17
+
18
+ export type PortionPatternGeometry = {
19
+ x: number
20
+ y: number
21
+ width: number
22
+ height: number
23
+ }
24
+
25
+ export type PortionPatternBoundaryGeometry = {
26
+ x1: number
27
+ y1: number
28
+ x2: number
29
+ y2: number
30
+ }
31
+
32
+ export type PortionPatternRenderData = PortionPatternOverlay & {
33
+ geometry: PortionPatternGeometry
34
+ boundaryGeometry: PortionPatternBoundaryGeometry | null
35
+ }
36
+
37
+ type GetPortionPatternOverlayArgs = {
38
+ patterns?: Record<string, LegendPattern>
39
+ datum: Record<string, any>
40
+ seriesKey: string
41
+ totalValue: unknown
42
+ }
43
+
44
+ type GetPortionPatternGeometryArgs = {
45
+ orientation: 'vertical' | 'horizontal'
46
+ bounds: PortionPatternGeometry
47
+ overlay: Pick<PortionPatternOverlay, 'ratio' | 'placement'>
48
+ }
49
+
50
+ type GetPortionPatternBoundaryGeometryArgs = {
51
+ orientation: 'vertical' | 'horizontal'
52
+ geometry: PortionPatternGeometry
53
+ overlay: Pick<PortionPatternOverlay, 'ratio' | 'placement'>
54
+ }
55
+
56
+ type GetPortionPatternRenderDataArgs = GetPortionPatternOverlayArgs & {
57
+ config: PortionPatternSupportConfig
58
+ orientation: 'vertical' | 'horizontal'
59
+ bounds: PortionPatternGeometry
60
+ }
61
+
62
+ export type ChartColumns = Record<string, Column>
63
+
64
+ export const findColumnConfigKey = (columns: ChartColumns, columnName: string): string | undefined =>
65
+ Object.entries(columns).find(
66
+ ([columnKey, column]) => column?.name === columnName || (!column?.name && columnKey === columnName)
67
+ )?.[0]
68
+
69
+ export const createPatternColumnConfig = (columnName: string): Column =>
70
+ ({
71
+ name: columnName,
72
+ label: columnName,
73
+ prefix: '',
74
+ suffix: '',
75
+ roundToPlace: 0,
76
+ commas: true,
77
+ dataTable: true,
78
+ tooltips: true
79
+ } as Column)
80
+
81
+ export const ensurePatternColumnConfig = (columns: ChartColumns, columnName?: string): ChartColumns => {
82
+ if (!columnName || findColumnConfigKey(columns, columnName)) return columns
83
+
84
+ return {
85
+ ...columns,
86
+ [columnName]: createPatternColumnConfig(columnName)
87
+ }
88
+ }
89
+
90
+ export const removeUnusedPatternColumnConfig = ({
91
+ columns,
92
+ columnName,
93
+ patterns,
94
+ protectedColumnNames = []
95
+ }: {
96
+ columns: ChartColumns
97
+ columnName?: string
98
+ patterns: Record<string, LegendPattern>
99
+ protectedColumnNames?: Array<string | undefined>
100
+ }): ChartColumns => {
101
+ if (!columnName) return columns
102
+
103
+ const isStillUsedByPattern = Object.values(patterns).some(
104
+ pattern => pattern.application === 'portion' && pattern.patternValueKey === columnName
105
+ )
106
+
107
+ if (isStillUsedByPattern || protectedColumnNames.includes(columnName)) return columns
108
+
109
+ const columnConfigKey = findColumnConfigKey(columns, columnName)
110
+ if (!columnConfigKey) return columns
111
+
112
+ const nextColumns = { ...columns }
113
+ delete nextColumns[columnConfigKey]
114
+ return nextColumns
115
+ }
116
+
117
+ const normalizeString = (value: unknown): string => String(value ?? '').trim()
118
+
119
+ const toPositiveFiniteNumber = (value: unknown): number | null => {
120
+ if (typeof value !== 'number' && typeof value !== 'string') return null
121
+ if (typeof value === 'string' && value.trim() === '') return null
122
+
123
+ const numericValue = Number(value)
124
+ return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : null
125
+ }
126
+
127
+ export const isPortionPatternSupported = (config: PortionPatternSupportConfig): boolean =>
128
+ ['Bar', 'Combo'].includes(config.visualizationType) &&
129
+ config.visualizationSubType !== 'stacked' &&
130
+ !config.isLollipopChart &&
131
+ config.yAxis?.type !== 'logarithmic'
132
+
133
+ /**
134
+ * Resolves the first configured portion pattern for a bar series. The first
135
+ * matching entry wins even when its row values are invalid, so manually
136
+ * authored duplicate targets remain deterministic.
137
+ */
138
+ export const getPortionPatternOverlay = ({
139
+ patterns,
140
+ datum,
141
+ seriesKey,
142
+ totalValue
143
+ }: GetPortionPatternOverlayArgs): PortionPatternOverlay | null => {
144
+ if (!patterns) return null
145
+
146
+ for (const patternKey in patterns) {
147
+ if (!Object.prototype.hasOwnProperty.call(patterns, patternKey)) continue
148
+
149
+ const pattern = patterns[patternKey]
150
+ if (pattern.application !== 'portion' || normalizeString(pattern.dataKey) !== seriesKey) continue
151
+
152
+ const total = toPositiveFiniteNumber(totalValue)
153
+ const portion = toPositiveFiniteNumber(datum?.[normalizeString(pattern.patternValueKey)])
154
+
155
+ if (total === null || portion === null || portion > total) return null
156
+
157
+ const patternColor = normalizeString(pattern.color)
158
+
159
+ return {
160
+ patternUrl: `url(#${getChartPatternId(patternKey)})`,
161
+ ratio: portion / total,
162
+ placement: pattern.placement === 'start' ? 'start' : 'end',
163
+ ...(patternColor ? { patternColor } : {})
164
+ }
165
+ }
166
+
167
+ return null
168
+ }
169
+
170
+ export const getPortionPatternGeometry = ({
171
+ orientation,
172
+ bounds,
173
+ overlay
174
+ }: GetPortionPatternGeometryArgs): PortionPatternGeometry => {
175
+ const { x, y, width, height } = bounds
176
+
177
+ if (orientation === 'horizontal') {
178
+ const overlayWidth = width * overlay.ratio
179
+ return {
180
+ x: overlay.placement === 'end' ? x + width - overlayWidth : x,
181
+ y,
182
+ width: overlayWidth,
183
+ height
184
+ }
185
+ }
186
+
187
+ const overlayHeight = height * overlay.ratio
188
+ return {
189
+ x,
190
+ y: overlay.placement === 'start' ? y + height - overlayHeight : y,
191
+ width,
192
+ height: overlayHeight
193
+ }
194
+ }
195
+
196
+ export const getPortionPatternBoundaryGeometry = ({
197
+ orientation,
198
+ geometry,
199
+ overlay
200
+ }: GetPortionPatternBoundaryGeometryArgs): PortionPatternBoundaryGeometry | null => {
201
+ if (overlay.ratio >= 1) return null
202
+
203
+ if (orientation === 'horizontal') {
204
+ const boundaryX = overlay.placement === 'end' ? geometry.x : geometry.x + geometry.width
205
+ return {
206
+ x1: boundaryX,
207
+ y1: geometry.y,
208
+ x2: boundaryX,
209
+ y2: geometry.y + geometry.height
210
+ }
211
+ }
212
+
213
+ const boundaryY = overlay.placement === 'end' ? geometry.y + geometry.height : geometry.y
214
+ return {
215
+ x1: geometry.x,
216
+ y1: boundaryY,
217
+ x2: geometry.x + geometry.width,
218
+ y2: boundaryY
219
+ }
220
+ }
221
+
222
+ export const getPortionPatternRenderData = ({
223
+ config,
224
+ orientation,
225
+ bounds,
226
+ patterns,
227
+ datum,
228
+ seriesKey,
229
+ totalValue
230
+ }: GetPortionPatternRenderDataArgs): PortionPatternRenderData | null => {
231
+ if (!isPortionPatternSupported(config)) return null
232
+
233
+ const overlay = getPortionPatternOverlay({ patterns, datum, seriesKey, totalValue })
234
+ if (!overlay) return null
235
+
236
+ const geometry = getPortionPatternGeometry({ orientation, bounds, overlay })
237
+
238
+ return {
239
+ ...overlay,
240
+ geometry,
241
+ boundaryGeometry: getPortionPatternBoundaryGeometry({ orientation, geometry, overlay })
242
+ }
243
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { addMinimumBarHeights } from '..'
3
+
4
+ const createSegment = ({ key, height, y, value }: { key: string; height: number; y: number; value: unknown }) => ({
5
+ key,
6
+ height,
7
+ y,
8
+ bar: {
9
+ data: {
10
+ [key]: value
11
+ }
12
+ }
13
+ })
14
+
15
+ describe('addMinimumBarHeights', () => {
16
+ it('gives an isolated tiny positive segment a minimum visible height while preserving its baseline', () => {
17
+ const barStacks = [
18
+ {
19
+ bars: [createSegment({ key: 'Tiny', height: 0.02, y: 99.98, value: 0.005 })]
20
+ }
21
+ ]
22
+
23
+ addMinimumBarHeights(barStacks)
24
+
25
+ expect(barStacks[0].bars[0].height).toBe(3)
26
+ expect(barStacks[0].bars[0].y).toBeCloseTo(97)
27
+ expect(barStacks[0].bars[0].y + barStacks[0].bars[0].height).toBeCloseTo(100)
28
+ })
29
+
30
+ it('leaves exact zero segments at zero height', () => {
31
+ const barStacks = [
32
+ {
33
+ bars: [createSegment({ key: 'Zero', height: 0, y: 100, value: 0 })]
34
+ }
35
+ ]
36
+
37
+ addMinimumBarHeights(barStacks)
38
+
39
+ expect(barStacks[0].bars[0].height).toBe(0)
40
+ expect(barStacks[0].bars[0].y).toBe(100)
41
+ })
42
+ })
@@ -131,4 +131,17 @@ describe('getPatternUrl', () => {
131
131
  expect(idA.startsWith('chart-pattern-A-B-')).toBe(true)
132
132
  expect(idB.startsWith('chart-pattern-A-B-')).toBe(true)
133
133
  })
134
+
135
+ it('does not treat portion patterns as exact-value patterns', () => {
136
+ expect(
137
+ getPatternUrl({
138
+ patterns: {
139
+ Pattern1: { application: 'portion', dataKey: 'y1', dataValue: 100, patternValueKey: 'portion' }
140
+ },
141
+ datum: { y1: 100, portion: 25 },
142
+ seriesKey: 'y1',
143
+ seriesValue: 100
144
+ })
145
+ ).toBeNull()
146
+ })
134
147
  })
@@ -0,0 +1,280 @@
1
+ import { getChartPatternId } from '../../../../helpers/getChartPatternId'
2
+ import {
3
+ createPatternColumnConfig,
4
+ ensurePatternColumnConfig,
5
+ findColumnConfigKey,
6
+ getPortionPatternBoundaryGeometry,
7
+ getPortionPatternGeometry,
8
+ getPortionPatternOverlay,
9
+ getPortionPatternRenderData,
10
+ isPortionPatternSupported,
11
+ removeUnusedPatternColumnConfig
12
+ } from '../portionPattern'
13
+
14
+ describe('portion pattern column configurations', () => {
15
+ it('creates only the standard tooltip and data-table settings needed by a pattern column', () => {
16
+ expect(createPatternColumnConfig('new_cases')).toEqual({
17
+ name: 'new_cases',
18
+ label: 'new_cases',
19
+ prefix: '',
20
+ suffix: '',
21
+ roundToPlace: 0,
22
+ commas: true,
23
+ dataTable: true,
24
+ tooltips: true
25
+ })
26
+ })
27
+
28
+ it('creates a configuration only when the selected column is not already configured', () => {
29
+ const columns = {
30
+ existing: {
31
+ name: 'new_cases',
32
+ label: 'New cases',
33
+ commas: false,
34
+ dataTable: false,
35
+ tooltips: false
36
+ }
37
+ } as any
38
+
39
+ expect(ensurePatternColumnConfig(columns, 'new_cases')).toBe(columns)
40
+ expect(ensurePatternColumnConfig(columns, 'previous_cases')).toEqual({
41
+ ...columns,
42
+ previous_cases: createPatternColumnConfig('previous_cases')
43
+ })
44
+ })
45
+
46
+ it('finds configurations stored under either the column name or an alias key', () => {
47
+ expect(findColumnConfigKey({ new_cases: { label: 'New cases' } } as any, 'new_cases')).toBe('new_cases')
48
+ expect(
49
+ findColumnConfigKey({ additionalColumn1: { name: 'new_cases', label: 'New cases' } } as any, 'new_cases')
50
+ ).toBe('additionalColumn1')
51
+ })
52
+
53
+ it('removes an obsolete pattern column configuration', () => {
54
+ const columns = {
55
+ additionalColumn1: { name: 'new_cases', label: 'New cases' },
56
+ source: { name: 'source', label: 'Source' }
57
+ } as any
58
+
59
+ expect(
60
+ removeUnusedPatternColumnConfig({
61
+ columns,
62
+ columnName: 'new_cases',
63
+ patterns: {}
64
+ })
65
+ ).toEqual({ source: columns.source })
66
+ })
67
+
68
+ it('preserves a column still used by another portion pattern', () => {
69
+ const columns = { new_cases: { name: 'new_cases', label: 'New cases' } } as any
70
+
71
+ expect(
72
+ removeUnusedPatternColumnConfig({
73
+ columns,
74
+ columnName: 'new_cases',
75
+ patterns: {
76
+ Pattern2: { application: 'portion', patternValueKey: 'new_cases' }
77
+ }
78
+ })
79
+ ).toBe(columns)
80
+ })
81
+
82
+ it('preserves columns owned by an axis or chart series', () => {
83
+ const columns = { cases: { name: 'cases', label: 'Cases' } } as any
84
+
85
+ expect(
86
+ removeUnusedPatternColumnConfig({
87
+ columns,
88
+ columnName: 'cases',
89
+ patterns: {},
90
+ protectedColumnNames: ['date', 'cases']
91
+ })
92
+ ).toBe(columns)
93
+ })
94
+ })
95
+
96
+ describe('portion pattern support', () => {
97
+ const supportedConfig = {
98
+ visualizationType: 'Bar',
99
+ visualizationSubType: 'regular',
100
+ isLollipopChart: false,
101
+ yAxis: { type: 'linear' }
102
+ } as any
103
+
104
+ it('supports regular Bar and Combo charts with linear axes', () => {
105
+ expect(isPortionPatternSupported(supportedConfig)).toBe(true)
106
+ expect(isPortionPatternSupported({ ...supportedConfig, visualizationType: 'Combo' })).toBe(true)
107
+ })
108
+
109
+ it.each([
110
+ ['stacked bars', { visualizationSubType: 'stacked' }],
111
+ ['lollipops', { isLollipopChart: true }],
112
+ ['logarithmic axes', { yAxis: { type: 'logarithmic' } }],
113
+ ['other chart types', { visualizationType: 'Line' }]
114
+ ])('does not support %s', (_description, overrides) => {
115
+ expect(isPortionPatternSupported({ ...supportedConfig, ...overrides } as any)).toBe(false)
116
+ })
117
+ })
118
+
119
+ describe('getPortionPatternOverlay', () => {
120
+ const getOverlay = (totalValue: unknown, portion: unknown, placement?: 'start' | 'end') =>
121
+ getPortionPatternOverlay({
122
+ patterns: {
123
+ Pattern1: {
124
+ application: 'portion',
125
+ dataKey: 'total',
126
+ patternValueKey: 'portion',
127
+ placement,
128
+ color: '#123456'
129
+ }
130
+ },
131
+ datum: { total: totalValue, portion },
132
+ seriesKey: 'total',
133
+ totalValue
134
+ })
135
+
136
+ it('calculates ratios for portions less than and equal to the total', () => {
137
+ expect(getOverlay(100, 25)).toEqual({
138
+ patternUrl: `url(#${getChartPatternId('Pattern1')})`,
139
+ ratio: 0.25,
140
+ placement: 'end',
141
+ patternColor: '#123456'
142
+ })
143
+ expect(getOverlay('100', '100')?.ratio).toBe(1)
144
+ })
145
+
146
+ it('preserves beginning placement', () => {
147
+ expect(getOverlay(100, 40, 'start')?.placement).toBe('start')
148
+ })
149
+
150
+ it.each([
151
+ ['missing total', undefined, 10],
152
+ ['nonnumeric total', 'not-a-number', 10],
153
+ ['zero total', 0, 0],
154
+ ['negative total', -100, 10],
155
+ ['missing portion', 100, undefined],
156
+ ['nonnumeric portion', 100, 'not-a-number'],
157
+ ['zero portion', 100, 0],
158
+ ['negative portion', 100, -10],
159
+ ['portion greater than total', 100, 101]
160
+ ])('returns no overlay for %s', (_description, totalValue, portion) => {
161
+ expect(getOverlay(totalValue, portion)).toBeNull()
162
+ })
163
+
164
+ it('uses only the first configured portion pattern for a target series', () => {
165
+ const overlay = getPortionPatternOverlay({
166
+ patterns: {
167
+ First: { application: 'portion', dataKey: 'total', patternValueKey: 'invalidPortion' },
168
+ Second: { application: 'portion', dataKey: 'total', patternValueKey: 'validPortion' }
169
+ },
170
+ datum: { total: 100, invalidPortion: 200, validPortion: 25 },
171
+ seriesKey: 'total',
172
+ totalValue: 100
173
+ })
174
+
175
+ expect(overlay).toBeNull()
176
+ })
177
+ })
178
+
179
+ describe('getPortionPatternGeometry', () => {
180
+ it.each([
181
+ ['vertical end', 'vertical', 'end', { x: 10, y: 20, width: 100, height: 40 }],
182
+ ['vertical start', 'vertical', 'start', { x: 10, y: 60, width: 100, height: 40 }],
183
+ ['horizontal end', 'horizontal', 'end', { x: 60, y: 20, width: 50, height: 80 }],
184
+ ['horizontal start', 'horizontal', 'start', { x: 10, y: 20, width: 50, height: 80 }]
185
+ ] as const)('calculates %s placement', (_description, orientation, placement, expected) => {
186
+ expect(
187
+ getPortionPatternGeometry({
188
+ orientation,
189
+ bounds: { x: 10, y: 20, width: 100, height: 80 },
190
+ overlay: { ratio: 0.5, placement }
191
+ })
192
+ ).toEqual(expected)
193
+ })
194
+ })
195
+
196
+ describe('getPortionPatternBoundaryGeometry', () => {
197
+ it.each([
198
+ ['vertical end', 'vertical', 'end', { x1: 10, y1: 60, x2: 110, y2: 60 }],
199
+ ['vertical start', 'vertical', 'start', { x1: 10, y1: 20, x2: 110, y2: 20 }],
200
+ ['horizontal end', 'horizontal', 'end', { x1: 10, y1: 20, x2: 10, y2: 100 }],
201
+ ['horizontal start', 'horizontal', 'start', { x1: 60, y1: 20, x2: 60, y2: 100 }]
202
+ ] as const)('calculates the %s internal divider', (_description, orientation, placement, expected) => {
203
+ expect(
204
+ getPortionPatternBoundaryGeometry({
205
+ orientation,
206
+ geometry: {
207
+ x: 10,
208
+ y: 20,
209
+ width: orientation === 'vertical' ? 100 : 50,
210
+ height: orientation === 'vertical' ? 40 : 80
211
+ },
212
+ overlay: { ratio: 0.5, placement }
213
+ })
214
+ ).toEqual(expected)
215
+ })
216
+
217
+ it('omits the divider when the portion covers the full bar', () => {
218
+ expect(
219
+ getPortionPatternBoundaryGeometry({
220
+ orientation: 'vertical',
221
+ geometry: { x: 10, y: 20, width: 100, height: 80 },
222
+ overlay: { ratio: 1, placement: 'end' }
223
+ })
224
+ ).toBeNull()
225
+ })
226
+ })
227
+
228
+ describe('getPortionPatternRenderData', () => {
229
+ it('returns a complete render model for supported valid portions', () => {
230
+ expect(
231
+ getPortionPatternRenderData({
232
+ config: {
233
+ visualizationType: 'Bar',
234
+ visualizationSubType: 'regular',
235
+ isLollipopChart: false,
236
+ yAxis: { type: 'linear' }
237
+ } as any,
238
+ orientation: 'vertical',
239
+ bounds: { x: 10, y: 20, width: 100, height: 80 },
240
+ patterns: {
241
+ Pattern1: {
242
+ application: 'portion',
243
+ dataKey: 'total',
244
+ patternValueKey: 'portion',
245
+ color: '#123456'
246
+ }
247
+ },
248
+ datum: { total: 100, portion: 25 },
249
+ seriesKey: 'total',
250
+ totalValue: 100
251
+ })
252
+ ).toEqual({
253
+ patternUrl: `url(#${getChartPatternId('Pattern1')})`,
254
+ ratio: 0.25,
255
+ placement: 'end',
256
+ patternColor: '#123456',
257
+ geometry: { x: 10, y: 20, width: 100, height: 20 },
258
+ boundaryGeometry: { x1: 10, y1: 40, x2: 110, y2: 40 }
259
+ })
260
+ })
261
+
262
+ it('returns no render model for unsupported configurations', () => {
263
+ expect(
264
+ getPortionPatternRenderData({
265
+ config: {
266
+ visualizationType: 'Bar',
267
+ visualizationSubType: 'stacked',
268
+ isLollipopChart: false,
269
+ yAxis: { type: 'linear' }
270
+ } as any,
271
+ orientation: 'vertical',
272
+ bounds: { x: 10, y: 20, width: 100, height: 80 },
273
+ patterns: {},
274
+ datum: {},
275
+ seriesKey: 'total',
276
+ totalValue: 100
277
+ })
278
+ ).toBeNull()
279
+ })
280
+ })