@cdc/chart 4.25.7 → 4.25.10

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 (95) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/cdcchart.js +39551 -37016
  3. package/examples/feature/__data__/planet-example-data.json +0 -30
  4. package/examples/grouped-bar-test.json +400 -0
  5. package/examples/private/d.json +382 -0
  6. package/examples/private/example-2.json +49784 -0
  7. package/examples/private/f2.json +1 -0
  8. package/examples/private/f4.json +1577 -0
  9. package/examples/private/forecast.json +1180 -0
  10. package/examples/private/lollipop.json +468 -0
  11. package/examples/private/new.json +48756 -0
  12. package/examples/private/pie-chart-legend.json +904 -0
  13. package/examples/suppressed_tooltip.json +480 -0
  14. package/index.html +10 -22
  15. package/package.json +25 -7
  16. package/src/CdcChart.tsx +10 -4
  17. package/src/CdcChartComponent.tsx +188 -32
  18. package/src/_stories/Chart.Anchors.stories.tsx +2 -2
  19. package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
  20. package/src/_stories/Chart.CI.stories.tsx +1 -1
  21. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  22. package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
  23. package/src/_stories/Chart.Filters.stories.tsx +2 -2
  24. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  25. package/src/_stories/Chart.Patterns.stories.tsx +19 -0
  26. package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
  27. package/src/_stories/Chart.stories.tsx +8 -5
  28. package/src/_stories/Chart.tooltip.stories.tsx +1 -1
  29. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  30. package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
  31. package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
  32. package/src/_stories/ChartEditor.stories.tsx +60 -60
  33. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  34. package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
  35. package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
  36. package/src/_stories/_mock/stacked-pattern-test.json +520 -0
  37. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
  38. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  39. package/src/components/BarChart/components/BarChart.Horizontal.tsx +170 -25
  40. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +139 -6
  41. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
  42. package/src/components/BarChart/components/BarChart.Vertical.tsx +172 -23
  43. package/src/components/BarChart/helpers/index.ts +43 -4
  44. package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
  45. package/src/components/BarChart/helpers/useBarChart.ts +25 -3
  46. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
  47. package/src/components/Brush/BrushChart.tsx +65 -10
  48. package/src/components/Brush/BrushController.tsx +37 -5
  49. package/src/components/Brush/types.tsx +8 -0
  50. package/src/components/DeviationBar.jsx +9 -6
  51. package/src/components/EditorPanel/EditorPanel.tsx +364 -39
  52. package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +2 -2
  54. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +414 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +30 -54
  56. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +115 -120
  57. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  58. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
  59. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
  60. package/src/components/Forecasting/Forecasting.tsx +36 -6
  61. package/src/components/ForestPlot/ForestPlot.tsx +11 -7
  62. package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
  63. package/src/components/Legend/Legend.Component.tsx +110 -2
  64. package/src/components/Legend/Legend.tsx +3 -1
  65. package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
  66. package/src/components/LegendWrapper.tsx +1 -1
  67. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +27 -26
  68. package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
  69. package/src/components/LineChart/index.tsx +2 -2
  70. package/src/components/LinearChart.tsx +26 -9
  71. package/src/components/PairedBarChart.jsx +6 -4
  72. package/src/components/PieChart/PieChart.tsx +170 -54
  73. package/src/components/Sankey/components/Sankey.tsx +7 -1
  74. package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
  75. package/src/data/initial-state.js +315 -292
  76. package/src/helpers/buildForecastPaletteMappings.ts +112 -0
  77. package/src/helpers/buildForecastPaletteOptions.ts +109 -0
  78. package/src/helpers/getColorScale.ts +72 -8
  79. package/src/helpers/getNewRuntime.ts +1 -1
  80. package/src/helpers/getTransformedData.ts +1 -1
  81. package/src/hooks/useChartHoverAnalytics.tsx +44 -0
  82. package/src/hooks/useReduceData.ts +105 -70
  83. package/src/hooks/useTooltip.tsx +58 -16
  84. package/src/index.jsx +6 -3
  85. package/src/scss/main.scss +12 -0
  86. package/src/store/chart.reducer.ts +1 -1
  87. package/src/test/CdcChart.test.jsx +8 -3
  88. package/src/types/ChartConfig.ts +30 -6
  89. package/src/types/ChartContext.ts +1 -0
  90. package/vite.config.js +1 -1
  91. package/vitest.config.ts +16 -0
  92. package/src/coreStyles_chart.scss +0 -3
  93. package/src/helpers/configHelpers.ts +0 -28
  94. package/src/helpers/generateColorsArray.ts +0 -8
  95. package/src/hooks/useColorPalette.js +0 -76
@@ -3,13 +3,14 @@ import { Group } from '@visx/group'
3
3
  import { type Column } from '@cdc/core/types/Column'
4
4
  import React from 'react'
5
5
  import { type ChartConfig } from '../../../types/ChartConfig'
6
+ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
6
7
 
7
8
  type LineChartBumpCircleProp = {
8
- config: ChartConfig,
9
- xScale: any,
10
- yScale: any,
11
- parseDate: any
12
- }
9
+ config: ChartConfig
10
+ xScale: any
11
+ yScale: any
12
+ parseDate: any
13
+ }
13
14
 
14
15
  const LineChartBumpCircle = (props: LineChartBumpCircleProp) => {
15
16
  const { config, xScale, yScale, parseDate } = props
@@ -33,47 +34,47 @@ const LineChartBumpCircle = (props: LineChartBumpCircleProp) => {
33
34
  return xScale.bandwidth ? xScale.bandwidth() / 2 + Number(xValue) : Number(xValue)
34
35
  }
35
36
 
36
-
37
37
  const getListItems = dataRow => {
38
38
  return Object.values(config.columns)
39
- ?.filter(column => column.tooltips).map(column => {
40
- const label = column.label || column.name;
41
- return `
39
+ ?.filter(column => column.tooltips)
40
+ .map(column => {
41
+ const label = column.label || column.name
42
+ return `
42
43
  <li className='tooltip-body'>
43
44
  <strong>${label}</strong>: ${dataRow[column.name]}
44
- </li>`;
45
- })
46
- .join(' ');
47
- }
45
+ </li>`
46
+ })
47
+ .join(' ')
48
+ }
48
49
 
49
50
  const getTooltip = dataRow => `<ul> ${getListItems(dataRow)} </ul>`
50
51
 
51
- const circles = config.runtime?.series.map((series) => {
52
+ const circles = config.runtime?.series.map(series => {
52
53
  return config.data.map((d, dataIndex) => {
53
54
  let series_dataKey = d[series.dataKey]
54
55
  let axis_dataKey = d[config.xAxis.dataKey]
55
56
  return (
56
57
  <React.Fragment key={`bump-circle-${series_dataKey}-${dataIndex}`}>
57
- <Group left={Number(config.runtime.yAxis.size)}>
58
+ <Group left={Number(config.runtime.yAxis.size)}>
58
59
  {series_dataKey && (
59
60
  <>
60
- <circle
61
+ <circle
61
62
  key={`bump-circle-${series_dataKey}-${dataIndex}`}
62
- data-tooltip-html={getTooltip(d)}
63
- data-tooltip-id={`bump-chart`}
64
- r={10}
65
- cx={Number(checkBandScale(xScale(handleX(axis_dataKey))))}
66
- cy={Number(yScale(series_dataKey))}
67
- stroke='#CACACA'
68
- strokeWidth={1}
69
- fill='#E5E4E2'
63
+ data-tooltip-html={getTooltip(d)}
64
+ data-tooltip-id={`bump-chart`}
65
+ r={10}
66
+ cx={Number(checkBandScale(xScale(handleX(axis_dataKey))))}
67
+ cy={Number(yScale(series_dataKey))}
68
+ stroke='#CACACA'
69
+ strokeWidth={1}
70
+ fill='#E5E4E2'
70
71
  />
71
72
  {series_dataKey.toString().length === 2 ? (
72
73
  // prettier-ignore
73
74
  <text
74
75
  x={Number(checkBandScale(xScale(handleX(axis_dataKey)))) - 7}
75
76
  y={Number(yScale(series_dataKey)) + 4}
76
- fill='#000000'
77
+ fill={APP_FONT_COLOR}
77
78
  fontSize={11.5}
78
79
  >
79
80
  {series_dataKey}
@@ -83,7 +84,7 @@ const LineChartBumpCircle = (props: LineChartBumpCircleProp) => {
83
84
  <text
84
85
  x={Number(checkBandScale(xScale(handleX(axis_dataKey)))) - 4}
85
86
  y={Number(yScale(series_dataKey)) + 4}
86
- fill='#000000'
87
+ fill={APP_FONT_COLOR}
87
88
  fontSize={11.5}
88
89
  >
89
90
  {series_dataKey}
@@ -105,7 +105,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
105
105
  <g
106
106
  transform={transformShape(pointData[config.xAxis.dataKey], pointData[filtered?.dataKey])}
107
107
  className={`visx-glyph-group${displayArea ? '' : '-hidden'}`}
108
- data-seriesIndex={seriesIndex}
108
+ data-seriesindex={seriesIndex}
109
109
  >
110
110
  <Shape
111
111
  fillOpacity={mode === 'ALWAYS_SHOW_POINTS' ? 1 : 0}
@@ -142,7 +142,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
142
142
  <g
143
143
  transform={transformShape(pointData[config.xAxis?.dataKey], pointData[filtered?.dataKey])}
144
144
  className={`visx-glyph-group${displayArea ? '' : '-hidden'}`}
145
- data-seriesIndex={seriesIndex}
145
+ data-seriesindex={seriesIndex}
146
146
  >
147
147
  <Shape size={dotSize} stroke={color} fill={color} />
148
148
  </g>
@@ -228,10 +228,10 @@ const LineChart = (props: LineChartProps) => {
228
228
  />
229
229
 
230
230
  {suppressedSegments.map((segment, index) => {
231
- return Object.entries(segment.data).map(([key, value]) => {
231
+ return Object.entries(segment.data).map(([key, value], entryIndex) => {
232
232
  return (
233
233
  <LinePath
234
- key={index}
234
+ key={`${index}-${key}-${entryIndex}`}
235
235
  data={value}
236
236
  x={d => xPos(d)}
237
237
  y={d =>
@@ -5,6 +5,7 @@ import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
5
5
  import { Group } from '@visx/group'
6
6
  import { Line, Bar } from '@visx/shape'
7
7
  import { Tooltip as ReactTooltip } from 'react-tooltip'
8
+ import 'react-tooltip/dist/react-tooltip.css'
8
9
  import { Text } from '@visx/text'
9
10
  import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
10
11
  import _ from 'lodash'
@@ -29,7 +30,7 @@ import CategoricalYAxis from './Axis/Categorical.Axis'
29
30
  import BrushChart from './Brush/BrushController'
30
31
 
31
32
  // Helpers
32
- import { isLegendWrapViewport, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
33
+ import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
33
34
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
34
35
  import { calcInitialHeight, handleAutoPaddingRight } from '../helpers/sizeHelpers'
35
36
  import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
@@ -42,6 +43,7 @@ import useScales, { getTickValues } from '../hooks/useScales'
42
43
 
43
44
  import getTopAxis from '../helpers/getTopAxis'
44
45
  import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
46
+ import { useChartHoverAnalytics } from '../hooks/useChartHoverAnalytics'
45
47
  import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
46
48
  import Annotation from './Annotations'
47
49
  import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
@@ -90,6 +92,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
90
92
  handleChartAriaLabels,
91
93
  handleLineType,
92
94
  handleDragStateChange,
95
+ interactionLabel,
93
96
  isDraggingAnnotation,
94
97
  legendRef,
95
98
  parseDate,
@@ -97,7 +100,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
97
100
  tableData,
98
101
  transformedData: data,
99
102
  seriesHighlight,
100
-
103
+
101
104
  } = useContext(ConfigContext)
102
105
 
103
106
  // CONFIG
@@ -154,8 +157,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
154
157
  const labelsOverflow = inlineLabel && !inlineLabelHasNoSpace
155
158
  const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
156
159
  const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
157
- const tickLabelFontSize = isMobileHeightViewport(currentViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
158
- const axisLabelFontSize = isMobileHeightViewport(currentViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
160
+ const tickLabelFontSize = isMobileFontViewport(currentViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
161
+ const axisLabelFontSize = isMobileFontViewport(currentViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
159
162
  const GET_TEXT_WIDTH_FONT = `normal ${tickLabelFontSize}px Nunito, sans-serif`
160
163
 
161
164
  // zero if not forest plot
@@ -204,7 +207,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
204
207
  const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
205
208
  const yMax = initialHeight + forestRowsHeight
206
209
 
207
- const isNoDataAvailable = config.filters && config.filters.values.length === 0 && data.length === 0
210
+ const isNoDataAvailable = config.filters?.length > 0 && data.length === 0
208
211
 
209
212
  const getXAxisData = d =>
210
213
  isDateScale(config.runtime.xAxis)
@@ -268,8 +271,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
268
271
  yScale,
269
272
  seriesScale,
270
273
  showTooltip,
271
- hideTooltip
274
+ hideTooltip,
275
+ interactionLabel
276
+ })
277
+
278
+ // Analytics tracking for chart hover
279
+ const { handleChartMouseEnter, handleChartMouseLeave } = useChartHoverAnalytics({
280
+ config,
281
+ interactionLabel
272
282
  })
283
+
273
284
  // get the number of months between the first and last date
274
285
  const { dataKey } = runtime.xAxis
275
286
  const dateSpanMonths =
@@ -678,8 +689,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
678
689
  role='img'
679
690
  aria-label={handleChartAriaLabels(config)}
680
691
  style={{ overflow: 'visible' }}
681
- onMouseLeave={() => setShowHoverLine(false)}
682
- onMouseEnter={() => setShowHoverLine(true)}
692
+ onMouseLeave={() => {
693
+ setShowHoverLine(false)
694
+ handleChartMouseLeave()
695
+ }}
696
+ onMouseEnter={() => {
697
+ setShowHoverLine(true)
698
+ handleChartMouseEnter()
699
+ }}
683
700
  >
684
701
  {!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
685
702
  {/* GRID LINES */}
@@ -1403,7 +1420,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1403
1420
  : yMax
1404
1421
  }
1405
1422
  left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
1406
- label={config[section].label}
1423
+ label={runtime[section].label}
1407
1424
  tickFormat={handleBottomTickFormatting}
1408
1425
  scale={xScale}
1409
1426
  stroke='#333'
@@ -113,7 +113,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
113
113
  const textFits = textWidth < barWidth - 5 // minus padding dx(5)
114
114
 
115
115
  return (
116
- <>
116
+ <React.Fragment key={`fragment-group1-${groupOne.dataKey}-${index}`}>
117
117
  <Group key={`group-${groupOne.dataKey}-${d[config.xAxis.dataKey]}`} className='horizontal'>
118
118
  <Bar
119
119
  id={`bar-${groupOne.dataKey}-${d[config.dataDescription?.xKey]}`}
@@ -124,6 +124,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
124
124
  width={xScale(d[config.series[0].dataKey])}
125
125
  height={barHeight}
126
126
  fill={groupOne.color}
127
+ onMouseEnter={() => {}}
127
128
  data-tooltip-html={dataTipOne(d)}
128
129
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
129
130
  stroke='#333'
@@ -145,7 +146,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
145
146
  </Text>
146
147
  )}
147
148
  </Group>
148
- </>
149
+ </React.Fragment>
149
150
  )
150
151
  })}
151
152
  {data
@@ -171,7 +172,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
171
172
  const isTextFits = textWidth < barWidth - 5 // minus padding dx(5)
172
173
 
173
174
  return (
174
- <>
175
+ <React.Fragment key={`fragment-group2-${groupTwo.dataKey}-${index}`}>
175
176
  <style>
176
177
  {`
177
178
  .bar-${groupTwo.dataKey}-${d[config.xAxis.dataKey]} {
@@ -189,6 +190,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
189
190
  width={xScale(d[config.series[1].dataKey])}
190
191
  height={barHeight}
191
192
  fill={groupTwo.color}
193
+ onMouseEnter={() => {}}
192
194
  data-tooltip-html={dataTipTwo(d)}
193
195
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
194
196
  strokeWidth={borderWidth}
@@ -210,7 +212,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
210
212
  </Text>
211
213
  )}
212
214
  </Group>
213
- </>
215
+ </React.Fragment>
214
216
  )
215
217
  })}
216
218
  </Group>
@@ -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
@@ -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
  >