@cdc/chart 4.25.3 → 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/dist/cdcchart.js +46641 -42561
- package/index.html +130 -129
- package/package.json +22 -27
- package/src/CdcChartComponent.tsx +75 -35
- package/src/_stories/Chart.CI.stories.tsx +10 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +68 -49
- package/src/_stories/Chart.stories.tsx +99 -86
- package/src/_stories/ChartPrefixSuffix.stories.tsx +29 -32
- package/{examples/private/line-issue.json → src/_stories/_mock/barchart_labels.mock.json} +150 -35
- package/src/_stories/_mock/dynamic_series_bar_config.json +1 -1
- package/src/_stories/_mock/dynamic_series_suppression_mock.json +610 -0
- package/{examples/private/not-loading.json → src/_stories/_mock/pie_calculated_area.json} +152 -95
- package/src/components/Annotations/components/AnnotationDropdown.tsx +2 -2
- package/src/components/AreaChart/components/AreaChart.jsx +33 -5
- package/src/components/Axis/Categorical.Axis.tsx +2 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -37
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +18 -8
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +8 -8
- package/src/components/BarChart/components/BarChart.Vertical.tsx +47 -36
- package/src/components/BarChart/components/{BarChart.jsx → BarChart.tsx} +23 -5
- package/src/components/BarChart/components/context.tsx +20 -2
- package/src/components/BarChart/helpers/getBarHeights.ts +47 -0
- package/src/components/BarChart/helpers/index.ts +5 -2
- package/src/components/BarChart/helpers/tests/getBarHeights.test.ts +83 -0
- package/src/{hooks → components/BarChart/helpers}/useBarChart.ts +9 -46
- package/src/components/BoxPlot/BoxPlot.tsx +2 -1
- package/src/components/Brush/BrushChart.tsx +73 -0
- package/src/components/Brush/BrushController..tsx +39 -0
- package/src/components/DeviationBar.jsx +2 -2
- package/src/components/EditorPanel/EditorPanel.tsx +232 -147
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +36 -36
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +52 -25
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +12 -4
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +8 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +5 -5
- package/src/components/ForestPlot/ForestPlot.tsx +2 -2
- package/src/components/HoverLine/HoverLine.tsx +74 -0
- package/src/components/Legend/Legend.Component.tsx +1 -1
- package/src/components/Legend/Legend.Suppression.tsx +59 -25
- package/src/components/Legend/helpers/createFormatLabels.tsx +28 -0
- package/src/components/Legend/helpers/index.ts +1 -1
- package/src/components/LineChart/LineChartProps.ts +3 -1
- package/src/components/LineChart/components/LineChart.Circle.tsx +72 -119
- package/src/components/LineChart/helpers.ts +133 -56
- package/src/components/LineChart/index.tsx +106 -55
- package/src/components/LinearChart.tsx +178 -198
- package/src/components/PairedBarChart.jsx +3 -2
- package/src/components/PieChart/PieChart.tsx +127 -102
- package/src/components/ScatterPlot/ScatterPlot.jsx +5 -0
- package/src/components/Sparkline/components/SparkLine.tsx +80 -18
- package/src/data/initial-state.js +11 -6
- package/src/helpers/countNumOfTicks.ts +1 -1
- package/src/helpers/dataHelpers.ts +23 -2
- package/src/helpers/getNewRuntime.ts +35 -0
- package/src/helpers/getPiePercent.ts +22 -0
- package/src/helpers/getTransformedData.ts +22 -0
- package/src/helpers/sizeHelpers.ts +1 -1
- package/src/helpers/tests/getNewRuntime.test.ts +82 -0
- package/src/helpers/tests/getPiePercent.test.ts +38 -0
- package/src/hooks/useMinMax.ts +21 -28
- package/src/hooks/useRightAxis.ts +5 -3
- package/src/hooks/useScales.ts +15 -6
- package/src/hooks/useTooltip.tsx +218 -203
- package/src/index.jsx +2 -2
- package/src/scss/main.scss +13 -6
- package/src/store/chart.actions.ts +2 -6
- package/src/store/chart.reducer.ts +23 -23
- package/src/types/ChartConfig.ts +11 -3
- package/src/types/ChartContext.ts +0 -2
- package/examples/private/DEV-8850-2.json +0 -493
- package/examples/private/DEV-9822.json +0 -574
- package/examples/private/DEV-9840.json +0 -553
- package/examples/private/DEV-9850-3.json +0 -461
- package/examples/private/chart.json +0 -1084
- package/examples/private/ci_formatted.json +0 -202
- package/examples/private/ci_issue.json +0 -3016
- package/examples/private/completed.json +0 -634
- package/examples/private/dem-data-long.csv +0 -20
- package/examples/private/dem-data-long.json +0 -36
- package/examples/private/demographic_data.csv +0 -157
- package/examples/private/demographic_data.json +0 -2654
- package/examples/private/demographic_dynamic.json +0 -443
- package/examples/private/demographic_standard.json +0 -560
- package/examples/private/ehdi.json +0 -29939
- package/examples/private/test.json +0 -493
- package/src/components/ZoomBrush.tsx +0 -251
|
@@ -6,6 +6,7 @@ import { Text } from '@visx/text'
|
|
|
6
6
|
|
|
7
7
|
import ConfigContext from '../ConfigContext'
|
|
8
8
|
import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
9
|
+
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
9
10
|
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
10
11
|
|
|
11
12
|
const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
@@ -47,8 +48,8 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
|
47
48
|
})
|
|
48
49
|
|
|
49
50
|
// Set label color
|
|
50
|
-
groupOne.labelColor = groupOne.color ? getContrastColor(
|
|
51
|
-
groupTwo.labelColor = groupTwo.color ? getContrastColor(
|
|
51
|
+
groupOne.labelColor = groupOne.color ? getContrastColor(APP_FONT_COLOR, groupOne.color) : APP_FONT_COLOR
|
|
52
|
+
groupTwo.labelColor = groupTwo.color ? getContrastColor(APP_FONT_COLOR, groupTwo.color) : APP_FONT_COLOR
|
|
52
53
|
|
|
53
54
|
const label = config.yAxis.label ? `${config.yAxis.label}: ` : ''
|
|
54
55
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useContext, useState, useEffect, useRef, useMemo } from 'react'
|
|
2
|
-
import { animated, useTransition,
|
|
2
|
+
import { animated, useTransition, to } from '@react-spring/web'
|
|
3
3
|
|
|
4
4
|
// visx
|
|
5
5
|
import { Pie } from '@visx/shape'
|
|
@@ -14,16 +14,9 @@ import { useTooltip as useCoveTooltip } from '../../hooks/useTooltip'
|
|
|
14
14
|
import useIntersectionObserver from '../../hooks/useIntersectionObserver'
|
|
15
15
|
import { handleChartAriaLabels } from '../../helpers/handleChartAriaLabels'
|
|
16
16
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
17
|
-
import LegendComponent from '../Legend/Legend.Component'
|
|
18
|
-
import { createFormatLabels } from '../Legend/helpers/createFormatLabels'
|
|
19
17
|
import { scaleOrdinal } from '@visx/scale'
|
|
20
18
|
import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
21
19
|
|
|
22
|
-
const enterUpdateTransition = ({ startAngle, endAngle }) => ({
|
|
23
|
-
startAngle,
|
|
24
|
-
endAngle
|
|
25
|
-
})
|
|
26
|
-
|
|
27
20
|
type TooltipData = {
|
|
28
21
|
data: {
|
|
29
22
|
[key: string]: string | number
|
|
@@ -53,28 +46,51 @@ const PieChart = props => {
|
|
|
53
46
|
const pivotColumns = Object.values(config.columns).filter(column => column.showInViz)
|
|
54
47
|
const dataNeedsPivot = pivotColumns.length > 0
|
|
55
48
|
const pivotKey = dataNeedsPivot ? 'pivotColumn' : undefined
|
|
49
|
+
const showPercentage = config.dataFormat.showPiePercent
|
|
50
|
+
const labelForCalcArea = 'Calculated Area'
|
|
51
|
+
|
|
56
52
|
const _data = useMemo(() => {
|
|
53
|
+
let baseData = []
|
|
54
|
+
|
|
57
55
|
if (dataNeedsPivot) {
|
|
58
|
-
let newData = []
|
|
59
56
|
const primaryColumn = config.yAxis.dataKey
|
|
60
57
|
const additionalColumns = pivotColumns.map(column => column.name)
|
|
61
58
|
const allColumns = [primaryColumn, ...additionalColumns]
|
|
62
59
|
const columnToUpdate = config.xAxis.dataKey
|
|
60
|
+
|
|
63
61
|
data.forEach(d => {
|
|
64
62
|
allColumns.forEach(col => {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
[pivotKey]:
|
|
63
|
+
const val = d[col]
|
|
64
|
+
if (val) {
|
|
65
|
+
baseData.push({
|
|
66
|
+
[pivotKey]: val,
|
|
69
67
|
[columnToUpdate]: `${d[columnToUpdate]} - ${col}`
|
|
70
68
|
})
|
|
71
69
|
}
|
|
72
70
|
})
|
|
73
71
|
})
|
|
74
|
-
|
|
72
|
+
} else {
|
|
73
|
+
baseData = [...data]
|
|
75
74
|
}
|
|
76
|
-
|
|
77
|
-
|
|
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])
|
|
78
94
|
|
|
79
95
|
const _colorScale = useMemo(() => {
|
|
80
96
|
if (dataNeedsPivot) {
|
|
@@ -85,15 +101,31 @@ const PieChart = props => {
|
|
|
85
101
|
const numberOfKeys = Object.entries(keys).length
|
|
86
102
|
let palette = config.customColors || colorPalettes[config.palette]
|
|
87
103
|
palette = palette.slice(0, numberOfKeys)
|
|
88
|
-
|
|
89
104
|
return scaleOrdinal({
|
|
90
105
|
domain: Object.keys(keys),
|
|
91
106
|
range: palette,
|
|
92
107
|
unknown: null
|
|
93
108
|
})
|
|
94
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
|
+
}
|
|
95
127
|
return colorScale
|
|
96
|
-
}, [
|
|
128
|
+
}, [_data, dataNeedsPivot, colorScale])
|
|
97
129
|
|
|
98
130
|
const triggerRef = useRef()
|
|
99
131
|
const dataRef = useIntersectionObserver(triggerRef, {
|
|
@@ -117,92 +149,77 @@ const PieChart = props => {
|
|
|
117
149
|
}
|
|
118
150
|
}, [dataRef?.isIntersecting, config.animate]) // eslint-disable-line
|
|
119
151
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
function AnimatedPie({ arcs, path, getKey, colorScale, onHover, onLeave }) {
|
|
153
|
+
const enterExit = ({ startAngle, endAngle }) => ({ startAngle, endAngle })
|
|
154
|
+
const transitions = useTransition(arcs, {
|
|
155
|
+
keys: getKey,
|
|
156
|
+
from: enterExit,
|
|
157
|
+
enter: enterExit,
|
|
158
|
+
update: enterExit,
|
|
159
|
+
leave: enterExit
|
|
126
160
|
})
|
|
127
161
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
hideTooltip()
|
|
135
|
-
}, 500)
|
|
136
|
-
return () => {
|
|
137
|
-
clearTimeout(timeout)
|
|
162
|
+
return transitions((styles, arc) => {
|
|
163
|
+
const key = getKey(arc)
|
|
164
|
+
let textColor = '#FFF'
|
|
165
|
+
|
|
166
|
+
if (key && _colorScale(key)) {
|
|
167
|
+
textColor = getContrastColor(textColor, _colorScale(arc.data[config.runtime.xAxis.dataKey]))
|
|
138
168
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
const roundTo = Number(config.dataFormat.roundTo) || 0
|
|
170
|
+
// Calculate the percentage of the full circle (360 degrees)
|
|
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
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Group key={key} className={`slice-${key}`}>
|
|
183
|
+
{/* ── the slice */}
|
|
184
|
+
<animated.path
|
|
185
|
+
d={to([styles.startAngle, styles.endAngle], (start, end) =>
|
|
186
|
+
path({ ...arc, startAngle: start, endAngle: end })
|
|
187
|
+
)}
|
|
188
|
+
fill={colorScale(key)}
|
|
189
|
+
onMouseEnter={e =>
|
|
190
|
+
onHover(e, {
|
|
191
|
+
data: arc.data,
|
|
192
|
+
dataXPosition: e.clientX,
|
|
193
|
+
dataYPosition: e.clientY,
|
|
194
|
+
startAngle: arc.startAngle,
|
|
195
|
+
endAngle: arc.endAngle
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
onMouseLeave={onLeave}
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
{/* ── the percentage label */}
|
|
202
|
+
{arc.endAngle - arc.startAngle > 0.1 && (
|
|
203
|
+
<animated.text
|
|
204
|
+
transform={to([styles.startAngle, styles.endAngle], (start, end) => {
|
|
205
|
+
const [x, y] = path.centroid({
|
|
206
|
+
...arc,
|
|
207
|
+
startAngle: start,
|
|
208
|
+
endAngle: end
|
|
209
|
+
})
|
|
210
|
+
return `translate(${x},${y})`
|
|
211
|
+
})}
|
|
212
|
+
textAnchor='middle'
|
|
213
|
+
pointerEvents='none'
|
|
214
|
+
fill={textColor}
|
|
156
215
|
>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)}
|
|
165
|
-
fill={_colorScale(arc.data[config.runtime.xAxis.dataKey])}
|
|
166
|
-
onMouseEnter={e => handleTooltipMouseOver(e, { data: arc.data[config.runtime.xAxis.dataKey], arc })}
|
|
167
|
-
onMouseLeave={e => handleTooltipMouseOff()}
|
|
168
|
-
/>
|
|
169
|
-
</Group>
|
|
170
|
-
)
|
|
171
|
-
})}
|
|
172
|
-
{transitions.map(({ item: arc, key }, i) => {
|
|
173
|
-
const roundTo = Number(config.dataFormat.roundTo) || 0
|
|
174
|
-
const [centroidX, centroidY] = path.centroid(arc)
|
|
175
|
-
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1
|
|
176
|
-
|
|
177
|
-
let textColor = '#FFF'
|
|
178
|
-
if (_colorScale(arc.data[config.runtime.xAxis.dataKey])) {
|
|
179
|
-
textColor = getContrastColor(textColor, _colorScale(arc.data[config.runtime.xAxis.dataKey]))
|
|
180
|
-
}
|
|
181
|
-
const degrees = ((arc.endAngle - arc.startAngle) * 180) / Math.PI
|
|
182
|
-
|
|
183
|
-
// Calculate the percentage of the full circle (360 degrees)
|
|
184
|
-
const percentageOfCircle = (degrees / 360) * 100
|
|
185
|
-
const roundedPercentage = percentageOfCircle.toFixed(roundTo)
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<animated.g key={`${key}${i}`}>
|
|
189
|
-
{hasSpaceForLabel && (
|
|
190
|
-
<Text
|
|
191
|
-
style={{ fill: textColor }}
|
|
192
|
-
x={centroidX}
|
|
193
|
-
y={centroidY}
|
|
194
|
-
dy='.33em'
|
|
195
|
-
textAnchor='middle'
|
|
196
|
-
pointerEvents='none'
|
|
197
|
-
>
|
|
198
|
-
{roundedPercentage + '%'}
|
|
199
|
-
</Text>
|
|
200
|
-
)}
|
|
201
|
-
</animated.g>
|
|
202
|
-
)
|
|
203
|
-
})}
|
|
204
|
-
</>
|
|
205
|
-
)
|
|
216
|
+
{/** compute text inside the spring callback */}
|
|
217
|
+
{roundedPercentage}
|
|
218
|
+
</animated.text>
|
|
219
|
+
)}
|
|
220
|
+
</Group>
|
|
221
|
+
)
|
|
222
|
+
})
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
let chartWidth = props.parentWidth
|
|
@@ -257,12 +274,20 @@ const PieChart = props => {
|
|
|
257
274
|
{/* prettier-ignore */}
|
|
258
275
|
<Pie
|
|
259
276
|
data={filteredData || _data}
|
|
260
|
-
pieValue={d => d[pivotKey || config.runtime.yAxis.dataKey]}
|
|
277
|
+
pieValue={d => parseFloat(d[pivotKey || config.runtime.yAxis.dataKey])}
|
|
261
278
|
pieSortValues={() => -1}
|
|
262
279
|
innerRadius={radius - donutThickness}
|
|
263
280
|
outerRadius={radius}
|
|
264
281
|
>
|
|
265
|
-
|
|
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
|
+
)}
|
|
266
291
|
</Pie>
|
|
267
292
|
</Group>
|
|
268
293
|
</svg>
|
|
@@ -18,17 +18,29 @@ type SparkLineProps = {
|
|
|
18
18
|
|
|
19
19
|
const SparkLine: React.FC<SparkLineProps> = props => {
|
|
20
20
|
const { width: parentWidth, height: parentHeight } = props
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
transformedData: data,
|
|
23
|
+
config,
|
|
24
|
+
parseDate,
|
|
25
|
+
formatDate,
|
|
26
|
+
seriesHighlight,
|
|
27
|
+
formatNumber,
|
|
28
|
+
colorScale,
|
|
29
|
+
handleChartAriaLabels
|
|
30
|
+
} = useContext(ConfigContext)
|
|
22
31
|
let width = Number(parentWidth)
|
|
23
32
|
const { minValue, maxValue } = useReduceData(config, data, ConfigContext)
|
|
24
33
|
|
|
25
|
-
const margin = { top: 5, right:
|
|
34
|
+
const margin = { top: 5, right: 20, bottom: 10, left: 10 }
|
|
26
35
|
const height = Number(parentHeight)
|
|
27
36
|
|
|
28
37
|
const xMax = width - config.runtime.yAxis.size
|
|
29
38
|
const yMax = height - margin.top - 20
|
|
30
39
|
|
|
31
|
-
const getXAxisData = d =>
|
|
40
|
+
const getXAxisData = d =>
|
|
41
|
+
config.runtime.xAxis.type === 'date'
|
|
42
|
+
? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
|
|
43
|
+
: d[config.runtime.originalXAxis.dataKey]
|
|
32
44
|
const getYAxisData = (d, seriesKey) => d[seriesKey]
|
|
33
45
|
|
|
34
46
|
let xScale
|
|
@@ -61,7 +73,10 @@ const SparkLine: React.FC<SparkLineProps> = props => {
|
|
|
61
73
|
range: [0, xMax]
|
|
62
74
|
})
|
|
63
75
|
|
|
64
|
-
yScale =
|
|
76
|
+
yScale =
|
|
77
|
+
config.runtime.xAxis.type === 'date'
|
|
78
|
+
? scaleLinear({ domain: [Math.min(...xAxisDataMapped), Math.max(...xAxisDataMapped)] })
|
|
79
|
+
: scalePoint({ domain: xAxisDataMapped, padding: 0.5 })
|
|
65
80
|
|
|
66
81
|
seriesScale = scalePoint({
|
|
67
82
|
domain: config.runtime.barSeriesKeys || config.runtime.seriesKeys,
|
|
@@ -94,7 +109,14 @@ const SparkLine: React.FC<SparkLineProps> = props => {
|
|
|
94
109
|
|
|
95
110
|
return (
|
|
96
111
|
<ErrorBoundary component='SparkLine'>
|
|
97
|
-
<svg
|
|
112
|
+
<svg
|
|
113
|
+
role='img'
|
|
114
|
+
aria-label={handleChartAriaLabels(config)}
|
|
115
|
+
width={parentWidth}
|
|
116
|
+
height={100}
|
|
117
|
+
className={'sparkline'}
|
|
118
|
+
tabIndex={0}
|
|
119
|
+
>
|
|
98
120
|
<title>{`Spark line graphic with the title ${config.title ? config.title : 'No Title Found'}`}</title>
|
|
99
121
|
{config.runtime.lineSeriesKeys?.length > 0
|
|
100
122
|
? config.runtime.lineSeriesKeys
|
|
@@ -104,30 +126,70 @@ const SparkLine: React.FC<SparkLineProps> = props => {
|
|
|
104
126
|
style={{ width }}
|
|
105
127
|
className='sparkline-group'
|
|
106
128
|
key={`series-${seriesKey}`}
|
|
107
|
-
opacity={
|
|
108
|
-
|
|
129
|
+
opacity={
|
|
130
|
+
config.legend.behavior === 'highlight' &&
|
|
131
|
+
seriesHighlight.length > 0 &&
|
|
132
|
+
seriesHighlight.indexOf(seriesKey) === -1
|
|
133
|
+
? 0.5
|
|
134
|
+
: 1
|
|
135
|
+
}
|
|
136
|
+
display={
|
|
137
|
+
config.legend.behavior === 'highlight' ||
|
|
138
|
+
seriesHighlight.length === 0 ||
|
|
139
|
+
seriesHighlight.indexOf(seriesKey) !== -1
|
|
140
|
+
? 'block'
|
|
141
|
+
: 'none'
|
|
142
|
+
}
|
|
109
143
|
>
|
|
110
|
-
{config.labels &&
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
{config.labels &&
|
|
145
|
+
data.map((d, dataIndex) => {
|
|
146
|
+
return (
|
|
147
|
+
<Group key={`series-${seriesKey}-point-${dataIndex}`}>
|
|
148
|
+
<Text
|
|
149
|
+
x={xScale(getXAxisData(d))}
|
|
150
|
+
y={yScale(getYAxisData(d, seriesKey))}
|
|
151
|
+
fill={
|
|
152
|
+
colorScale
|
|
153
|
+
? colorScale(
|
|
154
|
+
config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey
|
|
155
|
+
)
|
|
156
|
+
: '#000'
|
|
157
|
+
}
|
|
158
|
+
textAnchor='middle'
|
|
159
|
+
>
|
|
160
|
+
{formatNumber(d[seriesKey])}
|
|
161
|
+
</Text>
|
|
162
|
+
</Group>
|
|
163
|
+
)
|
|
164
|
+
})}
|
|
119
165
|
<LinePath
|
|
120
166
|
curve={allCurves.curveLinear}
|
|
121
167
|
data={data}
|
|
122
168
|
x={d => xScale(getXAxisData(d))}
|
|
123
169
|
y={d => yScale(getYAxisData(d, seriesKey))}
|
|
124
|
-
stroke={
|
|
170
|
+
stroke={
|
|
171
|
+
colorScale
|
|
172
|
+
? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey)
|
|
173
|
+
: '#000'
|
|
174
|
+
}
|
|
125
175
|
strokeWidth={2}
|
|
126
176
|
strokeOpacity={1}
|
|
127
177
|
shapeRendering='geometricPrecision'
|
|
128
178
|
markerEnd={`url(#${'arrow'}--${index})`}
|
|
129
179
|
/>
|
|
130
|
-
<MarkerArrow
|
|
180
|
+
<MarkerArrow
|
|
181
|
+
id={`arrow--${index}`}
|
|
182
|
+
refX={2}
|
|
183
|
+
size={6}
|
|
184
|
+
markerEnd={`url(#${'arrow'}--${index})`}
|
|
185
|
+
strokeOpacity={1}
|
|
186
|
+
fillOpacity={1}
|
|
187
|
+
fill={
|
|
188
|
+
colorScale
|
|
189
|
+
? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey)
|
|
190
|
+
: '#000'
|
|
191
|
+
}
|
|
192
|
+
/>
|
|
131
193
|
</Group>
|
|
132
194
|
<AxisBottom
|
|
133
195
|
top={yMax + margin.top}
|
|
@@ -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',
|
|
@@ -168,16 +174,15 @@ export default {
|
|
|
168
174
|
groupBy: '',
|
|
169
175
|
shape: 'circle',
|
|
170
176
|
tickRotation: '',
|
|
177
|
+
order: 'dataColumn',
|
|
171
178
|
hideBorder: {
|
|
172
179
|
side: false,
|
|
173
180
|
topBottom: true
|
|
174
181
|
},
|
|
175
|
-
position: 'right'
|
|
176
|
-
|
|
177
|
-
brush: {
|
|
178
|
-
height: 45,
|
|
179
|
-
active: false
|
|
182
|
+
position: 'right',
|
|
183
|
+
orderedValues: []
|
|
180
184
|
},
|
|
185
|
+
|
|
181
186
|
exclusions: {
|
|
182
187
|
active: false,
|
|
183
188
|
keys: []
|
|
@@ -25,7 +25,7 @@ export const countNumOfTicks = ({ axis, max, runtime, currentViewport, isHorizon
|
|
|
25
25
|
}
|
|
26
26
|
if (Number(tickCount) > Number(max) && !isHorizontal) {
|
|
27
27
|
// cap it and round it so its an integer
|
|
28
|
-
tickCount = Number(min) < 0 ? Math.round(max) * 2 : Math.round(max)
|
|
28
|
+
tickCount = Math.max(2, Number(min) < 0 ? Math.round(max) * 2 : Math.round(max))
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -1,10 +1,31 @@
|
|
|
1
|
+
import { Series } from '@cdc/core/types/Series'
|
|
1
2
|
import { ChartConfig } from '../types/ChartConfig'
|
|
2
3
|
|
|
3
4
|
export const getSeriesWithData = (config: ChartConfig) => {
|
|
4
|
-
const { filters, data, runtime } = config
|
|
5
|
+
const { filters, data, runtime, legend } = config
|
|
6
|
+
const { colorCode } = legend
|
|
5
7
|
const { series } = runtime
|
|
6
8
|
|
|
7
9
|
const filteredData = data.filter(d => filters.every(f => d[f.columnName] === f.active))
|
|
10
|
+
const colorCodeSeries = colorCode && Array.from(new Set(filteredData.map(d => d[colorCode])))
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
const result = series
|
|
13
|
+
.flatMap(s => {
|
|
14
|
+
if (!colorCode || s.type !== 'Bar') return s
|
|
15
|
+
return colorCodeSeries.map(c => ({ ...s, colorCodeSeries: c }))
|
|
16
|
+
})
|
|
17
|
+
.map(s => ({
|
|
18
|
+
...s,
|
|
19
|
+
data: filteredData
|
|
20
|
+
.filter(d => !s.dynamicCategory || d[s.dynamicCategory] === s.dataKey)
|
|
21
|
+
.filter(d => !s.colorCodeSeries || d[colorCode] === s.colorCodeSeries)
|
|
22
|
+
.filter(d => {
|
|
23
|
+
const key = s.dynamicCategory ? s.originalDataKey : s.dataKey
|
|
24
|
+
return d[key] || d[key] === 0
|
|
25
|
+
})
|
|
26
|
+
}))
|
|
27
|
+
.filter(s => s.data.length)
|
|
28
|
+
.map(s => s.colorCodeSeries || s.name || s.dataKey)
|
|
29
|
+
|
|
30
|
+
return result
|
|
10
31
|
}
|
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,7 @@ import { clamp } from 'lodash'
|
|
|
2
2
|
|
|
3
3
|
import { isMobileHeightViewport } from '@cdc/core/helpers/viewports'
|
|
4
4
|
import { ChartConfig, ViewportSize } from '../types/ChartConfig'
|
|
5
|
-
import { EDITOR_WIDTH } from '
|
|
5
|
+
import { EDITOR_WIDTH } from '@cdc/core/helpers/constants'
|
|
6
6
|
|
|
7
7
|
export function getOrientation(
|
|
8
8
|
{ orientation, heights, visualizationType }: Pick<ChartConfig, 'orientation' | 'heights' | 'visualizationType'>,
|