@cdc/chart 4.25.11 → 4.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  3. package/dist/cdcchart.js +51401 -50814
  4. package/examples/default.json +378 -0
  5. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  6. package/examples/feature/annotations/index.json +3 -6
  7. package/examples/feature/horizon/horizon-chart.json +395 -0
  8. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  9. package/examples/line-chart-states.json +1085 -0
  10. package/examples/private/123.json +694 -0
  11. package/examples/private/DEV-12100.json +1303 -0
  12. package/examples/private/anchor-issue.json +4094 -0
  13. package/examples/private/backwards-slider.json +10430 -0
  14. package/examples/private/cat-y.json +1235 -0
  15. package/examples/private/data-points.json +228 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/height.json +3915 -0
  18. package/examples/private/links.json +569 -0
  19. package/examples/private/quadrant.txt +30 -0
  20. package/examples/private/test-forecast.json +5510 -0
  21. package/examples/private/timeline-data.json +1 -0
  22. package/examples/private/timeline.json +389 -0
  23. package/examples/private/warming-stripe-test.json +2578 -0
  24. package/examples/private/warming-stripes.json +4763 -0
  25. package/examples/radar-chart-simple.json +133 -0
  26. package/examples/radar-chart.json +148 -0
  27. package/examples/tech-adoption-with-links.json +560 -0
  28. package/index.html +1 -36
  29. package/package.json +59 -60
  30. package/src/CdcChartComponent.tsx +206 -89
  31. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  32. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  33. package/src/_stories/Chart.CI.stories.tsx +13 -0
  34. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  35. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  36. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  37. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  38. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  39. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  40. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  41. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  42. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  43. package/src/_stories/Chart.Regions.Categorical.stories.tsx +161 -0
  44. package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
  45. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
  46. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  47. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  48. package/src/_stories/Chart.stories.tsx +45 -0
  49. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  50. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  51. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  52. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  53. package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
  54. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  55. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  56. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  57. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  58. package/src/_stories/ChartBrush.stories.tsx +57 -0
  59. package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
  60. package/src/_stories/ChartEditor.stories.tsx +7 -0
  61. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  62. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  63. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  64. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  65. package/src/_stories/TechAdoptionWithLinks.stories.tsx +34 -0
  66. package/src/_stories/_mock/brush_continuous.json +86 -0
  67. package/src/_stories/_mock/brush_date_large.json +176 -0
  68. package/src/_stories/_mock/brush_enabled.json +326 -0
  69. package/src/_stories/_mock/brush_mock.json +2 -69
  70. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  71. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  72. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  73. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  74. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  75. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  76. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  77. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  78. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  79. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  80. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  81. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  82. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  83. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  84. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  85. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  86. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
  87. package/src/components/Axis/BottomAxis.tsx +270 -0
  88. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  89. package/src/components/Axis/LeftAxis.tsx +404 -0
  90. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  91. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  92. package/src/components/Axis/README.md +94 -0
  93. package/src/components/Axis/RightAxis.tsx +108 -0
  94. package/src/components/Axis/axis.constants.ts +21 -0
  95. package/src/components/Axis/index.ts +7 -0
  96. package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
  97. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  98. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  99. package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
  100. package/src/components/BarChart/components/BarChart.tsx +7 -1
  101. package/src/components/BarChart/components/context.tsx +1 -0
  102. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  103. package/src/components/Brush/BrushSelector.tsx +1390 -0
  104. package/src/components/Brush/MiniChartPreview.tsx +400 -0
  105. package/src/components/DeviationBar.jsx +9 -7
  106. package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
  107. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  108. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  109. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
  110. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
  111. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  112. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  113. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
  114. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
  115. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  116. package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
  117. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  118. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  119. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  120. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  121. package/src/components/HorizonChart/index.tsx +3 -0
  122. package/src/components/Legend/Legend.Component.tsx +52 -4
  123. package/src/components/Legend/Legend.tsx +4 -3
  124. package/src/components/Legend/LegendValueRange.tsx +77 -0
  125. package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
  126. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  127. package/src/components/Legend/helpers/index.ts +10 -6
  128. package/src/components/LineChart/helpers/README.md +292 -0
  129. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  130. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  131. package/src/components/LineChart/index.tsx +44 -8
  132. package/src/components/LinearChart/README.md +109 -0
  133. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  134. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  135. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  136. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  137. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  138. package/src/components/LinearChart.tsx +338 -1082
  139. package/src/components/PairedBarChart.jsx +20 -3
  140. package/src/components/PieChart/PieChart.tsx +1 -1
  141. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  142. package/src/components/RadarChart/RadarChart.tsx +298 -0
  143. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  144. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  145. package/src/components/RadarChart/helpers.ts +83 -0
  146. package/src/components/RadarChart/index.tsx +3 -0
  147. package/src/components/Regions/components/Regions.tsx +365 -122
  148. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  149. package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
  150. package/src/components/WarmingStripes/WarmingStripes.tsx +230 -0
  151. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  152. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  153. package/src/components/WarmingStripes/index.tsx +3 -0
  154. package/src/data/initial-state.js +17 -2
  155. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  156. package/src/helpers/getExcludedData.ts +4 -0
  157. package/src/helpers/getMinMax.ts +12 -7
  158. package/src/helpers/handleChartAriaLabels.ts +19 -19
  159. package/src/helpers/handleLineType.ts +22 -18
  160. package/src/helpers/sizeHelpers.ts +0 -20
  161. package/src/helpers/smallMultiplesHelpers.ts +1 -1
  162. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  163. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  164. package/src/hooks/useScales.ts +18 -1
  165. package/src/hooks/useTooltip.tsx +34 -10
  166. package/src/scss/DataTable.scss +0 -4
  167. package/src/scss/main.scss +22 -3
  168. package/src/selectors/README.md +68 -0
  169. package/src/store/chart.reducer.ts +2 -0
  170. package/src/test/CdcChart.test.jsx +1 -1
  171. package/src/types/ChartConfig.ts +21 -0
  172. package/src/types/ChartContext.ts +1 -0
  173. package/src/types/Horizon.ts +64 -0
  174. package/src/types/Label.ts +1 -0
  175. package/src/utils/analyticsTracking.ts +19 -0
  176. package/LICENSE +0 -201
  177. package/src/components/Annotations/components/helpers/index.tsx +0 -46
  178. package/src/components/Brush/BrushChart.tsx +0 -128
  179. package/src/components/Brush/BrushController.tsx +0 -71
  180. package/src/components/Brush/types.tsx +0 -8
  181. package/src/components/BrushChart.tsx +0 -223
@@ -1,5 +1,5 @@
1
1
  import parse from 'html-react-parser'
2
- import React from 'react'
2
+ import React, { useMemo } from 'react'
3
3
  import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
4
4
  import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
5
5
  import LegendShape from '@cdc/core/components/LegendShape'
@@ -17,6 +17,8 @@ import { DimensionsType } from '@cdc/core/types/Dimensions'
17
17
  import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
18
18
  import LegendLineShape from './LegendLine.Shape'
19
19
  import LegendGroup from './LegendGroup'
20
+ import LegendValueRange from './LegendValueRange'
21
+ import { getHorizonLayerColors, getHorizonMaxValue } from '../../components/HorizonChart/helpers/getHorizonLayerColors'
20
22
  import { getSeriesWithData } from '../../helpers/dataHelpers'
21
23
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
22
24
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
@@ -28,6 +30,7 @@ interface LegendProps {
28
30
  config: ChartConfig
29
31
  currentViewport: ViewportSize
30
32
  formatLabels: (labels: Label[]) => Label[]
33
+ formatNumber?: (value: number, axis?: string) => string
31
34
  highlight: Function
32
35
  handleShowAll: Function
33
36
  ref: React.Ref<() => void>
@@ -48,6 +51,7 @@ const Legend: React.FC<LegendProps> = forwardRef(
48
51
  handleShowAll,
49
52
  currentViewport,
50
53
  formatLabels,
54
+ formatNumber,
51
55
  skipId = 'legend',
52
56
  dimensions,
53
57
  transformedData: data,
@@ -60,7 +64,10 @@ const Legend: React.FC<LegendProps> = forwardRef(
60
64
  const { series } = runtime
61
65
 
62
66
  const seriesWithData = getSeriesWithData(config)
63
- const dontFilterLegendItems = !series.length || legend.unified || !seriesWithData.length
67
+ // For Radar charts, seriesWithData contains dimension keys but legend shows entity names
68
+ // so we skip the series filter for radar charts
69
+ const isRadarChart = config.visualizationType === 'Radar'
70
+ const dontFilterLegendItems = !series.length || legend.unified || !seriesWithData.length || isRadarChart
64
71
 
65
72
  const isLegendBottom =
66
73
  legend?.position === 'bottom' ||
@@ -74,6 +81,29 @@ const Legend: React.FC<LegendProps> = forwardRef(
74
81
  const { HighLightedBarUtils } = useHighlightedBars(config)
75
82
  let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
76
83
 
84
+ const horizonLegendData = useMemo(() => {
85
+ if (config.visualizationType !== 'Horizon Chart') {
86
+ return null
87
+ }
88
+ const numLayers = config.horizon?.numLayers || 4
89
+ const runtimeSeriesKeys = config.runtime?.seriesKeys
90
+ const seriesKeys =
91
+ (Array.isArray(runtimeSeriesKeys) && runtimeSeriesKeys.length > 0
92
+ ? runtimeSeriesKeys
93
+ : config.series?.map(s => s.dataKey)) || []
94
+ const maxValue = getHorizonMaxValue(data, seriesKeys)
95
+ const layerColors = getHorizonLayerColors(config, numLayers)
96
+
97
+ return { numLayers, maxValue, layerColors }
98
+ }, [
99
+ config.visualizationType,
100
+ config.horizon?.numLayers,
101
+ config.runtime?.seriesKeys,
102
+ config.series,
103
+ config.general?.palette?.name,
104
+ data
105
+ ])
106
+
77
107
  if (!legend) return null
78
108
  return (
79
109
  <aside
@@ -99,6 +129,20 @@ const Legend: React.FC<LegendProps> = forwardRef(
99
129
  />
100
130
  <LegendGroup formatLabels={formatLabels} />
101
131
 
132
+ {/* Value Range Legend for Horizon Chart (and future chart types) */}
133
+ {horizonLegendData && (
134
+ <LegendValueRange
135
+ maxValue={horizonLegendData.maxValue}
136
+ numRanges={horizonLegendData.numLayers}
137
+ colors={horizonLegendData.layerColors}
138
+ formatNumber={formatNumber}
139
+ innerClasses={innerClasses}
140
+ shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
141
+ onClick={undefined}
142
+ reverseLabelOrder={config.legend.reverseLabelOrder}
143
+ />
144
+ )}
145
+
102
146
  <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
103
147
  {labels => {
104
148
  return (
@@ -130,8 +174,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
130
174
  } else className.push('highlighted')
131
175
  }
132
176
 
133
- if (config.legend.style === 'gradient' || config.legend.groupBy) {
134
- return <></>
177
+ if (
178
+ config.legend.style === 'gradient' ||
179
+ config.legend.groupBy ||
180
+ config.visualizationType === 'Horizon Chart'
181
+ ) {
182
+ return null
135
183
  }
136
184
 
137
185
  return (
@@ -3,7 +3,6 @@ import ConfigContext from '../../ConfigContext'
3
3
  import LegendComponent from './Legend.Component'
4
4
  import { createFormatLabels } from './helpers/createFormatLabels'
5
5
 
6
- /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
7
6
  const Legend = forwardRef((props, ref) => {
8
7
  // prettier-ignore
9
8
  const {
@@ -18,13 +17,14 @@ const Legend = forwardRef((props, ref) => {
18
17
  currentViewport,
19
18
  dimensions,
20
19
  getTextWidth,
21
- transformedData
20
+ transformedData,
21
+ formatNumber
22
22
  } = useContext(ConfigContext)
23
23
  if (!config.legend) return null
24
24
  // create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default
25
25
  const { interactionLabel } = props
26
26
 
27
- const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
27
+ const createLegendLabels = createFormatLabels(config, tableData, data, colorScale, formatNumber)
28
28
 
29
29
  return (
30
30
  <Fragment>
@@ -41,6 +41,7 @@ const Legend = forwardRef((props, ref) => {
41
41
  handleShowAll={handleShowAll}
42
42
  currentViewport={currentViewport}
43
43
  formatLabels={createLegendLabels}
44
+ formatNumber={formatNumber}
44
45
  interactionLabel={interactionLabel}
45
46
  />
46
47
  </Fragment>
@@ -0,0 +1,77 @@
1
+ import React from 'react'
2
+ import { LegendItem, LegendLabel } from '@visx/legend'
3
+ import LegendShape from '@cdc/core/components/LegendShape'
4
+ import { generateValueRanges, GenerateValueRangesOptions, ValueRange } from './helpers/generateValueRanges'
5
+
6
+ export type LegendValueRangeProps = GenerateValueRangesOptions & {
7
+ colors: string[]
8
+ shape?: 'circle' | 'square'
9
+ onClick?: (index: number, range: ValueRange) => void
10
+ innerClasses?: string[]
11
+ reverseLabelOrder?: boolean
12
+ }
13
+
14
+ /**
15
+ * LegendValueRange - Generic component for displaying value range legends
16
+ *
17
+ * Used by Horizon Charts to show layer intensity ranges (e.g., 1-100, 101-200).
18
+ * Can be reused by other chart types that need binned value legends.
19
+ */
20
+ const LegendValueRange: React.FC<LegendValueRangeProps> = ({
21
+ minValue = 0,
22
+ maxValue,
23
+ numRanges,
24
+ distribution = 'equal',
25
+ formatNumber,
26
+ colors,
27
+ shape = 'square',
28
+ onClick,
29
+ innerClasses = ['legend-container__inner'],
30
+ reverseLabelOrder = false
31
+ }) => {
32
+ const ranges = generateValueRanges({ minValue, maxValue, numRanges, distribution, formatNumber })
33
+
34
+ if (ranges.length === 0) return null
35
+
36
+ // Reverse both ranges and colors when reverseLabelOrder is true
37
+ const displayRanges = reverseLabelOrder ? [...ranges].reverse() : ranges
38
+ const displayColors = reverseLabelOrder ? [...colors].reverse() : colors
39
+
40
+ return (
41
+ <div className={innerClasses.join(' ')}>
42
+ {displayRanges.map((range, i) => {
43
+ const color = displayColors[i % displayColors.length]
44
+ const isClickable = typeof onClick === 'function'
45
+ const className = ['legend-item', `legend-text--range-${i}`, !isClickable && 'not-clickable'].filter(Boolean)
46
+
47
+ return (
48
+ <LegendItem
49
+ className={className.join(' ')}
50
+ tabIndex={isClickable ? 0 : -1}
51
+ key={`legend-range-${i}`}
52
+ onKeyDown={e => {
53
+ if (isClickable && e.key === 'Enter') {
54
+ e.preventDefault()
55
+ onClick(i, range)
56
+ }
57
+ }}
58
+ onClick={e => {
59
+ if (isClickable) {
60
+ e.preventDefault()
61
+ onClick(i, range)
62
+ }
63
+ }}
64
+ role={isClickable ? 'button' : undefined}
65
+ >
66
+ <LegendShape shape={shape} fill={color} />
67
+ <LegendLabel align='left' className='m-0'>
68
+ {range.label}
69
+ </LegendLabel>
70
+ </LegendItem>
71
+ )
72
+ })}
73
+ </div>
74
+ )
75
+ }
76
+
77
+ export default LegendValueRange
@@ -5,23 +5,172 @@ import {
5
5
  twoColorPalette
6
6
  } from '@cdc/core/data/colorPalettes'
7
7
  import { getCurrentPaletteName, getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
8
- import { chartPaletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
8
+ import { chartPaletteMigrationMap, paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
9
9
  import { getPaletteAccessor } from '@cdc/core/helpers/getPaletteAccessor'
10
10
  import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
11
11
  import { isV1Palette } from '@cdc/core/helpers/palettes/utils'
12
12
  import { v2ColorDistribution } from '@cdc/core/helpers/palettes/colorDistributions'
13
13
  import { updatePaletteNames } from '@cdc/core/helpers/updatePaletteNames'
14
14
  import { buildForecastPaletteMappings } from '../../../helpers/buildForecastPaletteMappings'
15
+ import { getFullColorPalette } from '../../../helpers/smallMultiplesHelpers'
15
16
  import { FaStar } from 'react-icons/fa'
16
17
  import { Label } from '../../../types/Label'
17
18
  import { ColorScale, TransformedData } from '../../../types/ChartContext'
18
19
  import { ChartConfig } from '../../../types/ChartConfig'
19
20
  import _ from 'lodash'
21
+ import { scaleSequential } from 'd3-scale'
22
+ import { interpolateRgbBasis } from 'd3-interpolate'
23
+ import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
20
24
 
21
25
  export const createFormatLabels =
22
- (config: ChartConfig, tableData: Object[], data: TransformedData[], colorScale: ColorScale) =>
26
+ (
27
+ config: ChartConfig,
28
+ tableData: Object[],
29
+ data: TransformedData[],
30
+ colorScale: ColorScale,
31
+ formatNumber: (value: any, axis: string) => string
32
+ ) =>
23
33
  (defaultLabels: Label[]): Label[] => {
24
34
  const { visualizationType, visualizationSubType, series, runtime, legend } = config
35
+
36
+ // Handle small multiples legend adjustments
37
+ // by-series + same: all tiles use same color, legend should show one color
38
+ if (config.smallMultiples?.mode === 'by-series' && config.smallMultiples?.colorMode === 'same') {
39
+ const baseColor = colorScale?.range()?.[0]
40
+ return defaultLabels.map((label, i) => ({
41
+ ...label,
42
+ value: baseColor
43
+ }))
44
+ }
45
+
46
+ // by-column + different: each tile gets different color, legend should show tile values with their colors
47
+ if (config.smallMultiples?.mode === 'by-column' && config.smallMultiples?.colorMode === 'different') {
48
+ const tileColumn = config.smallMultiples.tileColumn
49
+ const tileValues = Array.from(new Set(data.map(d => d[tileColumn])))
50
+ .filter(Boolean)
51
+ .sort()
52
+ const tilePalette = getFullColorPalette(config, tileValues.length)
53
+
54
+ return tileValues.map((value, index) => ({
55
+ datum: value,
56
+ index,
57
+ text: config.smallMultiples.tileTitles?.[value] || String(value),
58
+ value: tilePalette[index]
59
+ }))
60
+ }
61
+
62
+ // Handle Warming Stripes legend
63
+ if (visualizationType === 'Warming Stripes') {
64
+ const valueKey = runtime.seriesKeys?.[0]
65
+ if (!valueKey || !data || data.length === 0) {
66
+ return []
67
+ }
68
+
69
+ // Calculate min and max values
70
+ const values = data.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
71
+ const minValue = Math.min(...values)
72
+ const maxValue = Math.max(...values)
73
+
74
+ // Get the color palette from config (same logic as WarmingStripes component)
75
+ const colorPalettesFiltered = filterChartColorPalettes(config)
76
+ const configPalette = config.general?.palette?.name
77
+ const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
78
+
79
+ const isReversedPalette = migratedPaletteName?.endsWith('reverse')
80
+ const basePaletteName = isReversedPalette ? migratedPaletteName.slice(0, -7) : migratedPaletteName
81
+
82
+ let palette =
83
+ colorPalettesFiltered[migratePaletteWithMap(basePaletteName, paletteMigrationMap, false)] ||
84
+ colorPalettesFiltered[basePaletteName] ||
85
+ colorPalettesFiltered[configPalette]
86
+
87
+ if (!palette || palette.length < 2) {
88
+ palette = [
89
+ '#053061',
90
+ '#2166ac',
91
+ '#4393c3',
92
+ '#92c5de',
93
+ '#d1e5f0',
94
+ '#f7f7f7',
95
+ '#fddbc7',
96
+ '#f4a582',
97
+ '#d6604d',
98
+ '#b2182b',
99
+ '#67001f'
100
+ ]
101
+ }
102
+
103
+ const shouldReverse = config.general?.palette?.isReversed || isReversedPalette
104
+ const finalPalette = shouldReverse ? [...palette].reverse() : palette
105
+ const warmingColorScale = scaleSequential(interpolateRgbBasis(finalPalette)).domain([minValue, maxValue])
106
+
107
+ // For gradient style, create smooth gradient with min/max labels only
108
+ if (legend?.style === 'gradient') {
109
+ // Create many color stops for smooth gradient (these are used for the gradient fill)
110
+ const numColorStops = 20
111
+ const colorStops = []
112
+ for (let i = 0; i < numColorStops; i++) {
113
+ const t = i / (numColorStops - 1)
114
+ const value = minValue + t * (maxValue - minValue)
115
+ colorStops.push(warmingColorScale(value))
116
+ }
117
+
118
+ // Create multiple stops for proper spacing, but only show labels at first and last
119
+ // This ensures the first label appears at the left edge and last at the right edge
120
+ const numPositions = 5 // Number of tick positions for spacing
121
+ const labels = []
122
+ for (let i = 0; i < numPositions; i++) {
123
+ const t = i / (numPositions - 1)
124
+ const value = minValue + t * (maxValue - minValue)
125
+ const isFirstOrLast = i === 0 || i === numPositions - 1
126
+
127
+ labels.push({
128
+ datum: String(value),
129
+ index: i,
130
+ text: isFirstOrLast ? formatNumber(value, 'left') : '',
131
+ value: colorStops[Math.floor(t * (numColorStops - 1))],
132
+ colors: colorStops
133
+ })
134
+ }
135
+
136
+ return labels
137
+ }
138
+
139
+ // For interval style, create ranges
140
+ const numIntervals = legend?.warmingStripesIntervals || 5
141
+ const range = maxValue - minValue
142
+ const intervalSize = range / numIntervals
143
+
144
+ const intervalLabels = []
145
+
146
+ for (let i = 0; i < numIntervals; i++) {
147
+ // Calculate interval boundaries
148
+ // Each interval after the first starts at the exact boundary point
149
+ const start = minValue + i * intervalSize
150
+ const end = i === numIntervals - 1 ? maxValue : minValue + (i + 1) * intervalSize
151
+ const midPoint = (start + end) / 2
152
+
153
+ // For display, show the actual start for first interval
154
+ // For subsequent intervals, format the boundary point
155
+ const displayStart = start
156
+ const displayEnd = i === numIntervals - 1 ? end : end
157
+
158
+ intervalLabels.push({
159
+ datum: String(midPoint),
160
+ index: i,
161
+ text:
162
+ i === 0
163
+ ? `${formatNumber(displayStart, 'left')} - < ${formatNumber(displayEnd, 'left')}`
164
+ : i === numIntervals - 1
165
+ ? `${formatNumber(displayStart, 'left')} - ${formatNumber(displayEnd, 'left')}`
166
+ : `${formatNumber(displayStart, 'left')} - < ${formatNumber(displayEnd, 'left')}`,
167
+ value: warmingColorScale(midPoint)
168
+ })
169
+ }
170
+
171
+ return intervalLabels
172
+ }
173
+
25
174
  const sortVertical = labels =>
26
175
  legend.verticalSorted
27
176
  ? _.sortBy(_.cloneDeep(labels), label => {
@@ -193,6 +342,19 @@ export const createFormatLabels =
193
342
  return reverseLabels(seriesLabels)
194
343
  }
195
344
 
345
+ // For Radar charts, use the entity names from runtime.seriesKeys (set from xAxis.dataKey values)
346
+ // not the series names (which are the dimension keys)
347
+ if (visualizationType === 'Radar') {
348
+ const entityNames = runtime.seriesKeys || []
349
+ const radarLabels = entityNames.map((val, i) => ({
350
+ datum: val,
351
+ index: i,
352
+ text: String(val),
353
+ value: colorScale(val)
354
+ }))
355
+ return reverseLabels(radarLabels)
356
+ }
357
+
196
358
  if (config.series.some(item => item.name)) {
197
359
  const uniqueLabels = Array.from(new Set(config.series.map(d => d.name || d.dataKey))).map((val, i) => ({
198
360
  datum: val,
@@ -0,0 +1,92 @@
1
+ /**
2
+ * generateValueRanges - Creates value range bins for legend display
3
+ *
4
+ * Supports equal interval distribution with scaffolding for future quantile support.
5
+ * Ranges are inclusive with no overlap, starting at minValue or 0 (e.g., 1-100, 101-200).
6
+ */
7
+
8
+ export type RangeDistribution = 'equal' | 'quantile'
9
+
10
+ export type ValueRange = {
11
+ min: number
12
+ max: number
13
+ label: string
14
+ }
15
+
16
+ export type GenerateValueRangesOptions = {
17
+ minValue?: number
18
+ maxValue: number
19
+ numRanges: number
20
+ distribution?: RangeDistribution
21
+ formatNumber?: (value: number, axis?: string) => string
22
+ }
23
+
24
+ /**
25
+ * Generates an array of value ranges for legend display
26
+ */
27
+ export const generateValueRanges = ({
28
+ minValue = 0,
29
+ maxValue,
30
+ numRanges,
31
+ distribution = 'equal',
32
+ formatNumber
33
+ }: GenerateValueRangesOptions): ValueRange[] => {
34
+ if (numRanges <= 0 || maxValue <= minValue) {
35
+ return []
36
+ }
37
+
38
+ const ranges: ValueRange[] = []
39
+
40
+ if (distribution === 'equal') {
41
+ const rangeSize = (maxValue - minValue) / numRanges
42
+
43
+ for (let i = 0; i < numRanges; i++) {
44
+ // Calculate raw boundaries
45
+ const rawMin = minValue + i * rangeSize
46
+ const rawMax = minValue + (i + 1) * rangeSize
47
+
48
+ // For display:
49
+ // - First range starts at floor of minValue
50
+ // - Subsequent ranges start at previous max + 1 (monotonic constraint)
51
+ // - Last range ends exactly at ceil of maxValue
52
+ let displayMin: number
53
+ let displayMax: number
54
+
55
+ if (i === 0) {
56
+ displayMin = Math.floor(rawMin)
57
+ } else {
58
+ // Start at previous range's max + 1 to avoid overlap
59
+ displayMin = ranges[i - 1].max + 1
60
+ }
61
+
62
+ if (i === numRanges - 1) {
63
+ // Last range ends exactly at maxValue
64
+ displayMax = Math.ceil(maxValue)
65
+ } else {
66
+ displayMax = Math.floor(rawMax)
67
+ }
68
+
69
+ // Ensure min <= max (can happen with very small ranges)
70
+ if (displayMin > displayMax) {
71
+ displayMax = displayMin
72
+ }
73
+
74
+ // Format numbers if formatter provided
75
+ const formattedMin = formatNumber ? formatNumber(displayMin, 'left') : displayMin.toLocaleString()
76
+ const formattedMax = formatNumber ? formatNumber(displayMax, 'left') : displayMax.toLocaleString()
77
+
78
+ ranges.push({
79
+ min: displayMin,
80
+ max: displayMax,
81
+ label: displayMin === displayMax ? formattedMin : `${formattedMin}–${formattedMax}`
82
+ })
83
+ }
84
+ } else if (distribution === 'quantile') {
85
+ // Scaffolding for future quantile-based distribution
86
+ // Would require passing in the actual data values to calculate percentiles
87
+ // Currently, silently fall back to equal interval distribution
88
+ return generateValueRanges({ minValue, maxValue, numRanges, distribution: 'equal', formatNumber })
89
+ }
90
+
91
+ return ranges
92
+ }
@@ -2,6 +2,15 @@ export const getGradientConfig = (config, formatLabels, colorScale) => {
2
2
  const defaultValue = [{ datum: '', index: 0, text: '', value: '' }]
3
3
 
4
4
  const formatted = formatLabels(defaultValue)
5
+
6
+ // For Warming Stripes, always use the formatted labels which contain the gradient stops
7
+ if (config.visualizationType === 'Warming Stripes' && config.legend.style === 'gradient') {
8
+ // Extract the colors array from the first item (all items have the same color stops)
9
+ const colors = formatted[0]?.colors || formatted.map(label => label?.value).filter(Boolean)
10
+ const labels = formatted.map(label => label?.text || label?.datum)
11
+ return { colors, labels }
12
+ }
13
+
5
14
  const colors = config.legend.colorCode ? formatted.map(label => label?.value) : colorScale?.range() ?? []
6
15
  const labels = config.legend.colorCode
7
16
  ? formatted.map(label => label?.text || label?.datum)
@@ -19,12 +28,7 @@ export const getMarginTop = (isLegendBottom, config) => {
19
28
  if (!isLegendBottom) {
20
29
  return '0px'
21
30
  }
22
- if (isLegendBottom && config.xAxis.brushActive && !config.legend.hide) {
23
- const additiolMargin = 25
24
- return `${DEFAULT_MARGIN_TOP + config.brush?.height + additiolMargin}px`
25
- } else {
26
- return `${DEFAULT_MARGIN_TOP}px`
27
- }
31
+ return `${DEFAULT_MARGIN_TOP}px`
28
32
  }
29
33
  export const getMarginBottom = (isLegendBottom, config) => {
30
34
  const isLegendTop = config.legend?.position === 'top' && !config.legend.hide