@cdc/chart 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.
Files changed (31) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +44099 -44436
  3. package/examples/feature/__data__/area-chart-date-apple.json +1 -5073
  4. package/examples/feature/area/area-chart-date-apple.json +73 -10316
  5. package/examples/feature/area/area-chart-date-city-temperature.json +204 -80
  6. package/examples/feature/area/area-chart-stacked.json +239 -0
  7. package/examples/feature/filters/bar-filter.json +5027 -0
  8. package/examples/feature/legend-highlights/highlights.json +567 -0
  9. package/index.html +9 -6
  10. package/package.json +3 -2
  11. package/src/{CdcChart.jsx → CdcChart.tsx} +77 -71
  12. package/src/components/AreaChart.Stacked.jsx +73 -0
  13. package/src/components/AreaChart.jsx +24 -26
  14. package/src/components/DeviationBar.jsx +67 -13
  15. package/src/components/EditorPanel.jsx +483 -452
  16. package/src/components/Forecasting.jsx +5 -5
  17. package/src/components/Legend.jsx +6 -5
  18. package/src/components/LineChart.Circle.tsx +108 -0
  19. package/src/components/{LineChart.jsx → LineChart.tsx} +10 -42
  20. package/src/components/LinearChart.jsx +460 -443
  21. package/src/components/PieChart.jsx +54 -25
  22. package/src/components/Series.jsx +63 -17
  23. package/src/components/SparkLine.jsx +7 -19
  24. package/src/data/initial-state.js +6 -0
  25. package/src/hooks/useEditorPermissions.js +87 -24
  26. package/src/hooks/useReduceData.js +5 -0
  27. package/src/hooks/useScales.js +1 -1
  28. package/src/hooks/useTooltip.jsx +19 -6
  29. package/src/scss/main.scss +6 -12
  30. package/src/components/DataTable.jsx +0 -494
  31. /package/src/{components → hooks}/useIntersectionObserver.jsx +0 -0
@@ -1,5 +1,6 @@
1
1
  import { useContext } from 'react'
2
2
  import ConfigContext from '../ConfigContext'
3
+ import { defaultStyles } from '@visx/tooltip'
3
4
 
4
5
  // third party
5
6
  import { localPoint } from '@visx/event'
@@ -30,6 +31,8 @@ export const useTooltip = props => {
30
31
  }
31
32
 
32
33
  const tooltipInformation = {
34
+ tooltipLeft: tooltipData.dataXPosition,
35
+ tooltipTop: tooltipData.dataYPosition,
33
36
  tooltipData: tooltipData
34
37
  }
35
38
 
@@ -50,11 +53,22 @@ export const useTooltip = props => {
50
53
  // Additional data for pie charts
51
54
  const { data: pieChartData, arc } = additionalChartData
52
55
 
53
- const closestXScaleValue = getXValueFromCoordinate(x)
56
+ const closestXScaleValue = getXValueFromCoordinate(x - Number(config.yAxis.size || 0))
54
57
 
55
58
  const includedSeries = visualizationType !== 'Pie' ? config.series.filter(series => series.tooltip === true).map(item => item.dataKey) : config.series.map(item => item.dataKey)
56
59
  includedSeries.push(config.xAxis.dataKey)
57
60
 
61
+ if (config.visualizationType === 'Forecasting') {
62
+ config.series.map(s => {
63
+ s.confidenceIntervals.map(c => {
64
+ if (c.showInTooltip) {
65
+ includedSeries.push(c.high)
66
+ includedSeries.push(c.low)
67
+ }
68
+ })
69
+ })
70
+ }
71
+
58
72
  const yScaleValues = getYScaleValues(closestXScaleValue, includedSeries)
59
73
 
60
74
  const xScaleValues = data.filter(d => d[xAxis.dataKey] === getClosestYValue(y))
@@ -103,14 +117,14 @@ export const useTooltip = props => {
103
117
  if (visualizationType === 'Pie') {
104
118
  return [
105
119
  [config.xAxis.dataKey, pieChartData],
106
- [config.runtime.yAxis.dataKey, formatNumber(arc.data[config.runtime.yAxis.dataKey])],
107
- ['Percent', `${Math.round((((arc.endAngle - arc.startAngle) * 180) / Math.PI / 360) * 100) + '%'}`]
120
+ [config.runtime.yAxis.dataKey, formatNumber(arc?.data[config.runtime.yAxis.dataKey])],
121
+ ['Percent', `${Math.round((((arc?.endAngle - arc?.startAngle) * 180) / Math.PI / 360) * 100) + '%'}`]
108
122
  ]
109
123
  }
110
124
 
111
125
  return getIncludedTooltipSeries()
112
126
  .filter(Boolean)
113
- .flatMap(seriesKey => {
127
+ .flatMap((seriesKey, index) => {
114
128
  return resolvedScaleValues[0][seriesKey] ? [[seriesKey, resolvedScaleValues[0][seriesKey], getAxisPosition(seriesKey)]] : []
115
129
  })
116
130
  }
@@ -131,7 +145,6 @@ export const useTooltip = props => {
131
145
  */
132
146
  const handleTooltipMouseOff = () => {
133
147
  if (config.visualizationType === 'Area Chart') {
134
- console.log('HERE IN OFF')
135
148
  setTimeout(() => {
136
149
  hideTooltip()
137
150
  }, 3000)
@@ -174,7 +187,7 @@ export const useTooltip = props => {
174
187
  // Find the closest x value by calculating the minimum distance
175
188
  let closestX = null
176
189
  let minDistance = Number.MAX_VALUE
177
- let offset = x - yAxis.size
190
+ let offset = x
178
191
 
179
192
  data.forEach(d => {
180
193
  const xPosition = xAxis.type === 'date' ? xScale(parseDate(d[xAxis.dataKey])) : xScale(d[xAxis.dataKey])
@@ -180,6 +180,12 @@
180
180
  align-items: flex-start !important;
181
181
  user-select: none;
182
182
  white-space: nowrap;
183
+
184
+ .visx-legend-label {
185
+ word-wrap: break-word;
186
+ white-space: pre-wrap;
187
+ word-break: break-word;
188
+ }
183
189
  }
184
190
 
185
191
  .vertical-sorted:not(.single-row) .legend-item {
@@ -676,14 +682,6 @@
676
682
  padding: 1em;
677
683
  }
678
684
 
679
- .cove-component__content:not(.no-borders) {
680
- border: 1px solid $lightGray;
681
- }
682
-
683
- .cove-component__header ~ .cove-component__content:not(.no-borders) {
684
- border-top: none !important;
685
- }
686
-
687
685
  .subtext {
688
686
  margin-top: 0px;
689
687
  }
@@ -691,10 +689,6 @@
691
689
  .isEditor {
692
690
  position: relative;
693
691
  }
694
-
695
- .subtext {
696
- margin-bottom: 15px;
697
- }
698
692
  }
699
693
 
700
694
  .cdc-open-viz-module .cove-component__content.sparkline {
@@ -1,494 +0,0 @@
1
- import React, { useContext, useEffect, useState, useMemo } from 'react'
2
- import { useTable, useSortBy, useResizeColumns, useBlockLayout } from 'react-table'
3
- import Papa from 'papaparse'
4
- import { Base64 } from 'js-base64'
5
- import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
6
-
7
- import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
8
- import LegendCircle from '@cdc/core/components/LegendCircle'
9
- import Icon from '@cdc/core/components/ui/Icon'
10
- import { DataTransform } from '@cdc/core/helpers/DataTransform'
11
-
12
- import ConfigContext from '../ConfigContext'
13
-
14
- import MediaControls from '@cdc/core/components/MediaControls'
15
-
16
- const DataTable = props => {
17
- // had to pass in runtimeData as prop to get the raw prop names in the inbound data (TT)
18
- const { runtimeData, isDebug } = props
19
-
20
- const { rawData, tableData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes, currentViewport } = useContext(ConfigContext)
21
-
22
- const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
23
- const [tableExpanded, setTableExpanded] = useState(config.table.expanded)
24
- const [accessibilityLabel, setAccessibilityLabel] = useState('')
25
- const isLegendBottom = ['sm', 'xs', 'xxs'].includes(currentViewport)
26
- const transform = new DataTransform()
27
-
28
- const DownloadButton = ({ data }, type) => {
29
- const fileName = `${config.title.substring(0, 50)}.csv`
30
-
31
- const csvData = Papa.unparse(data)
32
-
33
- const saveBlob = () => {
34
- //@ts-ignore
35
- if (typeof window.navigator.msSaveBlob === 'function') {
36
- const dataBlob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
37
- //@ts-ignore
38
- window.navigator.msSaveBlob(dataBlob, fileName)
39
- }
40
- }
41
-
42
- // - trying to eliminate console error that occurs if formatted with prettier
43
- // prettier-ignore
44
- switch (type) {
45
- case 'download':
46
- return (<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border margin-sm`}>Download Data (CSV)</a>)
47
- default:
48
- return (<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`no-border`}>Download Data (CSV)</a>)
49
- }
50
- }
51
-
52
- // Creates columns structure for the table
53
- const tableColumns = useMemo(() => {
54
- const newTableColumns =
55
- config.visualizationType === 'Pie'
56
- ? []
57
- : config.visualizationType === 'Box Plot'
58
- ? [
59
- {
60
- Header: 'Measures',
61
- Cell: props => {
62
- const resolveName = () => {
63
- let {
64
- boxplot: { labels }
65
- } = config
66
- const columnLookup = {
67
- columnMean: labels.mean,
68
- columnMax: labels.maximum,
69
- columnMin: labels.minimum,
70
- columnIqr: labels.iqr,
71
- columnCategory: 'Category',
72
- columnMedian: labels.median,
73
- columnFirstQuartile: labels.q1,
74
- columnThirdQuartile: labels.q3,
75
- columnOutliers: labels.outliers,
76
- values: labels.values,
77
- columnTotal: labels.total,
78
- columnSd: 'Standard Deviation',
79
- nonOutlierValues: 'Non Outliers',
80
- columnLowerBounds: labels.lowerBounds,
81
- columnUpperBounds: labels.upperBounds
82
- }
83
-
84
- let resolvedName = columnLookup[props.row.original[0]]
85
-
86
- return resolvedName
87
- }
88
-
89
- return resolveName()
90
- }
91
- }
92
- ]
93
- : [
94
- {
95
- Header: ' ',
96
- Cell: ({ row }) => {
97
- const getSeriesLabel = () => {
98
- let userUpdatedSeriesName = config.series.filter(series => series.dataKey === row.original)?.[0]?.name
99
-
100
- if (userUpdatedSeriesName) return userUpdatedSeriesName
101
- if (config.runtimeSeriesLabels) return config.runtime.seriesLabels[row.original]
102
- return row.original
103
- }
104
- return (
105
- <>
106
- {config.visualizationType !== 'Pie' && (
107
- <LegendCircle
108
- fill={
109
- // non-dynamic legend
110
- !config.legend.dynamicLegend && config.visualizationType !== 'Forecasting'
111
- ? colorScale(getSeriesLabel())
112
- : config.legend.dynamicLegend
113
- ? colorPalettes[config.palette][row.index]
114
- : // fallback
115
- '#000'
116
- }
117
- />
118
- )}
119
- <span>{getSeriesLabel()}</span>
120
- </>
121
- )
122
- },
123
- id: 'series-label',
124
- sortType: 'custom',
125
- canSort: true
126
- }
127
- ]
128
- if (config.visualizationType !== 'Box Plot') {
129
- data.forEach((d, index) => {
130
- const resolveTableHeader = () => {
131
- if (config.runtime[section].type === 'date') return formatDate(parseDate(d[config.runtime.originalXAxis.dataKey]))
132
- if (config.runtime[section].type === 'continuous') return numberFormatter(d[config.runtime.originalXAxis.dataKey], 'bottom')
133
- return d[config.runtime.originalXAxis.dataKey]
134
- }
135
- const newCol = {
136
- Header: resolveTableHeader(),
137
- Cell: ({ row }) => {
138
- let leftAxisItems = config.series.filter(item => item?.axis === 'Left')
139
- let rightAxisItems = config.series.filter(item => item?.axis === 'Right')
140
- let resolvedAxis = ''
141
-
142
- leftAxisItems.map(leftSeriesItem => {
143
- if (leftSeriesItem.dataKey === row.original) resolvedAxis = 'left'
144
- })
145
-
146
- rightAxisItems.map(rightSeriesItem => {
147
- if (rightSeriesItem.dataKey === row.original) resolvedAxis = 'right'
148
- })
149
-
150
- if (config.visualizationType !== 'Combo') resolvedAxis = 'left'
151
-
152
- return <>{numberFormatter(d[row.original], resolvedAxis)}</>
153
- },
154
- id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
155
- sortType: 'custom',
156
- canSort: true,
157
- defaultCanSort: true
158
- }
159
-
160
- newTableColumns.push(newCol)
161
- })
162
- }
163
-
164
- if (config.visualizationType === 'Box Plot') {
165
- config.boxplot.tableData.map((plot, index) => {
166
- const newCol = {
167
- Header: plot.columnCategory,
168
- Cell: props => {
169
- let resolveCell = () => {
170
- if (Number(props.row.id) === 0) return true
171
- if (Number(props.row.id) === 1) return plot.columnMax
172
- if (Number(props.row.id) === 2) return plot.columnThirdQuartile
173
- if (Number(props.row.id) === 3) return plot.columnMedian
174
- if (Number(props.row.id) === 4) return plot.columnFirstQuartile
175
- if (Number(props.row.id) === 5) return plot.columnMin
176
- if (Number(props.row.id) === 6) return plot.columnTotal
177
- if (Number(props.row.id) === 7) return plot.columnSd
178
- if (Number(props.row.id) === 8) return plot.columnMean
179
- if (Number(props.row.id) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
180
- if (Number(props.row.id) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
181
- return <p>-</p>
182
- }
183
- return resolveCell()
184
- },
185
- id: `${index}`,
186
- canSort: false
187
- }
188
-
189
- return newTableColumns.push(newCol)
190
- })
191
- }
192
-
193
- return newTableColumns
194
- }, [config, colorScale]) // eslint-disable-line
195
-
196
- // prettier-ignore
197
- const tableData = useMemo(() => (
198
- config.visualizationType === 'Pie'
199
- ? [config.yAxis.dataKey]
200
- : config.visualizationType === 'Box Plot'
201
- ? Object.entries(config.boxplot.tableData[0])
202
- : config.runtime.seriesKeys),
203
- [config.runtime.seriesKeys]) // eslint-disable-line
204
-
205
- // Change accessibility label depending on expanded status
206
- useEffect(() => {
207
- const expandedLabel = 'Accessible data table.'
208
- const collapsedLabel = 'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.'
209
-
210
- if (tableExpanded === true && accessibilityLabel !== expandedLabel) {
211
- setAccessibilityLabel(expandedLabel)
212
- }
213
-
214
- if (tableExpanded === false && accessibilityLabel !== collapsedLabel) {
215
- setAccessibilityLabel(collapsedLabel)
216
- }
217
- // eslint-disable-next-line react-hooks/exhaustive-deps
218
- }, [tableExpanded])
219
-
220
- const defaultColumn = useMemo(
221
- () => ({
222
- minWidth: 150,
223
- width: 200,
224
- maxWidth: 400
225
- }),
226
- []
227
- )
228
- const upIcon = (
229
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
230
- <path d='M0 5l5-5 5 5z' />
231
- </svg>
232
- )
233
- const downIcon = (
234
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
235
- <path d='M0 0l5 5 5-5z' />
236
- </svg>
237
- )
238
- const getSpecificCellData = (array, value) => {
239
- return array.filter(data => JSON.stringify(data).toLowerCase().indexOf(value.toLowerCase()) !== -1)
240
- }
241
- const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(
242
- {
243
- columns: tableColumns,
244
- data: tableData,
245
- defaultColumn,
246
- disableSortRemove: true, // otherwise 3rd click on header removes sorting entirely
247
- sortTypes: {
248
- custom: (rowA, rowB, columnId) => {
249
- // NOTE:
250
- // 1) Main issue causing all this code:
251
- // rowA and rowB are coming in with all values undefined
252
- // - if it passed the values we could just use the columnId to get the correct sort value
253
- // but since it's not there we have to go through a bunch of code to get it because
254
- // we also do not know the Y axis data key (TT)
255
- // 2). if formattedData did not truncate the strings we could get it from there
256
- // but Hispanic or Latino is truncated to just Hispanic as the key
257
- // and 'White, Non-Hispanic/Latino' gets truncated to remove the /Latino
258
-
259
- // rowA.original - is the row data field name to access the value
260
- // columnId = the column indicator typically date or date--index
261
- let a, b
262
- if (columnId === 'series-label') {
263
- // comparing strings
264
- a = rowA.original
265
- b = rowB.original
266
- return a.localeCompare(b)
267
- }
268
-
269
- let dataKey = config.xAxis.dataKey
270
- let columnIdIndexRemoved = columnId.split('--')[0] // have to remove index for compare
271
- //get all the data from that column
272
- let colData = runtimeData.filter(obj => {
273
- // problem is dates can be in different formats
274
- if (config.xAxis.type === 'date' && !isNaN(Date.parse(obj[dataKey])) && !isNaN(Date.parse(columnIdIndexRemoved))) {
275
- // must convert to datetime number to compare
276
- return parseDate(obj[dataKey]).getTime() === parseDate(columnIdIndexRemoved).getTime()
277
- } else {
278
- return obj[dataKey] === columnIdIndexRemoved // have to remove index
279
- }
280
- })
281
-
282
- if (colData === undefined || colData[0] === undefined) {
283
- return -1
284
- }
285
-
286
- let rowA_cellObj = getSpecificCellData(colData, rowA.original)
287
- let rowB_cellObj = getSpecificCellData(colData, rowB.original)
288
-
289
- // - ** REMOVE any data points NOT selected in the data series ***
290
- // I dont understand why not selected data series are still sent down in the data
291
- // - would be better to scrub outside of here (TT)
292
- let newRowA_cellObj = []
293
- let newRowB_cellObj = []
294
- if (config.runtime.seriesKeys) {
295
- config.runtime.seriesKeys.forEach(seriesKey => {
296
- if (seriesKey in rowA_cellObj[0]) newRowA_cellObj.push(rowA_cellObj[0][seriesKey])
297
- if (seriesKey in rowB_cellObj[0]) newRowB_cellObj.push(rowB_cellObj[0][seriesKey])
298
- })
299
- // copy back over
300
- rowA_cellObj[0] = newRowA_cellObj
301
- rowB_cellObj[0] = newRowB_cellObj
302
- }
303
-
304
- // REMOVE the following:
305
- // - value equal to column date
306
- // - value that is the .original
307
- // - any data still in that's not really a number
308
- let rowA_valueObj = Object.values(rowA_cellObj[0]).filter(value => value !== columnIdIndexRemoved && value !== rowA.original && !isNaN(value))
309
- let rowB_valueObj = Object.values(rowB_cellObj[0]).filter(value => value !== columnIdIndexRemoved && value !== rowB.original && !isNaN(value))
310
-
311
- // NOW we can get the sort values from the cell object
312
- a = rowA_valueObj.length > 1 ? rowA_valueObj[rowA.id] : rowA_valueObj[0]
313
- b = rowB_valueObj.length > 1 ? rowB_valueObj[rowB.id] : rowB_valueObj[0]
314
-
315
- // force null and undefined to the bottom
316
- a = a === null || a === undefined ? '' : transform.cleanDataPoint(a)
317
- b = b === null || b === undefined ? '' : transform.cleanDataPoint(b)
318
- if (a === '' || a === null) {
319
- if (b === '' || b === null) {
320
- return 0 // Both empty/null
321
- }
322
- return -1 // Sort a to an index lower than b
323
- }
324
- if (b === '' || b === null) {
325
- if (a === '' || a === null) {
326
- return 0 // Both empty/null
327
- }
328
- return 1 // Sort b to an index lower than a
329
- }
330
- // End code for forcing NULLS to bottom
331
-
332
- // convert any strings that are actually numbers to proper data type
333
- const aNum = Number(a)
334
-
335
- if (!Number.isNaN(aNum)) {
336
- a = aNum
337
- }
338
-
339
- const bNum = Number(b)
340
-
341
- if (!Number.isNaN(bNum)) {
342
- b = bNum
343
- }
344
- // remove iso code prefixes
345
- if (typeof a === 'string') {
346
- a = a.replace('us-', '')
347
- a = displayGeoName(a)
348
- }
349
-
350
- if (typeof b === 'string') {
351
- b = b.replace('us-', '')
352
- b = displayGeoName(b)
353
- }
354
-
355
- // force any string values to lowercase
356
- a = typeof a === 'string' ? a.toLowerCase() : a
357
- b = typeof b === 'string' ? b.toLowerCase() : b
358
-
359
- // When comparing a number to a string, always send string to bottom
360
- if (typeof a === 'number' && typeof b === 'string') {
361
- return 1
362
- }
363
-
364
- if (typeof b === 'number' && typeof a === 'string') {
365
- return -1
366
- }
367
-
368
- // Return either 1 or -1 to indicate a sort priority
369
- if (a > b) {
370
- return 1
371
- }
372
- if (a < b) {
373
- return -1
374
- }
375
- // returning 0, undefined or any falsey value will use subsequent sorts or
376
- // the index as a tiebreaker
377
- return 0
378
- }
379
- },
380
- initialState: {
381
- sortBy: [{ id: 'series-label', desc: false }] // default sort on 1st column -
382
- }
383
- },
384
- useSortBy,
385
- useBlockLayout,
386
- useResizeColumns
387
- )
388
-
389
- // sort continuous x axis scaling for data tables, ie. xAxis should read 1,2,3,4,5
390
- if (config.xAxis.type === 'continuous' && headerGroups) {
391
- data.sort((a, b) => a[config.xAxis.dataKey] - b[config.xAxis.dataKey])
392
- }
393
-
394
- return (
395
- <ErrorBoundary component='DataTable'>
396
- <MediaControls.Section classes={['download-links']}>
397
- <MediaControls.Link config={config} />
398
- {config.table.download && <DownloadButton data={rawData} type='link' />}
399
- </MediaControls.Section>
400
-
401
- <section style={{ marginTop: !isLegendBottom ? config.dynamicMarginTop / 4 + 'px' : '0px' }} id={config?.title ? `dataTableSection__${config?.title.replace(/\s/g, '')}` : `dataTableSection`} className={`data-table-container`} aria-label={accessibilityLabel}>
402
- <div
403
- role='button'
404
- className={tableExpanded ? 'data-table-heading' : 'collapsed data-table-heading'}
405
- tabIndex={0}
406
- onClick={() => {
407
- setTableExpanded(!tableExpanded)
408
- }}
409
- onKeyDown={e => {
410
- if (e.keyCode === 13) {
411
- setTableExpanded(!tableExpanded)
412
- }
413
- }}
414
- >
415
- <Icon display={tableExpanded ? 'minus' : 'plus'} base />
416
- {config.table.label}
417
- </div>
418
- <div className='table-container' hidden={!tableExpanded} style={{ maxHeight: config.table.limitHeight && `${config.table.height}px`, overflowY: 'scroll' }}>
419
- <table className={tableExpanded ? 'data-table' : 'data-table cdcdataviz-sr-only'} {...getTableProps()} aria-rowcount={config?.series?.length ? config?.series?.length : '-1'}>
420
- <caption className='cdcdataviz-sr-only visually-hidden'>{config.table.caption ? config.table.caption : config.table.label ? config.table.label : 'Data Table'}</caption>
421
- <thead>
422
- {headerGroups.map((headerGroup, index) => (
423
- <tr {...headerGroup.getHeaderGroupProps()} key={`headerGroups--${index}`}>
424
- {' '}
425
- {headerGroup.headers.map((column, index) => (
426
- <th
427
- tabIndex='0'
428
- title={column.Header}
429
- key={`trth--${index}`}
430
- role='columnheader'
431
- scope='col'
432
- {...column.getHeaderProps(column.getSortByToggleProps())}
433
- className={column.isSorted && column.isSortedDesc ? 'sort sort-desc' : 'sort sort-asc'}
434
- {...(column.isSorted && column.isSortedDesc ? { 'aria-sort': 'descending' } : { 'aria-sort': 'ascending' })}
435
- >
436
- {column.render('Header')}
437
- {column.isSorted && <span className={'sort-icon'}>{column.isSortedDesc ? downIcon : upIcon}</span>}
438
- </th>
439
- ))}
440
- </tr>
441
- ))}
442
- </thead>
443
- <tbody {...getTableBodyProps()}>
444
- {rows.map((row, index) => {
445
- prepareRow(row)
446
- return (
447
- <tr {...row.getRowProps()} key={`tbody__tr-${index}`} className={`row-${String(config.visualizationType).replace(' ', '-')}--${index}`}>
448
- {row.cells.map((cell, index) => {
449
- return (
450
- <td tabIndex='0' {...cell.getCellProps()} key={`tbody__tr__td-${index}`} role='gridcell'>
451
- {cell.render('Cell')}
452
- </td>
453
- )
454
- })}
455
- </tr>
456
- )
457
- })}
458
- </tbody>
459
- </table>
460
- {config.regions && config.regions.length > 0 && config.visualizationType !== 'Box Plot' ? (
461
- <table className='region-table data-table'>
462
- <caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
463
- <thead>
464
- <tr>
465
- <th>Region Name</th>
466
- <th>Start Date</th>
467
- <th>End Date</th>
468
- </tr>
469
- </thead>
470
- <tbody>
471
- {config.regions.map((region, index) => {
472
- if (config.visualizationType === 'Box Plot') return false
473
- if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
474
-
475
- return (
476
- <tr key={`row-${region.label}--${index}`}>
477
- <td>{region.label}</td>
478
- <td>{formatDate(parseDate(region.from))}</td>
479
- <td>{formatDate(parseDate(region.to))}</td>
480
- </tr>
481
- )
482
- })}
483
- </tbody>
484
- </table>
485
- ) : (
486
- ''
487
- )}
488
- </div>
489
- </section>
490
- </ErrorBoundary>
491
- )
492
- }
493
-
494
- export default DataTable