@cdc/chart 4.26.1 → 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 (132) 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 +45357 -43655
  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/line-chart-states.json +1085 -0
  9. package/examples/private/123.json +694 -0
  10. package/examples/private/anchor-issue.json +4094 -0
  11. package/examples/private/backwards-slider.json +10430 -0
  12. package/examples/private/georgia.csv +160 -0
  13. package/examples/private/timeline-data.json +1 -0
  14. package/examples/private/timeline.json +389 -0
  15. package/examples/radar-chart-simple.json +133 -0
  16. package/examples/radar-chart.json +148 -0
  17. package/index.html +1 -31
  18. package/package.json +57 -59
  19. package/src/CdcChartComponent.tsx +99 -18
  20. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  21. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  22. package/src/_stories/Chart.CI.stories.tsx +13 -0
  23. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  24. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  25. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  26. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  27. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  28. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  29. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  30. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  31. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  32. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  33. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  34. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  35. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  36. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  37. package/src/_stories/Chart.stories.tsx +37 -0
  38. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  39. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  40. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  41. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  42. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  43. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  44. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  45. package/src/_stories/ChartBrush.stories.tsx +7 -0
  46. package/src/_stories/ChartEditor.stories.tsx +7 -0
  47. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  48. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  49. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  50. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  51. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  52. package/src/_stories/_mock/brush_continuous.json +86 -0
  53. package/src/_stories/_mock/brush_date_large.json +176 -0
  54. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  55. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  56. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  57. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  58. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  59. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  60. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  61. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  62. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  63. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  64. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  65. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  66. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  67. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  68. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  69. package/src/components/Axis/BottomAxis.tsx +270 -0
  70. package/src/components/Axis/LeftAxis.tsx +404 -0
  71. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  72. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  73. package/src/components/Axis/README.md +94 -0
  74. package/src/components/Axis/RightAxis.tsx +108 -0
  75. package/src/components/Axis/axis.constants.ts +21 -0
  76. package/src/components/Axis/index.ts +7 -0
  77. package/src/components/BarChart/components/BarChart.tsx +7 -1
  78. package/src/components/Brush/BrushSelector.tsx +154 -22
  79. package/src/components/Brush/MiniChartPreview.tsx +138 -21
  80. package/src/components/EditorPanel/EditorPanel.tsx +25 -11
  81. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  82. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +81 -1
  83. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -1
  84. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  85. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  86. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -1
  87. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  88. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  89. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  90. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  91. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  92. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  93. package/src/components/HorizonChart/index.tsx +3 -0
  94. package/src/components/Legend/Legend.Component.tsx +52 -4
  95. package/src/components/Legend/Legend.tsx +1 -1
  96. package/src/components/Legend/LegendValueRange.tsx +77 -0
  97. package/src/components/Legend/helpers/createFormatLabels.tsx +13 -0
  98. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  99. package/src/components/LineChart/helpers/README.md +292 -0
  100. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  101. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  102. package/src/components/LineChart/index.tsx +44 -8
  103. package/src/components/LinearChart/README.md +109 -0
  104. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  105. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  106. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  107. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  108. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  109. package/src/components/LinearChart.tsx +250 -1059
  110. package/src/components/PieChart/PieChart.tsx +1 -1
  111. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  112. package/src/components/RadarChart/RadarChart.tsx +298 -0
  113. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  114. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  115. package/src/components/RadarChart/helpers.ts +83 -0
  116. package/src/components/RadarChart/index.tsx +3 -0
  117. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  118. package/src/data/initial-state.js +14 -1
  119. package/src/helpers/getExcludedData.ts +4 -0
  120. package/src/helpers/handleChartAriaLabels.ts +19 -19
  121. package/src/helpers/handleLineType.ts +22 -18
  122. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  123. package/src/hooks/useScales.ts +7 -0
  124. package/src/hooks/useTooltip.tsx +3 -0
  125. package/src/scss/main.scss +5 -0
  126. package/src/selectors/README.md +68 -0
  127. package/src/store/chart.reducer.ts +2 -0
  128. package/src/types/ChartConfig.ts +18 -0
  129. package/src/types/ChartContext.ts +1 -0
  130. package/src/types/Horizon.ts +64 -0
  131. package/preview.html +0 -1616
  132. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -1,4 +1,4 @@
1
- import { useContext, useState } from 'react'
1
+ import { useContext, useState, useCallback, useEffect } from 'react'
2
2
  import ConfigContext from '../../ConfigContext'
3
3
  import { Group } from '@visx/group'
4
4
  import { scaleSequential } from 'd3-scale'
@@ -6,7 +6,6 @@ import { interpolateRgbBasis } from 'd3-interpolate'
6
6
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
7
7
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
8
8
  import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
9
- import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
10
9
  import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
11
10
  import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
12
11
  import { hasTrackedHover, markHoverTracked } from '../../utils/analyticsTracking'
@@ -16,10 +15,26 @@ type WarmingStripesProps = {
16
15
  yScale: any
17
16
  xMax: number
18
17
  yMax: number
18
+ synchronizedXValue?: any
19
+ showTooltip: (args: any) => void
20
+ handleTooltipMouseOff: () => void
19
21
  }
20
22
 
21
- const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
22
- const { transformedData: data, config, formatNumber, interactionLabel, currentViewport } = useContext(ConfigContext)
23
+ const WarmingStripes = ({
24
+ xMax,
25
+ yMax,
26
+ synchronizedXValue,
27
+ showTooltip,
28
+ handleTooltipMouseOff
29
+ }: WarmingStripesProps) => {
30
+ const {
31
+ transformedData: data,
32
+ config,
33
+ formatNumber,
34
+ interactionLabel,
35
+ currentViewport,
36
+ handleSmallMultipleHover
37
+ } = useContext(ConfigContext)
23
38
 
24
39
  const [currentHover, setCurrentHover] = useState<number | null>(null)
25
40
 
@@ -28,18 +43,14 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
28
43
  const valueKey = config.runtime.seriesKeys?.[0]
29
44
  const xAxisDataKey = config.runtime.originalXAxis?.dataKey || config.xAxis?.dataKey
30
45
 
31
- if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
32
- return null
33
- }
34
-
35
46
  // Determine max stripes based on viewport
36
47
  const isMobile = ['xxs', 'xs', 'sm', 'md'].includes(currentViewport)
37
48
  const maxStripes = isMobile ? 60 : 200
38
49
 
39
50
  // Sample data if we have more than the max allowed stripes
40
- let displayData = data
41
- if (data.length > maxStripes) {
42
- const step = data.length / maxStripes
51
+ let displayData = data || []
52
+ if (displayData.length > maxStripes) {
53
+ const step = displayData.length / maxStripes
43
54
  displayData = []
44
55
  for (let i = 0; i < maxStripes; i++) {
45
56
  const index = Math.floor(i * step)
@@ -48,7 +59,7 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
48
59
  }
49
60
 
50
61
  // Calculate the min and max values for the color scale
51
- const values = data.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
62
+ const values = displayData.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
52
63
  const minValue = Math.min(...values)
53
64
  const maxValue = Math.max(...values)
54
65
 
@@ -94,15 +105,54 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
94
105
  // Calculate stripe width based on available space and display data
95
106
  const stripeWidth = xMax / displayData.length
96
107
 
97
- const handleTooltip = (item: any) => {
98
- const xValue = item[xAxisDataKey]
99
- const yValue = item[valueKey]
100
- const formattedValue = formatNumber(yValue, 'left')
108
+ // Build tooltip data in COVE format and trigger showTooltip
109
+ const showStripeTooltip = useCallback(
110
+ (item: any, index: number, mouseY?: number) => {
111
+ const value = Number(item[valueKey])
112
+ if (isNaN(value)) return
113
+
114
+ const formattedValue = formatNumber(value, 'left')
115
+
116
+ // Pass raw x-axis value — TooltipListItem handles date formatting
117
+ const tooltipItems = [
118
+ [xAxisDataKey, item[xAxisDataKey]],
119
+ [valueKey, formattedValue, 'left']
120
+ ]
121
+
122
+ const dataXPosition = index * stripeWidth + stripeWidth / 2 + Number(config.yAxis.size)
123
+ const dataYPosition = mouseY ?? yMax / 2
124
+
125
+ showTooltip({
126
+ tooltipLeft: dataXPosition + 10,
127
+ tooltipTop: dataYPosition,
128
+ tooltipData: {
129
+ data: tooltipItems,
130
+ dataXPosition: dataXPosition + 10,
131
+ dataYPosition
132
+ }
133
+ })
134
+ },
135
+ [valueKey, xAxisDataKey, stripeWidth, config.yAxis.size, yMax, formatNumber, showTooltip]
136
+ )
137
+
138
+ // Handle incoming synchronized tooltip from sibling small multiple tiles
139
+ useEffect(() => {
140
+ if (synchronizedXValue === null || synchronizedXValue === undefined) {
141
+ setCurrentHover(null)
142
+ handleTooltipMouseOff()
143
+ return
144
+ }
145
+
146
+ const matchIndex = displayData.findIndex(item => String(item[xAxisDataKey]) === String(synchronizedXValue))
147
+
148
+ if (matchIndex >= 0) {
149
+ setCurrentHover(matchIndex)
150
+ showStripeTooltip(displayData[matchIndex], matchIndex)
151
+ }
152
+ }, [synchronizedXValue])
101
153
 
102
- return `<div>
103
- <strong>${config.xAxis.label || xAxisDataKey}:</strong> ${xValue}<br/>
104
- <strong>${config.runtime.seriesLabels?.[valueKey] || valueKey}:</strong> ${formattedValue}
105
- </div>`
154
+ if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
155
+ return null
106
156
  }
107
157
 
108
158
  return (
@@ -126,13 +176,10 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
126
176
  fill={fillColor}
127
177
  fillOpacity={isMuted ? 0.5 : 1}
128
178
  stroke='none'
129
- data-tooltip-html={handleTooltip(item)}
130
- data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
131
179
  tabIndex={-1}
132
180
  style={{ cursor: 'pointer', transition: 'fill-opacity 0.2s ease' }}
133
- onMouseEnter={() => {
181
+ onMouseEnter={e => {
134
182
  if (currentHover !== index) {
135
- // Only publish analytics event once per visualization (shared tracking)
136
183
  const vizId = String(config.runtime.uniqueId)
137
184
  if (!hasTrackedHover(vizId)) {
138
185
  publishAnalyticsEvent({
@@ -148,8 +195,31 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
148
195
  }
149
196
  setCurrentHover(index)
150
197
  }
198
+
199
+ // Show COVE tooltip using mouse Y position
200
+ const svgEl = (e.currentTarget as SVGRectElement).ownerSVGElement
201
+ const svgRect = svgEl?.getBoundingClientRect()
202
+ const mouseY = svgRect ? e.clientY - svgRect.top : yMax / 2
203
+ showStripeTooltip(item, index, mouseY)
204
+
205
+ if (handleSmallMultipleHover) {
206
+ handleSmallMultipleHover(item[xAxisDataKey], yMax / 2)
207
+ }
208
+ }}
209
+ onMouseMove={e => {
210
+ // Update tooltip Y position as mouse moves within the stripe
211
+ const svgEl = (e.currentTarget as SVGRectElement).ownerSVGElement
212
+ const svgRect = svgEl?.getBoundingClientRect()
213
+ const mouseY = svgRect ? e.clientY - svgRect.top : yMax / 2
214
+ showStripeTooltip(item, index, mouseY)
215
+ }}
216
+ onMouseLeave={() => {
217
+ setCurrentHover(null)
218
+ handleTooltipMouseOff()
219
+ if (handleSmallMultipleHover) {
220
+ handleSmallMultipleHover(null, null)
221
+ }
151
222
  }}
152
- onMouseLeave={() => setCurrentHover(null)}
153
223
  />
154
224
  )
155
225
  })}
@@ -45,7 +45,8 @@ const createInitialState = () => {
45
45
  showSuppressedSymbol: true,
46
46
  showZeroValueData: true,
47
47
  hideNullValue: true,
48
- palette: paletteDefaults
48
+ palette: paletteDefaults,
49
+ useIntelligentLineChartLabels: false
49
50
  },
50
51
  padding: {
51
52
  left: 5,
@@ -298,6 +299,18 @@ const createInitialState = () => {
298
299
  area: {
299
300
  isStacked: false
300
301
  },
302
+ radar: {
303
+ gridRings: 5,
304
+ showGridRings: true,
305
+ gridRingStyle: 'polygons',
306
+ scaleMin: 0,
307
+ scaleMax: '',
308
+ fillOpacity: 0.3,
309
+ showPoints: true,
310
+ pointRadius: 4,
311
+ strokeWidth: 2,
312
+ axisLabelOffset: 15
313
+ },
301
314
  sankey: {
302
315
  title: {
303
316
  defaultColor: 'black'
@@ -2,6 +2,10 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
2
2
  import _ from 'lodash'
3
3
  import { ChartConfig } from '../types/ChartConfig'
4
4
  export const getExcludedData = (newConfig: ChartConfig, data: object[]) => {
5
+ if (!Array.isArray(data)) {
6
+ console.warn('COVE: getExcludedData received non-array data:', typeof data)
7
+ return []
8
+ }
5
9
  let newExcludedData = data
6
10
  if (newConfig.exclusions && newConfig.exclusions.active) {
7
11
  if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
@@ -1,19 +1,19 @@
1
- export const handleChartAriaLabels = (state, testing = false) => {
2
- if (testing) console.log(`handleChartAriaLabels Testing On:`, state) // eslint-disable-line
3
- try {
4
- if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
5
- let ariaLabel = ''
6
-
7
- if (state.visualizationType) {
8
- ariaLabel += `${state.visualizationType} chart`
9
- }
10
-
11
- if (state.title && state.visualizationType) {
12
- ariaLabel += ` with the title: ${state.title}`
13
- }
14
-
15
- return ariaLabel
16
- } catch (e) {
17
- console.error('COVE: ', e.message) // eslint-disable-line
18
- }
19
- }
1
+ export const handleChartAriaLabels = state => {
2
+ try {
3
+ if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
4
+ let ariaLabel = ''
5
+
6
+ if (state.visualizationType) {
7
+ ariaLabel += `${state.visualizationType} chart`
8
+ }
9
+
10
+ if (state.title && state.visualizationType) {
11
+ ariaLabel += ` with the title: ${state.title}`
12
+ }
13
+
14
+ return ariaLabel
15
+ } catch (e) {
16
+ console.error('COVE: ', e.message) // eslint-disable-line
17
+ return 'Data visualization container'
18
+ }
19
+ }
@@ -1,18 +1,22 @@
1
- export const handleLineType = lineType => {
2
- switch (lineType) {
3
- case 'dashed-sm':
4
- return '5 5'
5
- case 'Dashed Small':
6
- return '5 5'
7
- case 'dashed-md':
8
- return '10 5'
9
- case 'Dashed Medium':
10
- return '10 5'
11
- case 'dashed-lg':
12
- return '15 5'
13
- case 'Dashed Large':
14
- return '15 5'
15
- default:
16
- return 0
17
- }
18
- }
1
+ const DASH_PATTERNS = {
2
+ SMALL: '5 5',
3
+ MEDIUM: '10 5',
4
+ LARGE: '15 5',
5
+ SOLID: 0
6
+ } as const
7
+
8
+ export const handleLineType = (lineType: string): string | number => {
9
+ switch (lineType) {
10
+ case 'dashed-sm':
11
+ case 'Dashed Small':
12
+ return DASH_PATTERNS.SMALL
13
+ case 'dashed-md':
14
+ case 'Dashed Medium':
15
+ return DASH_PATTERNS.MEDIUM
16
+ case 'dashed-lg':
17
+ case 'Dashed Large':
18
+ return DASH_PATTERNS.LARGE
19
+ default:
20
+ return DASH_PATTERNS.SOLID
21
+ }
22
+ }
@@ -8,6 +8,7 @@ interface UseProgrammaticTooltipProps {
8
8
  setShowHoverLine: (show: boolean) => void
9
9
  handleTooltipMouseOver: (event: MouseEvent, additionalChartData?: any) => void
10
10
  hideTooltip: () => void
11
+ setSynchronizedXValue?: (value: any) => void
11
12
  }
12
13
 
13
14
  /**
@@ -21,7 +22,8 @@ export const useProgrammaticTooltip = ({
21
22
  setPoint,
22
23
  setShowHoverLine,
23
24
  handleTooltipMouseOver,
24
- hideTooltip
25
+ hideTooltip,
26
+ setSynchronizedXValue
25
27
  }: UseProgrammaticTooltipProps) => {
26
28
  // Internal SVG ref for DOM manipulation
27
29
  const internalSvgRef = useRef<SVGSVGElement>(null)
@@ -50,6 +52,15 @@ export const useProgrammaticTooltip = ({
50
52
  * @param {number} yCoordinate - Exact Y coordinate to use
51
53
  */
52
54
  triggerTooltipAtDataValue: (xAxisValue: any, yCoordinate: number) => {
55
+ // Warming Stripes positions rects by index (with data sampling), not via xScale,
56
+ // so synthetic mouse events won't map to the correct data points.
57
+ // Route through synchronizedXValue state instead, which WarmingStripes
58
+ // resolves to the matching stripe and calls showTooltip directly.
59
+ if (config.visualizationType === 'Warming Stripes') {
60
+ setSynchronizedXValue?.(xAxisValue)
61
+ return
62
+ }
63
+
53
64
  const pixelX = getCoordinateFromXValue(xAxisValue)
54
65
  const adjustedX = pixelX + Number(config.yAxis.size || 0)
55
66
 
@@ -86,10 +97,20 @@ export const useProgrammaticTooltip = ({
86
97
  hideTooltip: () => {
87
98
  hideTooltip()
88
99
  setShowHoverLine(false)
100
+ setSynchronizedXValue?.(null)
89
101
  }
90
102
  })
91
103
  },
92
- [getCoordinateFromXValue, config.yAxis.size, setPoint, setShowHoverLine, handleTooltipMouseOver, hideTooltip]
104
+ [
105
+ getCoordinateFromXValue,
106
+ config.yAxis.size,
107
+ config.visualizationType,
108
+ setPoint,
109
+ setShowHoverLine,
110
+ handleTooltipMouseOver,
111
+ hideTooltip,
112
+ setSynchronizedXValue
113
+ ]
93
114
  )
94
115
 
95
116
  return internalSvgRef
@@ -121,6 +121,11 @@ const useScales = (properties: useScaleProps) => {
121
121
  range: [0, xMax]
122
122
  })
123
123
 
124
+ let yScaleAnnotation = scaleLinear({
125
+ domain: [0, 100],
126
+ range: [0, yMax]
127
+ })
128
+
124
129
  // handle Horizontal bars
125
130
  if (isHorizontal) {
126
131
  xScale = composeXScale({ min: min * 1.03, max, xMax, config })
@@ -372,6 +377,7 @@ const useScales = (properties: useScaleProps) => {
372
377
  g2xScale,
373
378
  xScaleNoPadding,
374
379
  xScaleAnnotation,
380
+ yScaleAnnotation,
375
381
  min,
376
382
  max,
377
383
  leftMax,
@@ -387,6 +393,7 @@ const getFirstDayOfMonth = ms => {
387
393
  }
388
394
 
389
395
  const dateFormatHasMonthButNoDays = dateFormat => {
396
+ if (!dateFormat) return false
390
397
  return (
391
398
  (dateFormat.includes('%b') ||
392
399
  dateFormat.includes('%B') ||
@@ -592,6 +592,7 @@ export const useTooltip = props => {
592
592
  case 'Line':
593
593
  case 'Area Chart':
594
594
  case 'Pie':
595
+ case 'Horizon Chart':
595
596
  return common
596
597
  case 'Combo':
597
598
  return [...common, ...ciItems]
@@ -600,6 +601,8 @@ export const useTooltip = props => {
600
601
 
601
602
  case 'Bar':
602
603
  return orientation === 'vertical' ? common : [runtime.yAxis.dataKey, ...runtime?.seriesKeys]
604
+ case 'Warming Stripes':
605
+ return common
603
606
  default:
604
607
  throw new Error('No visualization type found in handleTooltipMouseOver')
605
608
  }
@@ -146,6 +146,11 @@
146
146
  cursor: pointer;
147
147
  transition: 0.2s all;
148
148
 
149
+ &.not-clickable,
150
+ &.not-clickable .legend-item {
151
+ cursor: default;
152
+ }
153
+
149
154
  &.inactive {
150
155
  opacity: 0.5;
151
156
  transition: 0.2s all;
@@ -0,0 +1,68 @@
1
+ # Config Selectors
2
+
3
+ Typed selector functions and hooks for extracting memoizable slices of chart configuration.
4
+
5
+ ## Purpose
6
+
7
+ These selectors help:
8
+ 1. **Reduce coupling** - Components depend on specific config slices, not the entire config object
9
+ 2. **Improve memoization** - Hooks use specific dependencies instead of the full config
10
+ 3. **Enhance testability** - Selectors can be easily mocked in tests
11
+ 4. **Document config usage** - Selectors serve as documentation for which config properties are used together
12
+
13
+ ## Usage
14
+
15
+ ### Direct Selectors
16
+
17
+ ```typescript
18
+ import { selectAxisConfig, selectVisualizationConfig } from '../selectors'
19
+
20
+ // In a component
21
+ const axisConfig = selectAxisConfig(config)
22
+ // Returns: { xAxis, yAxis, runtime: { xAxis, yAxis, originalXAxis } }
23
+ ```
24
+
25
+ ### Hook Versions (Memoized)
26
+
27
+ ```typescript
28
+ import { useAxisConfig, useVisualizationConfig } from '../selectors'
29
+
30
+ // In a component - automatically memoized
31
+ const axisConfig = useAxisConfig(config)
32
+ const vizConfig = useVisualizationConfig(config)
33
+ ```
34
+
35
+ ## Available Selectors
36
+
37
+ | Selector | Hook | Description |
38
+ |----------|------|-------------|
39
+ | `selectAxisConfig` | `useAxisConfig` | X/Y axis configuration |
40
+ | `selectVisualizationConfig` | `useVisualizationConfig` | Chart type and orientation |
41
+ | `selectDisplayConfig` | `useDisplayConfig` | Animation, debug flags |
42
+ | `selectTooltipConfig` | `useTooltipConfig` | Tooltip settings |
43
+ | `selectLegendConfig` | - | Legend position and visibility |
44
+ | `selectDataFormatConfig` | - | Number formatting options |
45
+ | `selectChartMessages` | - | UI message strings |
46
+ | `selectBrushConfig` | - | Brush/filter settings |
47
+ | `selectForestPlotConfig` | - | Forest plot specific options |
48
+ | `selectSmallMultiplesConfig` | - | Small multiples mode |
49
+
50
+ ## Type Exports
51
+
52
+ Each selector has a corresponding type export:
53
+
54
+ ```typescript
55
+ import type { AxisConfig, VisualizationConfig } from '../selectors'
56
+ ```
57
+
58
+ ## Adding New Selectors
59
+
60
+ 1. Add the selector function that extracts config properties
61
+ 2. Optionally add a hook version with proper dependencies
62
+ 3. Export the type using `ReturnType<typeof selectXxx>`
63
+ 4. Update this README
64
+
65
+ ## Notes
66
+
67
+ - Selectors are created but integration into LinearChart is deferred
68
+ - Future work: gradually replace direct `config.x.y` accesses with selector usage
@@ -75,5 +75,7 @@ export const reducer = (state: ChartState, action: ChartActions): ChartState =>
75
75
  return { ...state, isDraggingAnnotation: action.payload }
76
76
  case 'SET_BRUSH_DATA':
77
77
  return { ...state, brushData: action.payload }
78
+ default:
79
+ return state
78
80
  }
79
81
  }
@@ -1,6 +1,7 @@
1
1
  import { Axis } from '@cdc/core/types/Axis'
2
2
  import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
3
3
  import { type ForestPlotConfigSettings } from './ForestPlot'
4
+ import { type HorizonConfigSettings } from './Horizon'
4
5
  import { type Column } from '@cdc/core/types/Column'
5
6
  import { type Series } from '@cdc/core/types/Series'
6
7
  import { Runtime } from '@cdc/core/types/Runtime'
@@ -18,6 +19,7 @@ type General = CoreGeneral & {
18
19
  customColors?: string[]
19
20
  customColorsOrdered?: string[]
20
21
  }
22
+ useIntelligentLineChartLabels?: boolean
21
23
  }
22
24
  import { type Link } from './../components/Sankey/types'
23
25
  import { type DataDescription } from '@cdc/core/types/DataDescription'
@@ -39,9 +41,11 @@ export type VisualizationType =
39
41
  | 'Box Plot'
40
42
  | 'Deviation Bar'
41
43
  | 'Forest Plot'
44
+ | 'Horizon Chart'
42
45
  | 'Line'
43
46
  | 'Paired Bar'
44
47
  | 'Pie'
48
+ | 'Radar'
45
49
  | 'Scatter Plot'
46
50
  | 'Spark Line'
47
51
  | 'Combo'
@@ -172,6 +176,7 @@ export type AllChartsConfig = {
172
176
  mobileVertical: number
173
177
  }
174
178
  highlightedBarValues: { value: any; color: string; borderWidth: number; legendLabel: string }[]
179
+ horizon?: HorizonConfigSettings
175
180
  introText: string
176
181
  isLollipopChart: boolean
177
182
  isLegendValue: boolean
@@ -267,6 +272,19 @@ export type AllChartsConfig = {
267
272
  default: string
268
273
  }
269
274
  }
275
+ radar?: {
276
+ gridRings: number
277
+ showGridRings: boolean
278
+ gridRingStyle: 'polygons' | 'circles'
279
+ scaleMin: number
280
+ scaleMax: number | string
281
+ showFill: boolean
282
+ fillOpacity: number
283
+ showPoints: boolean
284
+ pointRadius: number
285
+ strokeWidth: number
286
+ axisLabelOffset: number
287
+ }
270
288
  } & MarkupConfig
271
289
 
272
290
  type ForestPlotConfig = {
@@ -36,6 +36,7 @@ type SharedChartContext = {
36
36
  setLegendIsolateValues?: Function
37
37
  svgRef?: React.RefObject<SVGSVGElement>
38
38
  handleSmallMultipleHover?: (xAxisValue: any, yCoordinate: number) => void
39
+ visibleAnnotations?: Annotation[]
39
40
  }
40
41
 
41
42
  // Line Chart Specific Context
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Configuration settings for Horizon Chart visualization
3
+ *
4
+ * Horizon charts display multiple time series as stacked rows,
5
+ * with each series shown as layered bands to compress vertical space.
6
+ */
7
+
8
+ /**
9
+ * Palette configuration structure (matches general.palette structure)
10
+ */
11
+ export type HorizonPalette = {
12
+ name?: string
13
+ version?: '1.0' | '2.0'
14
+ isReversed?: boolean
15
+ customColors?: string[]
16
+ customColorsOrdered?: string[]
17
+ }
18
+
19
+ export type HorizonConfigSettings = {
20
+ /**
21
+ * Number of horizon layers/bands per series
22
+ * More layers = finer granularity, but denser visual
23
+ * @default 4
24
+ */
25
+ numLayers: number
26
+
27
+ /**
28
+ * Rendering mode for data values
29
+ * - 'offset': All values treated as positive (absolute values)
30
+ * - 'mirror': Negative values mirrored with contrasting colors. Not currently used.
31
+ * @default 'offset'
32
+ */
33
+ mode: 'offset' | 'mirror'
34
+
35
+ /**
36
+ * Gap in pixels between series bands
37
+ * Bands will fill available chart height minus gaps
38
+ * @default 15
39
+ */
40
+ bandGap: number
41
+
42
+ /**
43
+ * Padding in pixels below the bottom band (above x-axis)
44
+ * @default 15
45
+ */
46
+ bottomPadding: number
47
+
48
+ /**
49
+ * Optional secondary palette for negative values (mirror mode only)
50
+ * Uses same structure as general.palette for consistency
51
+ * Not exposed in UI initially - scaffolded for future use
52
+ */
53
+ negativePalette?: HorizonPalette
54
+ }
55
+
56
+ /**
57
+ * Default configuration for Horizon charts
58
+ */
59
+ export const HORIZON_DEFAULTS: HorizonConfigSettings = {
60
+ numLayers: 4,
61
+ mode: 'offset',
62
+ bandGap: 15,
63
+ bottomPadding: 15
64
+ }