@cdc/core 4.23.10-alpha → 4.23.11
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/assets/icon-deviation-bar.svg +1 -0
- package/components/DataTable/DataTable.tsx +205 -0
- package/components/DataTable/components/BoxplotHeader.tsx +16 -0
- package/components/DataTable/components/CellAnchor.tsx +44 -0
- package/components/DataTable/components/ChartHeader.tsx +91 -0
- package/components/DataTable/components/ExpandCollapse.tsx +21 -0
- package/components/DataTable/components/Icons.tsx +10 -0
- package/components/DataTable/components/MapHeader.tsx +56 -0
- package/components/DataTable/components/SkipNav.tsx +7 -0
- package/components/DataTable/helpers/boxplotCellMatrix.tsx +64 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +78 -0
- package/components/DataTable/helpers/customSort.ts +55 -0
- package/components/DataTable/helpers/getChartCellValue.ts +55 -0
- package/components/DataTable/helpers/getDataSeriesColumns.ts +28 -0
- package/components/DataTable/helpers/getSeriesName.ts +26 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +56 -0
- package/components/DataTable/helpers/regionCellMatrix.tsx +13 -0
- package/components/DataTable/helpers/standardizeState.js +76 -0
- package/components/DataTable/index.ts +1 -0
- package/components/DataTable/types/TableConfig.ts +57 -0
- package/components/DownloadButton.tsx +29 -0
- package/components/LegendCircle.jsx +2 -2
- package/components/Table/Table.tsx +49 -0
- package/components/Table/components/Cell.tsx +9 -0
- package/components/Table/components/GroupRow.tsx +16 -0
- package/components/Table/components/Row.tsx +19 -0
- package/components/Table/index.ts +1 -0
- package/components/Table/types/CellMatrix.ts +4 -0
- package/components/_stories/DataTable.stories.tsx +62 -0
- package/components/_stories/Table.stories.tsx +53 -0
- package/components/_stories/_mocks/dashboard_no_filter.json +121 -0
- package/components/_stories/_mocks/example-city-state.json +808 -0
- package/components/_stories/styles.scss +9 -0
- package/components/managers/{DataDesigner.jsx → DataDesigner.tsx} +96 -87
- package/components/ui/Title/Title.scss +95 -0
- package/components/ui/Title/index.tsx +34 -0
- package/components/ui/_stories/Title.stories.tsx +21 -0
- package/helpers/DataTransform.ts +41 -18
- package/helpers/cove/string.ts +11 -0
- package/package.json +2 -2
- package/styles/_data-table.scss +1 -0
- package/styles/heading-colors.scss +0 -3
- package/styles/v2/layout/_component.scss +0 -11
- package/types/Axis.ts +6 -0
- package/types/Color.ts +5 -0
- package/types/ComponentStyles.ts +7 -0
- package/types/ComponentThemes.ts +13 -0
- package/types/EditorColumnProperties.ts +8 -0
- package/types/Runtime.ts +9 -0
- package/types/Series.ts +1 -0
- package/types/Visualization.ts +21 -0
- package/components/DataTable.jsx +0 -759
package/components/DataTable.jsx
DELETED
|
@@ -1,759 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState, memo, useMemo } from 'react'
|
|
2
|
-
|
|
3
|
-
import Papa from 'papaparse'
|
|
4
|
-
import ExternalIcon from '../assets/external-link.svg'
|
|
5
|
-
import Icon from '@cdc/core/components/ui/Icon'
|
|
6
|
-
|
|
7
|
-
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
8
|
-
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
9
|
-
import MediaControls from '@cdc/core/components/MediaControls'
|
|
10
|
-
|
|
11
|
-
import { parseDate, formatDate } from '@cdc/core/helpers/cove/date'
|
|
12
|
-
import { formatNumber } from '@cdc/core/helpers/cove/number'
|
|
13
|
-
|
|
14
|
-
import Loading from '@cdc/core/components/Loading'
|
|
15
|
-
|
|
16
|
-
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
17
|
-
const DataTable = props => {
|
|
18
|
-
const { config, dataConfig, tableTitle, indexTitle, vizTitle, rawData, runtimeData, headerColor, colorScale, expandDataTable, columns, displayDataAsText, applyLegendToRow, displayGeoName, navigationHandler, viewport, formatLegendLocation, tabbingId, isDebug } = props
|
|
19
|
-
|
|
20
|
-
/* eslint-disable no-console */
|
|
21
|
-
if (isDebug) {
|
|
22
|
-
console.log('core/DataTable: props=', props)
|
|
23
|
-
console.log('core/DataTable: runtimeData=', runtimeData)
|
|
24
|
-
console.log('core/DataTable: rawData=', rawData)
|
|
25
|
-
console.log('core/DataTable: config=', config)
|
|
26
|
-
}
|
|
27
|
-
/* eslint-enable no-console */
|
|
28
|
-
|
|
29
|
-
const [expanded, setExpanded] = useState(expandDataTable)
|
|
30
|
-
|
|
31
|
-
const [sortBy, setSortBy] = useState({ column: config.type === 'map' ? 'geo' : 'date', asc: false, colIndex: null })
|
|
32
|
-
|
|
33
|
-
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
34
|
-
|
|
35
|
-
const fileName = `${vizTitle || 'data-table'}.csv`
|
|
36
|
-
|
|
37
|
-
const isVertical = !(config.type === 'chart' && !config.table?.showVertical)
|
|
38
|
-
|
|
39
|
-
const customSort = (a, b) => {
|
|
40
|
-
let valueA = a
|
|
41
|
-
let valueB = b
|
|
42
|
-
|
|
43
|
-
// Treat booleans and nulls as an empty string
|
|
44
|
-
valueA = valueA === false || valueA === true || valueA === null ? '' : valueA
|
|
45
|
-
valueB = valueB === false || valueB == true || valueB === null ? '' : valueB
|
|
46
|
-
|
|
47
|
-
const trimmedA = String(valueA).trim()
|
|
48
|
-
const trimmedB = String(valueB).trim()
|
|
49
|
-
|
|
50
|
-
if (config.xAxis?.dataKey === sortBy.column && config.xAxis.type === 'date') {
|
|
51
|
-
let dateA = parseDate(config.xAxis.dateParseFormat, trimmedA)
|
|
52
|
-
|
|
53
|
-
let dateB = parseDate(config.xAxis.dateParseFormat, trimmedB)
|
|
54
|
-
|
|
55
|
-
if (dateA && dateA.getTime) dateA = dateA.getTime()
|
|
56
|
-
|
|
57
|
-
if (dateB && dateB.getTime) dateB = dateB.getTime()
|
|
58
|
-
|
|
59
|
-
return !sortBy.asc ? dateA - dateB : dateB - dateA
|
|
60
|
-
}
|
|
61
|
-
// Check if values are numbers
|
|
62
|
-
const isNumA = !isNaN(Number(valueA)) && valueA !== undefined && valueA !== null && trimmedA !== ''
|
|
63
|
-
const isNumB = !isNaN(Number(valueB)) && valueB !== undefined && valueB !== null && trimmedB !== ''
|
|
64
|
-
|
|
65
|
-
// Handle empty strings or spaces
|
|
66
|
-
if (trimmedA === '' && trimmedB !== '') return !sortBy.asc ? -1 : 1
|
|
67
|
-
if (trimmedA !== '' && trimmedB === '') return !sortBy.asc ? 1 : -1
|
|
68
|
-
|
|
69
|
-
// Both are numbers: Compare numerically
|
|
70
|
-
if (isNumA && isNumB) {
|
|
71
|
-
return !sortBy.asc ? Number(valueA) - Number(valueB) : Number(valueB) - Number(valueA)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Only A is a number
|
|
75
|
-
if (isNumA) {
|
|
76
|
-
return !sortBy.asc ? -1 : 1
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Only B is a number
|
|
80
|
-
if (isNumB) {
|
|
81
|
-
return !sortBy.asc ? 1 : -1
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Neither are numbers: Compare as strings
|
|
85
|
-
return !sortBy.asc ? trimmedA.localeCompare(trimmedB) : trimmedB.localeCompare(trimmedA)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Optionally wrap cell with anchor if config defines a navigation url
|
|
89
|
-
const getCellAnchor = (markup, row) => {
|
|
90
|
-
if (columns.navigate && row[columns.navigate.name]) {
|
|
91
|
-
markup = (
|
|
92
|
-
<span
|
|
93
|
-
onClick={() => navigationHandler(row[columns.navigate.name])}
|
|
94
|
-
className='table-link'
|
|
95
|
-
title='Click for more information (Opens in a new window)'
|
|
96
|
-
role='link'
|
|
97
|
-
tabIndex='0'
|
|
98
|
-
onKeyDown={e => {
|
|
99
|
-
if (e.keyCode === 13) {
|
|
100
|
-
navigationHandler(row[columns.navigate.name])
|
|
101
|
-
}
|
|
102
|
-
}}
|
|
103
|
-
>
|
|
104
|
-
{markup}
|
|
105
|
-
<ExternalIcon className='inline-icon' />
|
|
106
|
-
</span>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return markup
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const rand = Math.random().toString(16).substr(2, 8)
|
|
114
|
-
const skipId = `btn__${rand}`
|
|
115
|
-
|
|
116
|
-
const mapLookup = {
|
|
117
|
-
'us-county': 'United States County Map',
|
|
118
|
-
'single-state': 'State Map',
|
|
119
|
-
us: 'United States Map',
|
|
120
|
-
world: 'World Map'
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const DownloadButton = memo(() => {
|
|
124
|
-
if (rawData !== undefined) {
|
|
125
|
-
let csvData
|
|
126
|
-
// only use fullGeoName on County maps and no other
|
|
127
|
-
if (config.general?.geoType === 'us-county') {
|
|
128
|
-
// Unparse + Add column for full Geo name along with State
|
|
129
|
-
csvData = Papa.unparse(rawData.map(row => ({ FullGeoName: formatLegendLocation(row[config.columns.geo.name]), ...row })))
|
|
130
|
-
} else {
|
|
131
|
-
// Just Unparse
|
|
132
|
-
csvData = Papa.unparse(rawData)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
|
|
136
|
-
|
|
137
|
-
const saveBlob = () => {
|
|
138
|
-
//@ts-ignore
|
|
139
|
-
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
140
|
-
//@ts-ignore
|
|
141
|
-
navigator.msSaveBlob(blob, fileName)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<a download={fileName} type='button' onClick={saveBlob} href={URL.createObjectURL(blob)} aria-label='Download this data in a CSV file format.' className={`${headerColor} no-border`} id={`${skipId}`} data-html2canvas-ignore role='button'>
|
|
147
|
-
Download Data (CSV)
|
|
148
|
-
</a>
|
|
149
|
-
)
|
|
150
|
-
} else {
|
|
151
|
-
return <></>
|
|
152
|
-
}
|
|
153
|
-
}, [rawData])
|
|
154
|
-
|
|
155
|
-
// Change accessibility label depending on expanded status
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
const expandedLabel = 'Accessible data table.'
|
|
158
|
-
const collapsedLabel = 'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.'
|
|
159
|
-
|
|
160
|
-
if (expanded === true && accessibilityLabel !== expandedLabel) {
|
|
161
|
-
setAccessibilityLabel(expandedLabel)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (expanded === false && accessibilityLabel !== collapsedLabel) {
|
|
165
|
-
setAccessibilityLabel(collapsedLabel)
|
|
166
|
-
}
|
|
167
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
-
}, [expanded])
|
|
169
|
-
|
|
170
|
-
switch (config.visualizationType) {
|
|
171
|
-
case 'Box Plot':
|
|
172
|
-
if (!config.boxplot) return <Loading />
|
|
173
|
-
break
|
|
174
|
-
case 'Line' || 'Bar' || 'Combo' || 'Pie' || 'Deviation Bar' || 'Paired Bar':
|
|
175
|
-
if (!runtimeData) return <Loading />
|
|
176
|
-
break
|
|
177
|
-
default:
|
|
178
|
-
if (!runtimeData) return <Loading />
|
|
179
|
-
break
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const rawRows = Object.keys(runtimeData)
|
|
183
|
-
const rows = isVertical
|
|
184
|
-
? rawRows.sort((a, b) => {
|
|
185
|
-
let sortVal = 0
|
|
186
|
-
if (config.type === 'map' && config.columns) {
|
|
187
|
-
sortVal = customSort(runtimeData[a][config.columns[sortBy.column].name], runtimeData[b][config.columns[sortBy.column].name])
|
|
188
|
-
}
|
|
189
|
-
if (config.type === 'chart' || config.type === 'dashboard') {
|
|
190
|
-
sortVal = customSort(runtimeData[a][sortBy.column], runtimeData[b][sortBy.column])
|
|
191
|
-
}
|
|
192
|
-
return sortVal
|
|
193
|
-
})
|
|
194
|
-
: rawRows
|
|
195
|
-
|
|
196
|
-
const genMapRows = rows => {
|
|
197
|
-
const allrows = rows.map(row => {
|
|
198
|
-
return (
|
|
199
|
-
<tr role='row'>
|
|
200
|
-
{Object.keys(columns)
|
|
201
|
-
.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
202
|
-
.map(column => {
|
|
203
|
-
let cellValue
|
|
204
|
-
|
|
205
|
-
if (column === 'geo') {
|
|
206
|
-
const rowObj = runtimeData[row]
|
|
207
|
-
const legendColor = applyLegendToRow(rowObj)
|
|
208
|
-
|
|
209
|
-
var labelValue
|
|
210
|
-
if ((config.general.geoType !== 'single-state' && config.general.geoType !== 'us-county') || config.general.type === 'us-geocode') {
|
|
211
|
-
labelValue = displayGeoName(row)
|
|
212
|
-
} else {
|
|
213
|
-
labelValue = formatLegendLocation(row)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
labelValue = getCellAnchor(labelValue, rowObj)
|
|
217
|
-
cellValue = (
|
|
218
|
-
<>
|
|
219
|
-
<LegendCircle fill={legendColor[0]} />
|
|
220
|
-
{labelValue}
|
|
221
|
-
</>
|
|
222
|
-
)
|
|
223
|
-
} else {
|
|
224
|
-
// check for special classes
|
|
225
|
-
let specialValFound = ''
|
|
226
|
-
if (config.legend.specialClasses && config.legend.specialClasses.length && typeof config.legend.specialClasses[0] === 'object') {
|
|
227
|
-
for (let i = 0; i < config.legend.specialClasses.length; i++) {
|
|
228
|
-
if (config.legend.specialClasses[i].key === config.columns[column].name) {
|
|
229
|
-
if (String(runtimeData[row][config.legend.specialClasses[i].key]) === config.legend.specialClasses[i].value) {
|
|
230
|
-
specialValFound = config.legend.specialClasses[i].label
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
cellValue = specialValFound ? displayDataAsText(specialValFound, column) : displayDataAsText(runtimeData[row][config.columns[column].name], column)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<td tabIndex='0' role='gridcell' onClick={e => (config.general.type === 'bubble' && config.general.allowMapZoom && config.general.geoType === 'world' ? setFilteredCountryCode(row) : true)}>
|
|
240
|
-
{cellValue}
|
|
241
|
-
</td>
|
|
242
|
-
)
|
|
243
|
-
})}
|
|
244
|
-
</tr>
|
|
245
|
-
)
|
|
246
|
-
})
|
|
247
|
-
return allrows
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const dataSeriesColumns = () => {
|
|
251
|
-
let tmpSeriesColumns
|
|
252
|
-
if (config.visualizationType !== 'Pie') {
|
|
253
|
-
tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey] : [] //, ...config.runtime.seriesLabelsAll
|
|
254
|
-
if (config.series) {
|
|
255
|
-
config.series.forEach(element => {
|
|
256
|
-
tmpSeriesColumns.push(element.dataKey)
|
|
257
|
-
})
|
|
258
|
-
} else if (runtimeData && runtimeData.length > 0) {
|
|
259
|
-
tmpSeriesColumns = Object.keys(runtimeData[0])
|
|
260
|
-
}
|
|
261
|
-
} else {
|
|
262
|
-
tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey, config.yAxis?.dataKey] : [config.yAxis?.dataKey] //Object.keys(runtimeData[0])
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// then add the additional Columns
|
|
266
|
-
if (config.columns && Object.keys(config.columns).length > 0) {
|
|
267
|
-
Object.keys(config.columns).forEach(function (key) {
|
|
268
|
-
var value = config.columns[key]
|
|
269
|
-
// add if not the index AND it is enabled to be added to data table
|
|
270
|
-
if (value.name !== config.xAxis?.dataKey && value.dataTable === true) {
|
|
271
|
-
tmpSeriesColumns.push(value.name)
|
|
272
|
-
}
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return tmpSeriesColumns
|
|
277
|
-
}
|
|
278
|
-
const dataSeriesColumnsSorted = () => {
|
|
279
|
-
return dataSeriesColumns().sort((a, b) => {
|
|
280
|
-
if (sortBy.column === '__series__') return customSort(a, b)
|
|
281
|
-
let row = runtimeData.find(d => d[config.xAxis?.dataKey] === sortBy.column)
|
|
282
|
-
const rowIndex = runtimeData[sortBy.colIndex - 1]
|
|
283
|
-
if (row) {
|
|
284
|
-
return customSort(row[a], row[b])
|
|
285
|
-
}
|
|
286
|
-
if (row === undefined && rowIndex) {
|
|
287
|
-
return customSort(rowIndex[a], rowIndex[b])
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
const getLabel = name => {
|
|
292
|
-
let custLabel = ''
|
|
293
|
-
if (config.columns && Object.keys(config.columns).length > 0) {
|
|
294
|
-
Object.keys(config.columns).forEach(function (key) {
|
|
295
|
-
var tmpColumn = config.columns[key]
|
|
296
|
-
// add if not the index AND it is enabled to be added to data table
|
|
297
|
-
if (tmpColumn.name === name) {
|
|
298
|
-
custLabel = tmpColumn.label
|
|
299
|
-
}
|
|
300
|
-
})
|
|
301
|
-
return custLabel
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const getSeriesName = column => {
|
|
306
|
-
// If a user sets the name on a series use that.
|
|
307
|
-
let userUpdatedSeriesName = config.series ? config.series.filter(series => series.dataKey === column)?.[0]?.name : ''
|
|
308
|
-
if (userUpdatedSeriesName) return userUpdatedSeriesName
|
|
309
|
-
|
|
310
|
-
if (config.runtimeSeriesLabels && config.runtimeSeriesLabels[column]) return config.runtimeSeriesLabels[column]
|
|
311
|
-
|
|
312
|
-
let custLabel = getLabel(column) ? getLabel(column) : column
|
|
313
|
-
let text = column === config.xAxis?.dataKey ? config.table.indexLabel : custLabel
|
|
314
|
-
|
|
315
|
-
return text
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const genChartHeader = (columns, data) => {
|
|
319
|
-
if (!data) return
|
|
320
|
-
if (isVertical) {
|
|
321
|
-
return (
|
|
322
|
-
<tr>
|
|
323
|
-
{dataSeriesColumns().map((column, index) => {
|
|
324
|
-
const text = getSeriesName(column)
|
|
325
|
-
|
|
326
|
-
return (
|
|
327
|
-
<th
|
|
328
|
-
key={`col-header-${column}__${index}`}
|
|
329
|
-
tabIndex='0'
|
|
330
|
-
title={text}
|
|
331
|
-
role='columnheader'
|
|
332
|
-
scope='col'
|
|
333
|
-
onClick={() => {
|
|
334
|
-
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false, colIndex: index })
|
|
335
|
-
}}
|
|
336
|
-
onKeyDown={e => {
|
|
337
|
-
if (e.keyCode === 13) {
|
|
338
|
-
setColIndex(index)
|
|
339
|
-
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false, colIndex: index })
|
|
340
|
-
}
|
|
341
|
-
}}
|
|
342
|
-
className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
343
|
-
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
344
|
-
>
|
|
345
|
-
{text}
|
|
346
|
-
{column === sortBy.column && <span className={'sort-icon'}>{!sortBy.asc ? upIcon : downIcon}</span>}
|
|
347
|
-
<button>
|
|
348
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
349
|
-
</button>
|
|
350
|
-
</th>
|
|
351
|
-
)
|
|
352
|
-
})}
|
|
353
|
-
</tr>
|
|
354
|
-
)
|
|
355
|
-
} else {
|
|
356
|
-
const sliceVal = config.visualizationType === 'Pie' ? 1 : 0
|
|
357
|
-
return (
|
|
358
|
-
<tr>
|
|
359
|
-
{['__series__', ...Object.keys(runtimeData)].slice(sliceVal).map((row, index) => {
|
|
360
|
-
let column = config.xAxis?.dataKey
|
|
361
|
-
let text = row !== '__series__' ? getChartCellValue(row, column) : '__series__'
|
|
362
|
-
return (
|
|
363
|
-
<th
|
|
364
|
-
key={`col-header-${text}__${index}`}
|
|
365
|
-
tabIndex='0'
|
|
366
|
-
title={text}
|
|
367
|
-
role='columnheader'
|
|
368
|
-
scope='col'
|
|
369
|
-
onClick={() => {
|
|
370
|
-
setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
|
|
371
|
-
}}
|
|
372
|
-
onKeyDown={e => {
|
|
373
|
-
if (e.keyCode === 13) {
|
|
374
|
-
setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
|
|
375
|
-
}
|
|
376
|
-
}}
|
|
377
|
-
className={sortBy.column === text ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
378
|
-
{...(sortBy.column === text ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
379
|
-
>
|
|
380
|
-
{text === '__series__' ? '' : text}
|
|
381
|
-
{index === sortBy.colIndex && <span className={'sort-icon'}>{!sortBy.asc ? upIcon : downIcon}</span>}
|
|
382
|
-
<button>
|
|
383
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === text ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
384
|
-
</button>
|
|
385
|
-
</th>
|
|
386
|
-
)
|
|
387
|
-
})}
|
|
388
|
-
</tr>
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// if its additional column, return formatting params
|
|
394
|
-
const isAdditionalColumn = column => {
|
|
395
|
-
let inthere = false
|
|
396
|
-
let formattingParams = {}
|
|
397
|
-
if (config.columns) {
|
|
398
|
-
Object.keys(config.columns).forEach(keycol => {
|
|
399
|
-
if (config.columns[keycol].name === column) {
|
|
400
|
-
inthere = true
|
|
401
|
-
formattingParams = {
|
|
402
|
-
addColPrefix: config.columns[keycol].prefix,
|
|
403
|
-
addColSuffix: config.columns[keycol].suffix,
|
|
404
|
-
addColRoundTo: config.columns[keycol].roundToPlace ? config.columns[keycol].roundToPlace : '',
|
|
405
|
-
addColCommas: config.columns[keycol].commas
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
})
|
|
409
|
-
}
|
|
410
|
-
return formattingParams
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const getChartCellValue = (row, column) => {
|
|
414
|
-
const rowObj = runtimeData[row]
|
|
415
|
-
let cellValue // placeholder for formatting below
|
|
416
|
-
let labelValue = rowObj[column] // just raw X axis string
|
|
417
|
-
if (column === config.xAxis?.dataKey) {
|
|
418
|
-
// not the prettiest, but helper functions work nicely here.
|
|
419
|
-
cellValue = config.xAxis?.type === 'date' ? formatDate(config.xAxis?.dateDisplayFormat, parseDate(config.xAxis?.dateParseFormat, labelValue)) : labelValue
|
|
420
|
-
} else {
|
|
421
|
-
let resolvedAxis = 'left'
|
|
422
|
-
let leftAxisItems = config.series ? config.series.filter(item => item?.axis === 'Left') : []
|
|
423
|
-
let rightAxisItems = config.series ? config.series.filter(item => item?.axis === 'Right') : []
|
|
424
|
-
|
|
425
|
-
leftAxisItems.map(leftSeriesItem => {
|
|
426
|
-
if (leftSeriesItem.dataKey === column) resolvedAxis = 'left'
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
rightAxisItems.map(rightSeriesItem => {
|
|
430
|
-
if (rightSeriesItem.dataKey === column) resolvedAxis = 'right'
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
let addColParams = isAdditionalColumn(column)
|
|
434
|
-
if (addColParams) {
|
|
435
|
-
cellValue = config.dataFormat ? formatNumber(runtimeData[row][column], resolvedAxis, false, config, addColParams) : runtimeData[row][column]
|
|
436
|
-
} else {
|
|
437
|
-
cellValue = config.dataFormat ? formatNumber(runtimeData[row][column], resolvedAxis, false, config) : runtimeData[row][column]
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return cellValue
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const getChartCell = (row, column) => {
|
|
445
|
-
return (
|
|
446
|
-
<td tabIndex='0' role='gridcell' id={`${runtimeData[config.runtime?.originalXAxis?.dataKey]}--${row}`}>
|
|
447
|
-
{getChartCellValue(row, column)}
|
|
448
|
-
</td>
|
|
449
|
-
)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const genChartRows = rows => {
|
|
453
|
-
if (isVertical) {
|
|
454
|
-
const allRows = rows.map((row, index) => {
|
|
455
|
-
return (
|
|
456
|
-
<tr key={`${row}__${index}`} role='row'>
|
|
457
|
-
{dataSeriesColumns().map(column => {
|
|
458
|
-
return getChartCell(row, column)
|
|
459
|
-
})}
|
|
460
|
-
</tr>
|
|
461
|
-
)
|
|
462
|
-
})
|
|
463
|
-
return allRows
|
|
464
|
-
} else {
|
|
465
|
-
const allRows = dataSeriesColumnsSorted().map(column => {
|
|
466
|
-
return (
|
|
467
|
-
<tr role='row'>
|
|
468
|
-
{config.visualizationType !== 'Pie' && (
|
|
469
|
-
<td>
|
|
470
|
-
{colorScale && colorScale(getSeriesName(column)) && <LegendCircle fill={colorScale(getSeriesName(column))} />}
|
|
471
|
-
{getSeriesName(column)}
|
|
472
|
-
</td>
|
|
473
|
-
)}
|
|
474
|
-
|
|
475
|
-
{rows.map(row => {
|
|
476
|
-
return getChartCell(row, column)
|
|
477
|
-
})}
|
|
478
|
-
</tr>
|
|
479
|
-
)
|
|
480
|
-
})
|
|
481
|
-
return allRows
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const upIcon = (
|
|
486
|
-
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
487
|
-
<path d='M0 5l5-5 5 5z' />
|
|
488
|
-
</svg>
|
|
489
|
-
)
|
|
490
|
-
const downIcon = (
|
|
491
|
-
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
492
|
-
<path d='M0 0l5 5 5-5z' />
|
|
493
|
-
</svg>
|
|
494
|
-
)
|
|
495
|
-
|
|
496
|
-
const limitHeight = {
|
|
497
|
-
maxHeight: config.table.limitHeight && `${config.table.height}px`,
|
|
498
|
-
overflowY: 'scroll',
|
|
499
|
-
marginBottom: '33px'
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const caption = useMemo(() => {
|
|
503
|
-
if (config.type === 'map') {
|
|
504
|
-
return config.table.caption ? config.table.caption : `Data table showing data for the ${mapLookup[config.general.geoType]} figure.`
|
|
505
|
-
} else {
|
|
506
|
-
return config.table.caption ? config.table.caption : `Data table showing data for the ${config.type} figure.`
|
|
507
|
-
}
|
|
508
|
-
}, [config.table.caption])
|
|
509
|
-
|
|
510
|
-
// prettier-ignore
|
|
511
|
-
const tableData = useMemo(() => (
|
|
512
|
-
config.visualizationType === 'Pie'
|
|
513
|
-
? [config.yAxis.dataKey]
|
|
514
|
-
: config.visualizationType === 'Box Plot'
|
|
515
|
-
? Object.entries(config.boxplot.tableData[0])
|
|
516
|
-
: config.runtime?.seriesKeys),
|
|
517
|
-
[config.runtime?.seriesKeys]) // eslint-disable-line
|
|
518
|
-
|
|
519
|
-
if (config.visualizationType !== 'Box Plot') {
|
|
520
|
-
const genMapHeader = columns => {
|
|
521
|
-
return (
|
|
522
|
-
<tr>
|
|
523
|
-
{Object.keys(columns)
|
|
524
|
-
.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
525
|
-
.map((column, index) => {
|
|
526
|
-
let text
|
|
527
|
-
if (column !== 'geo') {
|
|
528
|
-
text = columns[column].label ? columns[column].label : columns[column].name
|
|
529
|
-
} else {
|
|
530
|
-
text = config.type === 'map' ? indexTitle : config.xAxis?.dataKey
|
|
531
|
-
}
|
|
532
|
-
if (config.type === 'map' && (text === undefined || text === '')) {
|
|
533
|
-
text = 'Location'
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
return (
|
|
537
|
-
<th
|
|
538
|
-
key={`col-header-${column}__${index}`}
|
|
539
|
-
id={column}
|
|
540
|
-
tabIndex='0'
|
|
541
|
-
title={text}
|
|
542
|
-
role='columnheader'
|
|
543
|
-
scope='col'
|
|
544
|
-
onClick={() => {
|
|
545
|
-
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
546
|
-
}}
|
|
547
|
-
onKeyDown={e => {
|
|
548
|
-
if (e.keyCode === 13) {
|
|
549
|
-
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
550
|
-
}
|
|
551
|
-
}}
|
|
552
|
-
className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
553
|
-
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
554
|
-
>
|
|
555
|
-
{text}
|
|
556
|
-
{sortBy.column === column && <span className={'sort-icon'}>{!sortBy.asc ? upIcon : downIcon}</span>}
|
|
557
|
-
<button>
|
|
558
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
559
|
-
</button>
|
|
560
|
-
</th>
|
|
561
|
-
)
|
|
562
|
-
})}
|
|
563
|
-
</tr>
|
|
564
|
-
)
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return (
|
|
568
|
-
<ErrorBoundary component='DataTable'>
|
|
569
|
-
<MediaControls.Section classes={['download-links']}>
|
|
570
|
-
<MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
|
|
571
|
-
{(config.table.download || config.general?.showDownloadButton) && <DownloadButton />}
|
|
572
|
-
</MediaControls.Section>
|
|
573
|
-
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
574
|
-
<a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
|
|
575
|
-
Skip Navigation or Skip to Content
|
|
576
|
-
</a>
|
|
577
|
-
<div
|
|
578
|
-
className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
579
|
-
onClick={() => {
|
|
580
|
-
setExpanded(!expanded)
|
|
581
|
-
}}
|
|
582
|
-
tabIndex='0'
|
|
583
|
-
onKeyDown={e => {
|
|
584
|
-
if (e.keyCode === 13) {
|
|
585
|
-
setExpanded(!expanded)
|
|
586
|
-
}
|
|
587
|
-
}}
|
|
588
|
-
>
|
|
589
|
-
<Icon display={expanded ? 'minus' : 'plus'} base />
|
|
590
|
-
{tableTitle}
|
|
591
|
-
</div>
|
|
592
|
-
<div className='table-container' style={limitHeight}>
|
|
593
|
-
<table height={expanded ? null : 0} role='table' aria-live='assertive' className={`${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}${isVertical ? '' : ' horizontal'}`} hidden={!expanded} aria-rowcount={config?.data?.length ? config.data.length : '-1'}>
|
|
594
|
-
<caption className='cdcdataviz-sr-only'>{caption}</caption>
|
|
595
|
-
<thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{config.type === 'map' ? genMapHeader(columns) : genChartHeader(columns, runtimeData)}</thead>
|
|
596
|
-
<tbody>{config.type === 'map' ? genMapRows(rows) : genChartRows(rows)}</tbody>
|
|
597
|
-
</table>
|
|
598
|
-
|
|
599
|
-
{/* REGION Data Table */}
|
|
600
|
-
{config.regions && config.regions.length > 0 && config.visualizationType !== 'Box Plot' ? (
|
|
601
|
-
<table className='region-table data-table'>
|
|
602
|
-
<caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
|
|
603
|
-
<thead>
|
|
604
|
-
<tr>
|
|
605
|
-
<th>Region Name</th>
|
|
606
|
-
<th>Start Date</th>
|
|
607
|
-
<th>End Date</th>
|
|
608
|
-
</tr>
|
|
609
|
-
</thead>
|
|
610
|
-
<tbody>
|
|
611
|
-
{config.regions.map((region, index) => {
|
|
612
|
-
if (config.visualizationType === 'Box Plot') return false
|
|
613
|
-
if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
|
|
614
|
-
// region.from and region.to had formatDate(parseDate()) on it
|
|
615
|
-
// but they returned undefined - removed both for now (TT)
|
|
616
|
-
return (
|
|
617
|
-
<tr key={`row-${region.label}--${index}`}>
|
|
618
|
-
<td>{region.label}</td>
|
|
619
|
-
<td>{region.from}</td>
|
|
620
|
-
<td>{region.to}</td>
|
|
621
|
-
</tr>
|
|
622
|
-
)
|
|
623
|
-
})}
|
|
624
|
-
</tbody>
|
|
625
|
-
</table>
|
|
626
|
-
) : (
|
|
627
|
-
''
|
|
628
|
-
)}
|
|
629
|
-
</div>
|
|
630
|
-
</section>
|
|
631
|
-
</ErrorBoundary>
|
|
632
|
-
)
|
|
633
|
-
} else {
|
|
634
|
-
// Render Data Table for Box Plots
|
|
635
|
-
const genBoxplotHeader = categories => {
|
|
636
|
-
let columns = ['Measures', ...categories]
|
|
637
|
-
return (
|
|
638
|
-
<tr>
|
|
639
|
-
{columns.map(column => {
|
|
640
|
-
return (
|
|
641
|
-
<th key={`col-header-${column}`} tabIndex='0' title={column} role='columnheader' scope='col'>
|
|
642
|
-
{column}
|
|
643
|
-
</th>
|
|
644
|
-
)
|
|
645
|
-
})}
|
|
646
|
-
</tr>
|
|
647
|
-
)
|
|
648
|
-
}
|
|
649
|
-
const resolveName = key => {
|
|
650
|
-
let {
|
|
651
|
-
boxplot: { labels }
|
|
652
|
-
} = config
|
|
653
|
-
const columnLookup = {
|
|
654
|
-
columnMean: labels.mean,
|
|
655
|
-
columnMax: labels.maximum,
|
|
656
|
-
columnMin: labels.minimum,
|
|
657
|
-
columnIqr: labels.iqr,
|
|
658
|
-
columnCategory: 'Category',
|
|
659
|
-
columnMedian: labels.median,
|
|
660
|
-
columnFirstQuartile: labels.q1,
|
|
661
|
-
columnThirdQuartile: labels.q3,
|
|
662
|
-
columnOutliers: labels.outliers,
|
|
663
|
-
values: labels.values,
|
|
664
|
-
columnTotal: labels.total,
|
|
665
|
-
columnSd: 'Standard Deviation',
|
|
666
|
-
nonOutlierValues: 'Non Outliers',
|
|
667
|
-
columnLowerBounds: labels.lowerBounds,
|
|
668
|
-
columnUpperBounds: labels.upperBounds
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
let resolvedName = columnLookup[key]
|
|
672
|
-
|
|
673
|
-
return resolvedName
|
|
674
|
-
}
|
|
675
|
-
let resolveCell = (rowid, plot) => {
|
|
676
|
-
if (Number(rowid) === 0) return true
|
|
677
|
-
if (Number(rowid) === 1) return plot.columnMax
|
|
678
|
-
if (Number(rowid) === 2) return plot.columnThirdQuartile
|
|
679
|
-
if (Number(rowid) === 3) return plot.columnMedian
|
|
680
|
-
if (Number(rowid) === 4) return plot.columnFirstQuartile
|
|
681
|
-
if (Number(rowid) === 5) return plot.columnMin
|
|
682
|
-
if (Number(rowid) === 6) return plot.columnTotal
|
|
683
|
-
if (Number(rowid) === 7) return plot.columnSd
|
|
684
|
-
if (Number(rowid) === 8) return plot.columnMean
|
|
685
|
-
if (Number(rowid) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
|
|
686
|
-
if (Number(rowid) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
|
|
687
|
-
return <p>-</p>
|
|
688
|
-
}
|
|
689
|
-
const genBoxplotRows = rows => {
|
|
690
|
-
// get list of data keys for each row
|
|
691
|
-
let dataKeys = rows.map(row => {
|
|
692
|
-
return row[0]
|
|
693
|
-
})
|
|
694
|
-
let columns = ['Measures', ...config.boxplot.categories]
|
|
695
|
-
const allrows = dataKeys.map((rowkey, index) => {
|
|
696
|
-
if (index === 0) return '' // we did header column separately
|
|
697
|
-
let rowClass = `row-Box-Plot--${index}`
|
|
698
|
-
return (
|
|
699
|
-
<tr role='row' key={`tbody__tr-${index}`} className={rowClass}>
|
|
700
|
-
{columns.map((column, colnum) => {
|
|
701
|
-
let cellValue
|
|
702
|
-
if (column === 'Measures') {
|
|
703
|
-
let labelValue = index > 0 ? resolveName(rowkey) : ''
|
|
704
|
-
cellValue = <>{labelValue}</>
|
|
705
|
-
} else {
|
|
706
|
-
cellValue = resolveCell(index, config.boxplot.plots[colnum - 1])
|
|
707
|
-
}
|
|
708
|
-
return (
|
|
709
|
-
<td tabIndex='0' key={`tbody__tr__td-${index}`} className='boxplot-td' role='gridcell'>
|
|
710
|
-
{cellValue}
|
|
711
|
-
</td>
|
|
712
|
-
)
|
|
713
|
-
})}
|
|
714
|
-
</tr>
|
|
715
|
-
)
|
|
716
|
-
})
|
|
717
|
-
return allrows
|
|
718
|
-
}
|
|
719
|
-
return (
|
|
720
|
-
<ErrorBoundary component='DataTable'>
|
|
721
|
-
{/* cove media results in error so disabling for now (TT)
|
|
722
|
-
<MediaControls.Section classes={['download-links']}>
|
|
723
|
-
<MediaControls.Link config={config} />
|
|
724
|
-
{config.general.showDownloadButton && <DownloadButton />}
|
|
725
|
-
</MediaControls.Section>
|
|
726
|
-
*/}
|
|
727
|
-
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
728
|
-
<a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
|
|
729
|
-
Skip Navigation or Skip to Content
|
|
730
|
-
</a>
|
|
731
|
-
<div
|
|
732
|
-
className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
733
|
-
onClick={() => {
|
|
734
|
-
setExpanded(!expanded)
|
|
735
|
-
}}
|
|
736
|
-
tabIndex='0'
|
|
737
|
-
onKeyDown={e => {
|
|
738
|
-
if (e.keyCode === 13) {
|
|
739
|
-
setExpanded(!expanded)
|
|
740
|
-
}
|
|
741
|
-
}}
|
|
742
|
-
>
|
|
743
|
-
<Icon display={expanded ? 'minus' : 'plus'} base />
|
|
744
|
-
{tableTitle}
|
|
745
|
-
</div>
|
|
746
|
-
<div className='table-container' style={limitHeight}>
|
|
747
|
-
<table height={expanded ? null : 0} role='table' aria-live='assertive' className={expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'} hidden={!expanded} aria-rowcount={'11'}>
|
|
748
|
-
<caption className='cdcdataviz-sr-only'>{caption}</caption>
|
|
749
|
-
<thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{genBoxplotHeader(config.boxplot.categories)}</thead>
|
|
750
|
-
<tbody>{genBoxplotRows(tableData)}</tbody>
|
|
751
|
-
</table>
|
|
752
|
-
</div>
|
|
753
|
-
</section>
|
|
754
|
-
</ErrorBoundary>
|
|
755
|
-
)
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
export default DataTable
|