@cdc/chart 4.25.11 → 4.26.2

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 (181) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  3. package/dist/cdcchart.js +51401 -50814
  4. package/examples/default.json +378 -0
  5. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  6. package/examples/feature/annotations/index.json +3 -6
  7. package/examples/feature/horizon/horizon-chart.json +395 -0
  8. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  9. package/examples/line-chart-states.json +1085 -0
  10. package/examples/private/123.json +694 -0
  11. package/examples/private/DEV-12100.json +1303 -0
  12. package/examples/private/anchor-issue.json +4094 -0
  13. package/examples/private/backwards-slider.json +10430 -0
  14. package/examples/private/cat-y.json +1235 -0
  15. package/examples/private/data-points.json +228 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/height.json +3915 -0
  18. package/examples/private/links.json +569 -0
  19. package/examples/private/quadrant.txt +30 -0
  20. package/examples/private/test-forecast.json +5510 -0
  21. package/examples/private/timeline-data.json +1 -0
  22. package/examples/private/timeline.json +389 -0
  23. package/examples/private/warming-stripe-test.json +2578 -0
  24. package/examples/private/warming-stripes.json +4763 -0
  25. package/examples/radar-chart-simple.json +133 -0
  26. package/examples/radar-chart.json +148 -0
  27. package/examples/tech-adoption-with-links.json +560 -0
  28. package/index.html +1 -36
  29. package/package.json +59 -60
  30. package/src/CdcChartComponent.tsx +206 -89
  31. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  32. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  33. package/src/_stories/Chart.CI.stories.tsx +13 -0
  34. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  35. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  36. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  37. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  38. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  39. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  40. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  41. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  42. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  43. package/src/_stories/Chart.Regions.Categorical.stories.tsx +161 -0
  44. package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
  45. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
  46. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  47. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  48. package/src/_stories/Chart.stories.tsx +45 -0
  49. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  50. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  51. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  52. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  53. package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
  54. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  55. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  56. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  57. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  58. package/src/_stories/ChartBrush.stories.tsx +57 -0
  59. package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
  60. package/src/_stories/ChartEditor.stories.tsx +7 -0
  61. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  62. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  63. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  64. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  65. package/src/_stories/TechAdoptionWithLinks.stories.tsx +34 -0
  66. package/src/_stories/_mock/brush_continuous.json +86 -0
  67. package/src/_stories/_mock/brush_date_large.json +176 -0
  68. package/src/_stories/_mock/brush_enabled.json +326 -0
  69. package/src/_stories/_mock/brush_mock.json +2 -69
  70. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  71. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  72. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  73. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  74. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  75. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  76. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  77. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  78. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  79. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  80. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  81. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  82. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  83. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  84. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  85. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  86. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
  87. package/src/components/Axis/BottomAxis.tsx +270 -0
  88. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  89. package/src/components/Axis/LeftAxis.tsx +404 -0
  90. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  91. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  92. package/src/components/Axis/README.md +94 -0
  93. package/src/components/Axis/RightAxis.tsx +108 -0
  94. package/src/components/Axis/axis.constants.ts +21 -0
  95. package/src/components/Axis/index.ts +7 -0
  96. package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
  97. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  98. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  99. package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
  100. package/src/components/BarChart/components/BarChart.tsx +7 -1
  101. package/src/components/BarChart/components/context.tsx +1 -0
  102. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  103. package/src/components/Brush/BrushSelector.tsx +1390 -0
  104. package/src/components/Brush/MiniChartPreview.tsx +400 -0
  105. package/src/components/DeviationBar.jsx +9 -7
  106. package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
  107. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  108. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  109. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
  110. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
  111. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  112. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  113. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
  114. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
  115. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  116. package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
  117. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  118. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  119. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  120. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  121. package/src/components/HorizonChart/index.tsx +3 -0
  122. package/src/components/Legend/Legend.Component.tsx +52 -4
  123. package/src/components/Legend/Legend.tsx +4 -3
  124. package/src/components/Legend/LegendValueRange.tsx +77 -0
  125. package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
  126. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  127. package/src/components/Legend/helpers/index.ts +10 -6
  128. package/src/components/LineChart/helpers/README.md +292 -0
  129. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  130. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  131. package/src/components/LineChart/index.tsx +44 -8
  132. package/src/components/LinearChart/README.md +109 -0
  133. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  134. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  135. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  136. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  137. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  138. package/src/components/LinearChart.tsx +338 -1082
  139. package/src/components/PairedBarChart.jsx +20 -3
  140. package/src/components/PieChart/PieChart.tsx +1 -1
  141. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  142. package/src/components/RadarChart/RadarChart.tsx +298 -0
  143. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  144. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  145. package/src/components/RadarChart/helpers.ts +83 -0
  146. package/src/components/RadarChart/index.tsx +3 -0
  147. package/src/components/Regions/components/Regions.tsx +365 -122
  148. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  149. package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
  150. package/src/components/WarmingStripes/WarmingStripes.tsx +230 -0
  151. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  152. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  153. package/src/components/WarmingStripes/index.tsx +3 -0
  154. package/src/data/initial-state.js +17 -2
  155. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  156. package/src/helpers/getExcludedData.ts +4 -0
  157. package/src/helpers/getMinMax.ts +12 -7
  158. package/src/helpers/handleChartAriaLabels.ts +19 -19
  159. package/src/helpers/handleLineType.ts +22 -18
  160. package/src/helpers/sizeHelpers.ts +0 -20
  161. package/src/helpers/smallMultiplesHelpers.ts +1 -1
  162. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  163. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  164. package/src/hooks/useScales.ts +18 -1
  165. package/src/hooks/useTooltip.tsx +34 -10
  166. package/src/scss/DataTable.scss +0 -4
  167. package/src/scss/main.scss +22 -3
  168. package/src/selectors/README.md +68 -0
  169. package/src/store/chart.reducer.ts +2 -0
  170. package/src/test/CdcChart.test.jsx +1 -1
  171. package/src/types/ChartConfig.ts +21 -0
  172. package/src/types/ChartContext.ts +1 -0
  173. package/src/types/Horizon.ts +64 -0
  174. package/src/types/Label.ts +1 -0
  175. package/src/utils/analyticsTracking.ts +19 -0
  176. package/LICENSE +0 -201
  177. package/src/components/Annotations/components/helpers/index.tsx +0 -46
  178. package/src/components/Brush/BrushChart.tsx +0 -128
  179. package/src/components/Brush/BrushController.tsx +0 -71
  180. package/src/components/Brush/types.tsx +0 -8
  181. package/src/components/BrushChart.tsx +0 -223
@@ -16,16 +16,19 @@ export const useEditorPermissions = () => {
16
16
  'Deviation Bar',
17
17
  'Forecasting',
18
18
  // 'Forest Plot',
19
+ 'Horizon Chart',
19
20
  'Line',
20
21
  'Paired Bar',
21
22
  'Pie',
23
+ 'Radar',
22
24
  'Scatter Plot',
23
25
  'Spark Line',
24
- 'Sankey'
26
+ 'Sankey',
27
+ 'Warming Stripes'
25
28
  ]
26
29
 
27
30
  const visSupportsDateCategoryAxis = () => {
28
- const disabledCharts = ['Forest Plot', 'Sankey']
31
+ const disabledCharts = ['Forest Plot', 'Radar', 'Sankey']
29
32
  if (disabledCharts.includes(visualizationType)) return false
30
33
  return true
31
34
  }
@@ -54,16 +57,31 @@ export const useEditorPermissions = () => {
54
57
  return true
55
58
  }
56
59
 
60
+ const visSupportsClickingLegend = () => {
61
+ const disabledCharts = ['Horizon Chart']
62
+ if (disabledCharts.includes(visualizationType)) return false
63
+ return true
64
+ }
65
+
66
+ const visSupportsDataAnnotations = () => {
67
+ const enabledCharts = ['Line', 'Bar', 'Combo', 'Area Chart', 'Forecasting']
68
+ if (enabledCharts.includes(visualizationType) && config.orientation !== 'horizontal') return true
69
+ return false
70
+ }
71
+
57
72
  const visHasLabelOnData = () => {
58
73
  const disabledCharts = [
59
74
  'Area Chart',
60
75
  'Box Plot',
76
+ 'Bump Chart',
77
+ 'Forest Plot',
78
+ 'Horizon Chart',
61
79
  'Pie',
80
+ 'Radar',
81
+ 'Sankey',
62
82
  'Scatter Plot',
63
- 'Forest Plot',
64
83
  'Spark Line',
65
- 'Sankey',
66
- 'Bump Chart'
84
+ 'Warming Stripes'
67
85
  ]
68
86
  if (disabledCharts.includes(visualizationType)) return false
69
87
  return true
@@ -72,12 +90,14 @@ export const useEditorPermissions = () => {
72
90
  const visCanAnimate = () => {
73
91
  const disabledCharts = [
74
92
  'Area Chart',
75
- 'Scatter Plot',
76
93
  'Box Plot',
94
+ 'Bump Chart',
77
95
  'Forest Plot',
78
- 'Spark Line',
96
+ 'Radar',
79
97
  'Sankey',
80
- 'Bump Chart'
98
+ 'Scatter Plot',
99
+ 'Spark Line',
100
+ 'Warming Stripes'
81
101
  ]
82
102
  if (disabledCharts.includes(visualizationType)) return false
83
103
  return true
@@ -93,6 +113,8 @@ export const useEditorPermissions = () => {
93
113
  return false
94
114
  case 'Sankey':
95
115
  return false
116
+ case 'Warming Stripes':
117
+ return true
96
118
  default:
97
119
  return true
98
120
  }
@@ -140,7 +162,12 @@ export const useEditorPermissions = () => {
140
162
  }
141
163
  const visHasBrushChart = () => {
142
164
  if (config.xAxis.type === 'categorical') return false
143
- return ['Line', 'Bar', 'Area Chart', 'Combo'].includes(visualizationType) && orientation === 'vertical'
165
+ // Allow Line charts, vertical Bar charts (both stacked and grouped), vertical Area charts, and Combo charts
166
+ if (visualizationType === 'Line' && orientation === 'vertical') return true
167
+ if (visualizationType === 'Bar' && orientation === 'vertical') return true
168
+ if (visualizationType === 'Area Chart' && orientation === 'vertical') return true
169
+ if (visualizationType === 'Combo' && orientation === 'vertical') return true
170
+ return false
144
171
  }
145
172
 
146
173
  const visHasBarBorders = () => {
@@ -153,6 +180,8 @@ export const useEditorPermissions = () => {
153
180
 
154
181
  const visHasDataCutoff = () => {
155
182
  switch (visualizationType) {
183
+ case 'Warming Stripes':
184
+ return false
156
185
  case 'Sankey':
157
186
  return false
158
187
  case 'Forest Plot':
@@ -168,7 +197,9 @@ export const useEditorPermissions = () => {
168
197
  }
169
198
  }
170
199
 
171
- const visHasSelectableLegendValues = !['Box Plot', 'Forest Plot', 'Spark Line'].includes(visualizationType)
200
+ const visHasSelectableLegendValues = !['Box Plot', 'Forest Plot', 'Spark Line', 'Warming Stripes'].includes(
201
+ visualizationType
202
+ )
172
203
  const visHasLegendAxisAlign = () => {
173
204
  return visualizationType === 'Bar' && visualizationSubType === 'stacked' && config.legend.behavior === 'isolate'
174
205
  }
@@ -177,7 +208,7 @@ export const useEditorPermissions = () => {
177
208
  }
178
209
 
179
210
  const visSupportsTooltipOpacity = () => {
180
- const disabledCharts = ['Spark Line', 'Sankey']
211
+ const disabledCharts = ['Spark Line', 'Sankey', 'Warming Stripes']
181
212
  if (disabledCharts.includes(visualizationType)) return false
182
213
  return true
183
214
  }
@@ -189,7 +220,7 @@ export const useEditorPermissions = () => {
189
220
  }
190
221
 
191
222
  const visSupportsSequentialPallete = () => {
192
- const disabledCharts = ['Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
223
+ const disabledCharts = ['Line', 'Paired Bar', 'Deviation Bar', 'Forest Plot', 'Forecasting', 'Sankey']
193
224
  if (disabledCharts.includes(visualizationType)) return false
194
225
  return true
195
226
  }
@@ -207,19 +238,19 @@ export const useEditorPermissions = () => {
207
238
  }
208
239
 
209
240
  const visSupportsDateCategoryAxisLabel = () => {
210
- const disabledCharts = ['Forest Plot', 'Spark Line', 'Bump Chart']
241
+ const disabledCharts = ['Forest Plot', 'Spark Line', 'Bump Chart', 'Warming Stripes']
211
242
  if (disabledCharts.includes(visualizationType)) return false
212
243
  return true
213
244
  }
214
245
 
215
246
  const visSupportsDateCategoryAxisLine = () => {
216
- const disabledCharts = ['Forest Plot', 'Spark Line']
247
+ const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
217
248
  if (disabledCharts.includes(visualizationType)) return false
218
249
  return true
219
250
  }
220
251
 
221
252
  const visSupportsDateCategoryAxisTicks = () => {
222
- const disabledCharts = ['Forest Plot', 'Spark Line']
253
+ const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
223
254
  if (disabledCharts.includes(visualizationType)) return false
224
255
  return true
225
256
  }
@@ -243,7 +274,16 @@ export const useEditorPermissions = () => {
243
274
  }
244
275
 
245
276
  const visSupportsRegions = () => {
246
- const disabledCharts = ['Forest Plot', 'Pie', 'Paired Bar', 'Spark Line', 'Sankey']
277
+ const disabledCharts = [
278
+ 'Forest Plot',
279
+ 'Horizon Chart',
280
+ 'Pie',
281
+ 'Paired Bar',
282
+ 'Radar',
283
+ 'Spark Line',
284
+ 'Sankey',
285
+ 'Warming Stripes'
286
+ ]
247
287
  if (disabledCharts.includes(visualizationType)) return false
248
288
  return true
249
289
  }
@@ -261,14 +301,13 @@ export const useEditorPermissions = () => {
261
301
  }
262
302
 
263
303
  const visSupportsFilters = () => {
264
- const disabledCharts = ['Forest Plot', 'Sankey']
304
+ const disabledCharts = ['Forest Plot', 'Sankey', 'Warming Stripes']
265
305
  if (disabledCharts.includes(visualizationType)) return false
266
306
  return true
267
307
  }
268
308
 
269
309
  const visSupportsValueAxisGridLines = () => {
270
310
  const disabledCharts = ['Forest Plot']
271
- if (orientation === 'horizontal') return false
272
311
  if (disabledCharts.includes(visualizationType)) return false
273
312
  return true
274
313
  }
@@ -302,25 +341,25 @@ export const useEditorPermissions = () => {
302
341
  }
303
342
 
304
343
  const visSupportsBarThickness = () => {
305
- const disabledCharts = ['Forest Plot']
344
+ const disabledCharts = ['Forest Plot', 'Warming Stripes']
306
345
  if (disabledCharts.includes(visualizationType)) return false
307
346
  return true
308
347
  }
309
348
 
310
349
  const visSupportsChartHeight = () => {
311
- const disabledCharts = ['Spark Line']
350
+ const disabledCharts = ['Spark Line', 'Warming Stripes']
312
351
  if (disabledCharts.includes(visualizationType)) return false
313
352
  return true
314
353
  }
315
354
  const visSupportsMobileChartHeight = () => {
316
355
  // TODO: this is a soft release. Support should eventually match visSupportsChartHeight
317
- const enabledCharts = ['Bar', 'Line', 'Combo', 'Area Chart']
356
+ const enabledCharts = ['Bar', 'Line', 'Combo', 'Area Chart', 'Radar']
318
357
  if (enabledCharts.includes(visualizationType)) return true
319
358
  return false
320
359
  }
321
360
 
322
361
  const visSupportsLeftValueAxis = () => {
323
- const disabledCharts = ['Spark Line', 'Sankey']
362
+ const disabledCharts = ['Radar', 'Spark Line', 'Sankey', 'Warming Stripes']
324
363
  if (disabledCharts.includes(visualizationType)) return false
325
364
  return true
326
365
  }
@@ -335,6 +374,7 @@ export const useEditorPermissions = () => {
335
374
  const disabledCharts = ['Spark Line', 'Sankey', 'Bump Chart']
336
375
  if (disabledCharts.includes(visualizationType)) return false
337
376
  if (config.orientation !== 'horizontal') return false
377
+ if (config.orientation === 'horizontal' && visualizationType === 'Bar' && !config.isLollipopChart) return false
338
378
  return true
339
379
  }
340
380
 
@@ -370,8 +410,8 @@ export const useEditorPermissions = () => {
370
410
  }
371
411
 
372
412
  const visSupportsSmallMultiples = () => {
373
- const enabledCharts = ['Line', 'Bar', 'Area Chart', 'Combo', 'Box Plot', 'Scatter Plot']
374
- if (enabledCharts.includes(visualizationType)) return true
413
+ const enabledCharts = ['Line', 'Bar', 'Area Chart', 'Combo', 'Box Plot', 'Scatter Plot', 'Warming Stripes']
414
+ if (enabledCharts.includes(visualizationType) && config.orientation !== 'horizontal') return true
375
415
  return false
376
416
  }
377
417
 
@@ -404,56 +444,58 @@ export const useEditorPermissions = () => {
404
444
  return {
405
445
  enabledChartTypes,
406
446
  visCanAnimate,
447
+ visHasaAdditionalLabelsOnBars,
407
448
  visHasAnchors,
408
449
  visHasBarBorders,
450
+ visHasBrushChart,
451
+ visHasCategoricalAxis,
409
452
  visHasDataCutoff,
410
- visHasLabelOnData,
411
453
  visHasDataSuppression,
454
+ visHasLabelOnData,
412
455
  visHasLegend,
413
456
  visHasLegendAxisAlign,
414
457
  visHasLegendColorCategory,
415
- visHasBrushChart,
416
458
  visHasNumbersOnBars,
417
- visHasaAdditionalLabelsOnBars,
459
+ visHasSelectableLegendValues,
460
+ visHasSingleSeriesTooltip,
418
461
  visSupportsBarSpace,
419
462
  visSupportsBarThickness,
420
463
  visSupportsChartHeight,
421
- visSupportsMobileChartHeight,
464
+ visSupportsClickingLegend,
465
+ visSupportsDataAnnotations,
422
466
  visSupportsDateCategoryAxis,
423
- visSupportsDateCategoryAxisMin,
424
- visSupportsDateCategoryAxisMax,
425
467
  visSupportsDateCategoryAxisLabel,
426
468
  visSupportsDateCategoryAxisLine,
469
+ visSupportsDateCategoryAxisMax,
470
+ visSupportsDateCategoryAxisMin,
471
+ visSupportsDateCategoryAxisPadding,
427
472
  visSupportsDateCategoryAxisTicks,
428
473
  visSupportsDateCategoryHeight,
429
474
  visSupportsDateCategoryNumTicks,
430
475
  visSupportsDateCategoryTickRotation,
431
- visSupportsDateCategoryAxisPadding,
476
+ visSupportsDynamicSeries,
432
477
  visSupportsFilters,
433
478
  visSupportsFootnotes,
434
479
  visSupportsLeftValueAxis,
480
+ visSupportsMobileChartHeight,
435
481
  visSupportsNonSequentialPallete,
436
482
  visSupportsPreliminaryData,
437
483
  visSupportsRankByValue,
484
+ visSupportsReactTooltip,
438
485
  visSupportsRegions,
439
486
  visSupportsResponsiveTicks,
440
487
  visSupportsReverseColorPalette,
441
488
  visSupportsSequentialPallete,
489
+ visSupportsSmallMultiples,
442
490
  visSupportsSuperTitle,
443
491
  visSupportsTooltipLines,
444
- visHasSelectableLegendValues,
445
492
  visSupportsTooltipOpacity,
446
493
  visSupportsValueAxisGridLines,
447
494
  visSupportsValueAxisLabels,
448
495
  visSupportsValueAxisLine,
449
- visSupportsValueAxisTicks,
450
- visSupportsReactTooltip,
451
496
  visSupportsValueAxisMax,
452
497
  visSupportsValueAxisMin,
453
- visSupportsDynamicSeries,
454
- visSupportsSmallMultiples,
455
- visSupportsYPadding,
456
- visHasSingleSeriesTooltip,
457
- visHasCategoricalAxis
498
+ visSupportsValueAxisTicks,
499
+ visSupportsYPadding
458
500
  }
459
501
  }
@@ -0,0 +1,131 @@
1
+ import React, { useContext, memo, useMemo } from 'react'
2
+
3
+ // cdc
4
+ import ConfigContext from '../../ConfigContext'
5
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
7
+
8
+ // visx
9
+ import { Bar } from '@visx/shape'
10
+ import { Group } from '@visx/group'
11
+
12
+ // components
13
+ import HorizonBand from './components/HorizonBand'
14
+
15
+ // helpers
16
+ import { calculateHorizonBands } from './helpers/calculateHorizonBands'
17
+
18
+ // types
19
+ import { HORIZON_DEFAULTS } from '../../types/Horizon'
20
+
21
+ type HorizonChartProps = {
22
+ xScale: any
23
+ yScale: any
24
+ xMax: number
25
+ yMax: number
26
+ handleTooltipMouseOver: (e: any, additionalData?: any) => void
27
+ handleTooltipMouseOff: () => void
28
+ tooltipData?: any
29
+ showTooltip?: boolean
30
+ }
31
+
32
+ const HorizonChart = ({ xScale, xMax, yMax, handleTooltipMouseOver, handleTooltipMouseOff }: HorizonChartProps) => {
33
+ // Get data and config from context
34
+ const { transformedData: data, config, colorScale, rawData, parseDate } = useContext(ConfigContext)
35
+
36
+ const horizonConfig = {
37
+ ...HORIZON_DEFAULTS,
38
+ ...config.horizon
39
+ }
40
+
41
+ // Get series keys for rendering rows
42
+ const seriesKeys =
43
+ (config.runtime?.seriesKeys?.length ? config.runtime.seriesKeys : config.series?.map(s => s.dataKey)) || []
44
+
45
+ // Calculate value range across all horizon series (for consistent scaling)
46
+ // Must be called before early returns to satisfy React hooks rules
47
+ const valueRange = useMemo(() => {
48
+ if (!data || data.length === 0 || seriesKeys.length === 0) {
49
+ return { min: 0, max: 0 }
50
+ }
51
+
52
+ let min = Infinity
53
+ let max = -Infinity
54
+
55
+ data.forEach((row: any) => {
56
+ seriesKeys.forEach((key: string) => {
57
+ const value = Math.abs(Number(row[key]) || 0)
58
+ if (value > 0) {
59
+ min = Math.min(min, value)
60
+ max = Math.max(max, value)
61
+ }
62
+ })
63
+ })
64
+
65
+ return {
66
+ min: min === Infinity ? 0 : min,
67
+ max: max === -Infinity ? 0 : max
68
+ }
69
+ }, [data, seriesKeys])
70
+
71
+ // Early returns after all hooks
72
+ if (!data || data.length === 0) return null
73
+ if (seriesKeys.length === 0) return null
74
+ if (xMax <= 0 || yMax <= 0) return null
75
+
76
+ // Calculate band dimensions to fill available space
77
+ const { bandHeight, getRowY } = calculateHorizonBands(
78
+ seriesKeys.length,
79
+ yMax,
80
+ horizonConfig.bandGap,
81
+ horizonConfig.bottomPadding
82
+ )
83
+
84
+ const getXPosition = value => {
85
+ if (config.xAxis.type === 'categorical') {
86
+ return xScale(value) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
87
+ }
88
+ if (isDateScale(config.xAxis)) {
89
+ const scaledValue = xScale(parseDate(value, false))
90
+ return scaledValue + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
91
+ }
92
+ return xScale(value)
93
+ }
94
+
95
+ return (
96
+ <ErrorBoundary component='HorizonChart'>
97
+ <Group className='horizon-chart' key='horizon-wrapper' left={Number(config.yAxis.size)} height={Number(yMax)}>
98
+ {seriesKeys.map((seriesKey, index) => {
99
+ const rowY = getRowY(index)
100
+ return (
101
+ <Group key={seriesKey} top={rowY} className='horizon-band-row'>
102
+ {/* Horizon band for this series */}
103
+ <HorizonBand
104
+ data={data}
105
+ seriesKey={seriesKey}
106
+ xAxisKey={config.xAxis.dataKey}
107
+ getXPosition={getXPosition}
108
+ bandHeight={bandHeight}
109
+ xMax={xMax}
110
+ numLayers={horizonConfig.numLayers}
111
+ colorScale={colorScale}
112
+ config={config}
113
+ globalMax={valueRange.max}
114
+ />
115
+ </Group>
116
+ )
117
+ })}
118
+ {/* Transparent bar for tooltip interaction */}
119
+ <Bar
120
+ width={Number(xMax)}
121
+ height={Number(yMax)}
122
+ fill='transparent'
123
+ onMouseMove={e => handleTooltipMouseOver(e, rawData)}
124
+ onMouseLeave={handleTooltipMouseOff}
125
+ />
126
+ </Group>
127
+ </ErrorBoundary>
128
+ )
129
+ }
130
+
131
+ export default memo(HorizonChart)
@@ -0,0 +1,160 @@
1
+ import React, { memo, useId, useMemo } from 'react'
2
+
3
+ // visx
4
+ import { AreaClosed } from '@visx/shape'
5
+ import { Group } from '@visx/group'
6
+ import { scaleLinear } from '@visx/scale'
7
+ import * as allCurves from '@visx/curve'
8
+ import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
9
+ import { getHorizonLayerColors } from '../helpers/getHorizonLayerColors'
10
+
11
+ type HorizonBandProps = {
12
+ data: any[]
13
+ seriesKey: string
14
+ xAxisKey: string
15
+ getXPosition: (value: any) => number
16
+ bandHeight: number
17
+ xMax: number
18
+ numLayers: number
19
+ colorScale: any
20
+ config: any
21
+ globalMax: number
22
+ }
23
+
24
+ /**
25
+ * HorizonBand renders a single series as a horizon chart
26
+ *
27
+ * Horizon charts work by:
28
+ * 1. Dividing the value range into N layers
29
+ * 2. Each layer shows values within its threshold range
30
+ * 3. Layers are stacked/overlapped to create the horizon effect
31
+ * 4. Higher values appear overlapped, usually darker depending on color palette (achieved through layer stacking)
32
+ */
33
+ const HorizonBand = ({
34
+ data,
35
+ seriesKey,
36
+ xAxisKey,
37
+ getXPosition,
38
+ bandHeight,
39
+ xMax,
40
+ numLayers,
41
+ config,
42
+ globalMax
43
+ }: HorizonBandProps) => {
44
+ // Create a unique, safe ID for clipPath (useId ensures uniqueness across instances)
45
+ // Must be called before any early returns to follow React's rules of hooks
46
+ const uniqueId = useId()
47
+ const safeSeriesKey = seriesKey.replace(/[^a-zA-Z0-9]/g, '-')
48
+ const clipId = `horizon-clip-${safeSeriesKey}-${uniqueId.replace(/:/g, '')}`
49
+
50
+ // Get the curve type from config (same as stacked area chart)
51
+ const curveType = allCurves[approvedCurveTypes[config.stackedAreaChartLineType || 'Linear']] || allCurves.curveLinear
52
+
53
+ // Process data: convert to absolute values and compute series max in single pass
54
+ const { processedData, seriesMax } = useMemo(() => {
55
+ let max = 0
56
+ const processed = data.map(d => {
57
+ const absValue = Math.abs(Number(d[seriesKey]) || 0)
58
+ if (absValue > max) max = absValue
59
+ return { ...d, [seriesKey]: absValue }
60
+ })
61
+ return { processedData: processed, seriesMax: max }
62
+ }, [data, seriesKey])
63
+
64
+ // Get layer colors using shared helper (memoized based on palette config and numLayers)
65
+ // Must be called before early returns to follow React's rules of hooks
66
+ const layerColors = useMemo(
67
+ () => getHorizonLayerColors(config, numLayers),
68
+ [
69
+ config.general?.palette?.name,
70
+ config.general?.palette?.isReversed,
71
+ config.general?.palette?.version,
72
+ config.general?.palette?.customColors,
73
+ numLayers
74
+ ]
75
+ )
76
+
77
+ // Use global max for scaling (ensures all series bands are comparable)
78
+ const maxValue = globalMax
79
+
80
+ // If no data, max is 0, or dimensions are invalid, don't render
81
+ if (maxValue === 0) return null
82
+ if (xMax <= 0 || bandHeight <= 0) return null
83
+
84
+ // Calculate the threshold for each layer
85
+ // Each layer represents 1/numLayers of the max value
86
+ const layerThreshold = maxValue / numLayers
87
+
88
+ // Create a y-scale for positioning within the band
89
+ // The scale maps values 0-layerThreshold to the full bandHeight
90
+ // Each layer uses the full band height, creating overlay effect
91
+ const yScale = scaleLinear({
92
+ domain: [0, layerThreshold],
93
+ range: [bandHeight, 0],
94
+ clamp: true // Clamp values above threshold
95
+ })
96
+
97
+ // Render layers from bottom to top
98
+ // Each layer shows values from (layerIndex * threshold) to ((layerIndex + 1) * threshold)
99
+ const layers = []
100
+
101
+ for (let layerIndex = 0; layerIndex < numLayers; layerIndex++) {
102
+ const layerMin = layerIndex * layerThreshold
103
+
104
+ // Short-circuit: if this layer's minimum exceeds the series max,
105
+ // no remaining layers can have visible data
106
+ if (layerMin >= seriesMax) break
107
+
108
+ // Build layer data and track hasData in a single pass
109
+ let hasData = false
110
+ const layerData = processedData.map(d => {
111
+ const rawValue = d[seriesKey]
112
+ // Calculate the value relative to this layer's base
113
+ const layerValue = Math.max(0, rawValue - layerMin)
114
+ // Clamp to the layer threshold
115
+ const clampedValue = Math.min(layerValue, layerThreshold)
116
+
117
+ if (clampedValue > 0) hasData = true
118
+
119
+ return {
120
+ x: d[xAxisKey],
121
+ y: clampedValue
122
+ }
123
+ })
124
+
125
+ if (!hasData) continue
126
+
127
+ // Get color for this layer from the distributed layer colors
128
+ const layerColor = layerColors[layerIndex]
129
+
130
+ layers.push(
131
+ <Group key={`layer-${layerIndex}`} top={0}>
132
+ <AreaClosed
133
+ data={layerData}
134
+ x={d => getXPosition(d.x)}
135
+ y={d => yScale(d.y)}
136
+ yScale={yScale}
137
+ curve={curveType}
138
+ fill={layerColor}
139
+ fillOpacity={1}
140
+ stroke='none'
141
+ />
142
+ </Group>
143
+ )
144
+ }
145
+
146
+ return (
147
+ <Group className='horizon-band'>
148
+ {/* Clip to band bounds */}
149
+ <defs>
150
+ <clipPath id={clipId}>
151
+ <rect x={0} y={0} width={xMax} height={bandHeight} />
152
+ </clipPath>
153
+ </defs>
154
+
155
+ <Group clipPath={`url(#${clipId})`}>{layers}</Group>
156
+ </Group>
157
+ )
158
+ }
159
+
160
+ export default memo(HorizonBand)
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Calculates the band dimensions for horizon chart rows
3
+ * Used by both HorizonChart (for rendering) and LeftAxis (for label positioning)
4
+ */
5
+
6
+ const MIN_BAND_HEIGHT = 10
7
+
8
+ export type HorizonBandCalculation = {
9
+ bandHeight: number
10
+ getRowY: (index: number) => number
11
+ }
12
+
13
+ export function calculateHorizonBands(
14
+ numSeries: number,
15
+ yMax: number,
16
+ bandGap: number | string,
17
+ bottomPadding: number | string = 15
18
+ ): HorizonBandCalculation {
19
+ const gap = Number(bandGap) || 0
20
+ const padding = Number(bottomPadding) || 0
21
+
22
+ const totalGapSpace = (numSeries - 1) * gap + padding
23
+ const bandHeight = Math.max((yMax - totalGapSpace) / numSeries, MIN_BAND_HEIGHT)
24
+ const getRowY = (index: number) => index * (bandHeight + gap)
25
+
26
+ return { bandHeight, getRowY }
27
+ }
@@ -0,0 +1,40 @@
1
+ import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
2
+ import { v2ColorDistribution } from '@cdc/core/helpers/palettes/colorDistributions'
3
+
4
+ /**
5
+ * Calculates the layer colors for a horizon chart based on palette configuration.
6
+ * Shared between HorizonBand rendering and Legend display.
7
+ */
8
+ export const getHorizonLayerColors = (config: any, numLayers: number): string[] => {
9
+ const paletteName = config.general?.palette?.name || 'sequential_blue'
10
+ const colorPalettes = filterChartColorPalettes(config)
11
+ const fullPalette = colorPalettes[paletteName] || Object.values(colorPalettes)[0] || ['#4292c6']
12
+
13
+ // Use v2ColorDistribution if we have a 9-color palette and numLayers <= 9
14
+ if (fullPalette.length === 9 && numLayers <= 9 && v2ColorDistribution[numLayers]) {
15
+ const indices = v2ColorDistribution[numLayers]
16
+ return indices.map((i: number) => fullPalette[i])
17
+ }
18
+
19
+ // Fallback: take first numLayers colors, or repeat if needed
20
+ return Array.from({ length: numLayers }, (_, i) => fullPalette[i % fullPalette.length])
21
+ }
22
+
23
+ /**
24
+ * Calculates the max value across all series in a horizon chart dataset.
25
+ * Used for consistent scaling across all bands.
26
+ */
27
+ export const getHorizonMaxValue = (data: any[], seriesKeys: string[]): number => {
28
+ if (!data || data.length === 0 || !seriesKeys || seriesKeys.length === 0) {
29
+ return 0
30
+ }
31
+
32
+ let max = 0
33
+ for (const row of data) {
34
+ for (const key of seriesKeys) {
35
+ const value = Math.abs(Number(row[key]) || 0)
36
+ if (value > max) max = value
37
+ }
38
+ }
39
+ return max
40
+ }
@@ -0,0 +1,3 @@
1
+ import HorizonChart from './HorizonChart'
2
+
3
+ export { HorizonChart }