@cdc/chart 4.23.8 → 4.23.10
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 +44114 -44410
- package/examples/feature/__data__/area-chart-date-apple.json +1 -5073
- package/examples/feature/area/area-chart-date-apple.json +73 -10316
- package/examples/feature/area/area-chart-date-city-temperature.json +204 -80
- package/examples/{private/confidence_interval_test.json → feature/area/area-chart-stacked.json} +65 -74
- package/examples/feature/filters/bar-filter.json +5027 -0
- package/examples/feature/legend-highlights/highlights.json +567 -0
- package/index.html +28 -7
- package/package.json +3 -2
- package/src/{CdcChart.jsx → CdcChart.tsx} +77 -71
- package/src/components/AreaChart.Stacked.jsx +73 -0
- package/src/components/AreaChart.jsx +24 -26
- package/src/components/BarChart.StackedVertical.jsx +2 -0
- package/src/components/DeviationBar.jsx +67 -13
- package/src/components/EditorPanel.jsx +493 -454
- package/src/components/Forecasting.jsx +5 -5
- package/src/components/Legend.jsx +17 -8
- package/src/components/LineChart.Circle.tsx +108 -0
- package/src/components/{LineChart.jsx → LineChart.tsx} +10 -42
- package/src/components/LinearChart.jsx +460 -443
- package/src/components/PieChart.jsx +54 -25
- package/src/components/Series.jsx +63 -17
- package/src/components/SparkLine.jsx +7 -19
- package/src/data/initial-state.js +10 -1
- package/src/hooks/useEditorPermissions.js +87 -24
- package/src/hooks/useLegendClasses.js +14 -11
- package/src/hooks/useReduceData.js +6 -1
- package/src/hooks/useScales.js +2 -2
- package/src/hooks/useTooltip.jsx +21 -8
- package/src/scss/legend.scss +206 -0
- package/src/scss/main.scss +25 -24
- package/examples/private/tooltip-issue.json +0 -45275
- package/src/components/DataTable.jsx +0 -374
- /package/src/{components → hooks}/useIntersectionObserver.jsx +0 -0
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import React, { useContext, useEffect, useState, useMemo } from 'react'
|
|
2
|
-
import { useTable, useSortBy, useResizeColumns, useBlockLayout } from 'react-table'
|
|
3
|
-
import Papa from 'papaparse'
|
|
4
|
-
import { Base64 } from 'js-base64'
|
|
5
|
-
import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
6
|
-
|
|
7
|
-
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
8
|
-
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
9
|
-
import Icon from '@cdc/core/components/ui/Icon'
|
|
10
|
-
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
11
|
-
|
|
12
|
-
import ConfigContext from '../ConfigContext'
|
|
13
|
-
|
|
14
|
-
import MediaControls from '@cdc/core/components/MediaControls'
|
|
15
|
-
|
|
16
|
-
export default function DataTable() {
|
|
17
|
-
const { rawData, tableData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes, currentViewport } = useContext(ConfigContext)
|
|
18
|
-
|
|
19
|
-
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
20
|
-
const [tableExpanded, setTableExpanded] = useState(config.table.expanded)
|
|
21
|
-
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
22
|
-
const isLegendBottom = ['sm', 'xs', 'xxs'].includes(currentViewport)
|
|
23
|
-
const transform = new DataTransform()
|
|
24
|
-
|
|
25
|
-
const DownloadButton = ({ data }, type) => {
|
|
26
|
-
const fileName = `${config.title.substring(0, 50)}.csv`
|
|
27
|
-
|
|
28
|
-
const csvData = Papa.unparse(data)
|
|
29
|
-
|
|
30
|
-
const saveBlob = () => {
|
|
31
|
-
//@ts-ignore
|
|
32
|
-
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
33
|
-
const dataBlob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
|
|
34
|
-
//@ts-ignore
|
|
35
|
-
window.navigator.msSaveBlob(dataBlob, fileName)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// - trying to eliminate console error that occurs if formatted with prettier
|
|
40
|
-
// prettier-ignore
|
|
41
|
-
switch (type) {
|
|
42
|
-
case 'download':
|
|
43
|
-
return (<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border margin-sm`}>Download Data (CSV)</a>)
|
|
44
|
-
default:
|
|
45
|
-
return (<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`no-border`}>Download Data (CSV)</a>)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Creates columns structure for the table
|
|
50
|
-
const tableColumns = useMemo(() => {
|
|
51
|
-
const newTableColumns =
|
|
52
|
-
config.visualizationType === 'Pie'
|
|
53
|
-
? []
|
|
54
|
-
: config.visualizationType === 'Box Plot'
|
|
55
|
-
? [
|
|
56
|
-
{
|
|
57
|
-
Header: 'Measures',
|
|
58
|
-
Cell: props => {
|
|
59
|
-
const resolveName = () => {
|
|
60
|
-
let {
|
|
61
|
-
boxplot: { labels }
|
|
62
|
-
} = config
|
|
63
|
-
const columnLookup = {
|
|
64
|
-
columnMean: labels.mean,
|
|
65
|
-
columnMax: labels.maximum,
|
|
66
|
-
columnMin: labels.minimum,
|
|
67
|
-
columnIqr: labels.iqr,
|
|
68
|
-
columnCategory: 'Category',
|
|
69
|
-
columnMedian: labels.median,
|
|
70
|
-
columnFirstQuartile: labels.q1,
|
|
71
|
-
columnThirdQuartile: labels.q3,
|
|
72
|
-
columnOutliers: labels.outliers,
|
|
73
|
-
values: labels.values,
|
|
74
|
-
columnTotal: labels.total,
|
|
75
|
-
columnSd: 'Standard Deviation',
|
|
76
|
-
nonOutlierValues: 'Non Outliers',
|
|
77
|
-
columnLowerBounds: labels.lowerBounds,
|
|
78
|
-
columnUpperBounds: labels.upperBounds
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let resolvedName = columnLookup[props.row.original[0]]
|
|
82
|
-
|
|
83
|
-
return resolvedName
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return resolveName()
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
: [
|
|
91
|
-
{
|
|
92
|
-
Header: ' ',
|
|
93
|
-
Cell: ({ row }) => {
|
|
94
|
-
const getSeriesLabel = () => {
|
|
95
|
-
let userUpdatedSeriesName = config.series.filter(series => series.dataKey === row.original)?.[0]?.name
|
|
96
|
-
|
|
97
|
-
if (userUpdatedSeriesName) return userUpdatedSeriesName
|
|
98
|
-
if (config.runtimeSeriesLabels) return config.runtime.seriesLabels[row.original]
|
|
99
|
-
return row.original
|
|
100
|
-
}
|
|
101
|
-
return (
|
|
102
|
-
<>
|
|
103
|
-
{config.visualizationType !== 'Pie' && (
|
|
104
|
-
<LegendCircle
|
|
105
|
-
fill={
|
|
106
|
-
// non-dynamic legend
|
|
107
|
-
!config.legend.dynamicLegend && config.visualizationType !== 'Forecasting'
|
|
108
|
-
? colorScale(getSeriesLabel())
|
|
109
|
-
: config.legend.dynamicLegend
|
|
110
|
-
? colorPalettes[config.palette][row.index]
|
|
111
|
-
: // fallback
|
|
112
|
-
'#000'
|
|
113
|
-
}
|
|
114
|
-
/>
|
|
115
|
-
)}
|
|
116
|
-
<span>{getSeriesLabel()}</span>
|
|
117
|
-
</>
|
|
118
|
-
)
|
|
119
|
-
},
|
|
120
|
-
id: 'series-label',
|
|
121
|
-
sortType: 'custom',
|
|
122
|
-
canSort: true
|
|
123
|
-
}
|
|
124
|
-
]
|
|
125
|
-
if (config.visualizationType !== 'Box Plot') {
|
|
126
|
-
data.forEach((d, index) => {
|
|
127
|
-
const resolveTableHeader = () => {
|
|
128
|
-
if (config.runtime[section].type === 'date') return formatDate(parseDate(d[config.runtime.originalXAxis.dataKey]))
|
|
129
|
-
if (config.runtime[section].type === 'continuous') return numberFormatter(d[config.runtime.originalXAxis.dataKey], 'bottom')
|
|
130
|
-
return d[config.runtime.originalXAxis.dataKey]
|
|
131
|
-
}
|
|
132
|
-
const newCol = {
|
|
133
|
-
Header: resolveTableHeader(),
|
|
134
|
-
Cell: ({ row }) => {
|
|
135
|
-
let leftAxisItems = config.series.filter(item => item?.axis === 'Left')
|
|
136
|
-
let rightAxisItems = config.series.filter(item => item?.axis === 'Right')
|
|
137
|
-
let resolvedAxis = ''
|
|
138
|
-
|
|
139
|
-
leftAxisItems.map(leftSeriesItem => {
|
|
140
|
-
if (leftSeriesItem.dataKey === row.original) resolvedAxis = 'left'
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
rightAxisItems.map(rightSeriesItem => {
|
|
144
|
-
if (rightSeriesItem.dataKey === row.original) resolvedAxis = 'right'
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
if (config.visualizationType !== 'Combo') resolvedAxis = 'left'
|
|
148
|
-
|
|
149
|
-
return <>{numberFormatter(d[row.original], resolvedAxis)}</>
|
|
150
|
-
},
|
|
151
|
-
id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
|
|
152
|
-
sortType: 'custom',
|
|
153
|
-
canSort: true
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
newTableColumns.push(newCol)
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (config.visualizationType === 'Box Plot') {
|
|
161
|
-
config.boxplot.tableData.map((plot, index) => {
|
|
162
|
-
const newCol = {
|
|
163
|
-
Header: plot.columnCategory,
|
|
164
|
-
Cell: props => {
|
|
165
|
-
let resolveCell = () => {
|
|
166
|
-
if (Number(props.row.id) === 0) return true
|
|
167
|
-
if (Number(props.row.id) === 1) return plot.columnMax
|
|
168
|
-
if (Number(props.row.id) === 2) return plot.columnThirdQuartile
|
|
169
|
-
if (Number(props.row.id) === 3) return plot.columnMedian
|
|
170
|
-
if (Number(props.row.id) === 4) return plot.columnFirstQuartile
|
|
171
|
-
if (Number(props.row.id) === 5) return plot.columnMin
|
|
172
|
-
if (Number(props.row.id) === 6) return plot.columnTotal
|
|
173
|
-
if (Number(props.row.id) === 7) return plot.columnSd
|
|
174
|
-
if (Number(props.row.id) === 8) return plot.columnMean
|
|
175
|
-
if (Number(props.row.id) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
|
|
176
|
-
if (Number(props.row.id) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
|
|
177
|
-
return <p>-</p>
|
|
178
|
-
}
|
|
179
|
-
return resolveCell()
|
|
180
|
-
},
|
|
181
|
-
id: `${index}`,
|
|
182
|
-
canSort: false
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return newTableColumns.push(newCol)
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return newTableColumns
|
|
190
|
-
}, [config, colorScale]) // eslint-disable-line
|
|
191
|
-
|
|
192
|
-
// prettier-ignore
|
|
193
|
-
const tableData = useMemo(() => (
|
|
194
|
-
config.visualizationType === 'Pie'
|
|
195
|
-
? [config.yAxis.dataKey]
|
|
196
|
-
: config.visualizationType === 'Box Plot'
|
|
197
|
-
? Object.entries(config.boxplot.tableData[0])
|
|
198
|
-
: config.runtime.seriesKeys),
|
|
199
|
-
[config.runtime.seriesKeys]) // eslint-disable-line
|
|
200
|
-
|
|
201
|
-
// Change accessibility label depending on expanded status
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
const expandedLabel = 'Accessible data table.'
|
|
204
|
-
const collapsedLabel = 'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.'
|
|
205
|
-
|
|
206
|
-
if (tableExpanded === true && accessibilityLabel !== expandedLabel) {
|
|
207
|
-
setAccessibilityLabel(expandedLabel)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (tableExpanded === false && accessibilityLabel !== collapsedLabel) {
|
|
211
|
-
setAccessibilityLabel(collapsedLabel)
|
|
212
|
-
}
|
|
213
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
|
-
}, [tableExpanded])
|
|
215
|
-
|
|
216
|
-
const defaultColumn = useMemo(
|
|
217
|
-
() => ({
|
|
218
|
-
minWidth: 150,
|
|
219
|
-
width: 200,
|
|
220
|
-
maxWidth: 400
|
|
221
|
-
}),
|
|
222
|
-
[]
|
|
223
|
-
)
|
|
224
|
-
const upIcon = (
|
|
225
|
-
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
226
|
-
<path d='M0 5l5-5 5 5z' />
|
|
227
|
-
</svg>
|
|
228
|
-
)
|
|
229
|
-
const downIcon = (
|
|
230
|
-
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
231
|
-
<path d='M0 0l5 5 5-5z' />
|
|
232
|
-
</svg>
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(
|
|
236
|
-
{
|
|
237
|
-
columns: tableColumns,
|
|
238
|
-
data: tableData,
|
|
239
|
-
defaultColumn,
|
|
240
|
-
disableSortRemove: true, // otherwise 3rd click on header removes sorting entirely
|
|
241
|
-
sortTypes: {
|
|
242
|
-
custom: (rowA, rowB, columnId) => {
|
|
243
|
-
// rowA.original - is the row data field name to access the value
|
|
244
|
-
// columnId = the column indicator
|
|
245
|
-
let dataKey = config.xAxis.dataKey
|
|
246
|
-
let colObj = config.data.filter(obj => {
|
|
247
|
-
return obj[dataKey] === columnId.split('--')[0] // have to remove index
|
|
248
|
-
})
|
|
249
|
-
if (colObj === undefined || colObj[0] === undefined) {
|
|
250
|
-
return -1
|
|
251
|
-
}
|
|
252
|
-
// NOW we can get the sort values
|
|
253
|
-
const a = transform.cleanDataPoint(colObj[0][rowA.original]) // issue was that a was UNDEFINED therefore it CANT SORT
|
|
254
|
-
const b = transform.cleanDataPoint(colObj[0][rowB.original])
|
|
255
|
-
|
|
256
|
-
if (a === undefined) {
|
|
257
|
-
return -1
|
|
258
|
-
}
|
|
259
|
-
if (!isNaN(Number(a)) && !isNaN(Number(b))) {
|
|
260
|
-
return Number(a) - Number(b)
|
|
261
|
-
}
|
|
262
|
-
return a.localeCompare(b)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
useSortBy,
|
|
267
|
-
useBlockLayout,
|
|
268
|
-
useResizeColumns
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
// sort continuous x axis scaling for data tables, ie. xAxis should read 1,2,3,4,5
|
|
272
|
-
if (config.xAxis.type === 'continuous' && headerGroups) {
|
|
273
|
-
data.sort((a, b) => a[config.xAxis.dataKey] - b[config.xAxis.dataKey])
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return (
|
|
277
|
-
<ErrorBoundary component='DataTable'>
|
|
278
|
-
<MediaControls.Section classes={['download-links']}>
|
|
279
|
-
<MediaControls.Link config={config} />
|
|
280
|
-
{config.table.download && <DownloadButton data={rawData} type='link' />}
|
|
281
|
-
</MediaControls.Section>
|
|
282
|
-
|
|
283
|
-
<section style={{ marginTop: !isLegendBottom ? config.dynamicMarginTop / 4 + 'px' : '0px' }} id={config?.title ? `dataTableSection__${config?.title.replace(/\s/g, '')}` : `dataTableSection`} className={`data-table-container`} aria-label={accessibilityLabel}>
|
|
284
|
-
<div
|
|
285
|
-
role='button'
|
|
286
|
-
className={tableExpanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
287
|
-
tabIndex={0}
|
|
288
|
-
onClick={() => {
|
|
289
|
-
setTableExpanded(!tableExpanded)
|
|
290
|
-
}}
|
|
291
|
-
onKeyDown={e => {
|
|
292
|
-
if (e.keyCode === 13) {
|
|
293
|
-
setTableExpanded(!tableExpanded)
|
|
294
|
-
}
|
|
295
|
-
}}
|
|
296
|
-
>
|
|
297
|
-
<Icon display={tableExpanded ? 'minus' : 'plus'} base />
|
|
298
|
-
{config.table.label}
|
|
299
|
-
</div>
|
|
300
|
-
<div className='table-container' hidden={!tableExpanded} style={{ maxHeight: config.table.limitHeight && `${config.table.height}px`, overflowY: 'scroll' }}>
|
|
301
|
-
<table className={tableExpanded ? 'data-table' : 'data-table cdcdataviz-sr-only'} {...getTableProps()} aria-rowcount={config?.series?.length ? config?.series?.length : '-1'}>
|
|
302
|
-
<caption className='cdcdataviz-sr-only visually-hidden'>{config.table.caption ? config.table.caption : config.table.label ? config.table.label : 'Data Table'}</caption>
|
|
303
|
-
<thead>
|
|
304
|
-
{headerGroups.map((headerGroup, index) => (
|
|
305
|
-
<tr {...headerGroup.getHeaderGroupProps()} key={`headerGroups--${index}`}>
|
|
306
|
-
{' '}
|
|
307
|
-
{headerGroup.headers.map((column, index) => (
|
|
308
|
-
<th
|
|
309
|
-
tabIndex='0'
|
|
310
|
-
title={column.Header}
|
|
311
|
-
key={`trth--${index}`}
|
|
312
|
-
role='columnheader'
|
|
313
|
-
scope='col'
|
|
314
|
-
{...column.getHeaderProps(column.getSortByToggleProps())}
|
|
315
|
-
className={column.isSorted && column.isSortedDesc ? 'sort sort-desc' : 'sort sort-asc'}
|
|
316
|
-
{...(column.isSorted && column.isSortedDesc ? { 'aria-sort': 'descending' } : { 'aria-sort': 'ascending' })}
|
|
317
|
-
>
|
|
318
|
-
{column.render('Header')}
|
|
319
|
-
{column.isSorted && <span className={'sort-icon'}>{column.isSortedDesc ? downIcon : upIcon}</span>}
|
|
320
|
-
</th>
|
|
321
|
-
))}
|
|
322
|
-
</tr>
|
|
323
|
-
))}
|
|
324
|
-
</thead>
|
|
325
|
-
<tbody {...getTableBodyProps()}>
|
|
326
|
-
{rows.map((row, index) => {
|
|
327
|
-
prepareRow(row)
|
|
328
|
-
return (
|
|
329
|
-
<tr {...row.getRowProps()} key={`tbody__tr-${index}`} className={`row-${String(config.visualizationType).replace(' ', '-')}--${index}`}>
|
|
330
|
-
{row.cells.map((cell, index) => {
|
|
331
|
-
return (
|
|
332
|
-
<td tabIndex='0' {...cell.getCellProps()} key={`tbody__tr__td-${index}`} role='gridcell'>
|
|
333
|
-
{cell.render('Cell')}
|
|
334
|
-
</td>
|
|
335
|
-
)
|
|
336
|
-
})}
|
|
337
|
-
</tr>
|
|
338
|
-
)
|
|
339
|
-
})}
|
|
340
|
-
</tbody>
|
|
341
|
-
</table>
|
|
342
|
-
{config.regions && config.regions.length > 0 && config.visualizationType !== 'Box Plot' ? (
|
|
343
|
-
<table className='region-table data-table'>
|
|
344
|
-
<caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
|
|
345
|
-
<thead>
|
|
346
|
-
<tr>
|
|
347
|
-
<th>Region Name</th>
|
|
348
|
-
<th>Start Date</th>
|
|
349
|
-
<th>End Date</th>
|
|
350
|
-
</tr>
|
|
351
|
-
</thead>
|
|
352
|
-
<tbody>
|
|
353
|
-
{config.regions.map((region, index) => {
|
|
354
|
-
if (config.visualizationType === 'Box Plot') return false
|
|
355
|
-
if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
|
|
356
|
-
|
|
357
|
-
return (
|
|
358
|
-
<tr key={`row-${region.label}--${index}`}>
|
|
359
|
-
<td>{region.label}</td>
|
|
360
|
-
<td>{formatDate(parseDate(region.from))}</td>
|
|
361
|
-
<td>{formatDate(parseDate(region.to))}</td>
|
|
362
|
-
</tr>
|
|
363
|
-
)
|
|
364
|
-
})}
|
|
365
|
-
</tbody>
|
|
366
|
-
</table>
|
|
367
|
-
) : (
|
|
368
|
-
''
|
|
369
|
-
)}
|
|
370
|
-
</div>
|
|
371
|
-
</section>
|
|
372
|
-
</ErrorBoundary>
|
|
373
|
-
)
|
|
374
|
-
}
|
|
File without changes
|