@cdc/chart 4.26.5 → 4.26.7

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 (128) hide show
  1. package/CONFIG.md +20 -14
  2. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  3. package/dist/cdcchart.js +36554 -34320
  4. package/examples/feature/heatmap/average-age-categorical.json +1 -1
  5. package/examples/feature/heatmap/calendar-heatmap.json +1 -1
  6. package/examples/feature/heatmap/sparse-aggregation.json +1 -1
  7. package/examples/private/bad-y-axis.json +543 -0
  8. package/examples/private/bar-chart-negative-value.json +455 -0
  9. package/examples/private/flueview.json +22051 -0
  10. package/package.json +3 -11
  11. package/src/CdcChart.tsx +7 -6
  12. package/src/CdcChartComponent.tsx +71 -18
  13. package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
  14. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  15. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  16. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  17. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  18. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  19. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  20. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  21. package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
  22. package/src/_stories/_mock/heatmap.json +1 -1
  23. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  24. package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
  25. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  26. package/src/components/Axis/LeftAxis.tsx +3 -0
  27. package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
  28. package/src/components/Axis/RightAxis.tsx +3 -0
  29. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  30. package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -2
  31. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  32. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
  33. package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
  34. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  35. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  36. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
  37. package/src/components/BarChart/helpers/useBarChart.ts +4 -5
  38. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  39. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  40. package/src/components/BoxPlot/helpers/index.ts +4 -3
  41. package/src/components/Brush/BrushSelector.tsx +1 -1
  42. package/src/components/EditorPanel/EditorPanel.tsx +457 -165
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
  47. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  48. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  49. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
  51. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  52. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  53. package/src/components/EditorPanel/components/panels.scss +10 -0
  54. package/src/components/EditorPanel/editor-panel.scss +47 -0
  55. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
  56. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
  57. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
  58. package/src/components/EditorPanel/useEditorPermissions.test.tsx +288 -11
  59. package/src/components/EditorPanel/useEditorPermissions.ts +46 -6
  60. package/src/components/Forecasting/Forecasting.tsx +1 -1
  61. package/src/components/HeatMap/components/HeatMap.tsx +22 -17
  62. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
  63. package/src/components/HeatMap/heatmap.css +15 -1
  64. package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
  65. package/src/components/Legend/Legend.Component.tsx +56 -61
  66. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  67. package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
  68. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  69. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  70. package/src/components/LineChart/helpers.test.ts +230 -1
  71. package/src/components/LineChart/helpers.ts +25 -14
  72. package/src/components/LineChart/index.tsx +23 -7
  73. package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
  74. package/src/components/LinearChart.tsx +51 -15
  75. package/src/components/PieChart/PieChart.tsx +117 -10
  76. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  77. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  78. package/src/components/ScatterPlot/ScatterPlot.jsx +5 -13
  79. package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
  80. package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
  81. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  82. package/src/data/initial-state.js +3 -0
  83. package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
  84. package/src/helpers/categoryOrder.ts +60 -0
  85. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  86. package/src/helpers/dataHelpers.ts +11 -1
  87. package/src/helpers/getAxisMaxOverride.ts +16 -0
  88. package/src/helpers/getBoxPlotConfig.ts +19 -12
  89. package/src/helpers/getCleanTopTickMax.ts +64 -0
  90. package/src/helpers/getColorScale.ts +18 -9
  91. package/src/helpers/getComboChartConfig.ts +0 -1
  92. package/src/helpers/getExcludedData.ts +0 -1
  93. package/src/helpers/getHasBoundarySuppression.ts +27 -0
  94. package/src/helpers/getMinMax.ts +25 -22
  95. package/src/helpers/getNewRuntime.ts +0 -2
  96. package/src/helpers/getPiePercent.ts +3 -3
  97. package/src/helpers/getYAxisDomainData.ts +38 -0
  98. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  99. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  100. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  101. package/src/helpers/seriesColumnSettings.ts +26 -7
  102. package/src/helpers/sizeHelpers.ts +0 -2
  103. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  104. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  105. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  106. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  107. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  108. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  109. package/src/helpers/tests/getColorScale.test.ts +197 -0
  110. package/src/helpers/tests/getMinMax.test.ts +211 -0
  111. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  112. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  113. package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
  114. package/src/helpers/tests/seriesColumnSettings.test.ts +76 -2
  115. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  116. package/src/hooks/tests/useRightAxis.test.tsx +268 -0
  117. package/src/hooks/tests/useScales.test.tsx +302 -27
  118. package/src/hooks/tests/useTooltip.test.tsx +132 -0
  119. package/src/hooks/useRightAxis.ts +35 -25
  120. package/src/hooks/useScales.ts +45 -28
  121. package/src/hooks/useTooltip.tsx +17 -11
  122. package/src/store/chart.reducer.ts +0 -1
  123. package/src/test/CdcChart.dataTable.test.tsx +216 -0
  124. package/src/types/ChartConfig.ts +6 -1
  125. package/src/types/ChartContext.ts +3 -0
  126. package/src/helpers/getYAxisAutoPadding.ts +0 -55
  127. package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
  128. package/src/helpers/tests/getYAxisAutoPadding.test.ts +0 -37
@@ -6,9 +6,12 @@ import { useEditorPermissions } from './useEditorPermissions'
6
6
  import { createMockChartContext, createMockConfig } from '../LinearChart/tests/mockConfigContext'
7
7
  import { ChartConfig } from '../../types/ChartConfig'
8
8
 
9
- const renderUseEditorPermissions = (configOverrides: Partial<ChartConfig>) => {
9
+ const renderUseEditorPermissions = (
10
+ configOverrides: Partial<ChartConfig>,
11
+ contextOverrides: Partial<ReturnType<typeof createMockChartContext>> = {}
12
+ ) => {
10
13
  const config = createMockConfig(configOverrides)
11
- const context = createMockChartContext(config)
14
+ const context = createMockChartContext(config, contextOverrides)
12
15
  const wrapper = ({ children }: React.PropsWithChildren) => (
13
16
  <ConfigContext.Provider value={context}>{children}</ConfigContext.Provider>
14
17
  )
@@ -17,7 +20,7 @@ const renderUseEditorPermissions = (configOverrides: Partial<ChartConfig>) => {
17
20
  }
18
21
 
19
22
  describe('useEditorPermissions', () => {
20
- it('shows manual y-axis padding controls when auto-padding mode is none', () => {
23
+ it('shows manual y-axis padding controls when there is no spaced inline label', () => {
21
24
  const { result } = renderUseEditorPermissions({
22
25
  yAxis: {
23
26
  ...createMockConfig().yAxis,
@@ -29,7 +32,7 @@ describe('useEditorPermissions', () => {
29
32
  expect(result.current.visSupportsYPadding()).toBe(true)
30
33
  })
31
34
 
32
- it('hides manual y-axis padding controls for inline-label auto-padding', () => {
35
+ it('hides manual y-axis padding controls for spaced inline labels when renderer headroom applies', () => {
33
36
  const { result } = renderUseEditorPermissions({
34
37
  yAxis: {
35
38
  ...createMockConfig().yAxis,
@@ -41,22 +44,34 @@ describe('useEditorPermissions', () => {
41
44
  expect(result.current.visSupportsYPadding()).toBe(false)
42
45
  })
43
46
 
44
- it('hides manual y-axis padding controls for top-title auto-padding', () => {
47
+ it('shows manual y-axis padding controls for horizontal charts with spaced inline labels', () => {
45
48
  const { result } = renderUseEditorPermissions({
49
+ orientation: 'horizontal',
46
50
  yAxis: {
47
51
  ...createMockConfig().yAxis,
48
- titlePlacement: 'top',
49
- label: 'Y-Axis',
50
- inlineLabel: ''
52
+ titlePlacement: 'side',
53
+ inlineLabel: 'per 100k'
51
54
  }
52
55
  })
53
56
 
54
- expect(result.current.visSupportsYPadding()).toBe(false)
57
+ expect(result.current.visSupportsYPadding()).toBe(true)
58
+ })
59
+
60
+ it('shows manual y-axis padding controls for Combo charts with spaced inline labels', () => {
61
+ const { result } = renderUseEditorPermissions({
62
+ visualizationType: 'Combo',
63
+ yAxis: {
64
+ ...createMockConfig().yAxis,
65
+ titlePlacement: 'side',
66
+ inlineLabel: 'per 100k'
67
+ }
68
+ })
69
+
70
+ expect(result.current.visSupportsYPadding()).toBe(true)
55
71
  })
56
72
 
57
- it('shows manual y-axis padding controls when a top y-axis title is hidden', () => {
73
+ it('shows manual y-axis padding controls for top y-axis titles', () => {
58
74
  const { result } = renderUseEditorPermissions({
59
- hideYAxisLabel: true,
60
75
  yAxis: {
61
76
  ...createMockConfig().yAxis,
62
77
  titlePlacement: 'top',
@@ -67,4 +82,266 @@ describe('useEditorPermissions', () => {
67
82
 
68
83
  expect(result.current.visSupportsYPadding()).toBe(true)
69
84
  })
85
+
86
+ it('supports series color assignments only for eligible authored-series charts', () => {
87
+ const supported = renderUseEditorPermissions({
88
+ visualizationType: 'Line',
89
+ series: [{ dataKey: 'cases', type: 'Line' }] as any
90
+ })
91
+
92
+ expect(supported.result.current.visSupportsSeriesColorAssignments()).toBe(true)
93
+
94
+ const unsupported = renderUseEditorPermissions({
95
+ visualizationType: 'Pie',
96
+ series: [{ dataKey: 'cases', type: 'Pie' }] as any
97
+ })
98
+
99
+ expect(unsupported.result.current.visSupportsSeriesColorAssignments()).toBe(false)
100
+ })
101
+
102
+ it('shows the data-label control for pie charts', () => {
103
+ const { result } = renderUseEditorPermissions({
104
+ visualizationType: 'Pie'
105
+ })
106
+
107
+ expect(result.current.visHasLabelOnData()).toBe(true)
108
+ })
109
+
110
+ it('shows right value axis controls for vertical Combo charts without requiring a right-axis series', () => {
111
+ const { result } = renderUseEditorPermissions({
112
+ visualizationType: 'Combo',
113
+ orientation: 'vertical',
114
+ series: [{ dataKey: 'Cases', type: 'Bar', axis: 'Left' }] as any
115
+ })
116
+
117
+ expect(result.current.visSupportsRightValueAxis()).toBe(true)
118
+ })
119
+
120
+ it('hides right value axis controls outside vertical Combo charts', () => {
121
+ const horizontalCombo = renderUseEditorPermissions({
122
+ visualizationType: 'Combo',
123
+ orientation: 'horizontal'
124
+ })
125
+ const verticalBar = renderUseEditorPermissions({
126
+ visualizationType: 'Bar',
127
+ orientation: 'vertical'
128
+ })
129
+
130
+ expect(horizontalCombo.result.current.visSupportsRightValueAxis()).toBe(false)
131
+ expect(verticalBar.result.current.visSupportsRightValueAxis()).toBe(false)
132
+ })
133
+
134
+ it('shows filter-domain behavior when a standalone chart has visible filters and automatic max', () => {
135
+ const { result } = renderUseEditorPermissions({
136
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
137
+ yAxis: {
138
+ ...createMockConfig().yAxis,
139
+ max: '',
140
+ type: 'linear'
141
+ }
142
+ })
143
+
144
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(true)
145
+ })
146
+
147
+ it('shows filter-domain behavior for horizontal bar charts when automatic max can respond to filters', () => {
148
+ const { result } = renderUseEditorPermissions({
149
+ visualizationType: 'Bar',
150
+ orientation: 'horizontal',
151
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
152
+ yAxis: {
153
+ ...createMockConfig().yAxis,
154
+ max: '',
155
+ type: 'linear'
156
+ }
157
+ })
158
+
159
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(true)
160
+ })
161
+
162
+ it('shows filter-domain behavior for dashboard charts without local filters', () => {
163
+ const { result } = renderUseEditorPermissions(
164
+ {
165
+ filters: [],
166
+ yAxis: {
167
+ ...createMockConfig().yAxis,
168
+ max: '',
169
+ type: 'linear'
170
+ }
171
+ },
172
+ { isDashboard: true }
173
+ )
174
+
175
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(true)
176
+ })
177
+
178
+ it('hides filter-domain behavior when vertical value-axis max is explicit', () => {
179
+ const { result } = renderUseEditorPermissions(
180
+ {
181
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
182
+ yAxis: {
183
+ ...createMockConfig().yAxis,
184
+ max: '100',
185
+ type: 'linear'
186
+ }
187
+ },
188
+ { isDashboard: true }
189
+ )
190
+
191
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
192
+ })
193
+
194
+ it('hides filter-domain behavior for horizontal charts when value-axis max is explicit', () => {
195
+ const { result } = renderUseEditorPermissions(
196
+ {
197
+ visualizationType: 'Bar',
198
+ orientation: 'horizontal',
199
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
200
+ xAxis: {
201
+ ...createMockConfig().xAxis,
202
+ max: '100'
203
+ },
204
+ yAxis: {
205
+ ...createMockConfig().yAxis,
206
+ max: '',
207
+ type: 'linear'
208
+ }
209
+ },
210
+ { isDashboard: true }
211
+ )
212
+
213
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
214
+ })
215
+
216
+ it('hides filter-domain behavior for unfiltered standalone charts', () => {
217
+ const { result } = renderUseEditorPermissions({
218
+ filters: [],
219
+ yAxis: {
220
+ ...createMockConfig().yAxis,
221
+ max: '',
222
+ type: 'linear'
223
+ }
224
+ })
225
+
226
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
227
+ })
228
+
229
+ it('hides filter-domain behavior for charts outside the value-axis allowlist', () => {
230
+ const { result } = renderUseEditorPermissions(
231
+ {
232
+ visualizationType: 'Bump Chart',
233
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
234
+ yAxis: {
235
+ ...createMockConfig().yAxis,
236
+ max: '',
237
+ type: 'linear'
238
+ }
239
+ },
240
+ { isDashboard: true }
241
+ )
242
+
243
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
244
+ })
245
+
246
+ it('hides filter-domain behavior for chart types with specialized value scales', () => {
247
+ const { result } = renderUseEditorPermissions(
248
+ {
249
+ visualizationType: 'Paired Bar',
250
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
251
+ yAxis: {
252
+ ...createMockConfig().yAxis,
253
+ max: '',
254
+ type: 'linear'
255
+ }
256
+ },
257
+ { isDashboard: true }
258
+ )
259
+
260
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
261
+ })
262
+
263
+ it('shows automatic max strategy for automatic numeric value axes', () => {
264
+ const { result } = renderUseEditorPermissions({
265
+ yAxis: {
266
+ ...createMockConfig().yAxis,
267
+ max: '',
268
+ type: 'linear'
269
+ }
270
+ })
271
+
272
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(true)
273
+ })
274
+
275
+ it('shows automatic max strategy for horizontal charts with automatic value axes', () => {
276
+ const { result } = renderUseEditorPermissions({
277
+ visualizationType: 'Bar',
278
+ orientation: 'horizontal',
279
+ xAxis: {
280
+ ...createMockConfig().xAxis,
281
+ max: ''
282
+ },
283
+ yAxis: {
284
+ ...createMockConfig().yAxis,
285
+ max: '',
286
+ type: 'linear'
287
+ }
288
+ })
289
+
290
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(true)
291
+ })
292
+
293
+ it('hides automatic max strategy when vertical value-axis max is explicit', () => {
294
+ const { result } = renderUseEditorPermissions({
295
+ yAxis: {
296
+ ...createMockConfig().yAxis,
297
+ max: '100',
298
+ type: 'linear'
299
+ }
300
+ })
301
+
302
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
303
+ })
304
+
305
+ it('hides automatic max strategy for horizontal charts when value-axis max is explicit', () => {
306
+ const { result } = renderUseEditorPermissions({
307
+ visualizationType: 'Bar',
308
+ orientation: 'horizontal',
309
+ xAxis: {
310
+ ...createMockConfig().xAxis,
311
+ max: '100'
312
+ },
313
+ yAxis: {
314
+ ...createMockConfig().yAxis,
315
+ max: '',
316
+ type: 'linear'
317
+ }
318
+ })
319
+
320
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
321
+ })
322
+
323
+ it('hides automatic max strategy for categorical value axes', () => {
324
+ const { result } = renderUseEditorPermissions({
325
+ yAxis: {
326
+ ...createMockConfig().yAxis,
327
+ max: '',
328
+ type: 'categorical'
329
+ }
330
+ })
331
+
332
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
333
+ })
334
+
335
+ it('hides automatic max strategy for chart types with specialized value scales', () => {
336
+ const { result } = renderUseEditorPermissions({
337
+ visualizationType: 'Paired Bar',
338
+ yAxis: {
339
+ ...createMockConfig().yAxis,
340
+ max: '',
341
+ type: 'linear'
342
+ }
343
+ })
344
+
345
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
346
+ })
70
347
  })
@@ -1,9 +1,12 @@
1
1
  import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../ConfigContext'
3
- import { getYAxisAutoPaddingMode } from '../../helpers/getYAxisAutoPaddingMode'
3
+ import { getYAxisFinalizationEligibility } from '../../helpers/getYAxisFinalizationEligibility'
4
+ import { hasSpacedInlineLabel } from '../../helpers/hasSpacedInlineLabel'
5
+ import { supportsSeriesColorAssignments } from '../../helpers/colorAssignmentHelpers'
6
+ import { hasVisibleVizFilters } from '@cdc/core/helpers/filterVisibility'
4
7
 
5
8
  export const useEditorPermissions = () => {
6
- const { config } = useContext(ConfigContext)
9
+ const { config, isDashboard } = useContext(ConfigContext)
7
10
  const { visualizationType, series, orientation, visualizationSubType } = config
8
11
 
9
12
  // Overall support for the chart types
@@ -79,7 +82,6 @@ export const useEditorPermissions = () => {
79
82
  'Forest Plot',
80
83
  'HeatMap',
81
84
  'Horizon Chart',
82
- 'Pie',
83
85
  'Radar',
84
86
  'Sankey',
85
87
  'Scatter Plot',
@@ -254,13 +256,13 @@ export const useEditorPermissions = () => {
254
256
  }
255
257
 
256
258
  const visSupportsDateCategoryAxisLine = () => {
257
- const disabledCharts = ['Forest Plot', 'HeatMap', 'Spark Line', 'Warming Stripes']
259
+ const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
258
260
  if (disabledCharts.includes(visualizationType)) return false
259
261
  return true
260
262
  }
261
263
 
262
264
  const visSupportsDateCategoryAxisTicks = () => {
263
- const disabledCharts = ['Forest Plot', 'HeatMap', 'Spark Line', 'Warming Stripes']
265
+ const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
264
266
  if (disabledCharts.includes(visualizationType)) return false
265
267
  return true
266
268
  }
@@ -317,6 +319,27 @@ export const useEditorPermissions = () => {
317
319
  return true
318
320
  }
319
321
 
322
+ const hasExplicitValueAxisMax = () => {
323
+ const valueAxisMax = orientation === 'horizontal' ? config.xAxis?.max : config.yAxis?.max
324
+ return valueAxisMax !== undefined && valueAxisMax !== null && valueAxisMax !== ''
325
+ }
326
+
327
+ const visSupportsAutomaticValueDomain = () => {
328
+ const enabledCharts = ['Area Chart', 'Bar', 'Combo', 'Deviation Bar', 'Forecasting', 'Line', 'Scatter Plot']
329
+ if (!enabledCharts.includes(visualizationType)) return false
330
+ if (config.yAxis?.type === 'categorical') return false
331
+ if (hasExplicitValueAxisMax()) return false
332
+ return true
333
+ }
334
+
335
+ const visSupportsAutoMaxStrategy = () => {
336
+ return visSupportsAutomaticValueDomain()
337
+ }
338
+
339
+ const visSupportsFilterDomainBehavior = () => {
340
+ return visSupportsAutomaticValueDomain() && Boolean(isDashboard || hasVisibleVizFilters(config.filters))
341
+ }
342
+
320
343
  const visSupportsValueAxisGridLines = () => {
321
344
  const disabledCharts = ['Forest Plot', 'HeatMap']
322
345
  if (disabledCharts.includes(visualizationType)) return false
@@ -375,6 +398,10 @@ export const useEditorPermissions = () => {
375
398
  return true
376
399
  }
377
400
 
401
+ const visSupportsRightValueAxis = () => {
402
+ return visualizationType === 'Combo' && orientation === 'vertical'
403
+ }
404
+
378
405
  const visSupportsRankByValue = () => {
379
406
  const disabledCharts = ['HeatMap', 'Spark Line']
380
407
  if (disabledCharts.includes(visualizationType)) return false
@@ -426,8 +453,17 @@ export const useEditorPermissions = () => {
426
453
  return false
427
454
  }
428
455
 
456
+ const visSupportsSeriesColorAssignments = () => supportsSeriesColorAssignments(config)
457
+
429
458
  const visSupportsYPadding = () => {
430
- return getYAxisAutoPaddingMode(config) === 'none'
459
+ const { shouldUseInlineLabelHeadroom } = getYAxisFinalizationEligibility({
460
+ config,
461
+ hasSpacedInlineLabel: hasSpacedInlineLabel(config),
462
+ hasValidExplicitLeftMax: false,
463
+ isHorizontal: orientation === 'horizontal'
464
+ })
465
+
466
+ return !shouldUseInlineLabelHeadroom
431
467
  }
432
468
 
433
469
  const visHasSingleSeriesTooltip = () => {
@@ -486,6 +522,8 @@ export const useEditorPermissions = () => {
486
522
  visSupportsDateCategoryTickRotation,
487
523
  visSupportsDynamicSeries,
488
524
  visSupportsFilters,
525
+ visSupportsAutoMaxStrategy,
526
+ visSupportsFilterDomainBehavior,
489
527
  visSupportsFootnotes,
490
528
  visSupportsLeftValueAxis,
491
529
  visSupportsMobileChartHeight,
@@ -495,8 +533,10 @@ export const useEditorPermissions = () => {
495
533
  visSupportsReactTooltip,
496
534
  visSupportsRegions,
497
535
  visSupportsResponsiveTicks,
536
+ visSupportsRightValueAxis,
498
537
  visSupportsReverseColorPalette,
499
538
  visSupportsSequentialPallete,
539
+ visSupportsSeriesColorAssignments,
500
540
  visSupportsSmallMultiples,
501
541
  visSupportsSuperTitle,
502
542
  visSupportsTooltipLines,
@@ -1,5 +1,5 @@
1
1
  import React, { useContext, useMemo } from 'react'
2
- import { replace } from 'lodash'
2
+ import replace from 'lodash/replace'
3
3
  // cdc
4
4
  import ConfigContext from '../../ConfigContext'
5
5
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
@@ -64,7 +64,7 @@ type HeatMapLayout = {
64
64
  const AXIS_TOP_WITH_TICKS = 36
65
65
  const AXIS_TOP_WITHOUT_TICKS = 24
66
66
  const AXIS_TOP_LABEL_SPACE = 28
67
- const AXIS_TICK_FONT_SIZE = 12
67
+ const AXIS_TICK_FONT_SIZE = 16
68
68
  const AXIS_TITLE_SPACE = 30
69
69
  const AXIS_TOP_TITLE_BASELINE = 18
70
70
  const X_AXIS_TITLE_LABEL_SPACE = 32
@@ -82,6 +82,7 @@ const EMPTY_VALUE_LABEL = 'No data'
82
82
  const MIN_CELL_VALUE_WIDTH = 18
83
83
  const MIN_CELL_VALUE_HEIGHT = 14
84
84
  const SIDE_Y_AXIS_TITLE_GAP = 20
85
+ const AXIS_TITLE_FONT_WEIGHT = 'bold'
85
86
 
86
87
  const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
87
88
 
@@ -129,7 +130,7 @@ const getXAxisTickLabelProps = (xAxisPosition: HeatMapXAxisPosition, xTickRotati
129
130
 
130
131
  if (!xTickRotation) {
131
132
  return {
132
- fontSize: 12,
133
+ fontSize: AXIS_TICK_FONT_SIZE,
133
134
  textAnchor: 'middle',
134
135
  angle: 0,
135
136
  dx: 0,
@@ -138,7 +139,7 @@ const getXAxisTickLabelProps = (xAxisPosition: HeatMapXAxisPosition, xTickRotati
138
139
  }
139
140
 
140
141
  return {
141
- fontSize: 12,
142
+ fontSize: AXIS_TICK_FONT_SIZE,
142
143
  textAnchor: xAxisPosition === 'top' ? 'start' : 'end',
143
144
  angle: xTickRotation,
144
145
  dx: isSteepRotation || xAxisPosition === 'top' ? 0 : '-0.5em',
@@ -313,20 +314,16 @@ const getTooltipColumns = (
313
314
  xDataKey: string | undefined,
314
315
  heatMapSeries: { dataKey: string }[]
315
316
  ): TooltipColumn[] =>
316
- Object.entries(config.columns || {}).reduce<TooltipColumn[]>((tooltipColumns, [_, value]) => {
317
- if (!value.tooltips || !value.name) return tooltipColumns
318
- if (value.name === xDataKey) return tooltipColumns
319
- if (heatMapSeries.some(series => series.dataKey === value.name)) return tooltipColumns
317
+ Object.entries(config.columns || {}).reduce<TooltipColumn[]>((tooltipColumns, [columnKey, value]) => {
318
+ const columnName = value.name || columnKey
319
+ if (!value.tooltips || !columnName) return tooltipColumns
320
+ if (columnName === xDataKey) return tooltipColumns
321
+ if (heatMapSeries.some(series => series.dataKey === columnName)) return tooltipColumns
320
322
 
321
323
  tooltipColumns.push({
322
- label: value.label || value.name,
323
- name: value.name,
324
- options: {
325
- addColPrefix: value.prefix,
326
- addColSuffix: value.suffix,
327
- addColRoundTo: value.roundToPlace,
328
- addColCommas: value.commas
329
- }
324
+ label: value.label || columnName,
325
+ name: columnName,
326
+ options: getSeriesColumnFormattingParams(value) || {}
330
327
  })
331
328
 
332
329
  return tooltipColumns
@@ -601,6 +598,7 @@ const HeatMap: React.FC<HeatMapProps> = ({ parentWidth, parentHeight }) => {
601
598
  x={xOffset + gridWidth / 2}
602
599
  y={xAxisPosition === 'top' ? -xAxisTitleDistance : gridHeight + xAxisTitleDistance}
603
600
  textAnchor='middle'
601
+ fontWeight={AXIS_TITLE_FONT_WEIGHT}
604
602
  >
605
603
  {xAxisLabel}
606
604
  </text>
@@ -611,7 +609,13 @@ const HeatMap: React.FC<HeatMapProps> = ({ parentWidth, parentHeight }) => {
611
609
 
612
610
  if (showTopYAxisTitle) {
613
611
  return (
614
- <text className='cdc-heatmap__axis-title' x={rowLabelTitleX} y={topYAxisTitleY} textAnchor='start'>
612
+ <text
613
+ className='cdc-heatmap__axis-title'
614
+ x={rowLabelTitleX}
615
+ y={topYAxisTitleY}
616
+ textAnchor='start'
617
+ fontWeight={AXIS_TITLE_FONT_WEIGHT}
618
+ >
615
619
  {yAxisLabel}
616
620
  </text>
617
621
  )
@@ -622,6 +626,7 @@ const HeatMap: React.FC<HeatMapProps> = ({ parentWidth, parentHeight }) => {
622
626
  className='cdc-heatmap__axis-title'
623
627
  transform={`translate(${sideYAxisTitleX}, ${yOffset + gridHeight / 2}) rotate(-90)`}
624
628
  textAnchor='middle'
629
+ fontWeight={AXIS_TITLE_FONT_WEIGHT}
625
630
  >
626
631
  {yAxisLabel}
627
632
  </text>
@@ -737,7 +742,7 @@ const HeatMap: React.FC<HeatMapProps> = ({ parentWidth, parentHeight }) => {
737
742
  hideAxisLine={Boolean(config.yAxis?.hideAxis)}
738
743
  hideTicks={Boolean(config.yAxis?.hideTicks)}
739
744
  tickLabelProps={() => ({
740
- fontSize: 12,
745
+ fontSize: AXIS_TICK_FONT_SIZE,
741
746
  textAnchor: 'end',
742
747
  angle: yTickRotation,
743
748
  dx: yTickRotation ? '-0.25em' : 0,
@@ -62,6 +62,10 @@ const HeatMapGradientLegend = () => {
62
62
  const ranges = getHeatMapDataGroupRanges(config, minValue, maxValue, formatNumber)
63
63
  const displayRanges = config.legend?.reverseLabelOrder ? [...ranges].reverse() : ranges
64
64
  const displayPalette = config.legend?.reverseLabelOrder ? [...palette].reverse() : palette
65
+ const displayLegendItems = displayRanges.map((range, index) => ({
66
+ color: displayPalette[index % displayPalette.length],
67
+ range
68
+ }))
65
69
 
66
70
  if (config.legend?.hide) return null
67
71
 
@@ -72,34 +76,44 @@ const HeatMapGradientLegend = () => {
72
76
 
73
77
  {isGradientLegend ? (
74
78
  <div className='cdc-heatmap__legend-scale'>
75
- <svg className='cdc-heatmap__legend-svg' height='50' width='100%'>
79
+ <svg className='cdc-heatmap__legend-svg' height='25' width='100%'>
76
80
  <rect x='0' y='0' width='100%' height='25' fill='#d3d3d3' />
77
- {displayPalette.map((color, index) => (
81
+ {displayLegendItems.map(({ color, range }, index) => (
78
82
  <rect
79
- key={`${color}-${index}`}
80
- x={`${(index / displayPalette.length) * 100}%`}
83
+ key={`heatmap-gradient-block-${range.min}-${range.max}-${color}`}
84
+ x={`${(index / displayLegendItems.length) * 100}%`}
81
85
  y='1'
82
- width={`${100 / displayPalette.length}%`}
86
+ width={`${100 / displayLegendItems.length}%`}
83
87
  height='23'
84
88
  fill={color}
85
89
  />
86
90
  ))}
87
-
88
- <text x='0' y='40' fontSize='14' textAnchor='start' fill='#333'>
89
- {displayRanges[0]?.label.split('\u2013')[0]}
90
- </text>
91
- <text x='100%' y='40' fontSize='14' textAnchor='end' fill='#333'>
92
- {displayRanges[displayRanges.length - 1]?.label.split('\u2013').pop()}
93
- </text>
94
91
  </svg>
92
+ <div
93
+ className='cdc-heatmap__legend-range-labels'
94
+ style={{ gridTemplateColumns: `repeat(${Math.max(displayLegendItems.length, 1)}, minmax(0, 1fr))` }}
95
+ >
96
+ {displayLegendItems.map(({ range }) => (
97
+ <span
98
+ className='cdc-heatmap__legend-range-label'
99
+ key={`heatmap-gradient-label-${range.min}-${range.max}`}
100
+ >
101
+ {range.label}
102
+ </span>
103
+ ))}
104
+ </div>
95
105
 
96
106
  {valueLabel && <div className='cdc-heatmap__legend-series-label'>{valueLabel}</div>}
97
107
  </div>
98
108
  ) : (
99
109
  <div className={innerClasses.join(' ')}>
100
- {displayRanges.map((range, index) => (
101
- <LegendItem className='legend-item not-clickable' key={`heatmap-legend-range-${index}`} tabIndex={-1}>
102
- <LegendShape shape={shape} fill={displayPalette[index % displayPalette.length]} />
110
+ {displayLegendItems.map(({ color, range }) => (
111
+ <LegendItem
112
+ className='legend-item not-clickable'
113
+ key={`heatmap-legend-range-${range.min}-${range.max}`}
114
+ tabIndex={-1}
115
+ >
116
+ <LegendShape shape={shape} fill={color} />
103
117
  <LegendLabel align='left' className='m-0'>
104
118
  {range.label}
105
119
  </LegendLabel>
@@ -3,7 +3,7 @@
3
3
  --cove-heatmap-cell-stroke-color: rgba(255, 255, 255, 0.7);
4
4
  --cove-heatmap-cell-stroke-width: 0.1px;
5
5
  --cove-heatmap-axis-title-color: var(--darkGray, #333);
6
- --cove-heatmap-axis-title-font-size: 14px;
6
+ --cove-heatmap-axis-title-font-size: 18px;
7
7
 
8
8
  width: 100%;
9
9
  }
@@ -63,6 +63,20 @@
63
63
  overflow: visible;
64
64
  }
65
65
 
66
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-range-labels {
67
+ color: #333;
68
+ display: grid;
69
+ font-size: 0.875rem;
70
+ line-height: 1.2;
71
+ margin-top: 0.35rem;
72
+ }
73
+
74
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-range-label {
75
+ min-width: 0;
76
+ overflow-wrap: anywhere;
77
+ text-align: center;
78
+ }
79
+
66
80
  .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-series-label {
67
81
  color: #333;
68
82
  font-weight: 600;