@cdc/chart 4.25.8 → 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.
- package/.claude/settings.local.json +9 -0
- package/dist/cdcchart.js +37524 -35243
- package/examples/feature/__data__/planet-example-data.json +0 -30
- package/examples/grouped-bar-test.json +400 -0
- package/examples/private/d.json +382 -0
- package/examples/private/example-2.json +49784 -0
- package/examples/private/f2.json +1 -0
- package/examples/private/f4.json +1577 -0
- package/examples/private/forecast.json +1180 -0
- package/examples/private/lollipop.json +468 -0
- package/examples/private/new.json +48756 -0
- package/examples/private/pie-chart-legend.json +904 -0
- package/examples/suppressed_tooltip.json +480 -0
- package/index.html +10 -22
- package/package.json +25 -7
- package/src/CdcChart.tsx +1 -2
- package/src/CdcChartComponent.tsx +174 -32
- package/src/_stories/Chart.Anchors.stories.tsx +2 -2
- package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
- package/src/_stories/Chart.CI.stories.tsx +1 -1
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
- package/src/_stories/Chart.Filters.stories.tsx +2 -2
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.Patterns.stories.tsx +19 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
- package/src/_stories/Chart.stories.tsx +8 -5
- package/src/_stories/Chart.tooltip.stories.tsx +1 -1
- package/src/_stories/ChartAnnotation.stories.tsx +1 -1
- package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
- package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
- package/src/_stories/ChartEditor.stories.tsx +60 -60
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
- package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
- package/src/_stories/_mock/stacked-pattern-test.json +520 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +159 -20
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
- package/src/components/BarChart/components/BarChart.Vertical.tsx +153 -21
- package/src/components/BarChart/helpers/index.ts +43 -4
- package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
- package/src/components/BarChart/helpers/useBarChart.ts +25 -3
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
- package/src/components/DeviationBar.jsx +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +364 -39
- package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +414 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +28 -20
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +115 -120
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
- package/src/components/Forecasting/Forecasting.tsx +36 -6
- package/src/components/ForestPlot/ForestPlot.tsx +11 -7
- package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
- package/src/components/Legend/Legend.Component.tsx +106 -13
- package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
- package/src/components/LegendWrapper.tsx +1 -1
- package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
- package/src/components/LineChart/index.tsx +2 -2
- package/src/components/LinearChart.tsx +22 -5
- package/src/components/PairedBarChart.jsx +6 -4
- package/src/components/PieChart/PieChart.tsx +170 -54
- package/src/components/Sankey/components/Sankey.tsx +7 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
- package/src/data/initial-state.js +315 -293
- package/src/helpers/buildForecastPaletteMappings.ts +112 -0
- package/src/helpers/buildForecastPaletteOptions.ts +109 -0
- package/src/helpers/getColorScale.ts +72 -8
- package/src/helpers/getNewRuntime.ts +1 -1
- package/src/helpers/getTransformedData.ts +1 -1
- package/src/hooks/useChartHoverAnalytics.tsx +44 -0
- package/src/hooks/useReduceData.ts +105 -70
- package/src/hooks/useTooltip.tsx +57 -15
- package/src/index.jsx +0 -2
- package/src/scss/main.scss +12 -0
- package/src/store/chart.reducer.ts +1 -1
- package/src/test/CdcChart.test.jsx +8 -3
- package/src/types/ChartConfig.ts +30 -6
- package/src/types/ChartContext.ts +1 -0
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/coreStyles_chart.scss +0 -3
- package/src/helpers/configHelpers.ts +0 -28
- package/src/helpers/generateColorsArray.ts +0 -8
- package/src/hooks/useColorPalette.js +0 -76
|
@@ -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'
|
|
@@ -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,
|
|
@@ -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
|
|
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
|
|
272
276
|
})
|
|
277
|
+
|
|
278
|
+
// Analytics tracking for chart hover
|
|
279
|
+
const { handleChartMouseEnter, handleChartMouseLeave } = useChartHoverAnalytics({
|
|
280
|
+
config,
|
|
281
|
+
interactionLabel
|
|
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={() =>
|
|
682
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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(
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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 '
|
|
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,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.
|
|
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
|
})
|