@cdc/chart 4.22.11 → 4.23.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/dist/cdcchart.js +54569 -16
- package/examples/Barchart_with_negative.json +34 -0
- package/examples/box-plot-data.json +71 -0
- package/examples/box-plot.csv +5 -0
- package/examples/box-plot.json +124 -0
- package/examples/dynamic-legends.json +1 -1
- package/examples/example-bar-chart-nonnumeric.json +36 -0
- package/examples/example-bar-chart.json +33 -0
- package/examples/example-combo-bar-nonnumeric.json +105 -0
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +3 -1
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +1 -1
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +86 -17
- package/examples/gallery/paired-bar/paired-bar-chart.json +65 -13
- package/examples/line-chart-nonnumeric.json +32 -0
- package/examples/line-chart.json +21 -63
- package/examples/new-data.csv +17 -0
- package/examples/newdata.json +90 -0
- package/examples/planet-combo-example-config.json +143 -20
- package/examples/planet-example-data-nonnumeric.json +56 -0
- package/examples/planet-example-data.json +2 -2
- package/examples/planet-pie-example-config-nonnumeric.json +30 -0
- package/examples/scatterplot-continuous.csv +17 -0
- package/examples/{private/yaxis-test.json → scatterplot.json} +53 -50
- package/examples/sparkline-chart-nonnumeric.json +76 -0
- package/examples/stacked-vertical-bar-example-negative.json +154 -0
- package/examples/stacked-vertical-bar-example-nonnumerics.json +154 -0
- package/{src/index.html → index.html} +18 -11
- package/package.json +29 -22
- package/src/{CdcChart.tsx → CdcChart.jsx} +193 -119
- package/src/components/BarChart.jsx +517 -0
- package/src/components/BoxPlot.jsx +88 -0
- package/src/components/{DataTable.tsx → DataTable.jsx} +125 -32
- package/src/components/{EditorPanel.js → EditorPanel.jsx} +376 -115
- package/src/components/Filters.jsx +125 -0
- package/src/components/Legend.jsx +303 -0
- package/src/components/{LineChart.tsx → LineChart.jsx} +87 -22
- package/src/components/{LinearChart.tsx → LinearChart.jsx} +172 -113
- package/src/components/{PairedBarChart.tsx → PairedBarChart.jsx} +46 -79
- package/src/components/{PieChart.tsx → PieChart.jsx} +29 -34
- package/src/components/ScatterPlot.jsx +48 -0
- package/src/components/{SparkLine.js → SparkLine.jsx} +49 -18
- package/src/components/useIntersectionObserver.jsx +29 -0
- package/src/data/initial-state.js +44 -8
- package/src/hooks/{useColorPalette.ts → useColorPalette.js} +10 -28
- package/src/hooks/{useReduceData.ts → useReduceData.js} +27 -13
- package/src/hooks/useRightAxis.js +3 -1
- package/src/index.jsx +16 -0
- package/src/scss/DataTable.scss +23 -1
- package/src/scss/main.scss +83 -32
- package/vite.config.js +4 -0
- 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
- package/src/components/BarChart.tsx +0 -579
- package/src/components/Legend.js +0 -284
- package/src/components/useIntersectionObserver.tsx +0 -27
- package/src/index.tsx +0 -18
- /package/src/{context.tsx → ConfigContext.jsx} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React, { useState, useContext } from 'react'
|
|
2
|
+
import ConfigContext from './../ConfigContext'
|
|
3
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
4
|
+
|
|
5
|
+
const useFilters = () => {
|
|
6
|
+
const { config, setConfig, filteredData, setFilteredData, excludedData, filterData, runtimeFilters } = useContext(ConfigContext)
|
|
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(ConfigContext)
|
|
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
|
+
<div className='filters-section__wrapper' style={{ flexWrap: 'wrap', display: 'flex', gap: '7px 15px', marginTop: '15px' }}>
|
|
109
|
+
<FilterList />
|
|
110
|
+
{config.filters.length > 0 && (
|
|
111
|
+
<div className='filter-section__buttons' style={{ width: '100%' }}>
|
|
112
|
+
<Button onClick={() => handleApplyButton(filters)} disabled={!showApplyButton} style={{ marginRight: '10px' }}>
|
|
113
|
+
{buttonText}
|
|
114
|
+
</Button>
|
|
115
|
+
<a href='#!' role='button' onClick={handleReset}>
|
|
116
|
+
{resetText}
|
|
117
|
+
</a>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default Filters
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import React, { useContext, useEffect } from 'react'
|
|
2
|
+
import ConfigContext from '../ConfigContext'
|
|
3
|
+
import parse from 'html-react-parser'
|
|
4
|
+
import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
|
|
5
|
+
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
6
|
+
|
|
7
|
+
import useLegendClasses from './../hooks/useLegendClasses'
|
|
8
|
+
|
|
9
|
+
const Legend = () => {
|
|
10
|
+
const { config, legend, colorScale, seriesHighlight, highlight, highlightReset, setSeriesHighlight, dynamicLegendItems, setDynamicLegendItems, transformedData: data, colorPalettes, rawData, setConfig, currentViewport } = useContext(ConfigContext)
|
|
11
|
+
|
|
12
|
+
const { innerClasses, containerClasses } = useLegendClasses(config)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (dynamicLegendItems.length === 0) return
|
|
16
|
+
|
|
17
|
+
let itemsToHighlight = dynamicLegendItems.map(item => item.text)
|
|
18
|
+
|
|
19
|
+
setSeriesHighlight(itemsToHighlight)
|
|
20
|
+
|
|
21
|
+
let colsToKeep = [...itemsToHighlight]
|
|
22
|
+
let tmpLabels = []
|
|
23
|
+
|
|
24
|
+
rawData.map(dataItem => {
|
|
25
|
+
let tmp = {}
|
|
26
|
+
colsToKeep.map(col => {
|
|
27
|
+
tmp[col] = isNaN(dataItem[col]) ? dataItem[col] : dataItem[col]
|
|
28
|
+
})
|
|
29
|
+
return tmp
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
colsToKeep.map(col => {
|
|
33
|
+
tmpLabels[col] = col
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (dynamicLegendItems.length > 0) {
|
|
37
|
+
setConfig({
|
|
38
|
+
...config,
|
|
39
|
+
runtime: {
|
|
40
|
+
...config.runtime,
|
|
41
|
+
seriesKeys: colsToKeep,
|
|
42
|
+
seriesLabels: tmpLabels
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}, [dynamicLegendItems])
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (dynamicLegendItems.length === 0) {
|
|
50
|
+
// loop through all labels and add keys
|
|
51
|
+
let resetSeriesNames = [...config.runtime.seriesLabelsAll]
|
|
52
|
+
let tmpLabels = []
|
|
53
|
+
config.runtime.seriesLabelsAll.map(item => {
|
|
54
|
+
resetSeriesNames.map(col => {
|
|
55
|
+
tmpLabels[col] = col
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
setConfig({
|
|
60
|
+
...config,
|
|
61
|
+
runtime: {
|
|
62
|
+
...config.runtime,
|
|
63
|
+
seriesKeys: config.runtime.seriesLabelsAll,
|
|
64
|
+
seriesLabels: tmpLabels
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}, [dynamicLegendItems])
|
|
69
|
+
|
|
70
|
+
const removeDynamicLegendItem = label => {
|
|
71
|
+
let newLegendItems = dynamicLegendItems.filter(item => item.text !== label.text)
|
|
72
|
+
let newLegendItemsText = newLegendItems.map(item => item.text)
|
|
73
|
+
setDynamicLegendItems(newLegendItems)
|
|
74
|
+
setSeriesHighlight(newLegendItemsText)
|
|
75
|
+
}
|
|
76
|
+
const handleDynamicLegendChange = e => {
|
|
77
|
+
setDynamicLegendItems([...dynamicLegendItems, JSON.parse(e.target.value)])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const createLegendLabels = (data, defaultLabels) => {
|
|
81
|
+
const colorCode = config.legend?.colorCode
|
|
82
|
+
if (config.visualizationType !== 'Bar' || config.visualizationSubType !== 'regular' || !colorCode || config.series?.length > 1) {
|
|
83
|
+
return defaultLabels
|
|
84
|
+
}
|
|
85
|
+
let palette = colorPalettes[config.palette]
|
|
86
|
+
|
|
87
|
+
while (data.length > palette.length) {
|
|
88
|
+
palette = palette.concat(palette)
|
|
89
|
+
}
|
|
90
|
+
palette = palette.slice(0, data.length)
|
|
91
|
+
//store uniq values to Set by colorCode
|
|
92
|
+
const set = new Set()
|
|
93
|
+
|
|
94
|
+
data.forEach(d => set.add(d[colorCode]))
|
|
95
|
+
|
|
96
|
+
// create labels with uniq values
|
|
97
|
+
const uniqeLabels = Array.from(set).map((val, i) => {
|
|
98
|
+
const newLabel = {
|
|
99
|
+
datum: val,
|
|
100
|
+
index: i,
|
|
101
|
+
text: val,
|
|
102
|
+
value: palette[i]
|
|
103
|
+
}
|
|
104
|
+
return newLabel
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return uniqeLabels
|
|
108
|
+
}
|
|
109
|
+
// in small screens update config legend position.
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (currentViewport === 'sm' || currentViewport === 'xs' || config.legend.position === 'left') {
|
|
112
|
+
setConfig({ ...config, legend: { ...config.legend, position: 'bottom' } })
|
|
113
|
+
}
|
|
114
|
+
setConfig({ ...config, legend: { ...config.legend, position: 'right' } })
|
|
115
|
+
}, [currentViewport])
|
|
116
|
+
|
|
117
|
+
if (!legend) return
|
|
118
|
+
|
|
119
|
+
if (!legend.dynamicLegend)
|
|
120
|
+
return config.visualizationType !== 'Box Plot' ? (
|
|
121
|
+
<aside
|
|
122
|
+
style={{ marginTop: config.legend.position === 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px', marginBottom: config.legend.position === 'bottom' ? '15px' : '0px' }}
|
|
123
|
+
id='legend'
|
|
124
|
+
className={containerClasses.join(' ')}
|
|
125
|
+
role='region'
|
|
126
|
+
aria-label='legend'
|
|
127
|
+
tabIndex={0}
|
|
128
|
+
>
|
|
129
|
+
{legend.label && <h2>{parse(legend.label)}</h2>}
|
|
130
|
+
{legend.description && <p>{parse(legend.description)}</p>}
|
|
131
|
+
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
132
|
+
{labels => (
|
|
133
|
+
<div className={innerClasses.join(' ')}>
|
|
134
|
+
{createLegendLabels(data, labels).map((label, i) => {
|
|
135
|
+
let className = 'legend-item'
|
|
136
|
+
let itemName = label.datum
|
|
137
|
+
|
|
138
|
+
// Filter excluded data keys from legend
|
|
139
|
+
if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (config.runtime.seriesLabels) {
|
|
144
|
+
let index = config.runtime.seriesLabelsAll.indexOf(itemName)
|
|
145
|
+
itemName = config.runtime.seriesKeys[index]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
|
|
149
|
+
className += ' inactive'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<LegendItem
|
|
154
|
+
className={className}
|
|
155
|
+
tabIndex={0}
|
|
156
|
+
key={`legend-quantile-${i}`}
|
|
157
|
+
onKeyPress={e => {
|
|
158
|
+
if (e.key === 'Enter') {
|
|
159
|
+
highlight(label)
|
|
160
|
+
}
|
|
161
|
+
}}
|
|
162
|
+
onClick={() => {
|
|
163
|
+
highlight(label)
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<LegendCircle fill={label.value} />
|
|
167
|
+
<LegendLabel align='left' margin='0 0 0 4px'>
|
|
168
|
+
{label.text}
|
|
169
|
+
</LegendLabel>
|
|
170
|
+
</LegendItem>
|
|
171
|
+
)
|
|
172
|
+
})}
|
|
173
|
+
{seriesHighlight.length > 0 && (
|
|
174
|
+
<button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
|
|
175
|
+
Reset
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</LegendOrdinal>
|
|
181
|
+
</aside>
|
|
182
|
+
) : (
|
|
183
|
+
<aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
|
|
184
|
+
{config.boxplot.legend.displayHowToReadText && <h3>{config.boxplot.legend.howToReadText}</h3>}
|
|
185
|
+
</aside>
|
|
186
|
+
)
|
|
187
|
+
return (
|
|
188
|
+
config.visualizationType !== 'Box Plot' && (
|
|
189
|
+
<aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
|
|
190
|
+
{legend.label && <h2>{parse(legend.label)}</h2>}
|
|
191
|
+
{legend.description && <p>{parse(legend.description)}</p>}
|
|
192
|
+
|
|
193
|
+
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
194
|
+
{labels => {
|
|
195
|
+
if (
|
|
196
|
+
Number(config.legend.dynamicLegendItemLimit) > dynamicLegendItems.length && // legend items are less than limit
|
|
197
|
+
dynamicLegendItems.length !== config.runtime.seriesLabelsAll.length
|
|
198
|
+
) {
|
|
199
|
+
// legend items are equal to series length
|
|
200
|
+
return (
|
|
201
|
+
<select className='dynamic-legend-dropdown' onChange={e => handleDynamicLegendChange(e)}>
|
|
202
|
+
<option className={'all'} tabIndex={0} value={JSON.stringify({ text: config.legend.dynamicLegendDefaultText })}>
|
|
203
|
+
{config.legend.dynamicLegendDefaultText}
|
|
204
|
+
</option>
|
|
205
|
+
{labels.map((label, i) => {
|
|
206
|
+
let className = 'legend-item'
|
|
207
|
+
let itemName = label.datum
|
|
208
|
+
let inDynamicList = false
|
|
209
|
+
|
|
210
|
+
// Filter excluded data keys from legend
|
|
211
|
+
if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (config.runtime.seriesLabels) {
|
|
216
|
+
let index = config.runtime.seriesLabelsAll.indexOf(itemName)
|
|
217
|
+
itemName = config.runtime.seriesKeys[index]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
|
|
221
|
+
className += ' inactive'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
dynamicLegendItems.map(listItem => {
|
|
225
|
+
if (listItem.text === label.text) {
|
|
226
|
+
inDynamicList = true
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
if (inDynamicList) return true
|
|
231
|
+
let palette = colorPalettes[config.palette]
|
|
232
|
+
|
|
233
|
+
label.value = palette[dynamicLegendItems.length]
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<option className={className} tabIndex={0} value={JSON.stringify(label)}>
|
|
237
|
+
{label.text}
|
|
238
|
+
</option>
|
|
239
|
+
)
|
|
240
|
+
})}
|
|
241
|
+
</select>
|
|
242
|
+
)
|
|
243
|
+
} else {
|
|
244
|
+
return config.legend.dynamicLegendItemLimitMessage
|
|
245
|
+
}
|
|
246
|
+
}}
|
|
247
|
+
</LegendOrdinal>
|
|
248
|
+
|
|
249
|
+
<div className='dynamic-legend-list'>
|
|
250
|
+
{dynamicLegendItems.map((label, i) => {
|
|
251
|
+
let className = ['legend-item']
|
|
252
|
+
let itemName = label.text
|
|
253
|
+
let palette = colorPalettes[config.palette]
|
|
254
|
+
|
|
255
|
+
// Filter excluded data keys from legend
|
|
256
|
+
if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (config.runtime.seriesLabels && !config.legend.dynamicLegend) {
|
|
261
|
+
let index = config.runtime.seriesLabelsAll.indexOf(itemName)
|
|
262
|
+
itemName = config.runtime.seriesKeys[index]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (seriesHighlight.length > 0 && !seriesHighlight.includes(itemName)) {
|
|
266
|
+
className.push('inactive')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (seriesHighlight.length === 0 && config.legend.dynamicLegend) {
|
|
270
|
+
className.push('inactive')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
<LegendItem className={className.join(' ')} tabIndex={0} key={`dynamic-legend-item-${i}`} alignItems='center'>
|
|
276
|
+
<button
|
|
277
|
+
className='btn-wrapper'
|
|
278
|
+
onClick={() => {
|
|
279
|
+
highlight(label)
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
<LegendCircle fill={palette[i]} config={config} />
|
|
283
|
+
<LegendLabel align='space-between' margin='4px 0 0 4px'>
|
|
284
|
+
{label.text}
|
|
285
|
+
</LegendLabel>
|
|
286
|
+
</button>
|
|
287
|
+
<button onClick={() => removeDynamicLegendItem(label)}>x</button>
|
|
288
|
+
</LegendItem>
|
|
289
|
+
</>
|
|
290
|
+
)
|
|
291
|
+
})}
|
|
292
|
+
</div>
|
|
293
|
+
{seriesHighlight.length < dynamicLegendItems.length && (
|
|
294
|
+
<button className={`legend-reset legend-reset--dynamic ${config.theme}`} onClick={highlightReset} tabIndex={0}>
|
|
295
|
+
Reset
|
|
296
|
+
</button>
|
|
297
|
+
)}
|
|
298
|
+
</aside>
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default Legend
|
|
@@ -7,12 +7,15 @@ import { Text } from '@visx/text'
|
|
|
7
7
|
|
|
8
8
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import ConfigContext from '../ConfigContext'
|
|
11
11
|
|
|
12
12
|
import useRightAxis from '../hooks/useRightAxis'
|
|
13
13
|
|
|
14
14
|
export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData, xMax, yMax, seriesStyle = 'Line' }) {
|
|
15
|
-
const { colorPalettes, transformedData: data, colorScale, seriesHighlight, config, formatNumber, formatDate, parseDate, updateConfig } = useContext
|
|
15
|
+
const { colorPalettes, transformedData: data, colorScale, seriesHighlight, config, formatNumber, formatDate, parseDate, isNumber, cleanData, updateConfig } = useContext(ConfigContext)
|
|
16
|
+
// Just do this once up front otherwise we end up
|
|
17
|
+
// calling clean several times on same set of data (TT)
|
|
18
|
+
const cleanedData = cleanData(data, config.xAxis.dataKey)
|
|
16
19
|
const { yScaleRight } = useRightAxis({ config, yMax, data, updateConfig })
|
|
17
20
|
|
|
18
21
|
const handleLineType = lineType => {
|
|
@@ -28,9 +31,22 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
const handleAxisFormating = (axis = 'left', label, value) => {
|
|
35
|
+
// if this is an x axis category/date value return without doing any formatting.
|
|
36
|
+
if (label === config.runtime.xAxis.label) return value
|
|
37
|
+
|
|
38
|
+
axis = String(axis).toLocaleLowerCase()
|
|
39
|
+
if (label) {
|
|
40
|
+
return `${label}: ${formatNumber(value, axis)}`
|
|
41
|
+
}
|
|
42
|
+
return `${formatNumber(value, axis)}`
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
return (
|
|
32
46
|
<ErrorBoundary component='LineChart'>
|
|
33
|
-
<Group left={config.runtime.yAxis.size}>
|
|
47
|
+
<Group left={config.runtime.yAxis.size ? parseInt(config.runtime.yAxis.size) : 66}>
|
|
48
|
+
{' '}
|
|
49
|
+
{/* left - expects a number not a string */}
|
|
34
50
|
{(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => {
|
|
35
51
|
let lineType = config.series.filter(item => item.dataKey === seriesKey)[0].type
|
|
36
52
|
const seriesData = config.series.filter(item => item.dataKey === seriesKey)
|
|
@@ -42,28 +58,41 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
42
58
|
opacity={config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(seriesKey) === -1 ? 0.5 : 1}
|
|
43
59
|
display={config.legend.behavior === 'highlight' || (seriesHighlight.length === 0 && !config.legend.dynamicLegend) || seriesHighlight.indexOf(seriesKey) !== -1 ? 'block' : 'none'}
|
|
44
60
|
>
|
|
45
|
-
{
|
|
46
|
-
|
|
61
|
+
{cleanedData.map((d, dataIndex) => {
|
|
62
|
+
// Find the series object from the config.series array that has a dataKey matching the seriesKey variable.
|
|
63
|
+
const series = config.series.find(({ dataKey }) => dataKey === seriesKey)
|
|
64
|
+
const { axis } = series
|
|
65
|
+
|
|
47
66
|
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
|
-
|
|
67
|
+
const yAxisValue = getYAxisData(d, seriesKey)
|
|
68
|
+
|
|
69
|
+
const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
|
|
70
|
+
const labeltype = axis === 'Right' ? 'rightLabel' : 'label'
|
|
71
|
+
let label = config.runtime.yAxis[labeltype]
|
|
72
|
+
// if has muiltiple series dont show legend value on tooltip
|
|
73
|
+
if (!hasMultipleSeries) label = config.isLegendValue ? config.runtime.seriesLabels[seriesKey] : label
|
|
74
|
+
|
|
75
|
+
let yAxisTooltip = handleAxisFormating(axis, label, yAxisValue)
|
|
76
|
+
let xAxisTooltip = handleAxisFormating(axis, config.runtime.xAxis.label, xAxisValue)
|
|
77
|
+
|
|
56
78
|
const tooltip = `<div>
|
|
79
|
+
${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && Object.keys(config.runtime.seriesLabels).length > 1 ? `${config.runtime.seriesLabels[seriesKey] || ''}<br/>` : ''}
|
|
57
80
|
${yAxisTooltip}<br />
|
|
58
|
-
${xAxisTooltip}
|
|
59
|
-
${config.seriesLabel ? `${config.seriesLabel}: ${seriesKey}` : ''}
|
|
81
|
+
${xAxisTooltip}
|
|
60
82
|
</div>`
|
|
61
83
|
let circleRadii = 4.5
|
|
84
|
+
|
|
62
85
|
return (
|
|
63
86
|
d[seriesKey] !== undefined &&
|
|
64
87
|
d[seriesKey] !== '' &&
|
|
65
88
|
d[seriesKey] !== null && (
|
|
89
|
+
// isNumber(d[seriesKey]) &&
|
|
90
|
+
// isNumber(getYAxisData(d, seriesKey)) &&
|
|
91
|
+
// isNumber(getXAxisData(d)) &&
|
|
92
|
+
// isNumber(yScaleRight(getXAxisData(d))) &&
|
|
93
|
+
// isNumber(yScale(getXAxisData(d))) &&
|
|
66
94
|
<Group key={`series-${seriesKey}-point-${dataIndex}`}>
|
|
95
|
+
{/* Render legend */}
|
|
67
96
|
<Text
|
|
68
97
|
display={config.labels ? 'block' : 'none'}
|
|
69
98
|
x={xScale(getXAxisData(d))}
|
|
@@ -77,12 +106,12 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
77
106
|
<circle
|
|
78
107
|
key={`${seriesKey}-${dataIndex}`}
|
|
79
108
|
r={circleRadii}
|
|
80
|
-
cx={xScale(getXAxisData(d))}
|
|
109
|
+
cx={Number(xScale(getXAxisData(d)))}
|
|
81
110
|
cy={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))}
|
|
82
111
|
fill={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
|
|
83
112
|
style={{ fill: colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000' }}
|
|
84
|
-
data-
|
|
85
|
-
data-
|
|
113
|
+
data-tooltip-html={tooltip}
|
|
114
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
86
115
|
/>
|
|
87
116
|
</Group>
|
|
88
117
|
)
|
|
@@ -91,7 +120,7 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
91
120
|
|
|
92
121
|
<LinePath
|
|
93
122
|
curve={allCurves.curveLinear}
|
|
94
|
-
data={
|
|
123
|
+
data={cleanedData}
|
|
95
124
|
x={d => xScale(getXAxisData(d))}
|
|
96
125
|
y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey)))}
|
|
97
126
|
stroke={
|
|
@@ -107,14 +136,50 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
107
136
|
strokeOpacity={1}
|
|
108
137
|
shapeRendering='geometricPrecision'
|
|
109
138
|
strokeDasharray={lineType ? handleLineType(lineType) : 0}
|
|
110
|
-
defined={(item,i) => {
|
|
111
|
-
return item[config.runtime.seriesLabels[seriesKey]] !==
|
|
139
|
+
defined={(item, i) => {
|
|
140
|
+
return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null && item[config.runtime.seriesLabels[seriesKey]] !== undefined
|
|
112
141
|
}}
|
|
113
142
|
/>
|
|
143
|
+
{config.animate && (
|
|
144
|
+
<LinePath
|
|
145
|
+
className='animation'
|
|
146
|
+
curve={allCurves.curveLinear}
|
|
147
|
+
data={cleanedData}
|
|
148
|
+
x={d => xScale(getXAxisData(d))}
|
|
149
|
+
y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey)))}
|
|
150
|
+
stroke='#fff'
|
|
151
|
+
strokeWidth={3}
|
|
152
|
+
strokeOpacity={1}
|
|
153
|
+
shapeRendering='geometricPrecision'
|
|
154
|
+
strokeDasharray={lineType ? handleLineType(lineType) : 0}
|
|
155
|
+
defined={(item, i) => {
|
|
156
|
+
return isNumber(item[config.runtime.seriesLabels[seriesKey]])
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Render series labels at end if each line if selected in the editor */}
|
|
162
|
+
{config.showLineSeriesLabels &&
|
|
163
|
+
(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map(seriesKey => {
|
|
164
|
+
let lastDatum
|
|
165
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
166
|
+
if (data[i][seriesKey]) {
|
|
167
|
+
lastDatum = data[i]
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!lastDatum) {
|
|
172
|
+
return <></>
|
|
173
|
+
}
|
|
174
|
+
return (
|
|
175
|
+
<text x={xScale(getXAxisData(lastDatum)) + 5} y={yScale(getYAxisData(lastDatum, seriesKey))} alignmentBaseline='middle' fill={config.colorMatchLineSeriesLabels && colorScale ? colorScale(config.runtime.seriesLabels[seriesKey] || seriesKey) : 'black'}>
|
|
176
|
+
{config.runtime.seriesLabels[seriesKey] || seriesKey}
|
|
177
|
+
</text>
|
|
178
|
+
)
|
|
179
|
+
})}
|
|
114
180
|
</Group>
|
|
115
181
|
)
|
|
116
182
|
})}
|
|
117
|
-
|
|
118
183
|
{/* Message when dynamic legend and nothing has been picked */}
|
|
119
184
|
{config.legend.dynamicLegend && seriesHighlight.length === 0 && (
|
|
120
185
|
<Text x={xMax / 2} y={yMax / 2} fill='black' textAnchor='middle' color='black'>
|