@cdc/chart 4.25.5-1 → 4.25.6
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/LICENSE +201 -0
- package/dist/cdcchart.js +32848 -27824
- package/index.html +130 -130
- package/package.json +2 -2
- package/src/CdcChartComponent.tsx +66 -26
- package/src/_stories/Chart.stories.tsx +99 -93
- package/src/_stories/ChartPrefixSuffix.stories.tsx +29 -32
- package/src/_stories/_mock/pie_calculated_area.json +417 -0
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -13
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +3 -14
- package/src/components/BarChart/components/BarChart.Vertical.tsx +2 -8
- package/src/components/Brush/BrushChart.tsx +73 -0
- package/src/components/Brush/BrushController..tsx +39 -0
- package/src/components/DeviationBar.jsx +0 -1
- package/src/components/EditorPanel/EditorPanel.tsx +232 -147
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +3 -2
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +2 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +8 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -4
- package/src/components/HoverLine/HoverLine.tsx +74 -0
- package/src/components/Legend/Legend.Suppression.tsx +47 -3
- package/src/components/Legend/helpers/index.ts +1 -1
- package/src/components/LineChart/index.tsx +3 -6
- package/src/components/LinearChart.tsx +161 -132
- package/src/components/PieChart/PieChart.tsx +58 -13
- package/src/data/initial-state.js +8 -5
- package/src/helpers/getNewRuntime.ts +35 -0
- package/src/helpers/getPiePercent.ts +22 -0
- package/src/helpers/getTransformedData.ts +22 -0
- package/src/helpers/tests/getNewRuntime.test.ts +82 -0
- package/src/helpers/tests/getPiePercent.test.ts +38 -0
- package/src/hooks/useRightAxis.ts +1 -1
- package/src/hooks/useScales.ts +8 -3
- package/src/hooks/useTooltip.tsx +24 -10
- package/src/store/chart.actions.ts +2 -6
- package/src/store/chart.reducer.ts +23 -23
- package/src/types/ChartConfig.ts +6 -3
- package/src/types/ChartContext.ts +0 -2
- package/src/components/ZoomBrush.tsx +0 -251
|
@@ -46,28 +46,51 @@ const PieChart = props => {
|
|
|
46
46
|
const pivotColumns = Object.values(config.columns).filter(column => column.showInViz)
|
|
47
47
|
const dataNeedsPivot = pivotColumns.length > 0
|
|
48
48
|
const pivotKey = dataNeedsPivot ? 'pivotColumn' : undefined
|
|
49
|
+
const showPercentage = config.dataFormat.showPiePercent
|
|
50
|
+
const labelForCalcArea = 'Calculated Area'
|
|
51
|
+
|
|
49
52
|
const _data = useMemo(() => {
|
|
53
|
+
let baseData = []
|
|
54
|
+
|
|
50
55
|
if (dataNeedsPivot) {
|
|
51
|
-
let newData = []
|
|
52
56
|
const primaryColumn = config.yAxis.dataKey
|
|
53
57
|
const additionalColumns = pivotColumns.map(column => column.name)
|
|
54
58
|
const allColumns = [primaryColumn, ...additionalColumns]
|
|
55
59
|
const columnToUpdate = config.xAxis.dataKey
|
|
60
|
+
|
|
56
61
|
data.forEach(d => {
|
|
57
62
|
allColumns.forEach(col => {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
[pivotKey]:
|
|
63
|
+
const val = d[col]
|
|
64
|
+
if (val) {
|
|
65
|
+
baseData.push({
|
|
66
|
+
[pivotKey]: val,
|
|
62
67
|
[columnToUpdate]: `${d[columnToUpdate]} - ${col}`
|
|
63
68
|
})
|
|
64
69
|
}
|
|
65
70
|
})
|
|
66
71
|
})
|
|
67
|
-
|
|
72
|
+
} else {
|
|
73
|
+
baseData = [...data]
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
|
|
76
|
+
// === ADD "OTHER" IF PERCENT MODE IS ENABLED ===
|
|
77
|
+
if (showPercentage) {
|
|
78
|
+
const total = baseData.reduce((sum, d) => {
|
|
79
|
+
const val = parseFloat(d[config.runtime.yAxis.dataKey])
|
|
80
|
+
return sum + (isNaN(val) ? 0 : val)
|
|
81
|
+
}, 0)
|
|
82
|
+
|
|
83
|
+
if (total < 100) {
|
|
84
|
+
const remaining = 100 - total
|
|
85
|
+
baseData.push({
|
|
86
|
+
[config.runtime.xAxis.dataKey]: labelForCalcArea,
|
|
87
|
+
[config.runtime.yAxis.dataKey]: remaining
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return baseData
|
|
93
|
+
}, [data, dataNeedsPivot, showPercentage, config])
|
|
71
94
|
|
|
72
95
|
const _colorScale = useMemo(() => {
|
|
73
96
|
if (dataNeedsPivot) {
|
|
@@ -78,15 +101,31 @@ const PieChart = props => {
|
|
|
78
101
|
const numberOfKeys = Object.entries(keys).length
|
|
79
102
|
let palette = config.customColors || colorPalettes[config.palette]
|
|
80
103
|
palette = palette.slice(0, numberOfKeys)
|
|
81
|
-
|
|
82
104
|
return scaleOrdinal({
|
|
83
105
|
domain: Object.keys(keys),
|
|
84
106
|
range: palette,
|
|
85
107
|
unknown: null
|
|
86
108
|
})
|
|
87
109
|
}
|
|
110
|
+
|
|
111
|
+
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
|
+
})
|
|
126
|
+
}
|
|
88
127
|
return colorScale
|
|
89
|
-
}, [
|
|
128
|
+
}, [_data, dataNeedsPivot, colorScale])
|
|
90
129
|
|
|
91
130
|
const triggerRef = useRef()
|
|
92
131
|
const dataRef = useIntersectionObserver(triggerRef, {
|
|
@@ -130,9 +169,15 @@ const PieChart = props => {
|
|
|
130
169
|
const roundTo = Number(config.dataFormat.roundTo) || 0
|
|
131
170
|
// Calculate the percentage of the full circle (360 degrees)
|
|
132
171
|
const degrees = ((arc.endAngle - arc.startAngle) * 180) / Math.PI
|
|
172
|
+
const valueFromData = parseFloat(arc.data[config.runtime.yAxis.dataKey])
|
|
173
|
+
const percentageToDisplay = showPercentage ? valueFromData : (degrees / 360) * 100
|
|
174
|
+
|
|
175
|
+
let roundedPercentage = percentageToDisplay.toFixed(roundTo) + '%'
|
|
176
|
+
// add missing pie part
|
|
177
|
+
if (arc.data[config.xAxis.dataKey] === labelForCalcArea && config.dataFormat.showPiePercent) {
|
|
178
|
+
roundedPercentage = '**'
|
|
179
|
+
}
|
|
133
180
|
|
|
134
|
-
const percentageOfCircle = (degrees / 360) * 100
|
|
135
|
-
const roundedPercentage = percentageOfCircle.toFixed(roundTo) + '%'
|
|
136
181
|
return (
|
|
137
182
|
<Group key={key} className={`slice-${key}`}>
|
|
138
183
|
{/* ── the slice */}
|
|
@@ -238,7 +283,7 @@ const PieChart = props => {
|
|
|
238
283
|
<AnimatedPie
|
|
239
284
|
{...pie}
|
|
240
285
|
getKey={d => d.data[config.runtime.xAxis.dataKey]}
|
|
241
|
-
|
|
286
|
+
colorScale={_colorScale}
|
|
242
287
|
onHover={handleTooltipMouseOver}
|
|
243
288
|
onLeave={handleTooltipMouseOff}
|
|
244
289
|
/>
|
|
@@ -89,6 +89,11 @@ export default {
|
|
|
89
89
|
topAxis: {
|
|
90
90
|
hasLine: false
|
|
91
91
|
},
|
|
92
|
+
brush: {
|
|
93
|
+
isActive: false,
|
|
94
|
+
isBrushing: false,
|
|
95
|
+
data: []
|
|
96
|
+
},
|
|
92
97
|
isLegendValue: false,
|
|
93
98
|
barThickness: 0.35,
|
|
94
99
|
barHeight: 25,
|
|
@@ -120,7 +125,8 @@ export default {
|
|
|
120
125
|
maxTickRotation: 0,
|
|
121
126
|
padding: 5,
|
|
122
127
|
showYearsOnce: false,
|
|
123
|
-
sortByRecentDate: false
|
|
128
|
+
sortByRecentDate: false,
|
|
129
|
+
brushActive: false
|
|
124
130
|
},
|
|
125
131
|
table: {
|
|
126
132
|
label: 'Data Table',
|
|
@@ -176,10 +182,7 @@ export default {
|
|
|
176
182
|
position: 'right',
|
|
177
183
|
orderedValues: []
|
|
178
184
|
},
|
|
179
|
-
|
|
180
|
-
height: 45,
|
|
181
|
-
active: false
|
|
182
|
-
},
|
|
185
|
+
|
|
183
186
|
exclusions: {
|
|
184
187
|
active: false,
|
|
185
188
|
keys: []
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const getNewRuntime = (visualizationConfig, newFilteredData) => {
|
|
4
|
+
const runtime = _.cloneDeep(visualizationConfig.runtime) || {}
|
|
5
|
+
runtime.series = []
|
|
6
|
+
runtime.seriesLabels = {}
|
|
7
|
+
runtime.seriesLabelsAll = []
|
|
8
|
+
const { filters, columns, dynamicSeriesType, dynamicSeriesLineType, xAxis } = visualizationConfig
|
|
9
|
+
if (newFilteredData?.length) {
|
|
10
|
+
const columnNames = Object.keys(newFilteredData[0])
|
|
11
|
+
columnNames.forEach(colName => {
|
|
12
|
+
const isNotXAxis = xAxis.dataKey !== colName
|
|
13
|
+
const isNotFiltered = !filters || !filters?.find(filter => filter.columnName === colName)
|
|
14
|
+
const noColConfiguration = !columns || Object.keys(columns).indexOf(colName) === -1
|
|
15
|
+
if (isNotXAxis && isNotFiltered && noColConfiguration) {
|
|
16
|
+
runtime.series.push({
|
|
17
|
+
dataKey: colName,
|
|
18
|
+
type: dynamicSeriesType,
|
|
19
|
+
lineType: dynamicSeriesLineType,
|
|
20
|
+
tooltip: true
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
runtime.seriesKeys = runtime.series
|
|
27
|
+
? runtime.series.map(series => {
|
|
28
|
+
runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
|
|
29
|
+
runtime.seriesLabelsAll.push(series.name || series.dataKey)
|
|
30
|
+
return series.dataKey
|
|
31
|
+
})
|
|
32
|
+
: []
|
|
33
|
+
|
|
34
|
+
return runtime
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const getPiePercent = (data: Record<string, any>[], seriesKey: string): Record<string, any>[] => {
|
|
4
|
+
// getonly the numeric values for each seriesKey
|
|
5
|
+
const numericValues = data.map(row => _.toNumber(row[seriesKey])).filter(v => !Number.isNaN(v))
|
|
6
|
+
|
|
7
|
+
const total = numericValues.reduce((sum, v) => sum + v, 0)
|
|
8
|
+
|
|
9
|
+
return data.map(row => {
|
|
10
|
+
const raw = _.toNumber(row[seriesKey])
|
|
11
|
+
if (Number.isNaN(raw)) {
|
|
12
|
+
// skip non-numeric / undefined
|
|
13
|
+
return row
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pct = total === 0 ? 0 : (raw / total) * 100
|
|
17
|
+
return {
|
|
18
|
+
...row,
|
|
19
|
+
[seriesKey]: pct
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type DataRow = Record<string, any>
|
|
2
|
+
|
|
3
|
+
export const getTransformedData = ({
|
|
4
|
+
brushData,
|
|
5
|
+
filteredData,
|
|
6
|
+
excludedData,
|
|
7
|
+
clean
|
|
8
|
+
}: {
|
|
9
|
+
brushData: DataRow[]
|
|
10
|
+
filteredData: DataRow[]
|
|
11
|
+
excludedData: DataRow[]
|
|
12
|
+
clean: (data: DataRow[]) => DataRow[]
|
|
13
|
+
}): DataRow[] => {
|
|
14
|
+
const data =
|
|
15
|
+
Array.isArray(brushData) && brushData.length > 0
|
|
16
|
+
? brushData
|
|
17
|
+
: Array.isArray(filteredData) && filteredData.length > 0
|
|
18
|
+
? filteredData
|
|
19
|
+
: excludedData
|
|
20
|
+
|
|
21
|
+
return clean(data)
|
|
22
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getNewRuntime } from '../getNewRuntime'
|
|
3
|
+
|
|
4
|
+
describe('getNewRuntime', () => {
|
|
5
|
+
it('should return a runtime object with default values when no data is provided', () => {
|
|
6
|
+
const visualizationConfig = { runtime: {} }
|
|
7
|
+
const newFilteredData = null
|
|
8
|
+
|
|
9
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
10
|
+
|
|
11
|
+
expect(result.series).toEqual([])
|
|
12
|
+
expect(result.seriesLabels).toEqual({})
|
|
13
|
+
expect(result.seriesLabelsAll).toEqual([])
|
|
14
|
+
expect(result.seriesKeys).toEqual([])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should populate runtime.series with valid series from newFilteredData', () => {
|
|
18
|
+
const visualizationConfig = {
|
|
19
|
+
runtime: {},
|
|
20
|
+
filters: [],
|
|
21
|
+
columns: {},
|
|
22
|
+
dynamicSeriesType: 'bar',
|
|
23
|
+
dynamicSeriesLineType: 'solid',
|
|
24
|
+
xAxis: { dataKey: 'x' }
|
|
25
|
+
}
|
|
26
|
+
const newFilteredData = [
|
|
27
|
+
{ x: 1, y: 10, z: 20 },
|
|
28
|
+
{ x: 2, y: 15, z: 25 }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
32
|
+
|
|
33
|
+
expect(result.series).toEqual([
|
|
34
|
+
{ dataKey: 'y', type: 'bar', lineType: 'solid', tooltip: true },
|
|
35
|
+
{ dataKey: 'z', type: 'bar', lineType: 'solid', tooltip: true }
|
|
36
|
+
])
|
|
37
|
+
expect(result.seriesKeys).toEqual(['y', 'z'])
|
|
38
|
+
expect(result.seriesLabels).toEqual({ y: 'y', z: 'z' })
|
|
39
|
+
expect(result.seriesLabelsAll).toEqual(['y', 'z'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should exclude series keys that match filters or columns', () => {
|
|
43
|
+
const visualizationConfig = {
|
|
44
|
+
runtime: {},
|
|
45
|
+
filters: [{ columnName: 'y' }],
|
|
46
|
+
columns: { z: {} },
|
|
47
|
+
dynamicSeriesType: 'bar',
|
|
48
|
+
dynamicSeriesLineType: 'solid',
|
|
49
|
+
xAxis: { dataKey: 'x' }
|
|
50
|
+
}
|
|
51
|
+
const newFilteredData = [
|
|
52
|
+
{ x: 1, y: 10, z: 20, w: 30 },
|
|
53
|
+
{ x: 2, y: 15, z: 25, w: 35 }
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
57
|
+
|
|
58
|
+
expect(result.series).toEqual([{ dataKey: 'w', type: 'bar', lineType: 'solid', tooltip: true }])
|
|
59
|
+
expect(result.seriesKeys).toEqual(['w'])
|
|
60
|
+
expect(result.seriesLabels).toEqual({ w: 'w' })
|
|
61
|
+
expect(result.seriesLabelsAll).toEqual(['w'])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should handle empty newFilteredData gracefully', () => {
|
|
65
|
+
const visualizationConfig = {
|
|
66
|
+
runtime: {},
|
|
67
|
+
filters: [],
|
|
68
|
+
columns: {},
|
|
69
|
+
dynamicSeriesType: 'bar',
|
|
70
|
+
dynamicSeriesLineType: 'solid',
|
|
71
|
+
xAxis: { dataKey: 'x' }
|
|
72
|
+
}
|
|
73
|
+
const newFilteredData = []
|
|
74
|
+
|
|
75
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
76
|
+
|
|
77
|
+
expect(result.series).toEqual([])
|
|
78
|
+
expect(result.seriesKeys).toEqual([])
|
|
79
|
+
expect(result.seriesLabels).toEqual({})
|
|
80
|
+
expect(result.seriesLabelsAll).toEqual([])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// getPiePercent.test.ts
|
|
2
|
+
import { getPiePercent } from '../getPiePercent'
|
|
3
|
+
|
|
4
|
+
describe('getPiePercent', () => {
|
|
5
|
+
it('cgets percentages for purely numeric strings', () => {
|
|
6
|
+
const data = [{ A: '1' }, { A: '3' }, { A: '6' }]
|
|
7
|
+
const result = getPiePercent(data, 'A')
|
|
8
|
+
|
|
9
|
+
// sum = 1 + 3 + 6 = 10
|
|
10
|
+
expect(result[0].A).toBeCloseTo((1 / 10) * 100) // 10%
|
|
11
|
+
expect(result[1].A).toBeCloseTo((3 / 10) * 100) // 30%
|
|
12
|
+
expect(result[2].A).toBeCloseTo((6 / 10) * 100) // 60%
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('shandle non numbers like "ABC', () => {
|
|
16
|
+
const data = [{ A: '1' }, { A: 'ABC' }, { A: '2' }]
|
|
17
|
+
const result = getPiePercent(data, 'A')
|
|
18
|
+
|
|
19
|
+
expect(result[0].A).toBeCloseTo((1 / 3) * 100)
|
|
20
|
+
expect(result[1].A).toBe('ABC')
|
|
21
|
+
expect(result[2].A).toBeCloseTo((2 / 3) * 100)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('handles all-zero total by producing 0%', () => {
|
|
25
|
+
const data = [{ A: '0' }, { A: '0' }]
|
|
26
|
+
const result = getPiePercent(data, 'A')
|
|
27
|
+
expect(result[0].A).toBe(0)
|
|
28
|
+
expect(result[1].A).toBe(0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('leaves rows missing the key entirely unchanged', () => {
|
|
32
|
+
const data = [{ A: '2' }, { B: 'foo' }]
|
|
33
|
+
const result = getPiePercent(data, 'A')
|
|
34
|
+
|
|
35
|
+
expect(result[0].A).toBeCloseTo(100)
|
|
36
|
+
expect(result[1]).toEqual({ B: 'foo' })
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -2,7 +2,7 @@ import { scaleLinear } from '@visx/scale'
|
|
|
2
2
|
import useReduceData from './useReduceData'
|
|
3
3
|
import { TOP_PADDING } from './useScales'
|
|
4
4
|
|
|
5
|
-
export default function useRightAxis({ config, yMax = 0, data = []
|
|
5
|
+
export default function useRightAxis({ config, yMax = 0, data = [] }) {
|
|
6
6
|
const hasRightAxis = config.visualizationType === 'Combo' && config.orientation === 'vertical'
|
|
7
7
|
const rightSeriesKeys =
|
|
8
8
|
config.series && config.series.filter(series => series.axis === 'Right').map(key => key.dataKey)
|
package/src/hooks/useScales.ts
CHANGED
|
@@ -79,13 +79,18 @@ const useScales = (properties: useScaleProps) => {
|
|
|
79
79
|
xScale = composeScaleBand(xAxisDataMappedSorted, [0, xMax], 1 - config.barThickness)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// handle Linear scaled viz
|
|
83
|
+
if (config.xAxis.type === 'date' && !isHorizontal) {
|
|
84
|
+
const sorted = sortXAxisData(xAxisDataMapped, config.xAxis.sortByRecentDate)
|
|
85
|
+
|
|
86
|
+
xScale = composeScaleBand(sorted, [0, xMax], 1 - config.barThickness)
|
|
87
|
+
xScale.type = scaleTypes.BAND
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
if (xAxis.type === 'date-time' || xAxis.type === 'continuous') {
|
|
83
91
|
let xAxisMin = Math.min(...xAxisDataMapped.map(Number))
|
|
84
92
|
let xAxisMax = Math.max(...xAxisDataMapped.map(Number))
|
|
85
93
|
let paddingRatio = config.xAxis.padding ? config.xAxis.padding * 0.01 : 0
|
|
86
|
-
if (config.brush.active) {
|
|
87
|
-
paddingRatio = config.barThickness * 0.2
|
|
88
|
-
}
|
|
89
94
|
|
|
90
95
|
xAxisMin -= paddingRatio * (xAxisMax - xAxisMin)
|
|
91
96
|
xAxisMax += visualizationType === 'Line' ? 0 : paddingRatio * (xAxisMax - xAxisMin)
|
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext } from 'react'
|
|
2
2
|
// Local imports
|
|
3
|
+
import parse from 'html-react-parser'
|
|
3
4
|
import ConfigContext from '../ConfigContext'
|
|
4
5
|
import { type ChartContext } from '../types/ChartContext'
|
|
5
6
|
import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
|
|
@@ -120,16 +121,25 @@ export const useTooltip = props => {
|
|
|
120
121
|
const pieData = additionalChartData?.data ?? {}
|
|
121
122
|
const startAngle = additionalChartData?.startAngle ?? 0
|
|
122
123
|
const endAngle = additionalChartData?.endAngle ?? 0
|
|
124
|
+
const actualPieValue = Number(additionalChartData.data[config?.yAxis?.dataKey])
|
|
123
125
|
|
|
124
126
|
const degrees = ((endAngle - startAngle) * 180) / Math.PI
|
|
125
127
|
const pctOf360 = (degrees / 360) * 100
|
|
126
|
-
const pctString =
|
|
128
|
+
const pctString = value => value.toFixed(roundTo) + '%'
|
|
129
|
+
const showPiePercent = config.dataFormat.showPiePercent || false
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
[
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
if (showPiePercent && pieData[config.xAxis.dataKey] === 'Calculated Area') {
|
|
132
|
+
tooltipItems.push(['', 'Calculated Area'])
|
|
133
|
+
} else {
|
|
134
|
+
tooltipItems.push(
|
|
135
|
+
[config.xAxis.dataKey, pieData[config.xAxis.dataKey]],
|
|
136
|
+
[
|
|
137
|
+
config.runtime.yAxis.dataKey,
|
|
138
|
+
showPiePercent ? pctString(actualPieValue) : formatNumber(pieData[config.runtime.yAxis.dataKey])
|
|
139
|
+
],
|
|
140
|
+
showPiePercent ? [] : ['Percent', pctString(pctOf360)]
|
|
141
|
+
)
|
|
142
|
+
}
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
if (visualizationType === 'Forest Plot') {
|
|
@@ -188,6 +198,8 @@ export const useTooltip = props => {
|
|
|
188
198
|
}
|
|
189
199
|
})
|
|
190
200
|
} else {
|
|
201
|
+
const dynamicSeries = config.series.find(s => s.dynamicCategory)
|
|
202
|
+
|
|
191
203
|
// Show Only the Hovered Series in Tooltip
|
|
192
204
|
const dataColumn = resolvedScaleValues[0]
|
|
193
205
|
const [seriesKey, value] = findDataKeyByThreshold(y, dataColumn)
|
|
@@ -198,7 +210,7 @@ export const useTooltip = props => {
|
|
|
198
210
|
tooltipItems.push([config.xAxis.dataKey, closestXScaleValue || xVal])
|
|
199
211
|
const formattedValue = getFormattedValue(seriesKey, value, config, getAxisPosition)
|
|
200
212
|
tooltipItems.push([seriesKey, formattedValue])
|
|
201
|
-
} else {
|
|
213
|
+
} else if (dynamicSeries) {
|
|
202
214
|
Object.keys(dataColumn).forEach(key => {
|
|
203
215
|
tooltipItems.push([key, dataColumn[key]])
|
|
204
216
|
})
|
|
@@ -546,7 +558,9 @@ export const useTooltip = props => {
|
|
|
546
558
|
config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ` : ''
|
|
547
559
|
)} ${config.xAxis.type === 'date' ? formattedDate : value}`}</li>
|
|
548
560
|
)
|
|
549
|
-
|
|
561
|
+
if (visualizationType === 'Pie' && config.dataFormat.showPiePercent && value === 'Calculated Area') {
|
|
562
|
+
return <li className='tooltip-heading'>{`${capitalize('Calculated Area')} `}</li>
|
|
563
|
+
}
|
|
550
564
|
if (key === config.xAxis.dataKey)
|
|
551
565
|
return (
|
|
552
566
|
<li className='tooltip-heading'>{`${capitalize(
|
|
@@ -571,14 +585,14 @@ export const useTooltip = props => {
|
|
|
571
585
|
let newValue = label || value
|
|
572
586
|
const style = displayGray ? { color: '#8b8b8a' } : {}
|
|
573
587
|
|
|
574
|
-
if (index == 1 && config.
|
|
588
|
+
if (index == 1 && config.yAxis?.inlineLabel) {
|
|
575
589
|
newValue = `${config.dataFormat.prefix}${newValue}${config.dataFormat.suffix}`
|
|
576
590
|
}
|
|
577
591
|
const activeLabel = getSeriesNameFromLabel(key)
|
|
578
592
|
const displayText = activeLabel ? `${activeLabel}: ${newValue}` : newValue
|
|
579
593
|
|
|
580
594
|
return (
|
|
581
|
-
<li style={style} className='tooltip-body'>
|
|
595
|
+
<li style={style} className='tooltip-body mb-1'>
|
|
582
596
|
{displayText}
|
|
583
597
|
</li>
|
|
584
598
|
)
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
2
2
|
import { ChartConfig } from '../types/ChartConfig'
|
|
3
|
-
|
|
4
|
-
type Action<T, P = undefined, R = undefined> = {
|
|
5
|
-
type: T
|
|
6
|
-
payload?: P
|
|
7
|
-
}
|
|
3
|
+
import { Action } from '@cdc/core/types/Action'
|
|
8
4
|
|
|
9
5
|
// Action Types
|
|
10
6
|
type SET_CONFIG = Action<'SET_CONFIG', ChartConfig>
|
|
@@ -34,7 +30,7 @@ type ChartActions =
|
|
|
34
30
|
| SET_CONTAINER
|
|
35
31
|
| SET_LOADED_EVENT
|
|
36
32
|
| SET_DRAG_ANNOTATIONS
|
|
37
|
-
| SET_BRUSH_CONFIG
|
|
38
33
|
| SET_LOADING
|
|
34
|
+
| SET_BRUSH_CONFIG
|
|
39
35
|
|
|
40
36
|
export default ChartActions
|
|
@@ -4,7 +4,28 @@ import { ChartConfig, type ViewportSize } from '../types/ChartConfig'
|
|
|
4
4
|
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
5
5
|
import _ from 'lodash'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type ChartState = {
|
|
8
|
+
isLoading: boolean
|
|
9
|
+
config: ChartConfig
|
|
10
|
+
stateData: object[]
|
|
11
|
+
colorScale: Function
|
|
12
|
+
excludedData: object[]
|
|
13
|
+
filteredData: object[]
|
|
14
|
+
seriesHighlight: string[]
|
|
15
|
+
currentViewport: ViewportSize
|
|
16
|
+
dimensions: DimensionsType
|
|
17
|
+
container: HTMLElement | null
|
|
18
|
+
coveLoadedEventRan: boolean
|
|
19
|
+
isDraggingAnnotation: boolean
|
|
20
|
+
imageId: string
|
|
21
|
+
brushConfig: {
|
|
22
|
+
data: object[]
|
|
23
|
+
isActive: boolean
|
|
24
|
+
isBrushing: boolean
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getInitialState = (configObj: ChartConfig): ChartState => {
|
|
8
29
|
return {
|
|
9
30
|
isLoading: true,
|
|
10
31
|
config: defaults,
|
|
@@ -28,28 +49,7 @@ export const getInitialState = (configObj: ChartConfig) => {
|
|
|
28
49
|
}
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
isLoading: boolean
|
|
33
|
-
config: ChartConfig
|
|
34
|
-
stateData: object[]
|
|
35
|
-
colorScale: Function
|
|
36
|
-
excludedData: object[]
|
|
37
|
-
filteredData: object[]
|
|
38
|
-
seriesHighlight: string[]
|
|
39
|
-
currentViewport: ViewportSize
|
|
40
|
-
dimensions: DimensionsType
|
|
41
|
-
container: HTMLElement | null
|
|
42
|
-
coveLoadedEventRan: boolean
|
|
43
|
-
isDraggingAnnotation: boolean
|
|
44
|
-
imageId: string
|
|
45
|
-
brushConfig: {
|
|
46
|
-
data: object[]
|
|
47
|
-
isActive: boolean
|
|
48
|
-
isBrushing: boolean
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const reducer = (state: State, action: ChartActions) => {
|
|
52
|
+
export const reducer = (state: ChartState, action: ChartActions): ChartState => {
|
|
53
53
|
switch (action.type) {
|
|
54
54
|
case 'SET_LOADING':
|
|
55
55
|
return { ...state, isLoading: action.payload }
|
package/src/types/ChartConfig.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Region } from '@cdc/core/types/Region'
|
|
|
16
16
|
import { VizFilter } from '@cdc/core/types/VizFilter'
|
|
17
17
|
import { type Annotation } from '@cdc/core/types/Annotation'
|
|
18
18
|
import { Version } from '@cdc/core/types/Version'
|
|
19
|
+
import Footnotes from '@cdc/core/types/Footnotes'
|
|
19
20
|
|
|
20
21
|
export type ViewportSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg'
|
|
21
22
|
export type ChartColumns = Record<string, Column>
|
|
@@ -69,7 +70,7 @@ type DataFormat = {
|
|
|
69
70
|
rightSuffix: string
|
|
70
71
|
roundTo: number
|
|
71
72
|
suffix: string
|
|
72
|
-
|
|
73
|
+
showPiePercent: boolean
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
type Exclusions = {
|
|
@@ -121,7 +122,8 @@ export type AllChartsConfig = {
|
|
|
121
122
|
boxplot: BoxPlot
|
|
122
123
|
brush: {
|
|
123
124
|
active: boolean
|
|
124
|
-
|
|
125
|
+
data: object[]
|
|
126
|
+
isBrushing: boolean
|
|
125
127
|
}
|
|
126
128
|
chartMessage: { noData?: string }
|
|
127
129
|
color: string
|
|
@@ -140,7 +142,8 @@ export type AllChartsConfig = {
|
|
|
140
142
|
exclusions: Exclusions
|
|
141
143
|
filters: VizFilter[]
|
|
142
144
|
filterBehavior: FilterBehavior
|
|
143
|
-
|
|
145
|
+
legacyFootnotes: string // this footnote functionality should be moved to the Footnotes component
|
|
146
|
+
footnotes: Footnotes
|
|
144
147
|
forestPlot: ForestPlotConfigSettings
|
|
145
148
|
formattedData: Object[] & { urlFiltered: boolean }
|
|
146
149
|
heights: {
|
|
@@ -12,7 +12,6 @@ export type TransformedData = {
|
|
|
12
12
|
|
|
13
13
|
type SharedChartContext = {
|
|
14
14
|
animatedChart?: boolean
|
|
15
|
-
brushConfig: { data: []; isBrushing: boolean; isActive: boolean }
|
|
16
15
|
capitalize: (value: string) => string
|
|
17
16
|
clean: Function
|
|
18
17
|
colorScale?: ColorScale
|
|
@@ -31,7 +30,6 @@ type SharedChartContext = {
|
|
|
31
30
|
legendIsolateValues?: string[]
|
|
32
31
|
legendRef?: React.RefObject<HTMLDivElement>
|
|
33
32
|
parentRef?: React.RefObject<HTMLDivElement>
|
|
34
|
-
setBrushConfig: Function
|
|
35
33
|
setLegendIsolateValues?: Function
|
|
36
34
|
svgRef?: React.RefObject<SVGSVGElement>
|
|
37
35
|
}
|