@cdc/chart 4.25.8 → 4.25.11

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 (145) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  3. package/dist/cdcchart.js +44236 -40355
  4. package/examples/feature/__data__/planet-example-data.json +0 -30
  5. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  6. package/examples/grouped-bar-test.json +400 -0
  7. package/examples/private/DEV-11825.json +573 -0
  8. package/examples/private/d.json +382 -0
  9. package/examples/private/example-2.json +49784 -0
  10. package/examples/private/f2.json +1 -0
  11. package/examples/private/f4.json +1577 -0
  12. package/examples/private/forecast.json +1180 -0
  13. package/examples/private/lollipop.json +468 -0
  14. package/examples/private/na.json +913 -0
  15. package/examples/private/new.json +48756 -0
  16. package/examples/private/pie-chart-legend.json +904 -0
  17. package/examples/private/test-data.csv +28 -0
  18. package/examples/suppressed_tooltip.json +480 -0
  19. package/index.html +2 -133
  20. package/package.json +25 -7
  21. package/src/CdcChart.tsx +9 -13
  22. package/src/CdcChartComponent.tsx +403 -92
  23. package/src/_stories/Chart.Anchors.stories.tsx +2 -2
  24. package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
  25. package/src/_stories/Chart.CI.stories.tsx +1 -1
  26. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  27. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  28. package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
  29. package/src/_stories/Chart.Filters.stories.tsx +2 -2
  30. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  31. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  32. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  33. package/src/_stories/Chart.Patterns.stories.tsx +20 -0
  34. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  35. package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
  36. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  37. package/src/_stories/Chart.stories.tsx +8 -5
  38. package/src/_stories/Chart.tooltip.stories.tsx +1 -1
  39. package/src/_stories/ChartAnnotation.stories.tsx +7 -4
  40. package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
  41. package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
  42. package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
  43. package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
  44. package/src/_stories/ChartEditor.stories.tsx +59 -60
  45. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  46. package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
  47. package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
  48. package/src/_stories/_mock/combo.json +451 -0
  49. package/src/_stories/_mock/editor-test-configs.json +376 -0
  50. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  51. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  52. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  53. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  54. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  55. package/src/_stories/_mock/pie_config.json +257 -62
  56. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  57. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  58. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  59. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  60. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  61. package/src/_stories/_mock/stacked-pattern-test.json +520 -0
  62. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
  63. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  64. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  65. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
  66. package/src/components/AreaChart/index.tsx +1 -2
  67. package/src/components/BarChart/components/BarChart.Horizontal.tsx +161 -22
  68. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
  69. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
  70. package/src/components/BarChart/components/BarChart.Vertical.tsx +155 -22
  71. package/src/components/BarChart/helpers/index.ts +43 -4
  72. package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
  73. package/src/components/BarChart/helpers/useBarChart.ts +25 -3
  74. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
  75. package/src/components/BoxPlot/helpers/index.ts +3 -3
  76. package/src/components/Brush/BrushChart.tsx +1 -1
  77. package/src/components/DeviationBar.jsx +9 -6
  78. package/src/components/EditorPanel/EditorPanel.tsx +563 -229
  79. package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
  80. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  81. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
  82. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +461 -0
  83. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +80 -67
  84. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
  85. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +188 -139
  86. package/src/components/EditorPanel/components/Panels/index.tsx +5 -1
  87. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
  88. package/src/components/EditorPanel/editor-panel.scss +0 -20
  89. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
  90. package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
  91. package/src/components/Forecasting/Forecasting.tsx +175 -27
  92. package/src/components/ForestPlot/ForestPlot.tsx +11 -7
  93. package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
  94. package/src/components/Legend/Legend.Component.tsx +114 -14
  95. package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
  96. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  97. package/src/components/LegendWrapper.tsx +1 -1
  98. package/src/components/LineChart/LineChartProps.ts +0 -3
  99. package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
  100. package/src/components/LineChart/helpers.ts +1 -1
  101. package/src/components/LineChart/index.tsx +38 -15
  102. package/src/components/LinearChart.tsx +96 -84
  103. package/src/components/PairedBarChart.jsx +6 -4
  104. package/src/components/PieChart/PieChart.tsx +170 -54
  105. package/src/components/Regions/components/Regions.tsx +3 -24
  106. package/src/components/Sankey/components/Sankey.tsx +7 -1
  107. package/src/components/Sankey/types/index.ts +1 -1
  108. package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
  109. package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
  110. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  111. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  112. package/src/components/SmallMultiples/index.ts +2 -0
  113. package/src/data/initial-state.js +327 -293
  114. package/src/helpers/buildForecastPaletteMappings.ts +112 -0
  115. package/src/helpers/buildForecastPaletteOptions.ts +71 -0
  116. package/src/helpers/getColorScale.ts +82 -8
  117. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
  118. package/src/helpers/getNewRuntime.ts +1 -1
  119. package/src/helpers/getTransformedData.ts +1 -1
  120. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  121. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  122. package/src/hooks/useChartHoverAnalytics.tsx +44 -0
  123. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  124. package/src/hooks/useReduceData.ts +105 -70
  125. package/src/hooks/useScales.ts +88 -34
  126. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  127. package/src/hooks/useTooltip.tsx +116 -29
  128. package/src/index.jsx +0 -2
  129. package/src/scss/main.scss +13 -80
  130. package/src/store/chart.actions.ts +2 -0
  131. package/src/store/chart.reducer.ts +5 -1
  132. package/src/test/CdcChart.test.jsx +8 -3
  133. package/src/types/ChartConfig.ts +53 -11
  134. package/src/types/ChartContext.ts +4 -0
  135. package/vite.config.js +1 -1
  136. package/vitest.config.ts +16 -0
  137. package/src/_stories/_mock/pie_data.json +0 -218
  138. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  139. package/src/coreStyles_chart.scss +0 -3
  140. package/src/helpers/configHelpers.ts +0 -28
  141. package/src/helpers/generateColorsArray.ts +0 -8
  142. package/src/helpers/sort.ts +0 -7
  143. package/src/hooks/useActiveElement.js +0 -19
  144. package/src/hooks/useChartClasses.js +0 -41
  145. package/src/hooks/useColorPalette.js +0 -76
@@ -7,10 +7,18 @@ import { Group } from '@visx/group'
7
7
  import { Text } from '@visx/text'
8
8
  import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
9
9
  import { colorPalettesChart as colorPalettes } from '@cdc/core/data/colorPalettes'
10
+ import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
11
+ import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
12
+ import {
13
+ v2ColorDistribution,
14
+ divergentColorDistribution,
15
+ colorblindColorDistribution
16
+ } from '@cdc/core/helpers/palettes/colorDistributions'
10
17
 
11
18
  // cove
12
- import ConfigContext from '../../ConfigContext'
19
+ import ConfigContext, { ChartDispatchContext } from '../../ConfigContext'
13
20
  import { useTooltip as useCoveTooltip } from '../../hooks/useTooltip'
21
+ import { useChartHoverAnalytics } from '../../hooks/useChartHoverAnalytics'
14
22
  import useIntersectionObserver from '../../hooks/useIntersectionObserver'
15
23
  import { handleChartAriaLabels } from '../../helpers/handleChartAriaLabels'
16
24
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
@@ -25,7 +33,14 @@ type TooltipData = {
25
33
  dataYPosition: number
26
34
  }
27
35
 
28
- const PieChart = props => {
36
+ type PieChartProps = {
37
+ parentWidth?: number
38
+ parentHeight?: number
39
+ interactionLabel?: string
40
+ }
41
+
42
+ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) => {
43
+ const { interactionLabel = '' } = props
29
44
  const {
30
45
  transformedData: data,
31
46
  config,
@@ -34,13 +49,22 @@ const PieChart = props => {
34
49
  seriesHighlight,
35
50
  isDraggingAnnotation
36
51
  } = useContext(ConfigContext)
52
+ const dispatch = useContext(ChartDispatchContext)
37
53
  const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } = useTooltip<TooltipData>()
38
54
  const { handleTooltipMouseOver, handleTooltipMouseOff, TooltipListItem } = useCoveTooltip({
39
55
  xScale: false,
40
56
  yScale: false,
41
57
  showTooltip,
42
- hideTooltip
58
+ hideTooltip,
59
+ interactionLabel
60
+ })
61
+
62
+ // Analytics tracking for chart hover
63
+ const { handleChartMouseEnter, handleChartMouseLeave } = useChartHoverAnalytics({
64
+ config,
65
+ interactionLabel
43
66
  })
67
+
44
68
  const [filteredData, setFilteredData] = useState(undefined)
45
69
  const [animatedPie, setAnimatePie] = useState(false)
46
70
  const pivotColumns = Object.values(config.columns).filter(column => column.showInViz)
@@ -90,42 +114,100 @@ const PieChart = props => {
90
114
  }
91
115
 
92
116
  return baseData
93
- }, [data, dataNeedsPivot, showPercentage, config])
117
+ }, [
118
+ data,
119
+ dataNeedsPivot,
120
+ showPercentage,
121
+ config.yAxis.dataKey,
122
+ config.xAxis.dataKey,
123
+ config.runtime.yAxis.dataKey,
124
+ config.runtime.xAxis.dataKey
125
+ ])
126
+
127
+ // Helper function to determine enhanced distribution type and apply it
128
+ const applyEnhancedColorDistribution = (config, palette, numberOfKeys) => {
129
+ const version = getColorPaletteVersion(config)
130
+ const configPalette = config.general?.palette?.name || config.palette
131
+
132
+ // Skip enhanced distribution if not v2, too many keys, or wrong palette length
133
+ if (version !== 2 || numberOfKeys > 9 || palette.length !== 9) {
134
+ return palette.slice(0, numberOfKeys)
135
+ }
136
+
137
+ const isSequential = configPalette && configPalette.includes('sequential')
138
+ const isDivergent = configPalette && configPalette.includes('divergent')
139
+ const isColorblindSafe =
140
+ configPalette && (configPalette.includes('colorblindsafe') || configPalette.includes('qualitative_standard'))
141
+
142
+ // Determine which distribution to use based on palette type
143
+ let distributionMap = null
144
+ if (isDivergent) {
145
+ distributionMap = divergentColorDistribution
146
+ } else if (isColorblindSafe) {
147
+ distributionMap = colorblindColorDistribution
148
+ } else if (isSequential) {
149
+ distributionMap = v2ColorDistribution
150
+ }
151
+
152
+ if (distributionMap && distributionMap[numberOfKeys]) {
153
+ const distributionIndices = distributionMap[numberOfKeys]
154
+ return distributionIndices.map((index: number) => palette[index])
155
+ }
156
+
157
+ return palette.slice(0, numberOfKeys)
158
+ }
159
+
160
+ // Helper function to extract keys from data
161
+ const extractDataKeys = (data, dataKey) => {
162
+ const keys = {}
163
+ data.forEach(d => {
164
+ if (!keys[d[dataKey]]) keys[d[dataKey]] = true
165
+ })
166
+ return Object.keys(keys)
167
+ }
168
+
169
+ // Helper function to create color scale for pie charts
170
+ const createPieColorScale = (data, config, isPercentageMode = false, labelForCalcArea = null) => {
171
+ const dataKeys = extractDataKeys(data, config.xAxis.dataKey)
172
+ const domainKeys = isPercentageMode ? dataKeys.filter(k => k !== labelForCalcArea) : dataKeys
173
+ const numberOfKeys = domainKeys.length
174
+
175
+ let palette = getPaletteColors(config, colorPalettes)
176
+ palette = applyEnhancedColorDistribution(config, palette, numberOfKeys)
177
+
178
+ const unknownColor = isPercentageMode
179
+ ? getComputedStyle(document.documentElement).getPropertyValue('--cool-gray-10').trim()
180
+ : null
181
+
182
+ return scaleOrdinal({
183
+ domain: domainKeys,
184
+ range: palette,
185
+ unknown: unknownColor
186
+ })
187
+ }
94
188
 
95
189
  const _colorScale = useMemo(() => {
190
+ // Always use the full _data for color scale to ensure legend shows all items
96
191
  if (dataNeedsPivot) {
97
- const keys = {}
98
- _data.forEach(d => {
99
- if (!keys[d[config.xAxis.dataKey]]) keys[d[config.xAxis.dataKey]] = true
100
- })
101
- const numberOfKeys = Object.entries(keys).length
102
- let palette = config.customColors || colorPalettes[config.palette]
103
- palette = palette.slice(0, numberOfKeys)
104
- return scaleOrdinal({
105
- domain: Object.keys(keys),
106
- range: palette,
107
- unknown: null
108
- })
192
+ return createPieColorScale(_data, config)
109
193
  }
110
194
 
111
195
  if (showPercentage) {
112
- const keys = {}
113
- _data.forEach(d => {
114
- keys[d[config.xAxis.dataKey]] = true
115
- })
116
- // take out Calculated Area so it falls back to `unknown`
117
- const domainKeys = Object.keys(keys).filter(k => k !== labelForCalcArea)
118
-
119
- const basePalette = (config.customColors || colorPalettes[config.palette]).slice(0, domainKeys.length)
120
- const COOL_GRAY_90 = getComputedStyle(document.documentElement).getPropertyValue('--cool-gray-10').trim()
121
- return scaleOrdinal({
122
- domain: domainKeys,
123
- range: basePalette,
124
- unknown: COOL_GRAY_90
125
- })
196
+ return createPieColorScale(_data, config, true, labelForCalcArea)
126
197
  }
127
- return colorScale
128
- }, [_data, dataNeedsPivot, colorScale])
198
+
199
+ // Handle normal pie chart case
200
+ return createPieColorScale(_data, config)
201
+ }, [
202
+ _data,
203
+ dataNeedsPivot,
204
+ showPercentage,
205
+ config.xAxis.dataKey,
206
+ config.general?.palette?.name,
207
+ config.general?.palette?.isReversed,
208
+ config.general?.palette?.customColors,
209
+ config.palette
210
+ ])
129
211
 
130
212
  const triggerRef = useRef()
131
213
  const dataRef = useIntersectionObserver(triggerRef, {
@@ -137,9 +219,9 @@ const PieChart = props => {
137
219
  const element = document.querySelector('.isEditor')
138
220
  if (element) {
139
221
  // parent element is visible
140
- setAnimatePie(prevState => true)
222
+ setAnimatePie(true)
141
223
  }
142
- })
224
+ }, [])
143
225
 
144
226
  useEffect(() => {
145
227
  if (dataRef?.isIntersecting && config.animate && !animatedPie) {
@@ -178,14 +260,22 @@ const PieChart = props => {
178
260
  roundedPercentage = '**'
179
261
  }
180
262
 
263
+ // Determine if this slice should be muted based on legend behavior
264
+ const isHighlighted =
265
+ seriesHighlight.length === 0 || seriesHighlight.indexOf(arc.data[config.runtime.xAxis.dataKey]) !== -1
266
+ const shouldMute = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && !isHighlighted
267
+ const sliceOpacity = shouldMute ? 0.3 : 1
268
+ const textOpacity = shouldMute ? 0.3 : 1
269
+
181
270
  return (
182
271
  <Group key={key} className={`slice-${key}`}>
183
272
  {/* ── the slice */}
184
273
  <animated.path
185
- d={to([styles.startAngle, styles.endAngle], (start, end) =>
274
+ d={to([styles.startAngle, styles.endAngle], (start: number, end: number) =>
186
275
  path({ ...arc, startAngle: start, endAngle: end })
187
276
  )}
188
277
  fill={colorScale(key)}
278
+ opacity={sliceOpacity}
189
279
  onMouseEnter={e =>
190
280
  onHover(e, {
191
281
  data: arc.data,
@@ -201,7 +291,7 @@ const PieChart = props => {
201
291
  {/* ── the percentage label */}
202
292
  {arc.endAngle - arc.startAngle > 0.1 && (
203
293
  <animated.text
204
- transform={to([styles.startAngle, styles.endAngle], (start, end) => {
294
+ transform={to([styles.startAngle, styles.endAngle], (start: number, end: number) => {
205
295
  const [x, y] = path.centroid({
206
296
  ...arc,
207
297
  startAngle: start,
@@ -212,6 +302,7 @@ const PieChart = props => {
212
302
  textAnchor='middle'
213
303
  pointerEvents='none'
214
304
  fill={textColor}
305
+ opacity={textOpacity}
215
306
  >
216
307
  {/** compute text inside the spring callback */}
217
308
  {roundedPercentage}
@@ -252,6 +343,27 @@ const PieChart = props => {
252
343
  }
253
344
  }, [seriesHighlight]) // eslint-disable-line
254
345
 
346
+ // Update the context colorScale when the pie chart's colorScale changes
347
+ // This ensures the Legend component uses the same colors as the pie chart
348
+ const prevColorScaleRef = useRef<{ domain: string; range: string } | null>(null)
349
+
350
+ useEffect(() => {
351
+ if (_colorScale && config.visualizationType === 'Pie') {
352
+ // Only dispatch if the domain or range has actually changed
353
+ const currentDomain = JSON.stringify(_colorScale.domain())
354
+ const currentRange = JSON.stringify(_colorScale.range())
355
+ const colorScaleKey = `${currentDomain}|${currentRange}`
356
+ const prevKey = prevColorScaleRef.current
357
+ ? `${prevColorScaleRef.current.domain}|${prevColorScaleRef.current.range}`
358
+ : null
359
+
360
+ if (colorScaleKey !== prevKey) {
361
+ prevColorScaleRef.current = { domain: currentDomain, range: currentRange }
362
+ dispatch({ type: 'SET_COLOR_SCALE', payload: _colorScale })
363
+ }
364
+ }
365
+ }, [_colorScale, config.visualizationType, dispatch])
366
+
255
367
  const getSvgClasses = () => {
256
368
  let classes = ['animated-pie', 'group']
257
369
  if (config.animate === false || animatedPie) {
@@ -269,26 +381,31 @@ const PieChart = props => {
269
381
  className={getSvgClasses()}
270
382
  role='img'
271
383
  aria-label={handleChartAriaLabels(config)}
384
+ onMouseEnter={handleChartMouseEnter}
385
+ onMouseLeave={() => {
386
+ handleTooltipMouseOff()
387
+ handleChartMouseLeave()
388
+ }}
272
389
  >
273
390
  <Group top={centerY} left={radius}>
274
391
  {/* prettier-ignore */}
275
392
  <Pie
276
- data={filteredData || _data}
277
- pieValue={d => parseFloat(d[pivotKey || config.runtime.yAxis.dataKey])}
278
- pieSortValues={() => -1}
279
- innerRadius={radius - donutThickness}
280
- outerRadius={radius}
281
- >
282
- {pie => (
283
- <AnimatedPie
284
- {...pie}
285
- getKey={d => d.data[config.runtime.xAxis.dataKey]}
286
- colorScale={_colorScale}
287
- onHover={handleTooltipMouseOver}
288
- onLeave={handleTooltipMouseOff}
289
- />
290
- )}
291
- </Pie>
393
+ data={filteredData || _data}
394
+ pieValue={d => parseFloat(d[pivotKey || config.runtime.yAxis.dataKey])}
395
+ pieSortValues={() => -1}
396
+ innerRadius={radius - donutThickness}
397
+ outerRadius={radius}
398
+ >
399
+ {pie => (
400
+ <AnimatedPie
401
+ {...pie}
402
+ getKey={d => d.data[config.runtime.xAxis.dataKey]}
403
+ colorScale={_colorScale}
404
+ onHover={handleTooltipMouseOver}
405
+ onLeave={handleTooltipMouseOff}
406
+ />
407
+ )}
408
+ </Pie>
292
409
  </Group>
293
410
  </svg>
294
411
  <div ref={triggerRef} />
@@ -304,7 +421,6 @@ const PieChart = props => {
304
421
  config.tooltips.opacity / 100
305
422
  }) !important`}</style>
306
423
  <TooltipWithBounds
307
- key={Math.random()}
308
424
  className={'tooltip cdc-open-viz-module'}
309
425
  left={tooltipLeft + centerX - radius}
310
426
  top={tooltipTop}
@@ -319,6 +435,6 @@ const PieChart = props => {
319
435
  </ErrorBoundary>
320
436
  </>
321
437
  )
322
- }
438
+ })
323
439
 
324
440
  export default PieChart
@@ -1,4 +1,4 @@
1
- import React, { MouseEventHandler, useContext } from 'react'
1
+ import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../../ConfigContext'
3
3
  import { ChartContext } from '../../../types/ChartContext'
4
4
  import { Text } from '@visx/text'
@@ -10,27 +10,10 @@ type RegionsProps = {
10
10
  yMax: number
11
11
  barWidth?: number
12
12
  totalBarsInGroup?: number
13
- handleTooltipMouseOff: MouseEventHandler<SVGElement>
14
- handleTooltipMouseOver: MouseEventHandler<SVGElement>
15
- handleTooltipClick: MouseEventHandler<SVGElement>
16
- tooltipData: unknown
17
- showTooltip: Function
18
- hideTooltip: Function
19
13
  }
20
14
 
21
15
  // TODO: should regions be removed on categorical axis?
22
- const Regions: React.FC<RegionsProps> = ({
23
- xScale,
24
- barWidth = 0,
25
- totalBarsInGroup = 1,
26
- yMax,
27
- handleTooltipMouseOff,
28
- handleTooltipMouseOver,
29
- handleTooltipClick,
30
- tooltipData,
31
- showTooltip,
32
- hideTooltip
33
- }) => {
16
+ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax }) => {
34
17
  const { parseDate, config } = useContext<ChartContext>(ConfigContext)
35
18
 
36
19
  const { runtime, regions, visualizationType, orientation, xAxis } = config
@@ -181,11 +164,7 @@ const Regions: React.FC<RegionsProps> = ({
181
164
  fill='red'
182
165
  className='regions regions-group--line zzz'
183
166
  key={region.label}
184
- onMouseMove={handleTooltipMouseOver}
185
- onMouseLeave={handleTooltipMouseOff}
186
- handleTooltipClick={handleTooltipClick}
187
- tooltipData={JSON.stringify(tooltipData)}
188
- showTooltip={showTooltip}
167
+ pointerEvents='none'
189
168
  >
190
169
  <HighlightedArea />
191
170
  <Text x={from + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
@@ -9,7 +9,7 @@ import { Text } from '@visx/text'
9
9
  // Cdc
10
10
  import './../sankey.scss'
11
11
  import 'react-tooltip/dist/react-tooltip.css'
12
- import ConfigContext from '@cdc/chart/src/ConfigContext'
12
+ import ConfigContext from '../../../ConfigContext'
13
13
  import type { ChartContext } from '../../../types/ChartContext'
14
14
  import type { SankeyNode, SankeyProps } from '../types'
15
15
  import useSankeyAlert from '../useSankeyAlert'
@@ -171,6 +171,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
171
171
  fill={nodeColor}
172
172
  fillOpacity={opacityValue}
173
173
  rx={sankeyConfig.rxValue}
174
+ onMouseEnter={() => {}}
174
175
  // todo: move enable tooltips to sankey
175
176
  data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
176
177
  data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
@@ -204,6 +205,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
204
205
  className='node-text'
205
206
  style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
206
207
  onClick={() => handleNodeClick(node.id)}
208
+ onMouseEnter={() => {}}
207
209
  data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
208
210
  data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
209
211
  >
@@ -220,6 +222,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
220
222
  textAnchor='start'
221
223
  style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
222
224
  onClick={() => handleNodeClick(node.id)}
225
+ onMouseEnter={() => {}}
223
226
  data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
224
227
  data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
225
228
  >
@@ -237,6 +240,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
237
240
  verticalAnchor='start'
238
241
  style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
239
242
  onClick={() => handleNodeClick(node.id)}
243
+ onMouseEnter={() => {}}
240
244
  data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
241
245
  data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
242
246
  >
@@ -248,6 +252,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
248
252
  <Text
249
253
  style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
250
254
  onClick={() => handleNodeClick(node.id)}
255
+ onMouseEnter={() => {}}
251
256
  data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
252
257
  data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
253
258
  x={node.x0! + textPositionHorizontal}
@@ -270,6 +275,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
270
275
  textAnchor='start'
271
276
  style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
272
277
  onClick={() => handleNodeClick(node.id)}
278
+ onMouseEnter={() => {}}
273
279
  data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
274
280
  data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
275
281
  >
@@ -1,6 +1,6 @@
1
1
  export type Link = { source: string; target: string; value: number; id: string }
2
2
 
3
- export type Data = {
3
+ type Data = {
4
4
  links: Link[]
5
5
  }
6
6
 
@@ -1,7 +1,9 @@
1
- import React, { useContext } from 'react'
1
+ import React, { useContext, useState } from 'react'
2
2
  import ConfigContext from '../../ConfigContext'
3
3
  import { Group } from '@visx/group'
4
4
  import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
5
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
6
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
5
7
 
6
8
  const ScatterPlot = ({ xScale, yScale }) => {
7
9
  const {
@@ -10,12 +12,17 @@ const ScatterPlot = ({ xScale, yScale }) => {
10
12
  tableData,
11
13
  formatNumber,
12
14
  seriesHighlight,
13
- colorPalettes
15
+ colorPalettes,
16
+ colorScale,
17
+ interactionLabel
14
18
  } = useContext(ConfigContext)
15
19
 
16
20
  // TODO: copied from line chart should probably be a constant somewhere.
17
21
  const circleRadii = 4.5
18
22
  const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
23
+
24
+ // Track current hover for analytics
25
+ const [currentHover, setCurrentHover] = useState({ dataIndex: null, seriesKey: null })
19
26
  // tooltips for additional columns
20
27
  const additionalColumns = Object.entries(config.columns)
21
28
  .filter(([_, value]) => value.tooltips)
@@ -52,14 +59,14 @@ const ScatterPlot = ({ xScale, yScale }) => {
52
59
  return config.runtime.seriesKeys.map((s, index) => {
53
60
  const transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s) === -1
54
61
  const displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(s) !== -1
55
- const seriesColor = config?.customColors ? config.customColors[index] : config.palette ? colorPalettes[config.palette][index] : '#000'
62
+ const seriesColor = config?.general?.palette?.customColors ? config.general.palette.customColors[index] : colorScale(config.runtime.seriesLabels?.[s] || s)
56
63
 
57
64
  let pointStyles = {
58
65
  filter: 'unset',
59
66
  opacity: 1,
60
67
  stroke: displayArea ? 'black' : ''
61
68
  }
62
- if (item[s]==='') {
69
+ if (item[s] === '') {
63
70
  return <> </>
64
71
  }
65
72
 
@@ -77,6 +84,27 @@ const ScatterPlot = ({ xScale, yScale }) => {
77
84
  data-tooltip-html={handleTooltip(item, s, dataIndex)}
78
85
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
79
86
  tabIndex={-1}
87
+ onMouseEnter={() => {
88
+ // Track hover analytics event if this is a new hover
89
+ if ((currentHover.dataIndex !== dataIndex || currentHover.seriesKey !== s)) {
90
+ const seriesName = config.runtime.seriesLabels?.[s] || s
91
+ const safeSeriesName = String(seriesName).replace(/[^a-zA-Z0-9]/g, '_')
92
+ const xAxisValue = item[config.xAxis.dataKey]
93
+ const yAxisValue = item[s]
94
+
95
+ publishAnalyticsEvent({
96
+ vizType: config?.type,
97
+ vizSubType: getVizSubType(config),
98
+ eventType: `chart_hover`,
99
+ eventAction: 'hover',
100
+ eventLabel: interactionLabel || 'unknown',
101
+ vizTitle: getVizTitle(config),
102
+ series: seriesName,
103
+ specifics: `series: ${String(safeSeriesName).toLowerCase()}, ${config.xAxis.dataKey}: ${xAxisValue}, ${s}: ${yAxisValue}`
104
+ })
105
+ setCurrentHover({ dataIndex, seriesKey: s })
106
+ }
107
+ }}
80
108
  />
81
109
  )
82
110
  })