@cdc/chart 4.26.3 → 4.26.5

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 (192) hide show
  1. package/CONFIG.md +489 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39024 -36587
  4. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  5. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  6. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  7. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  8. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  9. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  10. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  11. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  12. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  13. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  14. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  15. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  16. package/examples/metadata-variables.json +1 -1
  17. package/examples/minimal-example.json +24 -0
  18. package/examples/private/bump-chart.json +1116 -0
  19. package/examples/private/test-123.json +624 -0
  20. package/examples/private/test2.json +35012 -0
  21. package/package.json +5 -4
  22. package/src/CdcChartComponent.tsx +144 -60
  23. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  24. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  25. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  26. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  27. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  28. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +108 -0
  29. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
  30. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  31. package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +13 -0
  32. package/src/_stories/ChartBar.Editor.stories.tsx +151 -85
  33. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  34. package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +13 -4
  35. package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
  36. package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
  37. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  38. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  39. package/src/_stories/_mock/heatmap.json +156 -0
  40. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  41. package/src/_stories/_mock/paired-bar-abbr.json +1 -1
  42. package/src/_stories/_mock/paired-bar.json +1 -1
  43. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  44. package/src/_stories/_mock/sankey_mock.json +78 -0
  45. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  46. package/src/components/Annotations/components/AnnotationDraggable.tsx +2 -2
  47. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  48. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  49. package/src/components/Axis/BottomAxis.tsx +1 -1
  50. package/src/components/Axis/LeftAxis.tsx +15 -14
  51. package/src/components/Axis/LeftAxisGridlines.tsx +14 -13
  52. package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
  53. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
  54. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
  55. package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
  56. package/src/components/BarChart/components/BarChart.tsx +2 -6
  57. package/src/components/BarChart/helpers/useBarChart.ts +1 -1
  58. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  59. package/src/components/Brush/BrushSelector.tsx +88 -187
  60. package/src/components/Brush/brushInteraction.ts +22 -0
  61. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  62. package/src/components/EditorPanel/EditorPanel.tsx +946 -960
  63. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  64. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +229 -165
  65. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  66. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -116
  67. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +159 -109
  68. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  69. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +14 -28
  70. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  71. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  72. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  73. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  74. package/src/components/EditorPanel/useEditorPermissions.ts +32 -21
  75. package/src/components/Forecasting/Forecasting.tsx +2 -2
  76. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  77. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  78. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  79. package/src/components/HeatMap/heatmap.css +71 -0
  80. package/src/components/HeatMap/helpers/index.ts +261 -0
  81. package/src/components/HeatMap/index.tsx +5 -0
  82. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  83. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  84. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  85. package/src/components/HoverLine/HoverLine.tsx +6 -8
  86. package/src/components/Legend/Legend.Component.tsx +4 -4
  87. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
  88. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  89. package/src/components/Legend/helpers/index.ts +0 -2
  90. package/src/components/LineChart/LineChartProps.ts +1 -0
  91. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +22 -24
  92. package/src/components/LineChart/helpers.test.ts +345 -0
  93. package/src/components/LineChart/helpers.ts +15 -5
  94. package/src/components/LineChart/index.tsx +18 -5
  95. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  96. package/src/components/LinearChart/tests/LinearChart.test.tsx +228 -7
  97. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  98. package/src/components/LinearChart.tsx +155 -77
  99. package/src/components/PairedBarChart.jsx +2 -2
  100. package/src/components/PieChart/PieChart.tsx +15 -10
  101. package/src/components/RadarChart/RadarChart.tsx +3 -1
  102. package/src/components/Regions/components/Regions.tsx +199 -35
  103. package/src/components/Sankey/components/Sankey.tsx +7 -3
  104. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  105. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  106. package/src/components/SmallMultiples/SmallMultipleTile.tsx +31 -3
  107. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  108. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  109. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  110. package/src/components/Sparkline/components/SparkLine.tsx +5 -10
  111. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  112. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  113. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  114. package/src/data/initial-state.js +12 -0
  115. package/src/helpers/axisLabelFontSize.ts +8 -0
  116. package/src/helpers/bumpChartTooltip.ts +54 -0
  117. package/src/helpers/calculateLeftYAxisWidth.ts +197 -0
  118. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  119. package/src/helpers/getMinMax.ts +2 -3
  120. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  121. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  122. package/src/helpers/handleChartAriaLabels.ts +23 -10
  123. package/src/helpers/seriesHighlight.ts +26 -0
  124. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  125. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  126. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  127. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  128. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  129. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  130. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  131. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  132. package/src/helpers/tooltipHelpers.ts +176 -0
  133. package/src/hooks/tests/useScales.test.tsx +105 -0
  134. package/src/hooks/useHighlightedBars.ts +9 -16
  135. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  136. package/src/hooks/useScales.ts +19 -9
  137. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  138. package/src/hooks/useTooltip.tsx +167 -38
  139. package/src/index.jsx +5 -2
  140. package/src/scss/main.scss +32 -49
  141. package/src/test/CdcChart.test.jsx +24 -0
  142. package/src/types/ChartConfig.ts +19 -0
  143. package/src/types/ChartContext.ts +2 -0
  144. package/examples/chart-regression-2.json +0 -2360
  145. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  146. package/examples/feature/__data__/city-temperature.json +0 -2198
  147. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  148. package/examples/feature/boxplot/box-plot-data.json +0 -71
  149. package/examples/feature/boxplot/boxplot-data.json +0 -156
  150. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  151. package/examples/feature/forest-plot/log.json +0 -26
  152. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  153. package/examples/region-issue.json +0 -2065
  154. package/examples/xaxis.json +0 -493
  155. package/src/_stories/Chart.SmallMultiples.stories.tsx +0 -63
  156. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  157. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  158. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  159. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  160. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  161. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  162. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  163. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  164. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  165. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  166. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  167. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  168. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  169. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  170. /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
  171. /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
  172. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  173. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  174. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  175. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  176. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  177. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  178. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  179. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  180. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  181. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  182. /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
  183. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  184. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  185. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  186. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  187. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  188. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  189. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  190. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  191. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  192. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -0,0 +1,114 @@
1
+ import { useContext, useMemo } from 'react'
2
+ import { LegendItem, LegendLabel } from '@visx/legend'
3
+ import LegendShape from '@cdc/core/components/LegendShape'
4
+ import ConfigContext from '../../../ConfigContext'
5
+ import { buildHeatMapData, getHeatMapBucketPalette, getHeatMapDataGroupRanges } from '../helpers'
6
+ import { ChartConfig } from '../../../types/ChartConfig'
7
+
8
+ const getHeatMapLegendClasses = (config: ChartConfig) => {
9
+ const { position = 'top', singleRow = true, verticalSorted } = config.legend || {}
10
+ const hideBorder = config.legend?.hideBorder
11
+ const hideSideBorder = hideBorder && typeof hideBorder === 'object' ? hideBorder.side : Boolean(hideBorder)
12
+ const hideTopBottomBorder = hideBorder && typeof hideBorder === 'object' ? hideBorder.topBottom : Boolean(hideBorder)
13
+ const containerClasses = ['legend-container', 'cdc-heatmap__legend', position].filter(Boolean)
14
+ const innerClasses = ['legend-container__inner']
15
+
16
+ if (['bottom', 'top'].includes(position)) {
17
+ innerClasses.push(singleRow ? 'single-row' : 'double-column', position)
18
+ }
19
+
20
+ if (['bottom', 'top'].includes(position) && verticalSorted) {
21
+ innerClasses.push('vertical-sorted')
22
+ }
23
+
24
+ if (
25
+ (['right', 'left'].includes(position) && hideSideBorder) ||
26
+ (['top', 'bottom'].includes(position) && hideTopBottomBorder)
27
+ ) {
28
+ containerClasses.push('border-0', 'p-0')
29
+ } else {
30
+ containerClasses.push('p-3')
31
+ }
32
+
33
+ return { containerClasses, innerClasses }
34
+ }
35
+
36
+ const HeatMapGradientLegend = () => {
37
+ const { filteredData, excludedData, config, formatNumber, parseDate } = useContext(ConfigContext)
38
+
39
+ const rows = filteredData?.length ? filteredData : excludedData || []
40
+ const xDataKey = config.xAxis?.dataKey
41
+ const heatMapSeries = config.series || []
42
+
43
+ const { minValue, maxValue } = useMemo(
44
+ () =>
45
+ buildHeatMapData({
46
+ data: rows as Record<string, any>[],
47
+ xDataKey,
48
+ series: heatMapSeries,
49
+ seriesLabels: config.runtime?.seriesLabels,
50
+ xAxisType: config.xAxis?.type,
51
+ parseDate
52
+ }),
53
+ [rows, xDataKey, heatMapSeries, config.runtime?.seriesLabels, config.xAxis?.type, parseDate]
54
+ )
55
+
56
+ const palette = getHeatMapBucketPalette(config)
57
+ const legendLabel = config.legend?.label
58
+ const valueLabel = legendLabel ? undefined : 'Value'
59
+ const { containerClasses, innerClasses } = getHeatMapLegendClasses(config)
60
+ const isGradientLegend = !config.legend?.style || config.legend?.style === 'gradient'
61
+ const shape = config.legend?.style === 'circles' ? 'circle' : 'square'
62
+ const ranges = getHeatMapDataGroupRanges(config, minValue, maxValue, formatNumber)
63
+ const displayRanges = config.legend?.reverseLabelOrder ? [...ranges].reverse() : ranges
64
+ const displayPalette = config.legend?.reverseLabelOrder ? [...palette].reverse() : palette
65
+
66
+ if (config.legend?.hide) return null
67
+
68
+ return (
69
+ <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
70
+ {config.legend?.label && <h3 className='cdc-heatmap__legend-title'>{config.legend.label}</h3>}
71
+ {config.legend?.description && <p className='cdc-heatmap__legend-description'>{config.legend.description}</p>}
72
+
73
+ {isGradientLegend ? (
74
+ <div className='cdc-heatmap__legend-scale'>
75
+ <svg className='cdc-heatmap__legend-svg' height='50' width='100%'>
76
+ <rect x='0' y='0' width='100%' height='25' fill='#d3d3d3' />
77
+ {displayPalette.map((color, index) => (
78
+ <rect
79
+ key={`${color}-${index}`}
80
+ x={`${(index / displayPalette.length) * 100}%`}
81
+ y='1'
82
+ width={`${100 / displayPalette.length}%`}
83
+ height='23'
84
+ fill={color}
85
+ />
86
+ ))}
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
+ </svg>
95
+
96
+ {valueLabel && <div className='cdc-heatmap__legend-series-label'>{valueLabel}</div>}
97
+ </div>
98
+ ) : (
99
+ <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]} />
103
+ <LegendLabel align='left' className='m-0'>
104
+ {range.label}
105
+ </LegendLabel>
106
+ </LegendItem>
107
+ ))}
108
+ </div>
109
+ )}
110
+ </aside>
111
+ )
112
+ }
113
+
114
+ export default HeatMapGradientLegend
@@ -0,0 +1,13 @@
1
+ import { type HeatMapConfig } from '../../types/ChartConfig'
2
+
3
+ export const MIN_HEATMAP_COLOR_BUCKETS = 1
4
+ export const MAX_HEATMAP_COLOR_BUCKETS = 9
5
+
6
+ export const HEATMAP_CONFIG_DEFAULTS: Required<HeatMapConfig> = {
7
+ cellPadding: 1,
8
+ rowLabelGap: 32,
9
+ columnLabelGap: 56,
10
+ colorBucketCount: 9,
11
+ xAxisPosition: 'top',
12
+ showCellValues: false
13
+ }
@@ -0,0 +1,71 @@
1
+ .cove-visualization .cdc-heatmap {
2
+ --cove-heatmap-empty-cell-color: var(--lightestGray, #f3f4f6);
3
+ --cove-heatmap-cell-stroke-color: rgba(255, 255, 255, 0.7);
4
+ --cove-heatmap-cell-stroke-width: 0.1px;
5
+ --cove-heatmap-axis-title-color: var(--darkGray, #333);
6
+ --cove-heatmap-axis-title-font-size: 14px;
7
+
8
+ width: 100%;
9
+ }
10
+
11
+ .cove-visualization .cdc-heatmap__cell {
12
+ stroke: var(--cove-heatmap-cell-stroke-color);
13
+ stroke-width: var(--cove-heatmap-cell-stroke-width);
14
+ }
15
+
16
+ .cove-visualization .cdc-heatmap__cell--empty {
17
+ fill: var(--cove-heatmap-empty-cell-color);
18
+ }
19
+
20
+ .cove-visualization .cdc-heatmap__cell-value {
21
+ font-weight: 700;
22
+ pointer-events: none;
23
+ user-select: none;
24
+ }
25
+
26
+ .cove-visualization .cdc-heatmap__axis-title {
27
+ fill: var(--cove-heatmap-axis-title-color);
28
+ font-size: var(--cove-heatmap-axis-title-font-size);
29
+ }
30
+
31
+ .cove-visualization .cdc-heatmap__body-wrap {
32
+ row-gap: 1rem;
33
+ }
34
+
35
+ .cove-visualization .cdc-heatmap__container .legend-wrapper {
36
+ row-gap: 0.75rem;
37
+ }
38
+
39
+ .cove-visualization
40
+ .cdc-heatmap__container.legend-bottom:not(.legend-hidden)
41
+ .legend-wrapper
42
+ aside.cdc-heatmap__legend {
43
+ margin-top: 0 !important;
44
+ }
45
+
46
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-title {
47
+ font-size: var(--legend-title-font-size);
48
+ font-weight: 700;
49
+ margin: 0 0 0.5rem;
50
+ }
51
+
52
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-description {
53
+ margin: 0 0 0.75rem;
54
+ }
55
+
56
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-scale {
57
+ width: 100%;
58
+ }
59
+
60
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-svg {
61
+ display: block;
62
+ max-width: 100%;
63
+ overflow: visible;
64
+ }
65
+
66
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-series-label {
67
+ color: #333;
68
+ font-weight: 600;
69
+ margin-top: 0.25rem;
70
+ text-align: center;
71
+ }
@@ -0,0 +1,261 @@
1
+ import { scaleQuantize } from 'd3-scale'
2
+ import { interpolateRgbBasis } from 'd3-interpolate'
3
+ import { ChartConfig } from '../../types/ChartConfig'
4
+ import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
5
+ import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
6
+ import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
7
+ import { HEATMAP_CONFIG_DEFAULTS, MAX_HEATMAP_COLOR_BUCKETS, MIN_HEATMAP_COLOR_BUCKETS } from '../heatmap.constants'
8
+
9
+ export type HeatMapSourceRow = Record<string, any>
10
+
11
+ export type HeatMapCell = {
12
+ rowKey: string
13
+ rowLabel: string
14
+ xValue: any
15
+ xLabel: string
16
+ value: number | null
17
+ sourceRows: HeatMapSourceRow[]
18
+ }
19
+
20
+ export type HeatMapColumn = {
21
+ key: string
22
+ label: string
23
+ rawValue: any
24
+ bins: HeatMapCell[]
25
+ }
26
+
27
+ export type HeatMapDataGroupRange = {
28
+ min: number
29
+ max: number
30
+ label: string
31
+ }
32
+
33
+ type HeatMapSeries = { dataKey: string; name?: string }
34
+ type FormatLegendValueFn = (value: number, axis?: string) => string
35
+
36
+ type BuildHeatMapDataOptions = {
37
+ data: HeatMapSourceRow[]
38
+ xDataKey: string
39
+ series?: HeatMapSeries[]
40
+ seriesLabels?: Record<string, string>
41
+ xAxisType?: string
42
+ parseDate?: (value: string, showError?: boolean) => Date
43
+ }
44
+
45
+ const DEFAULT_HEATMAP_PALETTE = [
46
+ '#eff6fb',
47
+ '#d8e8f7',
48
+ '#bdd9ef',
49
+ '#96c0e0',
50
+ '#67a3ce',
51
+ '#4286be',
52
+ '#2068aa',
53
+ '#0c4f8f',
54
+ '#08306b'
55
+ ]
56
+
57
+ const toDisplayLabel = (value: any): string => {
58
+ if (value === null || value === undefined || value === '') return 'No Data'
59
+ return String(value)
60
+ }
61
+
62
+ const isDateAxisType = (xAxisType?: string) => ['date', 'date-time'].includes(xAxisType || '')
63
+
64
+ const getSeriesLabel = (seriesEntry: HeatMapSeries, seriesLabels?: Record<string, string>) =>
65
+ seriesLabels?.[seriesEntry.dataKey] || seriesEntry.name || seriesEntry.dataKey
66
+
67
+ const createEmptyCell = (xValue: any, rowKey: string, rowLabel: string): HeatMapCell => ({
68
+ rowKey,
69
+ rowLabel,
70
+ xValue,
71
+ xLabel: toDisplayLabel(xValue),
72
+ value: null,
73
+ sourceRows: []
74
+ })
75
+
76
+ const getCellKey = (xValue: any, rowKey: string) => `${String(xValue)}__${String(rowKey)}`
77
+
78
+ const getHeatMapValueDomain = (aggregated: Map<string, HeatMapCell>) => {
79
+ const values = Array.from(aggregated.values())
80
+ .map(cell => cell.value)
81
+ .filter((value): value is number => typeof value === 'number' && !Number.isNaN(value))
82
+
83
+ return {
84
+ minValue: values.length ? Math.min(...values) : 0,
85
+ maxValue: values.length ? Math.max(...values) : 0
86
+ }
87
+ }
88
+
89
+ const sortXValues = (
90
+ values: any[],
91
+ xAxisType: string | undefined,
92
+ parseDate?: (value: string, showError?: boolean) => Date
93
+ ) => {
94
+ if (!isDateAxisType(xAxisType)) {
95
+ return values
96
+ }
97
+
98
+ return [...values].sort((a, b) => {
99
+ const left = parseDate ? parseDate(String(a), false).getTime() : new Date(a).getTime()
100
+ const right = parseDate ? parseDate(String(b), false).getTime() : new Date(b).getTime()
101
+ return left - right
102
+ })
103
+ }
104
+
105
+ export const buildHeatMapData = ({
106
+ data,
107
+ xDataKey,
108
+ series,
109
+ seriesLabels,
110
+ xAxisType,
111
+ parseDate
112
+ }: BuildHeatMapDataOptions) => {
113
+ const rows = Array.isArray(data) ? data : []
114
+ const hasSeriesMode = Array.isArray(series) && series.length > 0
115
+
116
+ if (!xDataKey || rows.length === 0 || !hasSeriesMode) {
117
+ return { columns: [] as HeatMapColumn[], rowLabels: [] as string[], minValue: 0, maxValue: 0 }
118
+ }
119
+
120
+ const xValues = Array.from(new Set(rows.map(row => row[xDataKey])))
121
+ const orderedXValues = sortXValues(xValues, xAxisType, parseDate)
122
+
123
+ const aggregated = new Map<string, HeatMapCell>()
124
+
125
+ rows.forEach(row => {
126
+ const xValue = row[xDataKey]
127
+
128
+ series.forEach(seriesEntry => {
129
+ const numericValue = Number(row[seriesEntry.dataKey])
130
+ if (Number.isNaN(numericValue)) return
131
+
132
+ const aggregateKey = getCellKey(xValue, seriesEntry.dataKey)
133
+ const existing = aggregated.get(aggregateKey)
134
+
135
+ if (existing) {
136
+ existing.value = (existing.value || 0) + numericValue
137
+ existing.sourceRows.push(row)
138
+ return
139
+ }
140
+
141
+ aggregated.set(aggregateKey, {
142
+ rowKey: seriesEntry.dataKey,
143
+ rowLabel: getSeriesLabel(seriesEntry, seriesLabels),
144
+ xValue,
145
+ xLabel: toDisplayLabel(xValue),
146
+ value: numericValue,
147
+ sourceRows: [row]
148
+ })
149
+ })
150
+ })
151
+
152
+ const orderedSeries = series.map(seriesEntry => ({
153
+ rowKey: seriesEntry.dataKey,
154
+ rowLabel: getSeriesLabel(seriesEntry, seriesLabels)
155
+ }))
156
+
157
+ const columns = orderedXValues.map(xValue => {
158
+ const bins = orderedSeries.map(({ rowKey, rowLabel }) => {
159
+ const aggregateKey = getCellKey(xValue, rowKey)
160
+ return aggregated.get(aggregateKey) || createEmptyCell(xValue, rowKey, rowLabel)
161
+ })
162
+
163
+ return {
164
+ key: String(xValue),
165
+ label: toDisplayLabel(xValue),
166
+ rawValue: xValue,
167
+ bins
168
+ }
169
+ })
170
+
171
+ const { minValue, maxValue } = getHeatMapValueDomain(aggregated)
172
+
173
+ return {
174
+ columns,
175
+ rowLabels: orderedSeries.map(item => item.rowLabel),
176
+ minValue,
177
+ maxValue
178
+ }
179
+ }
180
+
181
+ export const getHeatMapPalette = (config: ChartConfig): string[] => {
182
+ const colorPalettes = filterChartColorPalettes(config)
183
+ const configPalette = config.general?.palette?.name
184
+ const migratedPaletteName = configPalette || getFallbackColorPalette(config)
185
+ const isReversedPalette = migratedPaletteName?.endsWith('reverse')
186
+ const basePaletteName = isReversedPalette ? migratedPaletteName.slice(0, -7) : migratedPaletteName
187
+
188
+ let palette =
189
+ config.general?.palette?.customColors ||
190
+ colorPalettes[migratePaletteWithMap(basePaletteName, paletteMigrationMap, false)] ||
191
+ colorPalettes[basePaletteName] ||
192
+ colorPalettes[configPalette]
193
+
194
+ if (!palette || palette.length < 2) {
195
+ palette = DEFAULT_HEATMAP_PALETTE
196
+ }
197
+
198
+ const shouldReverse = config.general?.palette?.isReversed || isReversedPalette
199
+ return shouldReverse ? [...palette].reverse() : palette
200
+ }
201
+
202
+ export const getHeatMapColorBucketCount = (config: ChartConfig) => {
203
+ const numericValue = Number(config.heatmap?.colorBucketCount)
204
+
205
+ if (!Number.isFinite(numericValue)) return HEATMAP_CONFIG_DEFAULTS.colorBucketCount
206
+
207
+ return Math.min(Math.max(Math.round(numericValue), MIN_HEATMAP_COLOR_BUCKETS), MAX_HEATMAP_COLOR_BUCKETS)
208
+ }
209
+
210
+ export const getHeatMapBucketPalette = (config: ChartConfig): string[] => {
211
+ const palette = getHeatMapPalette(config)
212
+ const bucketCount = getHeatMapColorBucketCount(config)
213
+
214
+ const interpolate = interpolateRgbBasis(palette)
215
+ return Array.from({ length: bucketCount }, (_, index) => {
216
+ return interpolate(bucketCount === 1 ? 0.5 : index / (bucketCount - 1))
217
+ })
218
+ }
219
+
220
+ const getCleanRangeBoundary = (value: number) => Number(value.toFixed(12))
221
+
222
+ const getFormattedRangeValue = (value: number, formatNumber?: FormatLegendValueFn) => {
223
+ const cleanValue = getCleanRangeBoundary(value)
224
+ return typeof formatNumber === 'function' ? String(formatNumber(cleanValue, 'left')) : String(cleanValue)
225
+ }
226
+
227
+ export const getHeatMapDataGroupRanges = (
228
+ config: ChartConfig,
229
+ minValue: number,
230
+ maxValue: number,
231
+ formatNumber?: FormatLegendValueFn
232
+ ): HeatMapDataGroupRange[] => {
233
+ if (maxValue <= minValue) {
234
+ const value = getCleanRangeBoundary(minValue)
235
+ return [{ min: value, max: value, label: getFormattedRangeValue(value, formatNumber) }]
236
+ }
237
+
238
+ const bucketCount = getHeatMapColorBucketCount(config)
239
+ const rangeSize = (maxValue - minValue) / bucketCount
240
+
241
+ return Array.from({ length: bucketCount }, (_, index) => {
242
+ const min = getCleanRangeBoundary(minValue + index * rangeSize)
243
+ const max = getCleanRangeBoundary(index === bucketCount - 1 ? maxValue : minValue + (index + 1) * rangeSize)
244
+ const formattedMin = getFormattedRangeValue(min, formatNumber)
245
+ const formattedMax = getFormattedRangeValue(max, formatNumber)
246
+
247
+ return {
248
+ min,
249
+ max,
250
+ label: min === max ? formattedMin : `${formattedMin}\u2013${formattedMax}`
251
+ }
252
+ })
253
+ }
254
+
255
+ export const getHeatMapColorScale = (config: ChartConfig, minValue: number, maxValue: number) => {
256
+ const bucketCount = getHeatMapColorBucketCount(config)
257
+ const palette = getHeatMapBucketPalette(config)
258
+ const domainMax = minValue === maxValue ? maxValue + 1 : maxValue
259
+
260
+ return scaleQuantize<string>().domain([minValue, domainMax]).range(palette)
261
+ }
@@ -0,0 +1,5 @@
1
+ import HeatMap from './components/HeatMap'
2
+
3
+ export { HeatMap }
4
+ export { default as HeatMapGradientLegend } from './components/HeatMapGradientLegend'
5
+ export default HeatMap