@cdc/core 4.23.4 → 4.23.6

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