@cdc/core 4.23.10-alpha → 4.23.11

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