@cdc/chart 4.26.5 → 4.26.6

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 (91) hide show
  1. package/CONFIG.md +12 -8
  2. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  3. package/dist/cdcchart.js +34093 -32767
  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/package.json +3 -3
  10. package/src/CdcChart.tsx +5 -4
  11. package/src/CdcChartComponent.tsx +32 -9
  12. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  13. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  14. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  15. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  16. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  17. package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
  18. package/src/_stories/_mock/heatmap.json +1 -1
  19. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  20. package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
  21. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  22. package/src/components/Axis/LeftAxis.tsx +3 -0
  23. package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
  24. package/src/components/Axis/RightAxis.tsx +3 -0
  25. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  26. package/src/components/EditorPanel/EditorPanel.tsx +367 -136
  27. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
  28. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  29. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  30. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  31. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
  32. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  33. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  34. package/src/components/EditorPanel/components/panels.scss +10 -0
  35. package/src/components/EditorPanel/editor-panel.scss +47 -0
  36. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
  37. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
  38. package/src/components/EditorPanel/useEditorPermissions.test.tsx +256 -11
  39. package/src/components/EditorPanel/useEditorPermissions.ts +41 -5
  40. package/src/components/HeatMap/components/HeatMap.tsx +22 -17
  41. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
  42. package/src/components/HeatMap/heatmap.css +15 -1
  43. package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
  44. package/src/components/Legend/Legend.Component.tsx +2 -2
  45. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  46. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  47. package/src/components/LineChart/helpers.test.ts +230 -1
  48. package/src/components/LineChart/helpers.ts +26 -13
  49. package/src/components/LineChart/index.tsx +21 -4
  50. package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
  51. package/src/components/LinearChart.tsx +50 -15
  52. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  53. package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
  54. package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
  55. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  56. package/src/data/initial-state.js +1 -0
  57. package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
  58. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  59. package/src/helpers/dataHelpers.ts +11 -1
  60. package/src/helpers/getAxisMaxOverride.ts +16 -0
  61. package/src/helpers/getCleanTopTickMax.ts +64 -0
  62. package/src/helpers/getColorScale.ts +18 -9
  63. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  64. package/src/helpers/getMinMax.ts +24 -22
  65. package/src/helpers/getYAxisDomainData.ts +38 -0
  66. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  67. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  68. package/src/helpers/seriesColumnSettings.ts +5 -4
  69. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  70. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  71. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  72. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  73. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  74. package/src/helpers/tests/getColorScale.test.ts +197 -0
  75. package/src/helpers/tests/getMinMax.test.ts +211 -0
  76. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  77. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  78. package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
  79. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  80. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  81. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  82. package/src/hooks/tests/useScales.test.tsx +307 -29
  83. package/src/hooks/useRightAxis.ts +30 -13
  84. package/src/hooks/useScales.ts +45 -28
  85. package/src/hooks/useTooltip.tsx +3 -1
  86. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  87. package/src/types/ChartConfig.ts +6 -1
  88. package/src/types/ChartContext.ts +3 -0
  89. package/src/helpers/getYAxisAutoPadding.ts +0 -55
  90. package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
  91. 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,234 @@ 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 filter-domain behavior when a standalone chart has visible filters and automatic max', () => {
103
+ const { result } = renderUseEditorPermissions({
104
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
105
+ yAxis: {
106
+ ...createMockConfig().yAxis,
107
+ max: '',
108
+ type: 'linear'
109
+ }
110
+ })
111
+
112
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(true)
113
+ })
114
+
115
+ it('shows filter-domain behavior for horizontal bar charts when automatic max can respond to filters', () => {
116
+ const { result } = renderUseEditorPermissions({
117
+ visualizationType: 'Bar',
118
+ orientation: 'horizontal',
119
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
120
+ yAxis: {
121
+ ...createMockConfig().yAxis,
122
+ max: '',
123
+ type: 'linear'
124
+ }
125
+ })
126
+
127
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(true)
128
+ })
129
+
130
+ it('shows filter-domain behavior for dashboard charts without local filters', () => {
131
+ const { result } = renderUseEditorPermissions(
132
+ {
133
+ filters: [],
134
+ yAxis: {
135
+ ...createMockConfig().yAxis,
136
+ max: '',
137
+ type: 'linear'
138
+ }
139
+ },
140
+ { isDashboard: true }
141
+ )
142
+
143
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(true)
144
+ })
145
+
146
+ it('hides filter-domain behavior when vertical value-axis max is explicit', () => {
147
+ const { result } = renderUseEditorPermissions(
148
+ {
149
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
150
+ yAxis: {
151
+ ...createMockConfig().yAxis,
152
+ max: '100',
153
+ type: 'linear'
154
+ }
155
+ },
156
+ { isDashboard: true }
157
+ )
158
+
159
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
160
+ })
161
+
162
+ it('hides filter-domain behavior for horizontal charts when value-axis max is explicit', () => {
163
+ const { result } = renderUseEditorPermissions(
164
+ {
165
+ visualizationType: 'Bar',
166
+ orientation: 'horizontal',
167
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
168
+ xAxis: {
169
+ ...createMockConfig().xAxis,
170
+ max: '100'
171
+ },
172
+ yAxis: {
173
+ ...createMockConfig().yAxis,
174
+ max: '',
175
+ type: 'linear'
176
+ }
177
+ },
178
+ { isDashboard: true }
179
+ )
180
+
181
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
182
+ })
183
+
184
+ it('hides filter-domain behavior for unfiltered standalone charts', () => {
185
+ const { result } = renderUseEditorPermissions({
186
+ filters: [],
187
+ yAxis: {
188
+ ...createMockConfig().yAxis,
189
+ max: '',
190
+ type: 'linear'
191
+ }
192
+ })
193
+
194
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
195
+ })
196
+
197
+ it('hides filter-domain behavior for charts outside the value-axis allowlist', () => {
198
+ const { result } = renderUseEditorPermissions(
199
+ {
200
+ visualizationType: 'Bump Chart',
201
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
202
+ yAxis: {
203
+ ...createMockConfig().yAxis,
204
+ max: '',
205
+ type: 'linear'
206
+ }
207
+ },
208
+ { isDashboard: true }
209
+ )
210
+
211
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
212
+ })
213
+
214
+ it('hides filter-domain behavior for chart types with specialized value scales', () => {
215
+ const { result } = renderUseEditorPermissions(
216
+ {
217
+ visualizationType: 'Paired Bar',
218
+ filters: [{ columnName: 'Region', showDropdown: true } as any],
219
+ yAxis: {
220
+ ...createMockConfig().yAxis,
221
+ max: '',
222
+ type: 'linear'
223
+ }
224
+ },
225
+ { isDashboard: true }
226
+ )
227
+
228
+ expect(result.current.visSupportsFilterDomainBehavior()).toBe(false)
229
+ })
230
+
231
+ it('shows automatic max strategy for automatic numeric value axes', () => {
232
+ const { result } = renderUseEditorPermissions({
233
+ yAxis: {
234
+ ...createMockConfig().yAxis,
235
+ max: '',
236
+ type: 'linear'
237
+ }
238
+ })
239
+
240
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(true)
241
+ })
242
+
243
+ it('shows automatic max strategy for horizontal charts with automatic value axes', () => {
244
+ const { result } = renderUseEditorPermissions({
245
+ visualizationType: 'Bar',
246
+ orientation: 'horizontal',
247
+ xAxis: {
248
+ ...createMockConfig().xAxis,
249
+ max: ''
250
+ },
251
+ yAxis: {
252
+ ...createMockConfig().yAxis,
253
+ max: '',
254
+ type: 'linear'
255
+ }
256
+ })
257
+
258
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(true)
259
+ })
260
+
261
+ it('hides automatic max strategy when vertical value-axis max is explicit', () => {
262
+ const { result } = renderUseEditorPermissions({
263
+ yAxis: {
264
+ ...createMockConfig().yAxis,
265
+ max: '100',
266
+ type: 'linear'
267
+ }
268
+ })
269
+
270
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
271
+ })
272
+
273
+ it('hides automatic max strategy for horizontal charts when value-axis max is explicit', () => {
274
+ const { result } = renderUseEditorPermissions({
275
+ visualizationType: 'Bar',
276
+ orientation: 'horizontal',
277
+ xAxis: {
278
+ ...createMockConfig().xAxis,
279
+ max: '100'
280
+ },
281
+ yAxis: {
282
+ ...createMockConfig().yAxis,
283
+ max: '',
284
+ type: 'linear'
285
+ }
286
+ })
287
+
288
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
289
+ })
290
+
291
+ it('hides automatic max strategy for categorical value axes', () => {
292
+ const { result } = renderUseEditorPermissions({
293
+ yAxis: {
294
+ ...createMockConfig().yAxis,
295
+ max: '',
296
+ type: 'categorical'
297
+ }
298
+ })
299
+
300
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
301
+ })
302
+
303
+ it('hides automatic max strategy for chart types with specialized value scales', () => {
304
+ const { result } = renderUseEditorPermissions({
305
+ visualizationType: 'Paired Bar',
306
+ yAxis: {
307
+ ...createMockConfig().yAxis,
308
+ max: '',
309
+ type: 'linear'
310
+ }
311
+ })
312
+
313
+ expect(result.current.visSupportsAutoMaxStrategy()).toBe(false)
314
+ })
70
315
  })
@@ -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
@@ -254,13 +257,13 @@ export const useEditorPermissions = () => {
254
257
  }
255
258
 
256
259
  const visSupportsDateCategoryAxisLine = () => {
257
- const disabledCharts = ['Forest Plot', 'HeatMap', 'Spark Line', 'Warming Stripes']
260
+ const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
258
261
  if (disabledCharts.includes(visualizationType)) return false
259
262
  return true
260
263
  }
261
264
 
262
265
  const visSupportsDateCategoryAxisTicks = () => {
263
- const disabledCharts = ['Forest Plot', 'HeatMap', 'Spark Line', 'Warming Stripes']
266
+ const disabledCharts = ['Forest Plot', 'Spark Line', 'Warming Stripes']
264
267
  if (disabledCharts.includes(visualizationType)) return false
265
268
  return true
266
269
  }
@@ -317,6 +320,27 @@ export const useEditorPermissions = () => {
317
320
  return true
318
321
  }
319
322
 
323
+ const hasExplicitValueAxisMax = () => {
324
+ const valueAxisMax = orientation === 'horizontal' ? config.xAxis?.max : config.yAxis?.max
325
+ return valueAxisMax !== undefined && valueAxisMax !== null && valueAxisMax !== ''
326
+ }
327
+
328
+ const visSupportsAutomaticValueDomain = () => {
329
+ const enabledCharts = ['Area Chart', 'Bar', 'Combo', 'Deviation Bar', 'Forecasting', 'Line', 'Scatter Plot']
330
+ if (!enabledCharts.includes(visualizationType)) return false
331
+ if (config.yAxis?.type === 'categorical') return false
332
+ if (hasExplicitValueAxisMax()) return false
333
+ return true
334
+ }
335
+
336
+ const visSupportsAutoMaxStrategy = () => {
337
+ return visSupportsAutomaticValueDomain()
338
+ }
339
+
340
+ const visSupportsFilterDomainBehavior = () => {
341
+ return visSupportsAutomaticValueDomain() && Boolean(isDashboard || hasVisibleVizFilters(config.filters))
342
+ }
343
+
320
344
  const visSupportsValueAxisGridLines = () => {
321
345
  const disabledCharts = ['Forest Plot', 'HeatMap']
322
346
  if (disabledCharts.includes(visualizationType)) return false
@@ -426,8 +450,17 @@ export const useEditorPermissions = () => {
426
450
  return false
427
451
  }
428
452
 
453
+ const visSupportsSeriesColorAssignments = () => supportsSeriesColorAssignments(config)
454
+
429
455
  const visSupportsYPadding = () => {
430
- return getYAxisAutoPaddingMode(config) === 'none'
456
+ const { shouldUseInlineLabelHeadroom } = getYAxisFinalizationEligibility({
457
+ config,
458
+ hasSpacedInlineLabel: hasSpacedInlineLabel(config),
459
+ hasValidExplicitLeftMax: false,
460
+ isHorizontal: orientation === 'horizontal'
461
+ })
462
+
463
+ return !shouldUseInlineLabelHeadroom
431
464
  }
432
465
 
433
466
  const visHasSingleSeriesTooltip = () => {
@@ -486,6 +519,8 @@ export const useEditorPermissions = () => {
486
519
  visSupportsDateCategoryTickRotation,
487
520
  visSupportsDynamicSeries,
488
521
  visSupportsFilters,
522
+ visSupportsAutoMaxStrategy,
523
+ visSupportsFilterDomainBehavior,
489
524
  visSupportsFootnotes,
490
525
  visSupportsLeftValueAxis,
491
526
  visSupportsMobileChartHeight,
@@ -497,6 +532,7 @@ export const useEditorPermissions = () => {
497
532
  visSupportsResponsiveTicks,
498
533
  visSupportsReverseColorPalette,
499
534
  visSupportsSequentialPallete,
535
+ visSupportsSeriesColorAssignments,
500
536
  visSupportsSmallMultiples,
501
537
  visSupportsSuperTitle,
502
538
  visSupportsTooltipLines,
@@ -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;