@cdc/core 4.23.9 → 4.23.10-alpha

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.
@@ -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,107 +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
39
- const customSort = (a, b) => {
40
- const isDateA = Date.parse(a)
41
- const isDateB = Date.parse(b)
42
-
43
- const isNumberA = !isNaN(a)
44
- const isNumberB = !isNaN(b)
45
-
46
- if (isDateA && isDateB) {
47
- return sortBy.asc ? new Date(a) - new Date(b) : new Date(b) - new Date(a)
48
- }
49
- if (isNumberA && isNumberB) {
50
- return sortBy.asc ? Number(a) - Number(b) : Number(b) - Number(a)
51
- }
52
- if (typeof a === 'string' && typeof b === 'string') {
53
- return sortBy.asc ? a.localeCompare(b) : b.localeCompare(a)
54
- }
55
-
56
- return 0
57
- }
58
- const customSortX = (a, b) => {
59
- const digitRegex = /\d+/
60
-
61
- const hasNumber = value => digitRegex.test(value)
37
+ const isVertical = !(config.type === 'chart' && !config.table?.showVertical)
62
38
 
63
- // force null and undefined to the bottom
64
- a = a === null || a === undefined ? '' : a
65
- b = b === null || b === undefined ? '' : b
66
-
67
- // check for dates first
68
- if (!isNaN(Date.parse(a)) && !isNaN(Date.parse(b))) {
69
- return Date.parse(a) - Date.parse(b)
70
- }
71
-
72
- // convert any strings that are actually numbers to proper data type
73
- const aNum = Number(a)
74
-
75
- if (!Number.isNaN(aNum)) {
76
- a = aNum
77
- }
39
+ const customSort = (a, b) => {
40
+ let valueA = a
41
+ let valueB = b
78
42
 
79
- const bNum = Number(b)
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
80
46
 
81
- if (!Number.isNaN(bNum)) {
82
- b = bNum
83
- }
47
+ const trimmedA = String(valueA).trim()
48
+ const trimmedB = String(valueB).trim()
84
49
 
85
- // remove iso code prefixes
86
- if (typeof a === 'string') {
87
- a = a.replace('us-', '')
88
- a = displayGeoName(a)
89
- }
50
+ if (config.xAxis?.dataKey === sortBy.column && config.xAxis.type === 'date') {
51
+ let dateA = parseDate(config.xAxis.dateParseFormat, trimmedA)
90
52
 
91
- if (typeof b === 'string') {
92
- b = b.replace('us-', '')
93
- b = displayGeoName(b)
94
- }
53
+ let dateB = parseDate(config.xAxis.dateParseFormat, trimmedB)
95
54
 
96
- // force any string values to lowercase
97
- a = typeof a === 'string' ? a.toLowerCase() : a
98
- b = typeof b === 'string' ? b.toLowerCase() : b
55
+ if (dateA && dateA.getTime) dateA = dateA.getTime()
99
56
 
100
- // 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.
101
- if (typeof a === 'string' && hasNumber(a) === true) {
102
- a = a.match(digitRegex)[0]
57
+ if (dateB && dateB.getTime) dateB = dateB.getTime()
103
58
 
104
- a = Number(a)
59
+ return !sortBy.asc ? dateA - dateB : dateB - dateA
105
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 !== ''
106
64
 
107
- if (typeof b === 'string' && hasNumber(b) === true) {
108
- b = b.match(digitRegex)[0]
65
+ // Handle empty strings or spaces
66
+ if (trimmedA === '' && trimmedB !== '') return !sortBy.asc ? -1 : 1
67
+ if (trimmedA !== '' && trimmedB === '') return !sortBy.asc ? 1 : -1
109
68
 
110
- b = Number(b)
69
+ // Both are numbers: Compare numerically
70
+ if (isNumA && isNumB) {
71
+ return !sortBy.asc ? Number(valueA) - Number(valueB) : Number(valueB) - Number(valueA)
111
72
  }
112
73
 
113
- // When comparing a number to a string, always send string to bottom
114
- if (typeof a === 'number' && typeof b === 'string') {
115
- return 1
74
+ // Only A is a number
75
+ if (isNumA) {
76
+ return !sortBy.asc ? -1 : 1
116
77
  }
117
78
 
118
- if (typeof b === 'number' && typeof a === 'string') {
119
- return -1
79
+ // Only B is a number
80
+ if (isNumB) {
81
+ return !sortBy.asc ? 1 : -1
120
82
  }
121
83
 
122
- // Return either 1 or -1 to indicate a sort priority
123
- if (a > b) {
124
- return 1
125
- }
126
- if (a < b) {
127
- return -1
128
- }
129
- // returning 0, undefined or any falsey value will use subsequent sorts or
130
- // the index as a tiebreaker
131
- return 0
84
+ // Neither are numbers: Compare as strings
85
+ return !sortBy.asc ? trimmedA.localeCompare(trimmedB) : trimmedB.localeCompare(trimmedA)
132
86
  }
133
87
 
134
88
  // Optionally wrap cell with anchor if config defines a navigation url
@@ -170,7 +124,7 @@ const DataTable = props => {
170
124
  if (rawData !== undefined) {
171
125
  let csvData
172
126
  // only use fullGeoName on County maps and no other
173
- if (config.general.geoType === 'us-county') {
127
+ if (config.general?.geoType === 'us-county') {
174
128
  // Unparse + Add column for full Geo name along with State
175
129
  csvData = Papa.unparse(rawData.map(row => ({ FullGeoName: formatLegendLocation(row[config.columns.geo.name]), ...row })))
176
130
  } else {
@@ -225,20 +179,19 @@ const DataTable = props => {
225
179
  break
226
180
  }
227
181
 
228
- const rows = Object.keys(runtimeData).sort((a, b) => {
229
- let sortVal = 0
230
- if (config.type === 'map' && config.columns) {
231
- sortVal = customSort(runtimeData[a][config.columns[sortBy.column].name], runtimeData[b][config.columns[sortBy.column].name])
232
- }
233
- if (config.type === 'chart') {
234
- sortVal = customSort(runtimeData[a][sortBy.column], runtimeData[b][sortBy.column])
235
- }
236
- return sortVal
237
- // if (!sortBy.asc) return sortVal
238
- // if (sortVal === 0) return 0
239
- // if (sortVal < 0) return 1
240
- // return -1
241
- })
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
242
195
 
243
196
  const genMapRows = rows => {
244
197
  const allrows = rows.map(row => {
@@ -297,20 +250,24 @@ const DataTable = props => {
297
250
  const dataSeriesColumns = () => {
298
251
  let tmpSeriesColumns
299
252
  if (config.visualizationType !== 'Pie') {
300
- tmpSeriesColumns = [config.xAxis.dataKey] //, ...config.runtime.seriesLabelsAll
301
- config.series.forEach(element => {
302
- tmpSeriesColumns.push(element.dataKey)
303
- })
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
+ }
304
261
  } else {
305
- tmpSeriesColumns = [config.xAxis.dataKey, config.yAxis.dataKey] //Object.keys(runtimeData[0])
262
+ tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey, config.yAxis?.dataKey] : [config.yAxis?.dataKey] //Object.keys(runtimeData[0])
306
263
  }
307
264
 
308
265
  // then add the additional Columns
309
- if (Object.keys(config.columns).length > 0) {
266
+ if (config.columns && Object.keys(config.columns).length > 0) {
310
267
  Object.keys(config.columns).forEach(function (key) {
311
268
  var value = config.columns[key]
312
269
  // add if not the index AND it is enabled to be added to data table
313
- if (value.name !== config.xAxis.dataKey && value.dataTable === true) {
270
+ if (value.name !== config.xAxis?.dataKey && value.dataTable === true) {
314
271
  tmpSeriesColumns.push(value.name)
315
272
  }
316
273
  })
@@ -318,10 +275,22 @@ const DataTable = props => {
318
275
 
319
276
  return tmpSeriesColumns
320
277
  }
321
-
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
+ }
322
291
  const getLabel = name => {
323
292
  let custLabel = ''
324
- if (Object.keys(config.columns).length > 0) {
293
+ if (config.columns && Object.keys(config.columns).length > 0) {
325
294
  Object.keys(config.columns).forEach(function (key) {
326
295
  var tmpColumn = config.columns[key]
327
296
  // add if not the index AND it is enabled to be added to data table
@@ -333,108 +302,184 @@ const DataTable = props => {
333
302
  }
334
303
  }
335
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
+
336
318
  const genChartHeader = (columns, data) => {
337
319
  if (!data) return
338
- return (
339
- <tr>
340
- {dataSeriesColumns().map(column => {
341
- let custLabel = getLabel(column) ? getLabel(column) : column
342
- let text = column === config.xAxis.dataKey ? config.table.indexLabel : custLabel
343
-
344
- // If a user sets the name on a series use that.
345
- let userUpdatedSeriesName = config.series.filter(series => series.dataKey === column)?.[0]?.name
346
- if (userUpdatedSeriesName) text = userUpdatedSeriesName
347
-
348
- return (
349
- <th
350
- key={`col-header-${column}`}
351
- tabIndex='0'
352
- title={text}
353
- role='columnheader'
354
- scope='col'
355
- onClick={() => {
356
- setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
357
- }}
358
- onKeyDown={e => {
359
- if (e.keyCode === 13) {
360
- setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
361
- }
362
- }}
363
- className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
364
- {...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
365
- >
366
- {text}
367
- {sortBy.column === column && <span className={'sort-icon'}>{!sortBy.asc ? upIcon : downIcon}</span>}
368
- <button>
369
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
370
- </button>
371
- </th>
372
- )
373
- })}
374
- </tr>
375
- )
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
+ }
376
391
  }
377
392
 
378
393
  // if its additional column, return formatting params
379
394
  const isAdditionalColumn = column => {
380
395
  let inthere = false
381
396
  let formattingParams = {}
382
- Object.keys(config.columns).forEach(keycol => {
383
- if (config.columns[keycol].name === column) {
384
- inthere = true
385
- formattingParams = {
386
- addColPrefix: config.columns[keycol].prefix,
387
- addColSuffix: config.columns[keycol].suffix,
388
- addColRoundTo: config.columns[keycol].roundToPlace ? config.columns[keycol].roundToPlace : '',
389
- addColCommas: config.columns[keycol].commas
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
+ }
390
407
  }
391
- }
392
- })
408
+ })
409
+ }
393
410
  return formattingParams
394
411
  }
395
412
 
396
- const genChartRows = rows => {
397
- const allRows = rows.map(row => {
398
- return (
399
- <tr role='row'>
400
- {dataSeriesColumns().map(column => {
401
- const rowObj = runtimeData[row]
402
- let cellValue // placeholder for formatting below
403
- let labelValue = rowObj[column] // just raw X axis string
404
- if (column === config.xAxis.dataKey) {
405
- // not the prettiest, but helper functions work nicely here.
406
- cellValue = <>{config.xAxis.type === 'date' ? formatDate(config.xAxis.dateDisplayFormat, parseDate(config.xAxis.dateParseFormat, labelValue)) : labelValue}</>
407
- } else {
408
- let resolvedAxis = 'left'
409
- let leftAxisItems = config.series.filter(item => item?.axis === 'Left')
410
- let rightAxisItems = config.series.filter(item => item?.axis === 'Right')
411
-
412
- leftAxisItems.map(leftSeriesItem => {
413
- if (leftSeriesItem.dataKey === column) resolvedAxis = 'left'
414
- })
415
-
416
- rightAxisItems.map(rightSeriesItem => {
417
- if (rightSeriesItem.dataKey === column) resolvedAxis = 'right'
418
- })
419
-
420
- let addColParams = isAdditionalColumn(column)
421
- if (addColParams) {
422
- cellValue = formatNumber(runtimeData[row][column], resolvedAxis, false, config, addColParams)
423
- } else {
424
- cellValue = formatNumber(runtimeData[row][column], resolvedAxis, false, config)
425
- }
426
- }
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') : []
427
424
 
428
- return (
429
- <td tabIndex='0' role='gridcell' id={`${runtimeData[config.runtime.originalXAxis.dataKey]}--${row}`}>
430
- {cellValue}
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)}
431
472
  </td>
432
- )
433
- })}
434
- </tr>
435
- )
436
- })
437
- return allRows
473
+ )}
474
+
475
+ {rows.map(row => {
476
+ return getChartCell(row, column)
477
+ })}
478
+ </tr>
479
+ )
480
+ })
481
+ return allRows
482
+ }
438
483
  }
439
484
 
440
485
  const upIcon = (
@@ -450,7 +495,8 @@ const DataTable = props => {
450
495
 
451
496
  const limitHeight = {
452
497
  maxHeight: config.table.limitHeight && `${config.table.height}px`,
453
- overflowY: 'scroll'
498
+ overflowY: 'scroll',
499
+ marginBottom: '33px'
454
500
  }
455
501
 
456
502
  const caption = useMemo(() => {
@@ -467,8 +513,8 @@ const DataTable = props => {
467
513
  ? [config.yAxis.dataKey]
468
514
  : config.visualizationType === 'Box Plot'
469
515
  ? Object.entries(config.boxplot.tableData[0])
470
- : config.runtime.seriesKeys),
471
- [config.runtime.seriesKeys]) // eslint-disable-line
516
+ : config.runtime?.seriesKeys),
517
+ [config.runtime?.seriesKeys]) // eslint-disable-line
472
518
 
473
519
  if (config.visualizationType !== 'Box Plot') {
474
520
  const genMapHeader = columns => {
@@ -476,12 +522,12 @@ const DataTable = props => {
476
522
  <tr>
477
523
  {Object.keys(columns)
478
524
  .filter(column => columns[column].dataTable === true && columns[column].name)
479
- .map(column => {
525
+ .map((column, index) => {
480
526
  let text
481
527
  if (column !== 'geo') {
482
528
  text = columns[column].label ? columns[column].label : columns[column].name
483
529
  } else {
484
- text = config.type === 'map' ? indexTitle : config.xAxis.dataKey
530
+ text = config.type === 'map' ? indexTitle : config.xAxis?.dataKey
485
531
  }
486
532
  if (config.type === 'map' && (text === undefined || text === '')) {
487
533
  text = 'Location'
@@ -489,7 +535,7 @@ const DataTable = props => {
489
535
 
490
536
  return (
491
537
  <th
492
- key={`col-header-${column}`}
538
+ key={`col-header-${column}__${index}`}
493
539
  id={column}
494
540
  tabIndex='0'
495
541
  title={text}
@@ -521,8 +567,8 @@ const DataTable = props => {
521
567
  return (
522
568
  <ErrorBoundary component='DataTable'>
523
569
  <MediaControls.Section classes={['download-links']}>
524
- <MediaControls.Link config={config} />
525
- {(config.table.download || config.general.showDownloadButton) && <DownloadButton />}
570
+ <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
571
+ {(config.table.download || config.general?.showDownloadButton) && <DownloadButton />}
526
572
  </MediaControls.Section>
527
573
  <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
528
574
  <a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
@@ -544,7 +590,7 @@ const DataTable = props => {
544
590
  {tableTitle}
545
591
  </div>
546
592
  <div className='table-container' style={limitHeight}>
547
- <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'}>
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'}>
548
594
  <caption className='cdcdataviz-sr-only'>{caption}</caption>
549
595
  <thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{config.type === 'map' ? genMapHeader(columns) : genChartHeader(columns, runtimeData)}</thead>
550
596
  <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