@cdc/chart 4.22.11 → 4.23.1
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/495.js +3 -0
- package/dist/703.js +1 -0
- package/dist/cdcchart.js +723 -6
- package/examples/box-plot-data.json +71 -0
- package/examples/box-plot.csv +5 -0
- package/examples/{private/yaxis-test.json → box-plot.json} +47 -56
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +3 -1
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +85 -16
- package/examples/new-data.csv +17 -0
- package/examples/newdata.json +90 -0
- package/package.json +3 -2
- package/src/CdcChart.tsx +150 -94
- package/src/components/BarChart.tsx +156 -226
- package/src/components/BoxPlot.js +92 -0
- package/src/components/DataTable.tsx +28 -12
- package/src/components/EditorPanel.js +151 -104
- package/src/components/Filters.js +131 -0
- package/src/components/Legend.js +8 -1
- package/src/components/LineChart.tsx +64 -13
- package/src/components/LinearChart.tsx +120 -81
- package/src/components/PairedBarChart.tsx +1 -1
- package/src/components/PieChart.tsx +12 -2
- package/src/components/useIntersectionObserver.tsx +9 -7
- package/src/data/initial-state.js +14 -8
- package/src/hooks/useReduceData.ts +8 -5
- package/src/index.html +51 -51
- package/src/scss/DataTable.scss +1 -1
- package/src/scss/main.scss +53 -22
- package/examples/private/filters.json +0 -170
- package/examples/private/line-test-data.json +0 -22
- package/examples/private/line-test-two.json +0 -210
- package/examples/private/line-test.json +0 -102
- package/examples/private/new.json +0 -48800
- package/examples/private/newtest.csv +0 -101
- package/examples/private/shawn.json +0 -1106
- package/examples/private/test.json +0 -10124
- package/examples/private/yaxis-testing.csv +0 -27
- package/examples/private/yaxis.json +0 -28
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useEffect, useState, useContext } from 'react'
|
|
2
|
+
import Context from './../context'
|
|
3
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
4
|
+
|
|
5
|
+
const useFilters = () => {
|
|
6
|
+
const { config, setConfig, filteredData, setFilteredData, excludedData, filterData, runtimeFilters } = useContext(Context)
|
|
7
|
+
const [showApplyButton, setShowApplyButton] = useState(false)
|
|
8
|
+
|
|
9
|
+
const sortAsc = (a, b) => {
|
|
10
|
+
return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sortDesc = (a, b) => {
|
|
14
|
+
return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const announceChange = text => {}
|
|
18
|
+
|
|
19
|
+
const changeFilterActive = (index, value) => {
|
|
20
|
+
let newFilters = config.filters
|
|
21
|
+
newFilters[index].active = value
|
|
22
|
+
setConfig({
|
|
23
|
+
...config,
|
|
24
|
+
filters: newFilters
|
|
25
|
+
})
|
|
26
|
+
setShowApplyButton(true)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleApplyButton = newFilters => {
|
|
30
|
+
setConfig({ ...config, filters: newFilters })
|
|
31
|
+
setFilteredData(filterData(newFilters, excludedData))
|
|
32
|
+
setShowApplyButton(false)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleReset = () => {
|
|
36
|
+
let newFilters = config.filters
|
|
37
|
+
|
|
38
|
+
// reset to first item in values array.
|
|
39
|
+
newFilters.map(filter => {
|
|
40
|
+
filter.active = filter.values[0]
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
setFilteredData(filterData(newFilters, excludedData))
|
|
44
|
+
setConfig({ ...config, filters: newFilters })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { handleApplyButton, changeFilterActive, announceChange, sortAsc, sortDesc, showApplyButton, handleReset }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const Filters = () => {
|
|
51
|
+
const { config } = useContext(Context)
|
|
52
|
+
const { handleApplyButton, changeFilterActive, announceChange, sortAsc, sortDesc, showApplyButton, handleReset } = useFilters()
|
|
53
|
+
const { filters } = config
|
|
54
|
+
const buttonText = 'Apply Filters'
|
|
55
|
+
const resetText = 'Reset All'
|
|
56
|
+
|
|
57
|
+
// A List of Dropdowns
|
|
58
|
+
const FilterList = () => {
|
|
59
|
+
if (config.filters) {
|
|
60
|
+
return config.filters.map((singleFilter, index) => {
|
|
61
|
+
const values = []
|
|
62
|
+
|
|
63
|
+
if (!singleFilter.order || singleFilter.order === '') {
|
|
64
|
+
singleFilter.order = 'asc'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (singleFilter.order === 'desc') {
|
|
68
|
+
singleFilter.values = singleFilter.values.sort(sortDesc)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (singleFilter.order === 'asc') {
|
|
72
|
+
singleFilter.values = singleFilter.values.sort(sortAsc)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
singleFilter.values.forEach((filterOption, index) => {
|
|
76
|
+
values.push(
|
|
77
|
+
<option key={index} value={filterOption}>
|
|
78
|
+
{filterOption}
|
|
79
|
+
</option>
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className='single-filter' key={index}>
|
|
85
|
+
<label htmlFor={`filter-${index}`}>{singleFilter.label}</label>
|
|
86
|
+
<select
|
|
87
|
+
id={`filter-${index}`}
|
|
88
|
+
className='filter-select'
|
|
89
|
+
data-index='0'
|
|
90
|
+
value={singleFilter.active}
|
|
91
|
+
onChange={e => {
|
|
92
|
+
changeFilterActive(index, e.target.value)
|
|
93
|
+
announceChange(`Filter ${singleFilter.label} value has been changed to ${e.target.value}, please reference the data table to see updated values.`)
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{values}
|
|
97
|
+
</select>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
} else {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<section className={`filters-section`} style={{ display: 'block', width: '100%' }}>
|
|
108
|
+
{config.filters.length > 0 && (
|
|
109
|
+
<>
|
|
110
|
+
<h3 className='filters-section__title'>Filters</h3>
|
|
111
|
+
<hr />
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
|
+
<div className='filters-section__wrapper' style={{ flexWrap: 'wrap', display: 'flex', gap: '7px 15px' }}>
|
|
115
|
+
<FilterList />
|
|
116
|
+
{config.filters.length > 0 && (
|
|
117
|
+
<div className='filter-section__buttons' style={{ width: '100%' }}>
|
|
118
|
+
<Button onClick={() => handleApplyButton(filters)} disabled={!showApplyButton} style={{ marginRight: '10px' }}>
|
|
119
|
+
{buttonText}
|
|
120
|
+
</Button>
|
|
121
|
+
<a href='#!' role='button' onClick={handleReset}>
|
|
122
|
+
{resetText}
|
|
123
|
+
</a>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default Filters
|
package/src/components/Legend.js
CHANGED
|
@@ -111,7 +111,14 @@ const Legend = () => {
|
|
|
111
111
|
|
|
112
112
|
if (!legend.dynamicLegend)
|
|
113
113
|
return (
|
|
114
|
-
<aside
|
|
114
|
+
<aside
|
|
115
|
+
style={{ marginTop: config.legend.position === 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px', marginBottom: config.legend.position === 'bottom' ? '15px' : '0px' }}
|
|
116
|
+
id='legend'
|
|
117
|
+
className={containerClasses.join(' ')}
|
|
118
|
+
role='region'
|
|
119
|
+
aria-label='legend'
|
|
120
|
+
tabIndex={0}
|
|
121
|
+
>
|
|
115
122
|
{legend.label && <h2>{parse(legend.label)}</h2>}
|
|
116
123
|
{legend.description && <p>{parse(legend.description)}</p>}
|
|
117
124
|
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
@@ -28,6 +28,14 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const handleAxisFormating = (axis = 'left', label, value) => {
|
|
32
|
+
axis = String(axis).toLocaleLowerCase()
|
|
33
|
+
if (label) {
|
|
34
|
+
return `${label}: ${formatNumber(value, axis)}`
|
|
35
|
+
}
|
|
36
|
+
return `${formatNumber(value, axis)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
return (
|
|
32
40
|
<ErrorBoundary component='LineChart'>
|
|
33
41
|
<Group left={config.runtime.yAxis.size}>
|
|
@@ -43,20 +51,26 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
43
51
|
display={config.legend.behavior === 'highlight' || (seriesHighlight.length === 0 && !config.legend.dynamicLegend) || seriesHighlight.indexOf(seriesKey) !== -1 ? 'block' : 'none'}
|
|
44
52
|
>
|
|
45
53
|
{data.map((d, dataIndex) => {
|
|
46
|
-
|
|
54
|
+
// Find the series object from the config.series array that has a dataKey matching the seriesKey variable.
|
|
55
|
+
const series = config.series.find(({ dataKey }) => dataKey === seriesKey)
|
|
56
|
+
const { axis } = series
|
|
57
|
+
|
|
47
58
|
const xAxisValue = config.runtime.xAxis.type === 'date' ? formatDate(parseDate(d[config.runtime.xAxis.dataKey])) : d[config.runtime.xAxis.dataKey]
|
|
48
|
-
const yAxisValue =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
const yAxisValue = getYAxisData(d, seriesKey)
|
|
60
|
+
|
|
61
|
+
const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
|
|
62
|
+
const labeltype = axis === 'Right' ? 'rightLabel' : 'label'
|
|
63
|
+
let label = config.runtime.yAxis[labeltype]
|
|
64
|
+
// if has muiltiple series dont show legend value on tooltip
|
|
65
|
+
if (!hasMultipleSeries) label = config.isLegendValue ? config.runtime.seriesLabels[seriesKey] : label
|
|
66
|
+
|
|
67
|
+
let yAxisTooltip = handleAxisFormating(axis, label, yAxisValue)
|
|
68
|
+
let xAxisTooltip = handleAxisFormating(axis, config.runtime.xAxis.label, xAxisValue)
|
|
69
|
+
|
|
56
70
|
const tooltip = `<div>
|
|
71
|
+
${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && Object.keys(config.runtime.seriesLabels).length > 1 ? `${config.runtime.seriesLabels[seriesKey] || ''}<br/>` : ''}
|
|
57
72
|
${yAxisTooltip}<br />
|
|
58
|
-
${xAxisTooltip}
|
|
59
|
-
${config.seriesLabel ? `${config.seriesLabel}: ${seriesKey}` : ''}
|
|
73
|
+
${xAxisTooltip}
|
|
60
74
|
</div>`
|
|
61
75
|
let circleRadii = 4.5
|
|
62
76
|
return (
|
|
@@ -107,10 +121,47 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
107
121
|
strokeOpacity={1}
|
|
108
122
|
shapeRendering='geometricPrecision'
|
|
109
123
|
strokeDasharray={lineType ? handleLineType(lineType) : 0}
|
|
110
|
-
defined={(item,i) => {
|
|
111
|
-
return item[config.runtime.seriesLabels[seriesKey]] !==
|
|
124
|
+
defined={(item, i) => {
|
|
125
|
+
return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null
|
|
112
126
|
}}
|
|
113
127
|
/>
|
|
128
|
+
{config.animate && (
|
|
129
|
+
<LinePath
|
|
130
|
+
className='animation'
|
|
131
|
+
curve={allCurves.curveLinear}
|
|
132
|
+
data={data}
|
|
133
|
+
x={d => xScale(getXAxisData(d))}
|
|
134
|
+
y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey)))}
|
|
135
|
+
stroke='#fff'
|
|
136
|
+
strokeWidth={3}
|
|
137
|
+
strokeOpacity={1}
|
|
138
|
+
shapeRendering='geometricPrecision'
|
|
139
|
+
strokeDasharray={lineType ? handleLineType(lineType) : 0}
|
|
140
|
+
defined={(item, i) => {
|
|
141
|
+
return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Render series labels at end if each line if selected in the editor */}
|
|
147
|
+
{config.showLineSeriesLabels &&
|
|
148
|
+
(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map(seriesKey => {
|
|
149
|
+
let lastDatum
|
|
150
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
151
|
+
if (data[i][seriesKey]) {
|
|
152
|
+
lastDatum = data[i]
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!lastDatum) {
|
|
157
|
+
return <></>
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
<text x={xScale(getXAxisData(lastDatum)) + 5} y={yScale(getYAxisData(lastDatum, seriesKey))} alignmentBaseline='middle' fill={config.colorMatchLineSeriesLabels && colorScale ? colorScale(config.runtime.seriesLabels[seriesKey] || seriesKey) : 'black'}>
|
|
161
|
+
{config.runtime.seriesLabels[seriesKey] || seriesKey}
|
|
162
|
+
</text>
|
|
163
|
+
)
|
|
164
|
+
})}
|
|
114
165
|
</Group>
|
|
115
166
|
)
|
|
116
167
|
})}
|
|
@@ -4,7 +4,7 @@ import ReactTooltip from 'react-tooltip'
|
|
|
4
4
|
import { Group } from '@visx/group'
|
|
5
5
|
import { Line } from '@visx/shape'
|
|
6
6
|
import { Text } from '@visx/text'
|
|
7
|
-
import { scaleLinear, scalePoint } from '@visx/scale'
|
|
7
|
+
import { scaleLinear, scalePoint, scaleBand } from '@visx/scale'
|
|
8
8
|
import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
|
|
9
9
|
|
|
10
10
|
import BarChart from './BarChart'
|
|
@@ -12,31 +12,34 @@ import LineChart from './LineChart'
|
|
|
12
12
|
import Context from '../context'
|
|
13
13
|
import PairedBarChart from './PairedBarChart'
|
|
14
14
|
import useIntersectionObserver from './useIntersectionObserver'
|
|
15
|
-
import
|
|
15
|
+
import CoveBoxPlot from './BoxPlot'
|
|
16
16
|
|
|
17
17
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
18
|
-
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
19
18
|
import '../scss/LinearChart.scss'
|
|
20
19
|
import useReduceData from '../hooks/useReduceData'
|
|
21
20
|
import useRightAxis from '../hooks/useRightAxis'
|
|
22
21
|
import useTopAxis from '../hooks/useTopAxis'
|
|
23
22
|
|
|
24
23
|
// TODO: Move scaling functions into hooks to manage complexity
|
|
25
|
-
|
|
26
|
-
// TODO: remove unused imports/variables
|
|
27
|
-
// TODO: consider moving logic into hooks
|
|
28
|
-
// TODO: formatting
|
|
29
24
|
export default function LinearChart() {
|
|
30
25
|
const { transformedData: data, dimensions, config, parseDate, formatDate, currentViewport, formatNumber, handleChartAriaLabels, updateConfig } = useContext<any>(Context)
|
|
31
26
|
let [width] = dimensions
|
|
32
|
-
const { minValue, maxValue, existPositiveValue } = useReduceData(config, data)
|
|
27
|
+
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
|
|
33
28
|
const [animatedChart, setAnimatedChart] = useState<boolean>(false)
|
|
34
|
-
const [animatedChartPlayed, setAnimatedChartPlayed] = useState<boolean>(false)
|
|
35
29
|
|
|
36
30
|
const triggerRef = useRef()
|
|
37
31
|
const dataRef = useIntersectionObserver(triggerRef, {
|
|
38
32
|
freezeOnceVisible: false
|
|
39
33
|
})
|
|
34
|
+
// Make sure the chart is visible if in the editor
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const element = document.querySelector('.isEditor')
|
|
37
|
+
if (element) {
|
|
38
|
+
// parent element is visible
|
|
39
|
+
setAnimatedChart(prevState => true)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
40
43
|
// If the chart is in view and set to animate and it has not already played
|
|
41
44
|
useEffect(() => {
|
|
42
45
|
if (dataRef?.isIntersecting === true && config.animate) {
|
|
@@ -49,10 +52,10 @@ export default function LinearChart() {
|
|
|
49
52
|
if (config && config.legend && !config.legend.hide && config.legend.position !== 'bottom' && (currentViewport === 'lg' || currentViewport === 'md')) {
|
|
50
53
|
width = width * 0.73
|
|
51
54
|
}
|
|
52
|
-
|
|
53
|
-
const height = config.aspectRatio ? width * config.aspectRatio : config.
|
|
55
|
+
const { horizontal: heightHorizontal } = config.heights
|
|
56
|
+
const height = config.aspectRatio ? width * config.aspectRatio : config.heights[config.orientation]
|
|
54
57
|
const xMax = width - config.runtime.yAxis.size - config.yAxis.rightAxisSize
|
|
55
|
-
const yMax = height - config.runtime.xAxis.size
|
|
58
|
+
const yMax = height - (config.orientation === 'horizontal' ? 0 : config.runtime.xAxis.size)
|
|
56
59
|
|
|
57
60
|
const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data, updateConfig })
|
|
58
61
|
const { hasTopAxis } = useTopAxis(config)
|
|
@@ -65,19 +68,29 @@ export default function LinearChart() {
|
|
|
65
68
|
let seriesScale
|
|
66
69
|
|
|
67
70
|
const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
|
|
68
|
-
const isMaxValid = existPositiveValue ?
|
|
69
|
-
const isMinValid = (
|
|
71
|
+
const isMaxValid = existPositiveValue ? enteredMaxValue >= maxValue : enteredMaxValue >= 0
|
|
72
|
+
const isMinValid = (enteredMinValue <= 0 && minValue >= 0) || (enteredMinValue <= minValue && minValue < 0)
|
|
70
73
|
|
|
71
74
|
if (data) {
|
|
72
75
|
let min = enteredMinValue && isMinValid ? enteredMinValue : minValue
|
|
73
76
|
let max = enteredMaxValue && isMaxValid ? enteredMaxValue : Number.MIN_VALUE
|
|
74
77
|
|
|
75
|
-
if ((config.visualizationType === 'Bar' || config.visualizationType === 'Combo') && min > 0) {
|
|
78
|
+
if ((config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && !isAllLine)) && min > 0) {
|
|
76
79
|
min = 0
|
|
77
80
|
}
|
|
81
|
+
if (config.visualizationType === 'Combo' && isAllLine) {
|
|
82
|
+
if ((enteredMinValue === undefined || enteredMinValue === null || enteredMinValue === '') && min > 0) {
|
|
83
|
+
min = 0
|
|
84
|
+
}
|
|
85
|
+
if (enteredMinValue) {
|
|
86
|
+
const isMinValid = +enteredMinValue < minValue
|
|
87
|
+
min = +enteredMinValue && isMinValid ? enteredMinValue : minValue
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
if (config.visualizationType === 'Line') {
|
|
79
|
-
const isMinValid =
|
|
80
|
-
min = enteredMinValue && isMinValid ?
|
|
92
|
+
const isMinValid = enteredMinValue < minValue
|
|
93
|
+
min = enteredMinValue && isMinValid ? enteredMinValue : minValue
|
|
81
94
|
}
|
|
82
95
|
//If data value max wasn't provided, calculate it
|
|
83
96
|
if (max === Number.MIN_VALUE) {
|
|
@@ -171,32 +184,72 @@ export default function LinearChart() {
|
|
|
171
184
|
}
|
|
172
185
|
}
|
|
173
186
|
|
|
174
|
-
|
|
175
|
-
|
|
187
|
+
const handleLeftTickFormatting = tick => {
|
|
188
|
+
if (config.runtime.yAxis.type === 'date') return formatDate(parseDate(tick))
|
|
189
|
+
if (config.orientation === 'vertical') return formatNumber(tick, 'left')
|
|
190
|
+
return tick
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const handleBottomTickFormatting = tick => {
|
|
194
|
+
if (config.runtime.xAxis.type === 'date') return formatDate(tick)
|
|
195
|
+
if (config.orientation === 'horizontal') return formatNumber(tick, 'bottom')
|
|
196
|
+
return tick
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const countNumOfTicks = axis => {
|
|
176
200
|
// function get number of ticks based on bar type & users value
|
|
177
|
-
const isHorizontal = config.orientation ==='horizontal'
|
|
178
|
-
const {numTicks} = config.runtime[axis]
|
|
179
|
-
let tickCount = undefined
|
|
180
|
-
|
|
181
|
-
if(axis === 'yAxis'){
|
|
182
|
-
tickCount =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
const isHorizontal = config.orientation === 'horizontal'
|
|
202
|
+
const { numTicks } = config.runtime[axis]
|
|
203
|
+
let tickCount = undefined
|
|
204
|
+
|
|
205
|
+
if (axis === 'yAxis') {
|
|
206
|
+
tickCount = isHorizontal && !numTicks ? data.length : isHorizontal && numTicks ? numTicks : !isHorizontal && !numTicks ? undefined : !isHorizontal && numTicks && numTicks
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (axis === 'xAxis') {
|
|
210
|
+
tickCount = isHorizontal && !numTicks ? undefined : isHorizontal && numTicks ? numTicks : !isHorizontal && !numTicks ? undefined : !isHorizontal && numTicks && numTicks
|
|
211
|
+
}
|
|
212
|
+
return tickCount
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Handle Box Plots
|
|
216
|
+
if (config.visualizationType === 'Box Plot') {
|
|
217
|
+
let minYValue
|
|
218
|
+
let maxYValue
|
|
219
|
+
let allOutliers = []
|
|
220
|
+
let allLowerBounds = config.boxplot.map(plot => plot.columnMin)
|
|
221
|
+
let allUpperBounds = config.boxplot.map(plot => plot.columnMax)
|
|
222
|
+
|
|
223
|
+
minYValue = Math.min(...allLowerBounds)
|
|
224
|
+
maxYValue = Math.max(...allUpperBounds)
|
|
225
|
+
|
|
226
|
+
const hasOutliers = config.boxplot.map(b => b.columnOutliers.map(outlier => allOutliers.push(outlier)))
|
|
227
|
+
|
|
228
|
+
if (hasOutliers) {
|
|
229
|
+
let outlierMin = Math.min(...allOutliers)
|
|
230
|
+
let outlierMax = Math.max(...allOutliers)
|
|
231
|
+
|
|
232
|
+
// check if outliers exceed standard bounds
|
|
233
|
+
if (outlierMin < minYValue) minYValue = outlierMin
|
|
234
|
+
if (outlierMax > maxYValue) maxYValue = outlierMax
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const seriesNames = data.map(d => d[config.xAxis.dataKey])
|
|
238
|
+
|
|
239
|
+
// Set Scales
|
|
240
|
+
yScale = scaleLinear({
|
|
241
|
+
range: [yMax, 0],
|
|
242
|
+
round: true,
|
|
243
|
+
domain: [minYValue, maxYValue]
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
xScale = scaleBand({
|
|
247
|
+
range: [0, xMax],
|
|
248
|
+
round: true,
|
|
249
|
+
domain: config.boxplot.categories,
|
|
250
|
+
padding: 0.4
|
|
251
|
+
})
|
|
252
|
+
}
|
|
200
253
|
|
|
201
254
|
useEffect(() => {
|
|
202
255
|
ReactTooltip.rebuild()
|
|
@@ -217,7 +270,7 @@ export default function LinearChart() {
|
|
|
217
270
|
const width = to - from
|
|
218
271
|
|
|
219
272
|
return (
|
|
220
|
-
<Group className='regions' left={config.runtime.yAxis.size} key={region.label}>
|
|
273
|
+
<Group className='regions' left={Number(config.runtime.yAxis.size)} key={region.label}>
|
|
221
274
|
<path
|
|
222
275
|
stroke='#333'
|
|
223
276
|
d={`M${from} -5
|
|
@@ -238,22 +291,16 @@ export default function LinearChart() {
|
|
|
238
291
|
|
|
239
292
|
{/* Y axis */}
|
|
240
293
|
{config.visualizationType !== 'Spark Line' && (
|
|
241
|
-
<AxisLeft
|
|
242
|
-
scale={yScale}
|
|
243
|
-
left={config.runtime.yAxis.size}
|
|
244
|
-
label={config.runtime.yAxis.label}
|
|
245
|
-
stroke='#333'
|
|
246
|
-
tickFormat={tick => (config.runtime.yAxis.type === 'date' ? formatDate(parseDate(tick)) : config.orientation === 'vertical' ? formatNumber(tick) : tick)}
|
|
247
|
-
numTicks={countNumOfTicks('yAxis')}
|
|
248
|
-
>
|
|
294
|
+
<AxisLeft scale={yScale} left={Number(config.runtime.yAxis.size)} label={config.runtime.yAxis.label} stroke='#333' tickFormat={tick => handleLeftTickFormatting(tick)} numTicks={countNumOfTicks('yAxis')}>
|
|
249
295
|
{props => {
|
|
250
|
-
const lollipopShapeSize = config.lollipopSize === 'large' ? 14 : config.lollipopSize === 'medium' ? 12 : 10
|
|
251
296
|
const axisCenter = config.runtime.horizontal ? (props.axisToPoint.y - props.axisFromPoint.y) / 2 : (props.axisFromPoint.y - props.axisToPoint.y) / 2
|
|
252
297
|
const horizontalTickOffset = yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
|
|
253
|
-
const belowBarPaddingFromTop = 9
|
|
254
298
|
return (
|
|
255
299
|
<Group className='left-axis'>
|
|
256
300
|
{props.ticks.map((tick, i) => {
|
|
301
|
+
const minY = props.ticks[0].to.y
|
|
302
|
+
const barMinHeight = 15 // 15 is the min height for bars by default
|
|
303
|
+
|
|
257
304
|
return (
|
|
258
305
|
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
259
306
|
{!config.runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke={config.yAxis.tickColor} display={config.runtime.horizontal ? 'none' : 'block'} />}
|
|
@@ -261,23 +308,22 @@ export default function LinearChart() {
|
|
|
261
308
|
{config.runtime.yAxis.gridLines ? <Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='rgba(0,0,0,0.3)' /> : ''}
|
|
262
309
|
|
|
263
310
|
{config.orientation === 'horizontal' && config.visualizationSubType !== 'stacked' && config.yAxis.labelPlacement === 'On Date/Category Axis' && !config.yAxis.hideLabel && (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
311
|
+
<Text
|
|
312
|
+
transform={`translate(${tick.to.x - 5}, ${config.isLollipopChart ? tick.to.y - minY : tick.to.y - minY + (Number(config.barHeight * config.series.length) - barMinHeight) / 2}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`}
|
|
313
|
+
verticalAnchor={'start'}
|
|
314
|
+
textAnchor={'end'}
|
|
315
|
+
>
|
|
316
|
+
{tick.formattedValue}
|
|
317
|
+
</Text>
|
|
270
318
|
)}
|
|
271
319
|
|
|
272
320
|
{config.orientation === 'horizontal' && config.visualizationSubType === 'stacked' && config.yAxis.labelPlacement === 'On Date/Category Axis' && !config.yAxis.hideLabel && (
|
|
273
|
-
|
|
274
|
-
<Text transform={`translate(${tick.to.x - 5}, ${tick.from.y - config.barHeight / 2 - 3}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`} verticalAnchor={config.isLollipopChart ? 'middle' : 'middle'} textAnchor={'end'}>
|
|
321
|
+
<Text transform={`translate(${tick.to.x - 5}, ${tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`} verticalAnchor={'start'} textAnchor={'end'}>
|
|
275
322
|
{tick.formattedValue}
|
|
276
323
|
</Text>
|
|
277
324
|
)}
|
|
278
325
|
|
|
279
326
|
{config.orientation === 'horizontal' && config.visualizationType === 'Paired Bar' && !config.yAxis.hideLabel && (
|
|
280
|
-
// 17 is a magic number from the offset in barchart.
|
|
281
327
|
<Text transform={`translate(${-15}, ${tick.from.y}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`} verticalAnchor={config.isLollipopChart ? 'middle' : 'middle'} textAnchor={'end'}>
|
|
282
328
|
{tick.formattedValue}
|
|
283
329
|
</Text>
|
|
@@ -304,7 +350,7 @@ export default function LinearChart() {
|
|
|
304
350
|
</Group>
|
|
305
351
|
)
|
|
306
352
|
})}
|
|
307
|
-
{!config.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#
|
|
353
|
+
{!config.yAxis.hideAxis && <Line from={props.axisFromPoint} to={config.runtime.horizontal ? { x: 0, y: Number(heightHorizontal) } : props.axisToPoint} stroke='#000' />}
|
|
308
354
|
{yScale.domain()[0] < 0 && <Line from={{ x: props.axisFromPoint.x, y: yScale(0) }} to={{ x: xMax, y: yScale(0) }} stroke='#333' />}
|
|
309
355
|
<Text className='y-label' textAnchor='middle' verticalAnchor='start' transform={`translate(${-1 * config.runtime.yAxis.size}, ${axisCenter}) rotate(-90)`} fontWeight='bold' fill={config.yAxis.labelColor}>
|
|
310
356
|
{props.label}
|
|
@@ -317,7 +363,7 @@ export default function LinearChart() {
|
|
|
317
363
|
|
|
318
364
|
{/* Right Axis */}
|
|
319
365
|
{hasRightAxis && (
|
|
320
|
-
<AxisRight scale={yScaleRight} left={width - config.yAxis.rightAxisSize} label={config.yAxis.rightLabel} tickFormat={tick => formatNumber(tick, 'right')} numTicks={config.runtime.yAxis.rightNumTicks || undefined} labelOffset={45}>
|
|
366
|
+
<AxisRight scale={yScaleRight} left={Number(width - config.yAxis.rightAxisSize)} label={config.yAxis.rightLabel} tickFormat={tick => formatNumber(tick, 'right')} numTicks={config.runtime.yAxis.rightNumTicks || undefined} labelOffset={45}>
|
|
321
367
|
{props => {
|
|
322
368
|
const axisCenter = config.runtime.horizontal ? (props.axisToPoint.y - props.axisFromPoint.y) / 2 : (props.axisFromPoint.y - props.axisToPoint.y) / 2
|
|
323
369
|
const horizontalTickOffset = yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
|
|
@@ -351,7 +397,7 @@ export default function LinearChart() {
|
|
|
351
397
|
{hasTopAxis && config.topAxis.hasLine && (
|
|
352
398
|
<AxisTop
|
|
353
399
|
stroke='#333'
|
|
354
|
-
left={config.runtime.yAxis.size}
|
|
400
|
+
left={Number(config.runtime.yAxis.size)}
|
|
355
401
|
scale={xScale}
|
|
356
402
|
hideTicks
|
|
357
403
|
hideZero
|
|
@@ -363,16 +409,7 @@ export default function LinearChart() {
|
|
|
363
409
|
|
|
364
410
|
{/* X axis */}
|
|
365
411
|
{config.visualizationType !== 'Paired Bar' && config.visualizationType !== 'Spark Line' && (
|
|
366
|
-
<AxisBottom
|
|
367
|
-
top={yMax}
|
|
368
|
-
left={config.runtime.yAxis.size}
|
|
369
|
-
label={config.runtime.xAxis.label}
|
|
370
|
-
tickFormat={tick => (config.runtime.xAxis.type === 'date' ? formatDate(tick) : config.orientation === 'horizontal' ? formatNumber(tick) : tick)}
|
|
371
|
-
scale={xScale}
|
|
372
|
-
stroke='#333'
|
|
373
|
-
tickStroke='#333'
|
|
374
|
-
numTicks={countNumOfTicks('xAxis')}
|
|
375
|
-
>
|
|
412
|
+
<AxisBottom top={yMax} left={Number(config.runtime.yAxis.size)} label={config.runtime.xAxis.label} tickFormat={handleBottomTickFormatting} scale={xScale} stroke='#333' tickStroke='#333' numTicks={countNumOfTicks('xAxis')}>
|
|
376
413
|
{props => {
|
|
377
414
|
const axisCenter = (props.axisToPoint.x - props.axisFromPoint.x) / 2
|
|
378
415
|
return (
|
|
@@ -397,7 +434,7 @@ export default function LinearChart() {
|
|
|
397
434
|
)
|
|
398
435
|
})}
|
|
399
436
|
{!config.xAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
|
|
400
|
-
<Text x={axisCenter} y={config.
|
|
437
|
+
<Text x={axisCenter} y={config.orientation === 'horizontal' ? config.xAxis.labelOffset : config.xAxis.size} textAnchor='middle' fontWeight='bold' fill={config.xAxis.labelColor}>
|
|
401
438
|
{props.label}
|
|
402
439
|
</Text>
|
|
403
440
|
</Group>
|
|
@@ -408,7 +445,7 @@ export default function LinearChart() {
|
|
|
408
445
|
|
|
409
446
|
{config.visualizationType === 'Paired Bar' && (
|
|
410
447
|
<>
|
|
411
|
-
<AxisBottom top={yMax} left={config.runtime.yAxis.size} label={config.runtime.xAxis.label} tickFormat={config.runtime.xAxis.type === 'date' ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={config.runtime.xAxis.numTicks || undefined}>
|
|
448
|
+
<AxisBottom top={yMax} left={Number(config.runtime.yAxis.size)} label={config.runtime.xAxis.label} tickFormat={config.runtime.xAxis.type === 'date' ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={config.runtime.xAxis.numTicks || undefined}>
|
|
412
449
|
{props => {
|
|
413
450
|
const axisCenter = (props.axisToPoint.x - props.axisFromPoint.x) / 2
|
|
414
451
|
return (
|
|
@@ -433,7 +470,7 @@ export default function LinearChart() {
|
|
|
433
470
|
</AxisBottom>
|
|
434
471
|
<AxisBottom
|
|
435
472
|
top={yMax}
|
|
436
|
-
left={config.runtime.yAxis.size}
|
|
473
|
+
left={Number(config.runtime.yAxis.size)}
|
|
437
474
|
label={config.runtime.xAxis.label}
|
|
438
475
|
tickFormat={config.runtime.xAxis.type === 'date' ? formatDate : config.runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick}
|
|
439
476
|
scale={g2xScale}
|
|
@@ -462,7 +499,7 @@ export default function LinearChart() {
|
|
|
462
499
|
{!config.runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
|
|
463
500
|
</Group>
|
|
464
501
|
<Group>
|
|
465
|
-
<Text transform={`translate(${xMax / 2}, ${
|
|
502
|
+
<Text transform={`translate(${xMax / 2}, ${yMax + 20}) rotate(-${0})`} verticalAnchor='start' textAnchor={'middle'} stroke='#333'>
|
|
466
503
|
{config.runtime.xAxis.label}
|
|
467
504
|
</Text>
|
|
468
505
|
</Group>
|
|
@@ -475,18 +512,20 @@ export default function LinearChart() {
|
|
|
475
512
|
{config.visualizationType === 'Paired Bar' && <PairedBarChart width={xMax} height={yMax} />}
|
|
476
513
|
|
|
477
514
|
{/* Bar chart */}
|
|
478
|
-
{config.visualizationType !== 'Line' && config.visualizationType !== 'Paired Bar' && (
|
|
515
|
+
{config.visualizationType !== 'Line' && config.visualizationType !== 'Paired Bar' && config.visualizationType !== 'Box Plot' && (
|
|
479
516
|
<>
|
|
480
517
|
<BarChart xScale={xScale} yScale={yScale} seriesScale={seriesScale} xMax={xMax} yMax={yMax} getXAxisData={getXAxisData} getYAxisData={getYAxisData} animatedChart={animatedChart} visible={animatedChart} />
|
|
481
518
|
</>
|
|
482
519
|
)}
|
|
483
520
|
|
|
484
521
|
{/* Line chart */}
|
|
485
|
-
{config.visualizationType !== 'Bar' && config.visualizationType !== 'Paired Bar' && (
|
|
522
|
+
{config.visualizationType !== 'Bar' && config.visualizationType !== 'Paired Bar' && config.visualizationType !== 'Box Plot' && (
|
|
486
523
|
<>
|
|
487
524
|
<LineChart xScale={xScale} yScale={yScale} getXAxisData={getXAxisData} getYAxisData={getYAxisData} xMax={xMax} yMax={yMax} seriesStyle={config.series} />
|
|
488
525
|
</>
|
|
489
526
|
)}
|
|
527
|
+
|
|
528
|
+
{config.visualizationType === 'Box Plot' && <CoveBoxPlot xScale={xScale} yScale={yScale} />}
|
|
490
529
|
</svg>
|
|
491
530
|
<ReactTooltip id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} html={true} type='light' arrowColor='rgba(0,0,0,0)' className='tooltip' />
|
|
492
531
|
<div className='animation-trigger' ref={triggerRef} />
|
|
@@ -97,7 +97,7 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
97
97
|
`}
|
|
98
98
|
</style>
|
|
99
99
|
<svg id='cdc-visualization__paired-bar-chart' width={width} height={height} viewBox={`0 0 ${width} ${height}`} role='img' tabIndex={0}>
|
|
100
|
-
<Group top={0} left={config.xAxis.size}>
|
|
100
|
+
<Group top={0} left={Number(config.xAxis.size)}>
|
|
101
101
|
{data
|
|
102
102
|
.filter(item => config.series[0].dataKey === groupOne.dataKey)
|
|
103
103
|
.map((d, index) => {
|