@cdc/core 4.23.3 → 4.23.5
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/external-link.svg +1 -0
- package/components/CoveMediaControls.jsx +6 -2
- package/components/DataTable.jsx +630 -0
- package/components/Filters.jsx +421 -0
- package/components/LegendCircle.jsx +2 -2
- package/components/ui/Icon.jsx +2 -1
- package/data/colorPalettes.js +3 -3
- package/helpers/DataTransform.js +54 -0
- package/helpers/isNumberLog.js +3 -3
- package/helpers/validateFipsCodeLength.js +1 -1
- package/package.json +2 -2
- package/styles/_data-table.scss +10 -0
- package/styles/_global.scss +5 -0
- package/styles/_series-list.scss +92 -0
- package/styles/base.scss +6 -0
- package/styles/filters.scss +122 -0
- package/styles/v2/themes/_color-definitions.scss +49 -0
- package/helpers/cleanData.js +0 -50
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M432 320h-32c-8.837 0-16 7.163-16 16v112H64V128h144c8.837 0 16-7.163 16-16V80c0-8.837-7.163-16-16-16H48C21.49 64 0 85.49 0 112v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V336c0-8.837-7.163-16-16-16zM488 0H360c-21.37 0-32.05 25.91-17 41l35.73 35.73L135 320.37a24 24 0 000 34L157.67 377a24 24 0 0034 0l243.61-243.68L471 169c15 15 41 4.5 41-17V24c0-13.255-10.745-24-24-24z" fill-rule="nonzero"/></svg>
|
|
@@ -83,7 +83,7 @@ const generateMedia = (state, type, elementToCapture) => {
|
|
|
83
83
|
console.warn('COVE: pdf downloads disabled')
|
|
84
84
|
break
|
|
85
85
|
default:
|
|
86
|
-
console.warn("generateMedia param 2 type must be 'image' or 'pdf'")
|
|
86
|
+
console.warn("COVE: generateMedia param 2 type must be 'image' or 'pdf'")
|
|
87
87
|
break
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -127,7 +127,11 @@ const Link = ({ config }) => {
|
|
|
127
127
|
|
|
128
128
|
// TODO: convert to standardized COVE section
|
|
129
129
|
const Section = ({ children, classes }) => {
|
|
130
|
-
return
|
|
130
|
+
return (
|
|
131
|
+
<section className={classes.join(' ')}>
|
|
132
|
+
<span>{children}</span>
|
|
133
|
+
</section>
|
|
134
|
+
)
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
const CoveMediaControls = () => null
|
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import React, { useEffect, useState, memo, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
import Papa from 'papaparse'
|
|
4
|
+
import ExternalIcon from '../assets/external-link.svg' // TODO: Move to Icon component
|
|
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 CoveMediaControls from '@cdc/core/components/CoveMediaControls'
|
|
10
|
+
|
|
11
|
+
import Loading from '@cdc/core/components/Loading'
|
|
12
|
+
|
|
13
|
+
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
14
|
+
const DataTable = props => {
|
|
15
|
+
const { config, tableTitle, indexTitle, vizTitle, rawData, runtimeData, headerColor, expandDataTable, columns, displayDataAsText, formatNumber, applyLegendToRow, displayGeoName, navigationHandler, viewport, formatLegendLocation, tabbingId, parseDate, formatDate, isDebug } = props
|
|
16
|
+
if (isDebug) console.log('core/DataTable: props=', props)
|
|
17
|
+
if (isDebug) console.log('core/DataTable: runtimeData=', runtimeData)
|
|
18
|
+
if (isDebug) console.log('core/DataTable: rawData=', rawData)
|
|
19
|
+
if (isDebug) console.log('core/DataTable: config=', config)
|
|
20
|
+
|
|
21
|
+
const [expanded, setExpanded] = useState(expandDataTable)
|
|
22
|
+
|
|
23
|
+
const [sortBy, setSortBy] = useState({ column: config.type === 'map' ? 'geo' : 'date', asc: false })
|
|
24
|
+
|
|
25
|
+
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
26
|
+
|
|
27
|
+
const fileName = `${vizTitle || 'data-table'}.csv`
|
|
28
|
+
|
|
29
|
+
// Catch all sorting method used on load by default but also on user click
|
|
30
|
+
// Having a custom method means we can add in any business logic we want going forward
|
|
31
|
+
const customSort = (a, b) => {
|
|
32
|
+
const digitRegex = /\d+/
|
|
33
|
+
|
|
34
|
+
const hasNumber = value => digitRegex.test(value)
|
|
35
|
+
|
|
36
|
+
// force null and undefined to the bottom
|
|
37
|
+
a = a === null || a === undefined ? '' : a
|
|
38
|
+
b = b === null || b === undefined ? '' : b
|
|
39
|
+
|
|
40
|
+
// convert any strings that are actually numbers to proper data type
|
|
41
|
+
const aNum = Number(a)
|
|
42
|
+
|
|
43
|
+
if (!Number.isNaN(aNum)) {
|
|
44
|
+
a = aNum
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const bNum = Number(b)
|
|
48
|
+
|
|
49
|
+
if (!Number.isNaN(bNum)) {
|
|
50
|
+
b = bNum
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// remove iso code prefixes
|
|
54
|
+
if (typeof a === 'string') {
|
|
55
|
+
a = a.replace('us-', '')
|
|
56
|
+
a = displayGeoName(a)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof b === 'string') {
|
|
60
|
+
b = b.replace('us-', '')
|
|
61
|
+
b = displayGeoName(b)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// force any string values to lowercase
|
|
65
|
+
a = typeof a === 'string' ? a.toLowerCase() : a
|
|
66
|
+
b = typeof b === 'string' ? b.toLowerCase() : b
|
|
67
|
+
|
|
68
|
+
// If the string contains a number, remove the text from the value and only sort by the number. Only uses the first number it finds.
|
|
69
|
+
if (typeof a === 'string' && hasNumber(a) === true) {
|
|
70
|
+
a = a.match(digitRegex)[0]
|
|
71
|
+
|
|
72
|
+
a = Number(a)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof b === 'string' && hasNumber(b) === true) {
|
|
76
|
+
b = b.match(digitRegex)[0]
|
|
77
|
+
|
|
78
|
+
b = Number(b)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// When comparing a number to a string, always send string to bottom
|
|
82
|
+
if (typeof a === 'number' && typeof b === 'string') {
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof b === 'number' && typeof a === 'string') {
|
|
87
|
+
return -1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Return either 1 or -1 to indicate a sort priority
|
|
91
|
+
if (a > b) {
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
94
|
+
if (a < b) {
|
|
95
|
+
return -1
|
|
96
|
+
}
|
|
97
|
+
// returning 0, undefined or any falsey value will use subsequent sorts or
|
|
98
|
+
// the index as a tiebreaker
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Optionally wrap cell with anchor if config defines a navigation url
|
|
103
|
+
const getCellAnchor = (markup, row) => {
|
|
104
|
+
if (columns.navigate && row[columns.navigate.name]) {
|
|
105
|
+
markup = (
|
|
106
|
+
<span
|
|
107
|
+
onClick={() => navigationHandler(row[columns.navigate.name])}
|
|
108
|
+
className='table-link'
|
|
109
|
+
title='Click for more information (Opens in a new window)'
|
|
110
|
+
role='link'
|
|
111
|
+
tabIndex='0'
|
|
112
|
+
onKeyDown={e => {
|
|
113
|
+
if (e.keyCode === 13) {
|
|
114
|
+
navigationHandler(row[columns.navigate.name])
|
|
115
|
+
}
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{markup}
|
|
119
|
+
<ExternalIcon className='inline-icon' />
|
|
120
|
+
</span>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return markup
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rand = Math.random().toString(16).substr(2, 8)
|
|
128
|
+
const skipId = `btn__${rand}`
|
|
129
|
+
|
|
130
|
+
const mapLookup = {
|
|
131
|
+
'us-county': 'United States County Map',
|
|
132
|
+
'single-state': 'State Map',
|
|
133
|
+
us: 'United States Map',
|
|
134
|
+
world: 'World Map'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const DownloadButton = memo(() => {
|
|
138
|
+
if (rawData !== undefined) {
|
|
139
|
+
let csvData
|
|
140
|
+
if (config.type === 'chart' || config.general.type === 'bubble') {
|
|
141
|
+
// Just Unparse
|
|
142
|
+
csvData = Papa.unparse(rawData)
|
|
143
|
+
} else if ((config.general.geoType !== 'single-state' && config.general.geoType !== 'us-county') || config.general.type === 'us-geocode') {
|
|
144
|
+
// Unparse + Add column for full Geo name
|
|
145
|
+
csvData = Papa.unparse(rawData.map(row => ({ FullGeoName: displayGeoName(row[config.columns.geo.name]), ...row })))
|
|
146
|
+
} else {
|
|
147
|
+
// Unparse + Add column for full Geo name along with State
|
|
148
|
+
csvData = Papa.unparse(rawData.map(row => ({ FullGeoName: formatLegendLocation(row[config.columns.geo.name]), ...row })))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
|
|
152
|
+
|
|
153
|
+
const saveBlob = () => {
|
|
154
|
+
//@ts-ignore
|
|
155
|
+
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
156
|
+
//@ts-ignore
|
|
157
|
+
navigator.msSaveBlob(blob, fileName)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<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'>
|
|
163
|
+
Download Data (CSV)
|
|
164
|
+
</a>
|
|
165
|
+
)
|
|
166
|
+
} else {
|
|
167
|
+
return <></>
|
|
168
|
+
}
|
|
169
|
+
}, [rawData])
|
|
170
|
+
|
|
171
|
+
// Change accessibility label depending on expanded status
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const expandedLabel = 'Accessible data table.'
|
|
174
|
+
const collapsedLabel = 'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.'
|
|
175
|
+
|
|
176
|
+
if (expanded === true && accessibilityLabel !== expandedLabel) {
|
|
177
|
+
setAccessibilityLabel(expandedLabel)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (expanded === false && accessibilityLabel !== collapsedLabel) {
|
|
181
|
+
setAccessibilityLabel(collapsedLabel)
|
|
182
|
+
}
|
|
183
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
184
|
+
}, [expanded])
|
|
185
|
+
|
|
186
|
+
switch (config.visualizationType) {
|
|
187
|
+
case 'Box Plot':
|
|
188
|
+
if (!config.boxplot) return <Loading />
|
|
189
|
+
break
|
|
190
|
+
case 'Line' || 'Bar' || 'Combo' || 'Pie' || 'Deviation Bar' || 'Paired Bar':
|
|
191
|
+
if (!runtimeData) return <Loading />
|
|
192
|
+
break
|
|
193
|
+
default:
|
|
194
|
+
if (!runtimeData) return <Loading />
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rows = Object.keys(runtimeData).sort((a, b) => {
|
|
199
|
+
let sortVal
|
|
200
|
+
if (config.columns.length > 0) {
|
|
201
|
+
sortVal = customSort(runtimeData[a][config.columns[sortBy.column].name], runtimeData[b][config.columns[sortBy.column].name])
|
|
202
|
+
}
|
|
203
|
+
if (!sortBy.asc) return sortVal
|
|
204
|
+
if (sortVal === 0) return 0
|
|
205
|
+
if (sortVal < 0) return 1
|
|
206
|
+
return -1
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
function genMapRows(rows) {
|
|
210
|
+
const allrows = rows.map(row => {
|
|
211
|
+
return (
|
|
212
|
+
<tr role='row'>
|
|
213
|
+
{Object.keys(columns)
|
|
214
|
+
.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
215
|
+
.map(column => {
|
|
216
|
+
let cellValue
|
|
217
|
+
|
|
218
|
+
if (column === 'geo') {
|
|
219
|
+
const rowObj = runtimeData[row]
|
|
220
|
+
const legendColor = applyLegendToRow(rowObj)
|
|
221
|
+
|
|
222
|
+
var labelValue
|
|
223
|
+
if ((config.general.geoType !== 'single-state' && config.general.geoType !== 'us-county') || config.general.type === 'us-geocode') {
|
|
224
|
+
labelValue = displayGeoName(row)
|
|
225
|
+
} else {
|
|
226
|
+
labelValue = formatLegendLocation(row)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
labelValue = getCellAnchor(labelValue, rowObj)
|
|
230
|
+
cellValue = (
|
|
231
|
+
<>
|
|
232
|
+
<LegendCircle fill={legendColor[0]} />
|
|
233
|
+
{labelValue}
|
|
234
|
+
</>
|
|
235
|
+
)
|
|
236
|
+
} else {
|
|
237
|
+
// check for special classes
|
|
238
|
+
let specialValFound = ''
|
|
239
|
+
if (config.legend.specialClasses && config.legend.specialClasses.length && typeof config.legend.specialClasses[0] === 'object') {
|
|
240
|
+
for (let i = 0; i < config.legend.specialClasses.length; i++) {
|
|
241
|
+
if (config.legend.specialClasses[i].key === config.columns[column].name) {
|
|
242
|
+
if (String(runtimeData[row][config.legend.specialClasses[i].key]) === config.legend.specialClasses[i].value) {
|
|
243
|
+
specialValFound = config.legend.specialClasses[i].label
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
cellValue = specialValFound ? displayDataAsText(specialValFound, column) : displayDataAsText(runtimeData[row][config.columns[column].name], column)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<td tabIndex='0' role='gridcell' onClick={e => (config.general.type === 'bubble' && config.general.allowMapZoom && config.general.geoType === 'world' ? setFilteredCountryCode(row) : true)}>
|
|
253
|
+
{cellValue}
|
|
254
|
+
</td>
|
|
255
|
+
)
|
|
256
|
+
})}
|
|
257
|
+
</tr>
|
|
258
|
+
)
|
|
259
|
+
})
|
|
260
|
+
return allrows
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const dataSeriesColumns = () => {
|
|
264
|
+
let tmpSeriesColumns
|
|
265
|
+
if (config.visualizationType !== 'Pie') {
|
|
266
|
+
tmpSeriesColumns = [config.xAxis.dataKey] //, ...config.runtime.seriesLabelsAll
|
|
267
|
+
config.series.forEach(element => {
|
|
268
|
+
tmpSeriesColumns.push(element.dataKey)
|
|
269
|
+
})
|
|
270
|
+
} else {
|
|
271
|
+
tmpSeriesColumns = [config.xAxis.dataKey, config.yAxis.dataKey] //Object.keys(runtimeData[0])
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// then add the additional Columns
|
|
275
|
+
if (Object.keys(config.columns).length > 0) {
|
|
276
|
+
Object.keys(config.columns).forEach(function (key) {
|
|
277
|
+
var value = config.columns[key]
|
|
278
|
+
// add if not the index AND it is enabled to be added to data table
|
|
279
|
+
if (value.name !== config.xAxis.dataKey && value.dataTable === true) {
|
|
280
|
+
tmpSeriesColumns.push(value.name)
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return tmpSeriesColumns
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const getLabel = name => {
|
|
289
|
+
let custLabel = ''
|
|
290
|
+
if (Object.keys(config.columns).length > 0) {
|
|
291
|
+
Object.keys(config.columns).forEach(function (key) {
|
|
292
|
+
var tmpColumn = config.columns[key]
|
|
293
|
+
// add if not the index AND it is enabled to be added to data table
|
|
294
|
+
if (tmpColumn.name === name) {
|
|
295
|
+
custLabel = tmpColumn.label
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
return custLabel
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function genChartHeader(columns, data) {
|
|
303
|
+
return (
|
|
304
|
+
<tr>
|
|
305
|
+
{dataSeriesColumns().map(column => {
|
|
306
|
+
let custLabel = getLabel(column) ? getLabel(column) : column
|
|
307
|
+
let text = column === config.xAxis.dataKey ? config.table.indexLabel : custLabel
|
|
308
|
+
return (
|
|
309
|
+
<th
|
|
310
|
+
key={`col-header-${column}`}
|
|
311
|
+
tabIndex='0'
|
|
312
|
+
title={text}
|
|
313
|
+
role='columnheader'
|
|
314
|
+
scope='col'
|
|
315
|
+
onClick={() => {
|
|
316
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
317
|
+
}}
|
|
318
|
+
onKeyDown={e => {
|
|
319
|
+
if (e.keyCode === 13) {
|
|
320
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
321
|
+
}
|
|
322
|
+
}}
|
|
323
|
+
className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
324
|
+
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
325
|
+
>
|
|
326
|
+
{text}
|
|
327
|
+
<button>
|
|
328
|
+
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
329
|
+
</button>
|
|
330
|
+
</th>
|
|
331
|
+
)
|
|
332
|
+
})}
|
|
333
|
+
</tr>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function genChartRows(rows) {
|
|
338
|
+
const allrows = rows.map(row => {
|
|
339
|
+
return (
|
|
340
|
+
<tr role='row'>
|
|
341
|
+
{dataSeriesColumns()
|
|
342
|
+
//.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
343
|
+
.map(column => {
|
|
344
|
+
let cellValue
|
|
345
|
+
if (column === config.xAxis.dataKey) {
|
|
346
|
+
const rowObj = runtimeData[row]
|
|
347
|
+
//const legendColor = applyLegendToRow(rowObj)
|
|
348
|
+
var labelValue = rowObj[column] // just raw X axis string
|
|
349
|
+
labelValue = getCellAnchor(labelValue, rowObj)
|
|
350
|
+
// no colors on row headers for charts bc it's Date not data
|
|
351
|
+
// Remove this - <LegendCircle fill={legendColor[row]} />
|
|
352
|
+
cellValue = <>{labelValue}</>
|
|
353
|
+
} else {
|
|
354
|
+
cellValue = displayDataAsText(runtimeData[row][column], column)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
//MAP SPECIFIC- change to CHART specific
|
|
358
|
+
// onClick = { e => (config.general.type === 'bubble' && config.general.allowMapZoom && config.general.geoType === 'world' ? setFilteredCountryCode(row) : true)}
|
|
359
|
+
return (
|
|
360
|
+
<td tabIndex='0' role='gridcell' id={`${runtimeData[config.runtime.originalXAxis.dataKey]}--${row}`}>
|
|
361
|
+
{cellValue}
|
|
362
|
+
</td>
|
|
363
|
+
)
|
|
364
|
+
})}
|
|
365
|
+
</tr>
|
|
366
|
+
)
|
|
367
|
+
})
|
|
368
|
+
return allrows
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const limitHeight = {
|
|
372
|
+
maxHeight: config.table.limitHeight && `${config.table.height}px`,
|
|
373
|
+
overflowY: 'scroll'
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const caption = useMemo(() => {
|
|
377
|
+
if (config.type === 'map') {
|
|
378
|
+
return config.table.caption ? config.table.caption : `Data table showing data for the ${mapLookup[config.general.geoType]} figure.`
|
|
379
|
+
} else {
|
|
380
|
+
return config.table.caption ? config.table.caption : `Data table showing data for the ${config.type} figure.`
|
|
381
|
+
}
|
|
382
|
+
}, [config.table.caption])
|
|
383
|
+
|
|
384
|
+
// prettier-ignore
|
|
385
|
+
const tableData = useMemo(() => (
|
|
386
|
+
config.visualizationType === 'Pie'
|
|
387
|
+
? [config.yAxis.dataKey]
|
|
388
|
+
: config.visualizationType === 'Box Plot'
|
|
389
|
+
? Object.entries(config.boxplot.tableData[0])
|
|
390
|
+
: config.runtime.seriesKeys),
|
|
391
|
+
[config.runtime.seriesKeys]) // eslint-disable-line
|
|
392
|
+
|
|
393
|
+
if (config.visualizationType !== 'Box Plot') {
|
|
394
|
+
function genMapHeader(columns) {
|
|
395
|
+
return (
|
|
396
|
+
<tr>
|
|
397
|
+
{Object.keys(columns)
|
|
398
|
+
.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
399
|
+
.map(column => {
|
|
400
|
+
let text
|
|
401
|
+
if (column !== 'geo') {
|
|
402
|
+
text = columns[column].label ? columns[column].label : columns[column].name
|
|
403
|
+
} else {
|
|
404
|
+
text = config.type === 'map' ? indexTitle : config.xAxis.dataKey
|
|
405
|
+
}
|
|
406
|
+
if (config.type === 'map' && (text === undefined || text === '')) {
|
|
407
|
+
text = 'Location'
|
|
408
|
+
}
|
|
409
|
+
return (
|
|
410
|
+
<th
|
|
411
|
+
key={`col-header-${column}`}
|
|
412
|
+
tabIndex='0'
|
|
413
|
+
title={text}
|
|
414
|
+
role='columnheader'
|
|
415
|
+
scope='col'
|
|
416
|
+
onClick={() => {
|
|
417
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
418
|
+
}}
|
|
419
|
+
onKeyDown={e => {
|
|
420
|
+
if (e.keyCode === 13) {
|
|
421
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
422
|
+
}
|
|
423
|
+
}}
|
|
424
|
+
className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
425
|
+
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
426
|
+
>
|
|
427
|
+
{text}
|
|
428
|
+
<button>
|
|
429
|
+
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
430
|
+
</button>
|
|
431
|
+
</th>
|
|
432
|
+
)
|
|
433
|
+
})}
|
|
434
|
+
</tr>
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<ErrorBoundary component='DataTable'>
|
|
440
|
+
<CoveMediaControls.Section classes={['download-links']}>
|
|
441
|
+
<CoveMediaControls.Link config={config} />
|
|
442
|
+
{(config.table.download || config.general.showDownloadButton) && <DownloadButton />}
|
|
443
|
+
</CoveMediaControls.Section>
|
|
444
|
+
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
445
|
+
<a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
|
|
446
|
+
Skip Navigation or Skip to Content
|
|
447
|
+
</a>
|
|
448
|
+
<div
|
|
449
|
+
className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
450
|
+
onClick={() => {
|
|
451
|
+
setExpanded(!expanded)
|
|
452
|
+
}}
|
|
453
|
+
tabIndex='0'
|
|
454
|
+
onKeyDown={e => {
|
|
455
|
+
if (e.keyCode === 13) {
|
|
456
|
+
setExpanded(!expanded)
|
|
457
|
+
}
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
<Icon display={expanded ? 'minus' : 'plus'} base />
|
|
461
|
+
{tableTitle}
|
|
462
|
+
</div>
|
|
463
|
+
<div className='table-container' style={limitHeight}>
|
|
464
|
+
<table height={expanded ? null : 0} role='table' aria-live='assertive' className={expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'} hidden={!expanded} aria-rowcount={config?.data?.length ? config.data.length : '-1'}>
|
|
465
|
+
<caption className='cdcdataviz-sr-only'>{caption}</caption>
|
|
466
|
+
<thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{config.type === 'map' ? genMapHeader(columns) : genChartHeader(columns, runtimeData)}</thead>
|
|
467
|
+
<tbody>{config.type === 'map' ? genMapRows(rows) : genChartRows(rows)}</tbody>
|
|
468
|
+
</table>
|
|
469
|
+
|
|
470
|
+
{/* REGION Data Table */}
|
|
471
|
+
{config.regions && config.regions.length > 0 && config.visualizationType !== 'Box Plot' ? (
|
|
472
|
+
<table className='region-table data-table'>
|
|
473
|
+
<caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
|
|
474
|
+
<thead>
|
|
475
|
+
<tr>
|
|
476
|
+
<th>Region Name</th>
|
|
477
|
+
<th>Start Date</th>
|
|
478
|
+
<th>End Date</th>
|
|
479
|
+
</tr>
|
|
480
|
+
</thead>
|
|
481
|
+
<tbody>
|
|
482
|
+
{config.regions.map((region, index) => {
|
|
483
|
+
if (config.visualizationType === 'Box Plot') return false
|
|
484
|
+
if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
|
|
485
|
+
// region.from and region.to had formatDate(parseDate()) on it
|
|
486
|
+
// but they returned undefined - removed both for now (TT)
|
|
487
|
+
return (
|
|
488
|
+
<tr key={`row-${region.label}--${index}`}>
|
|
489
|
+
<td>{region.label}</td>
|
|
490
|
+
<td>{region.from}</td>
|
|
491
|
+
<td>{region.to}</td>
|
|
492
|
+
</tr>
|
|
493
|
+
)
|
|
494
|
+
})}
|
|
495
|
+
</tbody>
|
|
496
|
+
</table>
|
|
497
|
+
) : (
|
|
498
|
+
''
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
</section>
|
|
502
|
+
</ErrorBoundary>
|
|
503
|
+
)
|
|
504
|
+
} else {
|
|
505
|
+
// Render Data Table for Box Plots
|
|
506
|
+
function genBoxplotHeader(categories) {
|
|
507
|
+
let columns = ['Measures', ...categories]
|
|
508
|
+
return (
|
|
509
|
+
<tr>
|
|
510
|
+
{columns.map(column => {
|
|
511
|
+
return (
|
|
512
|
+
<th key={`col-header-${column}`} tabIndex='0' title={column} role='columnheader' scope='col'>
|
|
513
|
+
{column}
|
|
514
|
+
</th>
|
|
515
|
+
)
|
|
516
|
+
})}
|
|
517
|
+
</tr>
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
const resolveName = key => {
|
|
521
|
+
let {
|
|
522
|
+
boxplot: { labels }
|
|
523
|
+
} = config
|
|
524
|
+
const columnLookup = {
|
|
525
|
+
columnMean: labels.mean,
|
|
526
|
+
columnMax: labels.maximum,
|
|
527
|
+
columnMin: labels.minimum,
|
|
528
|
+
columnIqr: labels.iqr,
|
|
529
|
+
columnCategory: 'Category',
|
|
530
|
+
columnMedian: labels.median,
|
|
531
|
+
columnFirstQuartile: labels.q1,
|
|
532
|
+
columnThirdQuartile: labels.q3,
|
|
533
|
+
columnOutliers: labels.outliers,
|
|
534
|
+
values: labels.values,
|
|
535
|
+
columnTotal: labels.total,
|
|
536
|
+
columnSd: 'Standard Deviation',
|
|
537
|
+
nonOutlierValues: 'Non Outliers',
|
|
538
|
+
columnLowerBounds: labels.lowerBounds,
|
|
539
|
+
columnUpperBounds: labels.upperBounds
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let resolvedName = columnLookup[key]
|
|
543
|
+
|
|
544
|
+
return resolvedName
|
|
545
|
+
}
|
|
546
|
+
let resolveCell = (rowid, plot) => {
|
|
547
|
+
if (Number(rowid) === 0) return true
|
|
548
|
+
if (Number(rowid) === 1) return plot.columnMax
|
|
549
|
+
if (Number(rowid) === 2) return plot.columnThirdQuartile
|
|
550
|
+
if (Number(rowid) === 3) return plot.columnMedian
|
|
551
|
+
if (Number(rowid) === 4) return plot.columnFirstQuartile
|
|
552
|
+
if (Number(rowid) === 5) return plot.columnMin
|
|
553
|
+
if (Number(rowid) === 6) return plot.columnTotal
|
|
554
|
+
if (Number(rowid) === 7) return plot.columnSd
|
|
555
|
+
if (Number(rowid) === 8) return plot.columnMean
|
|
556
|
+
if (Number(rowid) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
|
|
557
|
+
if (Number(rowid) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
|
|
558
|
+
return <p>-</p>
|
|
559
|
+
}
|
|
560
|
+
function genBoxplotRows(rows) {
|
|
561
|
+
// get list of data keys for each row
|
|
562
|
+
let dataKeys = rows.map(row => {
|
|
563
|
+
return row[0]
|
|
564
|
+
})
|
|
565
|
+
let columns = ['Measures', ...config.boxplot.categories]
|
|
566
|
+
const allrows = dataKeys.map((rowkey, index) => {
|
|
567
|
+
if (index === 0) return '' // we did header column separately
|
|
568
|
+
let rowClass = `row-Box-Plot--${index}`
|
|
569
|
+
return (
|
|
570
|
+
<tr role='row' key={`tbody__tr-${index}`} className={rowClass}>
|
|
571
|
+
{columns.map((column, colnum) => {
|
|
572
|
+
let cellValue
|
|
573
|
+
if (column === 'Measures') {
|
|
574
|
+
let labelValue = index > 0 ? resolveName(rowkey) : ''
|
|
575
|
+
cellValue = <>{labelValue}</>
|
|
576
|
+
} else {
|
|
577
|
+
cellValue = resolveCell(index, config.boxplot.plots[colnum - 1])
|
|
578
|
+
}
|
|
579
|
+
return (
|
|
580
|
+
<td tabIndex='0' key={`tbody__tr__td-${index}`} className='boxplot-td' role='gridcell'>
|
|
581
|
+
{cellValue}
|
|
582
|
+
</td>
|
|
583
|
+
)
|
|
584
|
+
})}
|
|
585
|
+
</tr>
|
|
586
|
+
)
|
|
587
|
+
})
|
|
588
|
+
return allrows
|
|
589
|
+
}
|
|
590
|
+
return (
|
|
591
|
+
<ErrorBoundary component='DataTable'>
|
|
592
|
+
{/* cove media results in error so disabling for now (TT)
|
|
593
|
+
<CoveMediaControls.Section classes={['download-links']}>
|
|
594
|
+
<CoveMediaControls.Link config={config} />
|
|
595
|
+
{config.general.showDownloadButton && <DownloadButton />}
|
|
596
|
+
</CoveMediaControls.Section>
|
|
597
|
+
*/}
|
|
598
|
+
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
599
|
+
<a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
|
|
600
|
+
Skip Navigation or Skip to Content
|
|
601
|
+
</a>
|
|
602
|
+
<div
|
|
603
|
+
className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
604
|
+
onClick={() => {
|
|
605
|
+
setExpanded(!expanded)
|
|
606
|
+
}}
|
|
607
|
+
tabIndex='0'
|
|
608
|
+
onKeyDown={e => {
|
|
609
|
+
if (e.keyCode === 13) {
|
|
610
|
+
setExpanded(!expanded)
|
|
611
|
+
}
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<Icon display={expanded ? 'minus' : 'plus'} base />
|
|
615
|
+
{tableTitle}
|
|
616
|
+
</div>
|
|
617
|
+
<div className='table-container' style={limitHeight}>
|
|
618
|
+
<table height={expanded ? null : 0} role='table' aria-live='assertive' className={expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'} hidden={!expanded} aria-rowcount={'11'}>
|
|
619
|
+
<caption className='cdcdataviz-sr-only'>{caption}</caption>
|
|
620
|
+
<thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{genBoxplotHeader(config.boxplot.categories)}</thead>
|
|
621
|
+
<tbody>{genBoxplotRows(tableData)}</tbody>
|
|
622
|
+
</table>
|
|
623
|
+
</div>
|
|
624
|
+
</section>
|
|
625
|
+
</ErrorBoundary>
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export default DataTable
|