@cdc/core 4.23.4 → 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>
@@ -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
@@ -1,4 +1,5 @@
1
- import React, { useState, useEffect } from 'react'
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { useId } from 'react'
2
3
 
3
4
  // CDC
4
5
  import Button from '@cdc/core/components/elements/Button'
@@ -72,7 +73,9 @@ export const useFilters = props => {
72
73
 
73
74
  const changeFilterActive = (index, value) => {
74
75
  let newFilters = visualizationConfig.type === 'map' ? [...filteredData] : [...visualizationConfig.filters]
76
+
75
77
  newFilters[index].active = value
78
+ setConfig({ ...visualizationConfig })
76
79
 
77
80
  // If this is a button filter type show the button.
78
81
  if (visualizationConfig.filterBehavior === 'Apply Button') {
@@ -183,6 +186,8 @@ const Filters = props => {
183
186
  const { config: visualizationConfig, filteredData, dimensions } = props
184
187
  const { filters, type, general, theme, filterBehavior } = visualizationConfig
185
188
  const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
189
+ const [selectedFilter, setSelectedFilter] = useState('')
190
+ const id = useId()
186
191
 
187
192
  // useFilters hook provides data and logic for handling various filter functions
188
193
  // prettier-ignore
@@ -205,6 +210,13 @@ const Filters = props => {
205
210
  }
206
211
  }, [dimensions])
207
212
 
213
+ useEffect(() => {
214
+ if (selectedFilter) {
215
+ let el = document.getElementById(selectedFilter.id)
216
+ if (el) el.focus()
217
+ }
218
+ }, [changeFilterActive, selectedFilter])
219
+
208
220
  const Filters = props => props.children
209
221
 
210
222
  const filterSectionClassList = ['filters-section', type === 'map' ? general.headerColor : theme]
@@ -241,10 +253,24 @@ const Filters = props => {
241
253
  const { filter: singleFilter, index: outerIndex } = props
242
254
  return (
243
255
  <section className='single-filters__tab-bar'>
244
- {singleFilter.values.map(filter => {
256
+ {singleFilter.values.map((filter, index) => {
245
257
  const buttonClassList = ['button__tab-bar', singleFilter.active === filter ? 'button__tab-bar--active' : '']
246
258
  return (
247
- <button className={buttonClassList.join(' ')} key={filter} onClick={e => changeFilterActive(outerIndex, filter)}>
259
+ <button
260
+ id={`${filter}-${outerIndex}-${index}-${id}`}
261
+ className={buttonClassList.join(' ')}
262
+ key={filter}
263
+ onClick={e => {
264
+ changeFilterActive(outerIndex, filter)
265
+ setSelectedFilter(e.target)
266
+ }}
267
+ onKeyDown={e => {
268
+ if (e.keyCode === 13) {
269
+ changeFilterActive(outerIndex, filter)
270
+ setSelectedFilter(e.target)
271
+ }
272
+ }}
273
+ >
248
274
  {filter}
249
275
  </button>
250
276
  )
@@ -300,8 +326,22 @@ const Filters = props => {
300
326
  const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
301
327
 
302
328
  pillValues.push(
303
- <div className='pill__wrapper'>
304
- <button className={pillClassList.join(' ')} onClick={e => changeFilterActive(outerIndex, filterOption)} name={label}>
329
+ <div className='pill__wrapper' key={`pill-${index}`}>
330
+ <button
331
+ id={`${filterOption}-${outerIndex}-${index}-${id}`}
332
+ className={pillClassList.join(' ')}
333
+ onKeyDown={e => {
334
+ if (e.keyCode === 13) {
335
+ changeFilterActive(outerIndex, filterOption)
336
+ setSelectedFilter(e.target)
337
+ }
338
+ }}
339
+ onClick={e => {
340
+ changeFilterActive(outerIndex, filterOption)
341
+ setSelectedFilter(e.target)
342
+ }}
343
+ name={label}
344
+ >
305
345
  {filterOption}
306
346
  </button>
307
347
  </div>
@@ -309,12 +349,25 @@ const Filters = props => {
309
349
 
310
350
  values.push(
311
351
  <option key={index} value={filterOption}>
312
- {filterOption}
352
+ {singleFilter.labels && singleFilter.labels[filterOption] ? singleFilter.labels[filterOption] : filterOption}
313
353
  </option>
314
354
  )
315
355
 
316
356
  tabValues.push(
317
- <button className={tabClassList.join(' ')} onClick={e => changeFilterActive(outerIndex, filterOption)}>
357
+ <button
358
+ id={`${filterOption}-${outerIndex}-${index}-${id}`}
359
+ className={tabClassList.join(' ')}
360
+ onClick={e => {
361
+ changeFilterActive(outerIndex, filterOption)
362
+ setSelectedFilter(e.target)
363
+ }}
364
+ onKeyDown={e => {
365
+ if (e.keyCode === 13) {
366
+ changeFilterActive(outerIndex, filterOption)
367
+ setSelectedFilter(e.target)
368
+ }
369
+ }}
370
+ >
318
371
  {filterOption}
319
372
  </button>
320
373
  )
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
 
3
- export default function LegendCircle({ fill }) {
3
+ export default function LegendCircle({ fill, borderColor }) {
4
4
  const styles = {
5
5
  marginRight: '5px',
6
6
  borderRadius: '300px',
@@ -8,7 +8,7 @@ export default function LegendCircle({ fill }) {
8
8
  display: 'inline-block',
9
9
  height: '1em',
10
10
  width: '1em',
11
- border: 'rgba(0,0,0,.3) 1px solid',
11
+ border: borderColor ? `${borderColor} 1px solid` : 'rgba(0,0,0,.3) 1px solid',
12
12
  backgroundColor: fill
13
13
  }
14
14
 
@@ -51,16 +51,16 @@ export const colorPalettes3 = {
51
51
  'monochrome-4': ['#C2C0FC', '#6a3d9a'],
52
52
  'monochrome-5': ['#fedab8', '#bf5b17'],
53
53
  'cool-1': ['#b2df8a', '#1f78b4'],
54
- 'cool-2': ['#a6cee3', '#33A02C'],
54
+ 'cool-2': ['#a6cee3', '#72D66B'],
55
55
  'cool-3': ['#C2C0FC', '#386cb0'],
56
- 'cool-4': ['#33A02c', '#6a3d9a'],
56
+ 'cool-4': ['#72D66B', '#6a3d9a'],
57
57
  'cool-5': ['#a6cee3', '#6a3d9a'],
58
58
  'warm-1': ['#e31a1c', '#fedab8'],
59
59
  'complementary-1': ['#1f78b4', '#e6ab02'],
60
60
  'complementary-2': ['#1f78b4', '#ff7f00'],
61
61
  'complementary-3': ['#6a3d9a', '#ff7f00'],
62
62
  'complementary-4': ['#6a3d9a', '#e6ab02'],
63
- 'complementary-5': ['#e31a90', '#1b9e77']
63
+ 'complementary-5': ['#df168c', '#1EB386']
64
64
  }
65
65
 
66
66
  export const colorPalettesChart = updatePaletteNames(colorPalettes2)
@@ -28,7 +28,7 @@ export default function validateFipsCodeLength(stateOrData) {
28
28
  }
29
29
 
30
30
  // Only includes data - get column name from somewhere else
31
- if (Array.isArray(stateOrData)) {
31
+ if (Array.isArray(stateOrData) && stateOrData.length > 0) {
32
32
  let columns = Object.keys(stateOrData[0])
33
33
 
34
34
  let potentialColumnNames = ['fips', 'FIPS', 'fips codes', 'FIPS CODES', 'Fips Codes', 'fips Codes', 'Fips codes', 'FIPS Codes']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdc/core",
3
- "version": "4.23.4",
3
+ "version": "4.23.5",
4
4
  "description": "Core components, styles, hooks, and helpers, for the CDC Open Visualization project",
5
5
  "moduleName": "CdcCore",
6
6
  "main": "dist/cdccore",
@@ -30,5 +30,5 @@
30
30
  "react": "^18.2.0",
31
31
  "react-dom": "^18.2.0"
32
32
  },
33
- "gitHead": "dcd395d76f70b2d113f2b4c6fe50a52522655cd1"
33
+ "gitHead": "34add3436994ca3cf13e51f313add4d70377f53e"
34
34
  }
@@ -39,6 +39,7 @@ table.data-table {
39
39
  border-collapse: collapse;
40
40
  overflow: auto;
41
41
  appearance: none;
42
+ table-layout: fixed;
42
43
  * {
43
44
  box-sizing: border-box;
44
45
  }
@@ -172,6 +173,15 @@ table.data-table {
172
173
  margin-left: 5px;
173
174
  }
174
175
  }
176
+
177
+ .boxplot-td {
178
+ //display: inline-block;
179
+ //box-sizing: border-box;
180
+ table-layout: fixed;
181
+ width: 200;
182
+ //min-width: 150px;
183
+ //max-width: 400px;
184
+ }
175
185
  }
176
186
 
177
187
  .no-data {