@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.
- package/LICENSE +201 -0
- package/dist/cdcchart.js +44099 -44436
- package/examples/feature/__data__/area-chart-date-apple.json +1 -5073
- package/examples/feature/area/area-chart-date-apple.json +73 -10316
- package/examples/feature/area/area-chart-date-city-temperature.json +204 -80
- package/examples/feature/area/area-chart-stacked.json +239 -0
- package/examples/feature/filters/bar-filter.json +5027 -0
- package/examples/feature/legend-highlights/highlights.json +567 -0
- package/index.html +9 -6
- package/package.json +3 -2
- package/src/{CdcChart.jsx → CdcChart.tsx} +77 -71
- package/src/components/AreaChart.Stacked.jsx +73 -0
- package/src/components/AreaChart.jsx +24 -26
- package/src/components/DeviationBar.jsx +67 -13
- package/src/components/EditorPanel.jsx +483 -452
- package/src/components/Forecasting.jsx +5 -5
- package/src/components/Legend.jsx +6 -5
- package/src/components/LineChart.Circle.tsx +108 -0
- package/src/components/{LineChart.jsx → LineChart.tsx} +10 -42
- package/src/components/LinearChart.jsx +460 -443
- package/src/components/PieChart.jsx +54 -25
- package/src/components/Series.jsx +63 -17
- package/src/components/SparkLine.jsx +7 -19
- package/src/data/initial-state.js +6 -0
- package/src/hooks/useEditorPermissions.js +87 -24
- package/src/hooks/useReduceData.js +5 -0
- package/src/hooks/useScales.js +1 -1
- package/src/hooks/useTooltip.jsx +19 -6
- package/src/scss/main.scss +6 -12
- package/src/components/DataTable.jsx +0 -494
- /package/src/{components → hooks}/useIntersectionObserver.jsx +0 -0
package/src/hooks/useTooltip.jsx
CHANGED
|
@@ -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
|
|
107
|
-
['Percent', `${Math.round((((arc
|
|
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
|
|
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])
|
package/src/scss/main.scss
CHANGED
|
@@ -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
|
|
File without changes
|