@cdc/core 4.23.8 → 4.23.10

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.
@@ -26,7 +26,7 @@ export const AdvancedEditor = ({ loadConfig, state, convertStateToConfig }) => {
26
26
  'markup-include': ['Markup Include', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/Markup-Include.html', <MarkupIncludeIcon />]
27
27
  }
28
28
 
29
- if (!state.type) return
29
+ if (!state.type) return <></>
30
30
  return (
31
31
  <>
32
32
  <a href={typeLookup[state.type][1]} target='_blank' rel='noopener noreferrer' className='guidance-link'>
@@ -15,7 +15,7 @@ import Loading from '@cdc/core/components/Loading'
15
15
 
16
16
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
17
17
  const DataTable = props => {
18
- const { config, tableTitle, indexTitle, vizTitle, rawData, runtimeData, headerColor, expandDataTable, columns, displayDataAsText, applyLegendToRow, displayGeoName, navigationHandler, viewport, formatLegendLocation, tabbingId, isDebug } = props
18
+ const { config, dataConfig, tableTitle, indexTitle, vizTitle, rawData, runtimeData, headerColor, colorScale, expandDataTable, columns, displayDataAsText, applyLegendToRow, displayGeoName, navigationHandler, viewport, formatLegendLocation, tabbingId, isDebug } = props
19
19
 
20
20
  /* eslint-disable no-console */
21
21
  if (isDebug) {
@@ -28,83 +28,61 @@ const DataTable = props => {
28
28
 
29
29
  const [expanded, setExpanded] = useState(expandDataTable)
30
30
 
31
- const [sortBy, setSortBy] = useState({ column: config.type === 'map' ? 'geo' : 'date', asc: false })
31
+ const [sortBy, setSortBy] = useState({ column: config.type === 'map' ? 'geo' : 'date', asc: false, colIndex: null })
32
32
 
33
33
  const [accessibilityLabel, setAccessibilityLabel] = useState('')
34
34
 
35
35
  const fileName = `${vizTitle || 'data-table'}.csv`
36
36
 
37
- // Catch all sorting method used on load by default but also on user click
38
- // Having a custom method means we can add in any business logic we want going forward
37
+ const isVertical = !(config.type === 'chart' && !config.table?.showVertical)
38
+
39
39
  const customSort = (a, b) => {
40
- const digitRegex = /\d+/
40
+ let valueA = a
41
+ let valueB = b
41
42
 
42
- const hasNumber = value => digitRegex.test(value)
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
43
46
 
44
- // force null and undefined to the bottom
45
- a = a === null || a === undefined ? '' : a
46
- b = b === null || b === undefined ? '' : b
47
+ const trimmedA = String(valueA).trim()
48
+ const trimmedB = String(valueB).trim()
47
49
 
48
- // convert any strings that are actually numbers to proper data type
49
- const aNum = Number(a)
50
+ if (config.xAxis?.dataKey === sortBy.column && config.xAxis.type === 'date') {
51
+ let dateA = parseDate(config.xAxis.dateParseFormat, trimmedA)
50
52
 
51
- if (!Number.isNaN(aNum)) {
52
- a = aNum
53
- }
53
+ let dateB = parseDate(config.xAxis.dateParseFormat, trimmedB)
54
54
 
55
- const bNum = Number(b)
55
+ if (dateA && dateA.getTime) dateA = dateA.getTime()
56
56
 
57
- if (!Number.isNaN(bNum)) {
58
- b = bNum
59
- }
57
+ if (dateB && dateB.getTime) dateB = dateB.getTime()
60
58
 
61
- // remove iso code prefixes
62
- if (typeof a === 'string') {
63
- a = a.replace('us-', '')
64
- a = displayGeoName(a)
59
+ return !sortBy.asc ? dateA - dateB : dateB - dateA
65
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 !== ''
66
64
 
67
- if (typeof b === 'string') {
68
- b = b.replace('us-', '')
69
- b = displayGeoName(b)
70
- }
65
+ // Handle empty strings or spaces
66
+ if (trimmedA === '' && trimmedB !== '') return !sortBy.asc ? -1 : 1
67
+ if (trimmedA !== '' && trimmedB === '') return !sortBy.asc ? 1 : -1
71
68
 
72
- // force any string values to lowercase
73
- a = typeof a === 'string' ? a.toLowerCase() : a
74
- b = typeof b === 'string' ? b.toLowerCase() : b
75
-
76
- // 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.
77
- if (typeof a === 'string' && hasNumber(a) === true) {
78
- a = a.match(digitRegex)[0]
79
-
80
- a = Number(a)
69
+ // Both are numbers: Compare numerically
70
+ if (isNumA && isNumB) {
71
+ return !sortBy.asc ? Number(valueA) - Number(valueB) : Number(valueB) - Number(valueA)
81
72
  }
82
73
 
83
- if (typeof b === 'string' && hasNumber(b) === true) {
84
- b = b.match(digitRegex)[0]
85
-
86
- b = Number(b)
74
+ // Only A is a number
75
+ if (isNumA) {
76
+ return !sortBy.asc ? -1 : 1
87
77
  }
88
78
 
89
- // When comparing a number to a string, always send string to bottom
90
- if (typeof a === 'number' && typeof b === 'string') {
91
- return 1
79
+ // Only B is a number
80
+ if (isNumB) {
81
+ return !sortBy.asc ? 1 : -1
92
82
  }
93
83
 
94
- if (typeof b === 'number' && typeof a === 'string') {
95
- return -1
96
- }
97
-
98
- // Return either 1 or -1 to indicate a sort priority
99
- if (a > b) {
100
- return 1
101
- }
102
- if (a < b) {
103
- return -1
104
- }
105
- // returning 0, undefined or any falsey value will use subsequent sorts or
106
- // the index as a tiebreaker
107
- return 0
84
+ // Neither are numbers: Compare as strings
85
+ return !sortBy.asc ? trimmedA.localeCompare(trimmedB) : trimmedB.localeCompare(trimmedA)
108
86
  }
109
87
 
110
88
  // Optionally wrap cell with anchor if config defines a navigation url
@@ -146,7 +124,7 @@ const DataTable = props => {
146
124
  if (rawData !== undefined) {
147
125
  let csvData
148
126
  // only use fullGeoName on County maps and no other
149
- if (config.general.geoType === 'us-county') {
127
+ if (config.general?.geoType === 'us-county') {
150
128
  // Unparse + Add column for full Geo name along with State
151
129
  csvData = Papa.unparse(rawData.map(row => ({ FullGeoName: formatLegendLocation(row[config.columns.geo.name]), ...row })))
152
130
  } else {
@@ -201,19 +179,19 @@ const DataTable = props => {
201
179
  break
202
180
  }
203
181
 
204
- const rows = Object.keys(runtimeData).sort((a, b) => {
205
- let sortVal
206
- if (config.type === 'map' && config.columns) {
207
- sortVal = customSort(runtimeData[a][config.columns[sortBy.column].name], runtimeData[b][config.columns[sortBy.column].name])
208
- }
209
- if (config.type === 'chart') {
210
- sortVal = customSort(runtimeData[a][sortBy.column], runtimeData[b][sortBy.column])
211
- }
212
- if (!sortBy.asc) return sortVal
213
- if (sortVal === 0) return 0
214
- if (sortVal < 0) return 1
215
- return -1
216
- })
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
217
195
 
218
196
  const genMapRows = rows => {
219
197
  const allrows = rows.map(row => {
@@ -272,20 +250,24 @@ const DataTable = props => {
272
250
  const dataSeriesColumns = () => {
273
251
  let tmpSeriesColumns
274
252
  if (config.visualizationType !== 'Pie') {
275
- tmpSeriesColumns = [config.xAxis.dataKey] //, ...config.runtime.seriesLabelsAll
276
- config.series.forEach(element => {
277
- tmpSeriesColumns.push(element.dataKey)
278
- })
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
+ }
279
261
  } else {
280
- tmpSeriesColumns = [config.xAxis.dataKey, config.yAxis.dataKey] //Object.keys(runtimeData[0])
262
+ tmpSeriesColumns = [config.xAxis?.dataKey, config.yAxis?.dataKey] //Object.keys(runtimeData[0])
281
263
  }
282
264
 
283
265
  // then add the additional Columns
284
- if (Object.keys(config.columns).length > 0) {
266
+ if (config.columns && Object.keys(config.columns).length > 0) {
285
267
  Object.keys(config.columns).forEach(function (key) {
286
268
  var value = config.columns[key]
287
269
  // add if not the index AND it is enabled to be added to data table
288
- if (value.name !== config.xAxis.dataKey && value.dataTable === true) {
270
+ if (value.name !== config.xAxis?.dataKey && value.dataTable === true) {
289
271
  tmpSeriesColumns.push(value.name)
290
272
  }
291
273
  })
@@ -293,10 +275,22 @@ const DataTable = props => {
293
275
 
294
276
  return tmpSeriesColumns
295
277
  }
296
-
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
+ }
297
291
  const getLabel = name => {
298
292
  let custLabel = ''
299
- if (Object.keys(config.columns).length > 0) {
293
+ if (config.columns && Object.keys(config.columns).length > 0) {
300
294
  Object.keys(config.columns).forEach(function (key) {
301
295
  var tmpColumn = config.columns[key]
302
296
  // add if not the index AND it is enabled to be added to data table
@@ -308,108 +302,180 @@ const DataTable = props => {
308
302
  }
309
303
  }
310
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
+
311
318
  const genChartHeader = (columns, data) => {
312
319
  if (!data) return
313
- return (
314
- <tr>
315
- {dataSeriesColumns().map(column => {
316
- let custLabel = getLabel(column) ? getLabel(column) : column
317
- let text = column === config.xAxis.dataKey ? config.table.indexLabel : custLabel
318
-
319
- // If a user sets the name on a series use that.
320
- let userUpdatedSeriesName = config.series.filter(series => series.dataKey === column)?.[0]?.name
321
- if (userUpdatedSeriesName) text = userUpdatedSeriesName
322
-
323
- return (
324
- <th
325
- key={`col-header-${column}`}
326
- tabIndex='0'
327
- title={text}
328
- role='columnheader'
329
- scope='col'
330
- onClick={() => {
331
- setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
332
- }}
333
- onKeyDown={e => {
334
- if (e.keyCode === 13) {
335
- setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
336
- }
337
- }}
338
- className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
339
- {...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
340
- >
341
- {text}
342
- {sortBy.column === column && <span className={'sort-icon'}>{!sortBy.asc ? upIcon : downIcon}</span>}
343
- <button>
344
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
345
- </button>
346
- </th>
347
- )
348
- })}
349
- </tr>
350
- )
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
+ }
351
390
  }
352
391
 
353
392
  // if its additional column, return formatting params
354
393
  const isAdditionalColumn = column => {
355
394
  let inthere = false
356
395
  let formattingParams = {}
357
- Object.keys(config.columns).forEach(keycol => {
358
- if (config.columns[keycol].name === column) {
359
- inthere = true
360
- formattingParams = {
361
- addColPrefix: config.columns[keycol].prefix,
362
- addColSuffix: config.columns[keycol].suffix,
363
- addColRoundTo: config.columns[keycol].roundToPlace ? config.columns[keycol].roundToPlace : '',
364
- addColCommas: config.columns[keycol].commas
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
+ }
365
406
  }
366
- }
367
- })
407
+ })
408
+ }
368
409
  return formattingParams
369
410
  }
370
411
 
371
- const genChartRows = rows => {
372
- const allRows = rows.map(row => {
373
- return (
374
- <tr role='row'>
375
- {dataSeriesColumns().map(column => {
376
- const rowObj = runtimeData[row]
377
- let cellValue // placeholder for formatting below
378
- let labelValue = rowObj[column] // just raw X axis string
379
- if (column === config.xAxis.dataKey) {
380
- // not the prettiest, but helper functions work nicely here.
381
- cellValue = <>{config.xAxis.type === 'date' ? formatDate(config.xAxis.dateDisplayFormat, parseDate(config.xAxis.dateParseFormat, labelValue)) : labelValue}</>
382
- } else {
383
- let resolvedAxis = 'left'
384
- let leftAxisItems = config.series.filter(item => item?.axis === 'Left')
385
- let rightAxisItems = config.series.filter(item => item?.axis === 'Right')
386
-
387
- leftAxisItems.map(leftSeriesItem => {
388
- if (leftSeriesItem.dataKey === column) resolvedAxis = 'left'
389
- })
390
-
391
- rightAxisItems.map(rightSeriesItem => {
392
- if (rightSeriesItem.dataKey === column) resolvedAxis = 'right'
393
- })
394
-
395
- let addColParams = isAdditionalColumn(column)
396
- if (addColParams) {
397
- cellValue = formatNumber(runtimeData[row][column], resolvedAxis, false, config, addColParams)
398
- } else {
399
- cellValue = formatNumber(runtimeData[row][column], resolvedAxis, false, config)
400
- }
401
- }
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') : []
402
423
 
403
- return (
404
- <td tabIndex='0' role='gridcell' id={`${runtimeData[config.runtime.originalXAxis.dataKey]}--${row}`}>
405
- {cellValue}
406
- </td>
407
- )
408
- })}
409
- </tr>
410
- )
411
- })
412
- return allRows
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
+ }
413
479
  }
414
480
 
415
481
  const upIcon = (
@@ -442,8 +508,8 @@ const DataTable = props => {
442
508
  ? [config.yAxis.dataKey]
443
509
  : config.visualizationType === 'Box Plot'
444
510
  ? Object.entries(config.boxplot.tableData[0])
445
- : config.runtime.seriesKeys),
446
- [config.runtime.seriesKeys]) // eslint-disable-line
511
+ : config.runtime?.seriesKeys),
512
+ [config.runtime?.seriesKeys]) // eslint-disable-line
447
513
 
448
514
  if (config.visualizationType !== 'Box Plot') {
449
515
  const genMapHeader = columns => {
@@ -451,12 +517,12 @@ const DataTable = props => {
451
517
  <tr>
452
518
  {Object.keys(columns)
453
519
  .filter(column => columns[column].dataTable === true && columns[column].name)
454
- .map(column => {
520
+ .map((column, index) => {
455
521
  let text
456
522
  if (column !== 'geo') {
457
523
  text = columns[column].label ? columns[column].label : columns[column].name
458
524
  } else {
459
- text = config.type === 'map' ? indexTitle : config.xAxis.dataKey
525
+ text = config.type === 'map' ? indexTitle : config.xAxis?.dataKey
460
526
  }
461
527
  if (config.type === 'map' && (text === undefined || text === '')) {
462
528
  text = 'Location'
@@ -464,7 +530,7 @@ const DataTable = props => {
464
530
 
465
531
  return (
466
532
  <th
467
- key={`col-header-${column}`}
533
+ key={`col-header-${column}__${index}`}
468
534
  id={column}
469
535
  tabIndex='0'
470
536
  title={text}
@@ -496,8 +562,8 @@ const DataTable = props => {
496
562
  return (
497
563
  <ErrorBoundary component='DataTable'>
498
564
  <MediaControls.Section classes={['download-links']}>
499
- <MediaControls.Link config={config} />
500
- {(config.table.download || config.general.showDownloadButton) && <DownloadButton />}
565
+ <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
566
+ {(config.table.download || config.general?.showDownloadButton) && <DownloadButton />}
501
567
  </MediaControls.Section>
502
568
  <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
503
569
  <a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
@@ -519,7 +585,7 @@ const DataTable = props => {
519
585
  {tableTitle}
520
586
  </div>
521
587
  <div className='table-container' style={limitHeight}>
522
- <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'}>
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'}>
523
589
  <caption className='cdcdataviz-sr-only'>{caption}</caption>
524
590
  <thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{config.type === 'map' ? genMapHeader(columns) : genChartHeader(columns, runtimeData)}</thead>
525
591
  <tbody>{config.type === 'map' ? genMapRows(rows) : genChartRows(rows)}</tbody>
@@ -289,6 +289,7 @@ const Filters = props => {
289
289
  <select
290
290
  id={`filter-${outerIndex}`}
291
291
  name={label}
292
+ aria-label={label}
292
293
  className='filter-select'
293
294
  data-index='0'
294
295
  value={active}
@@ -312,7 +313,7 @@ const Filters = props => {
312
313
  delete filtersToLoop.fromHash
313
314
 
314
315
  return filtersToLoop.map((singleFilter, outerIndex) => {
315
- if(singleFilter.showDropdown === false) return
316
+ if (singleFilter.showDropdown === false) return
316
317
 
317
318
  const values = []
318
319
  const pillValues = []
@@ -382,11 +383,11 @@ const Filters = props => {
382
383
  return (
383
384
  <div className={classList.join(' ')} key={outerIndex}>
384
385
  <>
385
- {label && <label htmlFor={label}>{label}</label>}
386
+ {label && <label htmlFor={`filter-${outerIndex}`}>{label}</label>}
386
387
  {filterStyle === 'tab' && !mobileFilterStyle && <Filters.Tabs tabs={tabValues} />}
387
388
  {filterStyle === 'pill' && !mobileFilterStyle && <Filters.Pills pills={pillValues} />}
388
389
  {filterStyle === 'tab bar' && !mobileFilterStyle && <Filters.TabBar filter={singleFilter} index={outerIndex} />}
389
- {(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown index={outerIndex} label={label} active={active} filters={values} />}
390
+ {(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown filter={singleFilter} index={outerIndex} label={label} active={active} filters={values} />}
390
391
  </>
391
392
  </div>
392
393
  )
@@ -394,7 +395,7 @@ const Filters = props => {
394
395
  }
395
396
  }
396
397
 
397
- if (visualizationConfig?.filters?.length === 0 || props?.filteredData?.length === 0) return
398
+ if (visualizationConfig?.filters?.length === 0) return
398
399
  return (
399
400
  <Filters>
400
401
  <Filters.Section>
@@ -417,7 +418,8 @@ Filters.propTypes = {
417
418
  // exclusions
418
419
  excludedData: PropTypes.array,
419
420
  // function for filtering the data
420
- filterData: PropTypes.func
421
+ filterData: PropTypes.func,
422
+ dimensions: PropTypes.array
421
423
  }
422
424
 
423
425
  export default Filters
@@ -1,14 +1,25 @@
1
- import React, { createContext, useContext, useState } from 'react'
1
+ import React, { ReactElement, createContext, useContext, useState } from 'react'
2
2
 
3
- export const GlobalContext = createContext({})
3
+ type Global = {
4
+ overlay: {
5
+ object: ReactElement,
6
+ show: boolean,
7
+ disableBgClose: boolean,
8
+ actions: {
9
+ openOverlay: (any, boolean?) => void,
10
+ toggleOverlay: (boolean?) => void
11
+ }
12
+ }
13
+ }
14
+ export const GlobalContext = createContext<Global>({} as Global)
4
15
  export const useGlobalContext = () => useContext(GlobalContext)
5
16
 
6
17
  export const GlobalContextProvider = ({ children }) => {
7
- const [globalContextData, setGlobalContextData] = useState({})
18
+ const [globalContextData, setGlobalContextData] = useState<Global>({} as Global)
8
19
 
9
20
  const openOverlay = (obj, disableBgClose = false) => {
10
21
  let payload = { object: obj, show: true, disableBgClose: disableBgClose }
11
- setGlobalContextData(context => ({ ...context, overlay: { ...payload } }))
22
+ setGlobalContextData(context => ({ ...context, overlay: { ...payload } } as Global))
12
23
  }
13
24
 
14
25
  const toggleOverlay = (display = false) => {
@@ -21,7 +32,7 @@ export const GlobalContextProvider = ({ children }) => {
21
32
  }))
22
33
  }
23
34
 
24
- const globalSettings = {
35
+ const globalSettings: Global = {
25
36
  overlay: {
26
37
  object: globalContextData.overlay?.object || null,
27
38
  show: globalContextData.overlay?.show || false,