@cdc/chart 4.25.5-1 → 4.25.6-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/dist/cdcchart.js +31118 -27459
- package/examples/private/0527.json +1 -0
- package/examples/private/DEV-8850-2.json +493 -0
- package/examples/private/DEV-9822.json +574 -0
- package/examples/private/DEV-9840.json +553 -0
- package/examples/private/DEV-9850-3.json +461 -0
- package/examples/private/chart.json +1084 -0
- package/examples/private/ci_formatted.json +202 -0
- package/examples/private/ci_issue.json +3016 -0
- package/examples/private/completed.json +634 -0
- package/examples/private/dem-data-long.csv +20 -0
- package/examples/private/dem-data-long.json +36 -0
- package/examples/private/demographic_data.csv +157 -0
- package/examples/private/demographic_data.json +2654 -0
- package/examples/private/demographic_dynamic.json +443 -0
- package/examples/private/demographic_standard.json +560 -0
- package/examples/private/ehdi.json +29939 -0
- package/examples/private/line-issue.json +497 -0
- package/examples/private/not-loading.json +360 -0
- package/examples/private/test.json +493 -0
- package/examples/private/testing-pie.json +436 -0
- 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 +246 -156
- 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/helpers.ts +7 -7
- package/src/components/LineChart/index.tsx +3 -6
- package/src/components/LinearChart.tsx +108 -72
- package/src/components/PieChart/PieChart.tsx +58 -13
- package/src/data/initial-state.js +8 -5
- package/src/helpers/countNumOfTicks.ts +4 -19
- 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/scss/main.scss +8 -4
- package/src/store/chart.actions.ts +2 -6
- package/src/store/chart.reducer.ts +23 -23
- package/src/types/ChartConfig.ts +7 -4
- package/src/types/ChartContext.ts +0 -2
- package/src/components/ZoomBrush.tsx +0 -251
|
@@ -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
|
)
|
package/src/scss/main.scss
CHANGED
|
@@ -513,10 +513,6 @@
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
[tabindex]:focus-visible {
|
|
517
|
-
outline: 2px solid rgb(0, 95, 204) !important;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
516
|
// ANIMATIONS
|
|
521
517
|
// Pie Chart Animations
|
|
522
518
|
.animated-pie {
|
|
@@ -743,3 +739,11 @@
|
|
|
743
739
|
.cdc-open-viz-module .debug {
|
|
744
740
|
border: 2px solid red;
|
|
745
741
|
}
|
|
742
|
+
|
|
743
|
+
// Only frontend styles are applied in WCMS/TP
|
|
744
|
+
// This helps match those styles when viewing in the editor
|
|
745
|
+
.modal.cdc-cove-editor *:focus-visible,
|
|
746
|
+
.cdc-open-viz-module *:focus-visible {
|
|
747
|
+
outline: dashed 2px rgba(255, 102, 1, 0.9) !important;
|
|
748
|
+
outline-offset: 3px !important;
|
|
749
|
+
}
|
|
@@ -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>
|
|
@@ -43,7 +44,7 @@ export interface PreliminaryDataItem {
|
|
|
43
44
|
iconCode: string
|
|
44
45
|
label: string
|
|
45
46
|
lineCode: string
|
|
46
|
-
|
|
47
|
+
seriesKeys: string[]
|
|
47
48
|
style: string
|
|
48
49
|
symbol: string
|
|
49
50
|
type: 'effect' | 'suppression'
|
|
@@ -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
|
}
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { Brush } from '@visx/brush'
|
|
2
|
-
import { Group } from '@visx/group'
|
|
3
|
-
import { Text } from '@visx/text'
|
|
4
|
-
import { FC, useContext, useEffect, useRef, useState } from 'react'
|
|
5
|
-
import ConfigContext from '../ConfigContext'
|
|
6
|
-
import { ScaleLinear, ScaleBand } from 'd3-scale'
|
|
7
|
-
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
8
|
-
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
9
|
-
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
10
|
-
import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
|
|
11
|
-
|
|
12
|
-
interface Props {
|
|
13
|
-
xScaleBrush: ScaleLinear<number, number>
|
|
14
|
-
yScale: ScaleBand<string>
|
|
15
|
-
xMax: number
|
|
16
|
-
yMax: number
|
|
17
|
-
}
|
|
18
|
-
const ZoomBrush: FC<Props> = props => {
|
|
19
|
-
const { tableData, config, parseDate, formatDate, setBrushConfig, dashboardConfig } = useContext(ConfigContext)
|
|
20
|
-
const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
|
|
21
|
-
const isDashboardFilters = sharedFilters?.length > 0
|
|
22
|
-
const [showTooltip, setShowTooltip] = useState(false)
|
|
23
|
-
const [brushKey, setBrushKey] = useState(0)
|
|
24
|
-
const brushRef = useRef(null)
|
|
25
|
-
const radius = 15
|
|
26
|
-
|
|
27
|
-
const [textProps, setTextProps] = useState({
|
|
28
|
-
startPosition: 0,
|
|
29
|
-
endPosition: 0,
|
|
30
|
-
startValue: '',
|
|
31
|
-
endValue: '',
|
|
32
|
-
xMax: props.xMax
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const initialPosition = {
|
|
36
|
-
start: { x: 0 },
|
|
37
|
-
end: { x: props.xMax }
|
|
38
|
-
}
|
|
39
|
-
const style = {
|
|
40
|
-
fill: '#474747',
|
|
41
|
-
stroke: 'blue',
|
|
42
|
-
fillOpacity: 0.8,
|
|
43
|
-
strokeOpacity: 0,
|
|
44
|
-
rx: radius
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const onBrushChange = event => {
|
|
48
|
-
setShowTooltip(false)
|
|
49
|
-
const filteredValues = event?.xValues?.filter(val => val !== undefined)
|
|
50
|
-
if (filteredValues?.length === 0) return
|
|
51
|
-
|
|
52
|
-
const dataKey = config.xAxis?.dataKey
|
|
53
|
-
|
|
54
|
-
const brushedData = tableData.filter(item => filteredValues.includes(item[dataKey]))
|
|
55
|
-
|
|
56
|
-
const endValue = filteredValues
|
|
57
|
-
.slice()
|
|
58
|
-
.reverse()
|
|
59
|
-
.find(item => item !== undefined)
|
|
60
|
-
const startValue = filteredValues.find(item => item !== undefined)
|
|
61
|
-
|
|
62
|
-
const formatIfDate = value => (isDateScale(config.runtime.xAxis) ? formatDate(parseDate(value)) : value)
|
|
63
|
-
|
|
64
|
-
setTextProps(prev => ({
|
|
65
|
-
...prev,
|
|
66
|
-
startPosition: brushRef.current?.state.start.x,
|
|
67
|
-
endPosition: brushRef.current?.state.end.x,
|
|
68
|
-
endValue: formatIfDate(endValue),
|
|
69
|
-
startValue: formatIfDate(startValue),
|
|
70
|
-
xMax: props.xMax
|
|
71
|
-
}))
|
|
72
|
-
|
|
73
|
-
setBrushConfig(prev => {
|
|
74
|
-
return {
|
|
75
|
-
...prev,
|
|
76
|
-
isBrushing: brushRef.current?.state.isBrushing,
|
|
77
|
-
data: brushedData
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
// reset brush if brush is off.
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (!config.brush?.active) {
|
|
84
|
-
setBrushKey(prevKey => prevKey + 1)
|
|
85
|
-
setBrushConfig({
|
|
86
|
-
data: [],
|
|
87
|
-
isActive: false,
|
|
88
|
-
isBrushing: false
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
}, [config.brush?.active])
|
|
92
|
-
|
|
93
|
-
// reset brush if filters or exclusions are ON each time
|
|
94
|
-
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
const isFiltersActive = config.filters?.some(filter => filter.active)
|
|
97
|
-
const isExclusionsActive = config.exclusions?.active
|
|
98
|
-
|
|
99
|
-
if ((isFiltersActive || isExclusionsActive || isDashboardFilters) && config.brush?.active) {
|
|
100
|
-
setBrushKey(prevKey => prevKey + 1)
|
|
101
|
-
setBrushConfig(prev => {
|
|
102
|
-
return {
|
|
103
|
-
...prev,
|
|
104
|
-
data: tableData
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
return () =>
|
|
109
|
-
setBrushConfig(prev => {
|
|
110
|
-
return {
|
|
111
|
-
...prev,
|
|
112
|
-
data: []
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
}, [config.filters, config.exclusions, config.brush?.active, isDashboardFilters])
|
|
116
|
-
|
|
117
|
-
const calculateTop = (): number => {
|
|
118
|
-
const tickRotation = Number(config.xAxis.tickRotation) > 0 ? Number(config.xAxis.tickRotation) : 0
|
|
119
|
-
let top = 0
|
|
120
|
-
const offSet = 30
|
|
121
|
-
if (!config.xAxis.label) {
|
|
122
|
-
if (!config.isResponsiveTicks && tickRotation) {
|
|
123
|
-
top = Number(tickRotation + config.xAxis.tickWidthMax) / 1.6
|
|
124
|
-
}
|
|
125
|
-
if (!config.isResponsiveTicks && !tickRotation) {
|
|
126
|
-
top = Number(config.xAxis.labelOffset) - offSet
|
|
127
|
-
}
|
|
128
|
-
if (config.isResponsiveTicks && config.dynamicMarginTop) {
|
|
129
|
-
top = Number(config.xAxis.labelOffset + config.xAxis.tickWidthMax / 1.6)
|
|
130
|
-
}
|
|
131
|
-
if (config.isResponsiveTicks && !config.dynamicMarginTop) {
|
|
132
|
-
top = Number(config.xAxis.labelOffset - offSet)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (config.xAxis.label) {
|
|
136
|
-
if (!config.isResponsiveTicks && tickRotation) {
|
|
137
|
-
top = Number(config.xAxis.tickWidthMax + tickRotation) + offSet
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!config.isResponsiveTicks && !tickRotation) {
|
|
141
|
-
top = config.xAxis.labelOffset + offSet
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (config.isResponsiveTicks && !tickRotation) {
|
|
145
|
-
top = Number(config.dynamicMarginTop ? config.dynamicMarginTop : config.xAxis.labelOffset) + offSet * 2
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return top
|
|
150
|
-
}
|
|
151
|
-
if (!['Line', 'Bar', 'Area Chart', 'Combo'].includes(config.visualizationType)) {
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return (
|
|
156
|
-
<ErrorBoundary component='Brush Chart'>
|
|
157
|
-
<Group
|
|
158
|
-
onMouseMove={() => {
|
|
159
|
-
// show tooltip only once before brush started
|
|
160
|
-
if (textProps.startPosition === 0 && (textProps.endPosition === 0 || textProps.endPosition === props.xMax)) {
|
|
161
|
-
setShowTooltip(true)
|
|
162
|
-
}
|
|
163
|
-
}}
|
|
164
|
-
onMouseLeave={() => setShowTooltip(false)}
|
|
165
|
-
display={config.brush?.active ? 'block' : 'none'}
|
|
166
|
-
top={Number(props.yMax) + calculateTop()}
|
|
167
|
-
left={Number(config.runtime.yAxis.size)}
|
|
168
|
-
pointerEvents='fill'
|
|
169
|
-
>
|
|
170
|
-
<rect fill='#949494' width={props.xMax} height={config.brush.height} rx={radius} />
|
|
171
|
-
<Brush
|
|
172
|
-
key={brushKey}
|
|
173
|
-
disableDraggingOverlay={true}
|
|
174
|
-
renderBrushHandle={props => (
|
|
175
|
-
<BrushHandle
|
|
176
|
-
left={Number(config.runtime.yAxis.size)}
|
|
177
|
-
showTooltip={showTooltip}
|
|
178
|
-
pixelDistance={textProps.endPosition - textProps.startPosition}
|
|
179
|
-
textProps={textProps}
|
|
180
|
-
{...props}
|
|
181
|
-
isBrushing={brushRef.current?.state.isBrushing}
|
|
182
|
-
/>
|
|
183
|
-
)}
|
|
184
|
-
innerRef={brushRef}
|
|
185
|
-
useWindowMoveEvents={true}
|
|
186
|
-
selectedBoxStyle={style}
|
|
187
|
-
xScale={props.xScaleBrush}
|
|
188
|
-
yScale={props.yScale}
|
|
189
|
-
width={props.xMax}
|
|
190
|
-
resizeTriggerAreas={['left', 'right']}
|
|
191
|
-
height={config.brush.height}
|
|
192
|
-
handleSize={8}
|
|
193
|
-
brushDirection='horizontal'
|
|
194
|
-
initialBrushPosition={initialPosition}
|
|
195
|
-
onChange={onBrushChange}
|
|
196
|
-
/>
|
|
197
|
-
</Group>
|
|
198
|
-
</ErrorBoundary>
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const BrushHandle = props => {
|
|
203
|
-
const { x, isBrushActive, isBrushing, className, textProps, showTooltip, left } = props
|
|
204
|
-
const pathWidth = 8
|
|
205
|
-
if (!isBrushActive) {
|
|
206
|
-
return null
|
|
207
|
-
}
|
|
208
|
-
// Flip the SVG path horizontally for the left handle
|
|
209
|
-
const isLeft = className.includes('left')
|
|
210
|
-
const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
|
|
211
|
-
const textAnchor = isLeft ? 'end' : 'start'
|
|
212
|
-
const tooltipText = isLeft ? ` Drag edges to focus on a specific segment ` : ''
|
|
213
|
-
const textWidth = getTextWidth(tooltipText, `${APP_FONT_SIZE / 1.1}px`)
|
|
214
|
-
|
|
215
|
-
return (
|
|
216
|
-
<>
|
|
217
|
-
{showTooltip && (
|
|
218
|
-
<Text
|
|
219
|
-
x={(Number(textProps.xMax) - textWidth) / 2}
|
|
220
|
-
dy={-12}
|
|
221
|
-
pointerEvents='visiblePainted'
|
|
222
|
-
fontSize={APP_FONT_SIZE / 1.1}
|
|
223
|
-
>
|
|
224
|
-
{tooltipText}
|
|
225
|
-
</Text>
|
|
226
|
-
)}
|
|
227
|
-
<Group left={x + pathWidth / 2} top={-2}>
|
|
228
|
-
<Text
|
|
229
|
-
pointerEvents='visiblePainted'
|
|
230
|
-
dominantBaseline='hanging'
|
|
231
|
-
x={isLeft ? 55 : -50}
|
|
232
|
-
y={25}
|
|
233
|
-
verticalAnchor='start'
|
|
234
|
-
textAnchor={textAnchor}
|
|
235
|
-
fontSize={APP_FONT_SIZE / 1.4}
|
|
236
|
-
>
|
|
237
|
-
{isLeft ? textProps.startValue : textProps.endValue}
|
|
238
|
-
</Text>
|
|
239
|
-
<path
|
|
240
|
-
cursor='ew-resize'
|
|
241
|
-
d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12'
|
|
242
|
-
fill={'#297EF1'}
|
|
243
|
-
strokeWidth='1'
|
|
244
|
-
transform={transform}
|
|
245
|
-
></path>
|
|
246
|
-
</Group>
|
|
247
|
-
</>
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export default ZoomBrush
|