@cdc/chart 4.26.4 → 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 (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -0,0 +1,128 @@
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
+ const displayLegendItems = displayRanges.map((range, index) => ({
66
+ color: displayPalette[index % displayPalette.length],
67
+ range
68
+ }))
69
+
70
+ if (config.legend?.hide) return null
71
+
72
+ return (
73
+ <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
74
+ {config.legend?.label && <h3 className='cdc-heatmap__legend-title'>{config.legend.label}</h3>}
75
+ {config.legend?.description && <p className='cdc-heatmap__legend-description'>{config.legend.description}</p>}
76
+
77
+ {isGradientLegend ? (
78
+ <div className='cdc-heatmap__legend-scale'>
79
+ <svg className='cdc-heatmap__legend-svg' height='25' width='100%'>
80
+ <rect x='0' y='0' width='100%' height='25' fill='#d3d3d3' />
81
+ {displayLegendItems.map(({ color, range }, index) => (
82
+ <rect
83
+ key={`heatmap-gradient-block-${range.min}-${range.max}-${color}`}
84
+ x={`${(index / displayLegendItems.length) * 100}%`}
85
+ y='1'
86
+ width={`${100 / displayLegendItems.length}%`}
87
+ height='23'
88
+ fill={color}
89
+ />
90
+ ))}
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>
105
+
106
+ {valueLabel && <div className='cdc-heatmap__legend-series-label'>{valueLabel}</div>}
107
+ </div>
108
+ ) : (
109
+ <div className={innerClasses.join(' ')}>
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} />
117
+ <LegendLabel align='left' className='m-0'>
118
+ {range.label}
119
+ </LegendLabel>
120
+ </LegendItem>
121
+ ))}
122
+ </div>
123
+ )}
124
+ </aside>
125
+ )
126
+ }
127
+
128
+ 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,85 @@
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: 18px;
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-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
+
80
+ .cove-visualization .cdc-heatmap__legend .cdc-heatmap__legend-series-label {
81
+ color: #333;
82
+ font-weight: 600;
83
+ margin-top: 0.25rem;
84
+ text-align: center;
85
+ }
@@ -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