@cdc/core 4.23.10 → 4.24.1

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 (74) hide show
  1. package/LICENSE +201 -0
  2. package/assets/icon-deviation-bar.svg +1 -0
  3. package/components/DataTable/DataTable.tsx +223 -0
  4. package/components/DataTable/components/BoxplotHeader.tsx +16 -0
  5. package/components/DataTable/components/CellAnchor.tsx +44 -0
  6. package/components/DataTable/components/ChartHeader.tsx +103 -0
  7. package/components/DataTable/components/ExpandCollapse.tsx +21 -0
  8. package/components/DataTable/components/Icons.tsx +10 -0
  9. package/components/DataTable/components/MapHeader.tsx +56 -0
  10. package/components/DataTable/components/SkipNav.tsx +7 -0
  11. package/components/DataTable/helpers/boxplotCellMatrix.tsx +64 -0
  12. package/components/DataTable/helpers/chartCellMatrix.tsx +92 -0
  13. package/components/DataTable/helpers/customColumns.ts +25 -0
  14. package/components/DataTable/helpers/customSort.ts +55 -0
  15. package/components/DataTable/helpers/getChartCellValue.ts +56 -0
  16. package/components/DataTable/helpers/getDataSeriesColumns.ts +29 -0
  17. package/components/DataTable/helpers/getSeriesName.ts +26 -0
  18. package/components/DataTable/helpers/mapCellMatrix.tsx +56 -0
  19. package/components/DataTable/helpers/regionCellMatrix.tsx +13 -0
  20. package/components/DataTable/helpers/standardizeState.js +76 -0
  21. package/components/DataTable/index.ts +1 -0
  22. package/components/DataTable/types/TableConfig.ts +52 -0
  23. package/components/DownloadButton.tsx +29 -0
  24. package/components/EditorPanel/DataTableEditor.tsx +133 -0
  25. package/components/EditorPanel/Inputs.tsx +150 -0
  26. package/components/Filters.jsx +3 -3
  27. package/components/LegendCircle.jsx +2 -2
  28. package/components/MediaControls.jsx +1 -1
  29. package/components/MultiSelect/MultiSelect.tsx +95 -0
  30. package/components/MultiSelect/index.ts +1 -0
  31. package/components/MultiSelect/multiselect.styles.css +50 -0
  32. package/components/Table/Table.tsx +69 -0
  33. package/components/Table/components/Cell.tsx +9 -0
  34. package/components/Table/components/GroupRow.tsx +20 -0
  35. package/components/Table/components/Row.tsx +26 -0
  36. package/components/Table/index.ts +1 -0
  37. package/components/Table/types/CellMatrix.ts +4 -0
  38. package/components/Table/types/RowType.ts +5 -0
  39. package/components/_stories/DataTable.stories.tsx +103 -0
  40. package/components/_stories/EditorPanel.stories.tsx +53 -0
  41. package/components/_stories/Inputs.stories.tsx +37 -0
  42. package/components/_stories/MultiSelect.stories.tsx +24 -0
  43. package/components/_stories/Table.stories.tsx +53 -0
  44. package/components/_stories/_mocks/dashboard_no_filter.json +121 -0
  45. package/components/_stories/_mocks/example-city-state.json +808 -0
  46. package/components/_stories/_mocks/row_type.json +42 -0
  47. package/components/_stories/styles.scss +9 -0
  48. package/components/inputs/{InputSelect.jsx → InputSelect.tsx} +15 -5
  49. package/components/managers/{DataDesigner.jsx → DataDesigner.tsx} +103 -94
  50. package/components/ui/{Icon.jsx → Icon.tsx} +3 -3
  51. package/components/ui/Title/Title.scss +95 -0
  52. package/components/ui/Title/index.tsx +34 -0
  53. package/components/ui/_stories/Title.stories.tsx +21 -0
  54. package/helpers/DataTransform.ts +75 -20
  55. package/helpers/cove/string.ts +11 -0
  56. package/helpers/fetchRemoteData.js +1 -1
  57. package/helpers/getFileExtension.ts +28 -5
  58. package/package.json +2 -2
  59. package/styles/_data-table.scss +3 -0
  60. package/styles/heading-colors.scss +0 -3
  61. package/styles/v2/layout/_component.scss +0 -11
  62. package/types/Axis.ts +41 -0
  63. package/types/Color.ts +5 -0
  64. package/types/Column.ts +15 -0
  65. package/types/ComponentStyles.ts +7 -0
  66. package/types/ComponentThemes.ts +13 -0
  67. package/types/EditorColumnProperties.ts +8 -0
  68. package/types/FilterBehavior.ts +1 -0
  69. package/types/Runtime.ts +29 -0
  70. package/types/Series.ts +1 -0
  71. package/types/Table.ts +18 -0
  72. package/types/UpdateFieldFunc.ts +1 -0
  73. package/types/Visualization.ts +21 -0
  74. package/components/DataTable.jsx +0 -754
@@ -1,754 +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 = [config.xAxis?.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
- return (
357
- <tr>
358
- {['__series__', ...Object.keys(runtimeData)].map((row, index) => {
359
- let column = config.xAxis?.dataKey
360
- let text = row !== '__series__' ? getChartCellValue(row, column) : '__series__'
361
- return (
362
- <th
363
- key={`col-header-${text}__${index}`}
364
- tabIndex='0'
365
- title={text}
366
- role='columnheader'
367
- scope='col'
368
- onClick={() => {
369
- setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
370
- }}
371
- onKeyDown={e => {
372
- if (e.keyCode === 13) {
373
- setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
374
- }
375
- }}
376
- className={sortBy.column === text ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
377
- {...(sortBy.column === text ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
378
- >
379
- {text === '__series__' ? '' : text}
380
- {index === sortBy.colIndex && <span className={'sort-icon'}>{!sortBy.asc ? upIcon : downIcon}</span>}
381
- <button>
382
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === text ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
383
- </button>
384
- </th>
385
- )
386
- })}
387
- </tr>
388
- )
389
- }
390
- }
391
-
392
- // if its additional column, return formatting params
393
- const isAdditionalColumn = column => {
394
- let inthere = false
395
- let formattingParams = {}
396
- if (config.columns) {
397
- Object.keys(config.columns).forEach(keycol => {
398
- if (config.columns[keycol].name === column) {
399
- inthere = true
400
- formattingParams = {
401
- addColPrefix: config.columns[keycol].prefix,
402
- addColSuffix: config.columns[keycol].suffix,
403
- addColRoundTo: config.columns[keycol].roundToPlace ? config.columns[keycol].roundToPlace : '',
404
- addColCommas: config.columns[keycol].commas
405
- }
406
- }
407
- })
408
- }
409
- return formattingParams
410
- }
411
-
412
- const getChartCellValue = (row, column) => {
413
- const rowObj = runtimeData[row]
414
- let cellValue // placeholder for formatting below
415
- let labelValue = rowObj[column] // just raw X axis string
416
- if (column === config.xAxis?.dataKey) {
417
- // not the prettiest, but helper functions work nicely here.
418
- cellValue = config.xAxis?.type === 'date' ? formatDate(config.xAxis?.dateDisplayFormat, parseDate(config.xAxis?.dateParseFormat, labelValue)) : labelValue
419
- } else {
420
- let resolvedAxis = 'left'
421
- let leftAxisItems = config.series ? config.series.filter(item => item?.axis === 'Left') : []
422
- let rightAxisItems = config.series ? config.series.filter(item => item?.axis === 'Right') : []
423
-
424
- leftAxisItems.map(leftSeriesItem => {
425
- if (leftSeriesItem.dataKey === column) resolvedAxis = 'left'
426
- })
427
-
428
- rightAxisItems.map(rightSeriesItem => {
429
- if (rightSeriesItem.dataKey === column) resolvedAxis = 'right'
430
- })
431
-
432
- let addColParams = isAdditionalColumn(column)
433
- if (addColParams) {
434
- cellValue = config.dataFormat ? formatNumber(runtimeData[row][column], resolvedAxis, false, config, addColParams) : runtimeData[row][column]
435
- } else {
436
- cellValue = config.dataFormat ? formatNumber(runtimeData[row][column], resolvedAxis, false, config) : runtimeData[row][column]
437
- }
438
- }
439
-
440
- return cellValue
441
- }
442
-
443
- const getChartCell = (row, column) => {
444
- return (
445
- <td tabIndex='0' role='gridcell' id={`${runtimeData[config.runtime?.originalXAxis?.dataKey]}--${row}`}>
446
- {getChartCellValue(row, column)}
447
- </td>
448
- )
449
- }
450
-
451
- const genChartRows = rows => {
452
- if (isVertical) {
453
- const allRows = rows.map((row, index) => {
454
- return (
455
- <tr key={`${row}__${index}`} role='row'>
456
- {dataSeriesColumns().map(column => {
457
- return getChartCell(row, column)
458
- })}
459
- </tr>
460
- )
461
- })
462
- return allRows
463
- } else {
464
- const allRows = dataSeriesColumnsSorted().map(column => {
465
- return (
466
- <tr role='row'>
467
- <td>
468
- {colorScale && colorScale(getSeriesName(column)) && <LegendCircle fill={colorScale(getSeriesName(column))} />}
469
- {getSeriesName(column)}
470
- </td>
471
- {rows.map(row => {
472
- return getChartCell(row, column)
473
- })}
474
- </tr>
475
- )
476
- })
477
- return allRows
478
- }
479
- }
480
-
481
- const upIcon = (
482
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
483
- <path d='M0 5l5-5 5 5z' />
484
- </svg>
485
- )
486
- const downIcon = (
487
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
488
- <path d='M0 0l5 5 5-5z' />
489
- </svg>
490
- )
491
-
492
- const limitHeight = {
493
- maxHeight: config.table.limitHeight && `${config.table.height}px`,
494
- overflowY: 'scroll'
495
- }
496
-
497
- const caption = useMemo(() => {
498
- if (config.type === 'map') {
499
- return config.table.caption ? config.table.caption : `Data table showing data for the ${mapLookup[config.general.geoType]} figure.`
500
- } else {
501
- return config.table.caption ? config.table.caption : `Data table showing data for the ${config.type} figure.`
502
- }
503
- }, [config.table.caption])
504
-
505
- // prettier-ignore
506
- const tableData = useMemo(() => (
507
- config.visualizationType === 'Pie'
508
- ? [config.yAxis.dataKey]
509
- : config.visualizationType === 'Box Plot'
510
- ? Object.entries(config.boxplot.tableData[0])
511
- : config.runtime?.seriesKeys),
512
- [config.runtime?.seriesKeys]) // eslint-disable-line
513
-
514
- if (config.visualizationType !== 'Box Plot') {
515
- const genMapHeader = columns => {
516
- return (
517
- <tr>
518
- {Object.keys(columns)
519
- .filter(column => columns[column].dataTable === true && columns[column].name)
520
- .map((column, index) => {
521
- let text
522
- if (column !== 'geo') {
523
- text = columns[column].label ? columns[column].label : columns[column].name
524
- } else {
525
- text = config.type === 'map' ? indexTitle : config.xAxis?.dataKey
526
- }
527
- if (config.type === 'map' && (text === undefined || text === '')) {
528
- text = 'Location'
529
- }
530
-
531
- return (
532
- <th
533
- key={`col-header-${column}__${index}`}
534
- id={column}
535
- tabIndex='0'
536
- title={text}
537
- role='columnheader'
538
- scope='col'
539
- onClick={() => {
540
- setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
541
- }}
542
- onKeyDown={e => {
543
- if (e.keyCode === 13) {
544
- setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
545
- }
546
- }}
547
- className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
548
- {...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
549
- >
550
- {text}
551
- {sortBy.column === column && <span className={'sort-icon'}>{!sortBy.asc ? upIcon : downIcon}</span>}
552
- <button>
553
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
554
- </button>
555
- </th>
556
- )
557
- })}
558
- </tr>
559
- )
560
- }
561
-
562
- return (
563
- <ErrorBoundary component='DataTable'>
564
- <MediaControls.Section classes={['download-links']}>
565
- <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
566
- {(config.table.download || config.general?.showDownloadButton) && <DownloadButton />}
567
- </MediaControls.Section>
568
- <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
569
- <a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
570
- Skip Navigation or Skip to Content
571
- </a>
572
- <div
573
- className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
574
- onClick={() => {
575
- setExpanded(!expanded)
576
- }}
577
- tabIndex='0'
578
- onKeyDown={e => {
579
- if (e.keyCode === 13) {
580
- setExpanded(!expanded)
581
- }
582
- }}
583
- >
584
- <Icon display={expanded ? 'minus' : 'plus'} base />
585
- {tableTitle}
586
- </div>
587
- <div className='table-container' style={limitHeight}>
588
- <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'}>
589
- <caption className='cdcdataviz-sr-only'>{caption}</caption>
590
- <thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{config.type === 'map' ? genMapHeader(columns) : genChartHeader(columns, runtimeData)}</thead>
591
- <tbody>{config.type === 'map' ? genMapRows(rows) : genChartRows(rows)}</tbody>
592
- </table>
593
-
594
- {/* REGION Data Table */}
595
- {config.regions && config.regions.length > 0 && config.visualizationType !== 'Box Plot' ? (
596
- <table className='region-table data-table'>
597
- <caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
598
- <thead>
599
- <tr>
600
- <th>Region Name</th>
601
- <th>Start Date</th>
602
- <th>End Date</th>
603
- </tr>
604
- </thead>
605
- <tbody>
606
- {config.regions.map((region, index) => {
607
- if (config.visualizationType === 'Box Plot') return false
608
- if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
609
- // region.from and region.to had formatDate(parseDate()) on it
610
- // but they returned undefined - removed both for now (TT)
611
- return (
612
- <tr key={`row-${region.label}--${index}`}>
613
- <td>{region.label}</td>
614
- <td>{region.from}</td>
615
- <td>{region.to}</td>
616
- </tr>
617
- )
618
- })}
619
- </tbody>
620
- </table>
621
- ) : (
622
- ''
623
- )}
624
- </div>
625
- </section>
626
- </ErrorBoundary>
627
- )
628
- } else {
629
- // Render Data Table for Box Plots
630
- const genBoxplotHeader = categories => {
631
- let columns = ['Measures', ...categories]
632
- return (
633
- <tr>
634
- {columns.map(column => {
635
- return (
636
- <th key={`col-header-${column}`} tabIndex='0' title={column} role='columnheader' scope='col'>
637
- {column}
638
- </th>
639
- )
640
- })}
641
- </tr>
642
- )
643
- }
644
- const resolveName = key => {
645
- let {
646
- boxplot: { labels }
647
- } = config
648
- const columnLookup = {
649
- columnMean: labels.mean,
650
- columnMax: labels.maximum,
651
- columnMin: labels.minimum,
652
- columnIqr: labels.iqr,
653
- columnCategory: 'Category',
654
- columnMedian: labels.median,
655
- columnFirstQuartile: labels.q1,
656
- columnThirdQuartile: labels.q3,
657
- columnOutliers: labels.outliers,
658
- values: labels.values,
659
- columnTotal: labels.total,
660
- columnSd: 'Standard Deviation',
661
- nonOutlierValues: 'Non Outliers',
662
- columnLowerBounds: labels.lowerBounds,
663
- columnUpperBounds: labels.upperBounds
664
- }
665
-
666
- let resolvedName = columnLookup[key]
667
-
668
- return resolvedName
669
- }
670
- let resolveCell = (rowid, plot) => {
671
- if (Number(rowid) === 0) return true
672
- if (Number(rowid) === 1) return plot.columnMax
673
- if (Number(rowid) === 2) return plot.columnThirdQuartile
674
- if (Number(rowid) === 3) return plot.columnMedian
675
- if (Number(rowid) === 4) return plot.columnFirstQuartile
676
- if (Number(rowid) === 5) return plot.columnMin
677
- if (Number(rowid) === 6) return plot.columnTotal
678
- if (Number(rowid) === 7) return plot.columnSd
679
- if (Number(rowid) === 8) return plot.columnMean
680
- if (Number(rowid) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
681
- if (Number(rowid) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
682
- return <p>-</p>
683
- }
684
- const genBoxplotRows = rows => {
685
- // get list of data keys for each row
686
- let dataKeys = rows.map(row => {
687
- return row[0]
688
- })
689
- let columns = ['Measures', ...config.boxplot.categories]
690
- const allrows = dataKeys.map((rowkey, index) => {
691
- if (index === 0) return '' // we did header column separately
692
- let rowClass = `row-Box-Plot--${index}`
693
- return (
694
- <tr role='row' key={`tbody__tr-${index}`} className={rowClass}>
695
- {columns.map((column, colnum) => {
696
- let cellValue
697
- if (column === 'Measures') {
698
- let labelValue = index > 0 ? resolveName(rowkey) : ''
699
- cellValue = <>{labelValue}</>
700
- } else {
701
- cellValue = resolveCell(index, config.boxplot.plots[colnum - 1])
702
- }
703
- return (
704
- <td tabIndex='0' key={`tbody__tr__td-${index}`} className='boxplot-td' role='gridcell'>
705
- {cellValue}
706
- </td>
707
- )
708
- })}
709
- </tr>
710
- )
711
- })
712
- return allrows
713
- }
714
- return (
715
- <ErrorBoundary component='DataTable'>
716
- {/* cove media results in error so disabling for now (TT)
717
- <MediaControls.Section classes={['download-links']}>
718
- <MediaControls.Link config={config} />
719
- {config.general.showDownloadButton && <DownloadButton />}
720
- </MediaControls.Section>
721
- */}
722
- <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
723
- <a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
724
- Skip Navigation or Skip to Content
725
- </a>
726
- <div
727
- className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
728
- onClick={() => {
729
- setExpanded(!expanded)
730
- }}
731
- tabIndex='0'
732
- onKeyDown={e => {
733
- if (e.keyCode === 13) {
734
- setExpanded(!expanded)
735
- }
736
- }}
737
- >
738
- <Icon display={expanded ? 'minus' : 'plus'} base />
739
- {tableTitle}
740
- </div>
741
- <div className='table-container' style={limitHeight}>
742
- <table height={expanded ? null : 0} role='table' aria-live='assertive' className={expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'} hidden={!expanded} aria-rowcount={'11'}>
743
- <caption className='cdcdataviz-sr-only'>{caption}</caption>
744
- <thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{genBoxplotHeader(config.boxplot.categories)}</thead>
745
- <tbody>{genBoxplotRows(tableData)}</tbody>
746
- </table>
747
- </div>
748
- </section>
749
- </ErrorBoundary>
750
- )
751
- }
752
- }
753
-
754
- export default DataTable