@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.
@@ -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 <section className={classes.join(' ')}>{children}</section>
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