@cdc/core 4.23.9 → 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,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 = [config.xAxis?.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,180 @@ 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
+ 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
+ }
376
390
  }
377
391
 
378
392
  // if its additional column, return formatting params
379
393
  const isAdditionalColumn = column => {
380
394
  let inthere = false
381
395
  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
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
+ }
390
406
  }
391
- }
392
- })
407
+ })
408
+ }
393
409
  return formattingParams
394
410
  }
395
411
 
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
- }
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') : []
427
423
 
428
- return (
429
- <td tabIndex='0' role='gridcell' id={`${runtimeData[config.runtime.originalXAxis.dataKey]}--${row}`}>
430
- {cellValue}
431
- </td>
432
- )
433
- })}
434
- </tr>
435
- )
436
- })
437
- 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
+ }
438
479
  }
439
480
 
440
481
  const upIcon = (
@@ -467,8 +508,8 @@ const DataTable = props => {
467
508
  ? [config.yAxis.dataKey]
468
509
  : config.visualizationType === 'Box Plot'
469
510
  ? Object.entries(config.boxplot.tableData[0])
470
- : config.runtime.seriesKeys),
471
- [config.runtime.seriesKeys]) // eslint-disable-line
511
+ : config.runtime?.seriesKeys),
512
+ [config.runtime?.seriesKeys]) // eslint-disable-line
472
513
 
473
514
  if (config.visualizationType !== 'Box Plot') {
474
515
  const genMapHeader = columns => {
@@ -476,12 +517,12 @@ const DataTable = props => {
476
517
  <tr>
477
518
  {Object.keys(columns)
478
519
  .filter(column => columns[column].dataTable === true && columns[column].name)
479
- .map(column => {
520
+ .map((column, index) => {
480
521
  let text
481
522
  if (column !== 'geo') {
482
523
  text = columns[column].label ? columns[column].label : columns[column].name
483
524
  } else {
484
- text = config.type === 'map' ? indexTitle : config.xAxis.dataKey
525
+ text = config.type === 'map' ? indexTitle : config.xAxis?.dataKey
485
526
  }
486
527
  if (config.type === 'map' && (text === undefined || text === '')) {
487
528
  text = 'Location'
@@ -489,7 +530,7 @@ const DataTable = props => {
489
530
 
490
531
  return (
491
532
  <th
492
- key={`col-header-${column}`}
533
+ key={`col-header-${column}__${index}`}
493
534
  id={column}
494
535
  tabIndex='0'
495
536
  title={text}
@@ -521,8 +562,8 @@ const DataTable = props => {
521
562
  return (
522
563
  <ErrorBoundary component='DataTable'>
523
564
  <MediaControls.Section classes={['download-links']}>
524
- <MediaControls.Link config={config} />
525
- {(config.table.download || config.general.showDownloadButton) && <DownloadButton />}
565
+ <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
566
+ {(config.table.download || config.general?.showDownloadButton) && <DownloadButton />}
526
567
  </MediaControls.Section>
527
568
  <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
528
569
  <a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
@@ -544,7 +585,7 @@ const DataTable = props => {
544
585
  {tableTitle}
545
586
  </div>
546
587
  <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'}>
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'}>
548
589
  <caption className='cdcdataviz-sr-only'>{caption}</caption>
549
590
  <thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>{config.type === 'map' ? genMapHeader(columns) : genChartHeader(columns, runtimeData)}</thead>
550
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