@cdc/core 4.24.1 → 4.24.3
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/assets/icon-sankey.svg +1 -0
- package/assets/icon-table.svg +1 -0
- package/components/DataTable/DataTable.tsx +44 -15
- package/components/DataTable/DataTableStandAlone.tsx +15 -0
- package/components/DataTable/components/CellAnchor.tsx +3 -1
- package/components/DataTable/components/ChartHeader.tsx +48 -12
- package/components/DataTable/components/DataTableEditorPanel.tsx +42 -0
- package/components/DataTable/components/MapHeader.tsx +10 -5
- package/components/DataTable/helpers/customColumns.ts +4 -2
- package/components/DataTable/helpers/customSort.ts +9 -0
- package/components/DataTable/helpers/getChartCellValue.ts +5 -3
- package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -2
- package/components/DataTable/helpers/getSeriesName.ts +15 -20
- package/components/DataTable/helpers/mapCellMatrix.tsx +4 -0
- package/components/DataTable/types/TableConfig.ts +12 -37
- package/components/EditorPanel/ColumnsEditor.tsx +311 -0
- package/components/EditorPanel/DataTableEditor.tsx +27 -28
- package/components/Filters.jsx +35 -16
- package/components/MultiSelect/MultiSelect.tsx +39 -20
- package/components/MultiSelect/multiselect.styles.css +44 -27
- package/components/NestedDropdown/NestedDropdown.tsx +257 -0
- package/components/NestedDropdown/index.ts +1 -0
- package/components/NestedDropdown/nesteddropdown.styles.css +70 -0
- package/components/Table/Table.tsx +1 -1
- package/components/_stories/MultiSelect.stories.tsx +10 -1
- package/components/_stories/NestedDropdown.stories.tsx +58 -0
- package/components/createBarElement.jsx +117 -0
- package/components/elements/ScreenReaderText.tsx +8 -0
- package/components/elements/SkipTo.tsx +14 -0
- package/components/ui/Icon.tsx +5 -1
- package/components/ui/Title/Title.scss +7 -1
- package/components/ui/Title/index.tsx +3 -3
- package/components/ui/Tooltip.jsx +1 -1
- package/components/ui/_stories/Colors.stories.tsx +92 -0
- package/components/ui/_stories/Icon.stories.tsx +17 -10
- package/data/colorPalettes.js +1 -6
- package/helpers/cove/accessibility.ts +23 -0
- package/helpers/cove/date.ts +19 -0
- package/helpers/coveUpdateWorker.js +4 -0
- package/helpers/fetchRemoteData.js +5 -5
- package/helpers/getViewport.ts +23 -0
- package/helpers/isDomainExternal.js +14 -0
- package/helpers/isSolr.js +13 -0
- package/helpers/queryStringUtils.js +26 -0
- package/helpers/tests/updateFieldFactory.test.ts +89 -0
- package/helpers/updateFieldFactory.ts +38 -0
- package/helpers/useDataVizClasses.js +2 -2
- package/helpers/ver/4.24.3.js +25 -0
- package/helpers/withDevTools.ts +50 -0
- package/package.json +4 -3
- package/styles/_data-table.scss +2 -20
- package/styles/_global-variables.scss +75 -0
- package/styles/base.scss +97 -69
- package/types/Action.ts +1 -0
- package/types/Axis.ts +3 -0
- package/types/BaseVisualizationType.ts +1 -0
- package/types/BoxPlot.ts +21 -0
- package/types/Column.ts +1 -0
- package/types/ConfidenceInterval.ts +1 -0
- package/types/General.ts +9 -0
- package/types/Legend.ts +18 -0
- package/types/Region.ts +10 -0
- package/types/Runtime.ts +3 -1
- package/types/Table.ts +5 -2
- package/types/UpdateFieldFunc.ts +1 -1
- package/types/ViewPort.ts +2 -0
- package/types/Visualization.ts +23 -5
- package/types/WCMSProps.ts +11 -0
- package/components/DataTable/components/SkipNav.tsx +0 -7
- package/helpers/cove/date.js +0 -9
- package/helpers/getViewport.js +0 -21
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96.31 82.55"><defs><style>.c{stroke-linecap:round;stroke-width:3px;}.c,.d,.e{stroke:#000;}.e{fill:none;}</style></defs><g id="a"/><g id="b"><path class="c" d="M6.88,39.98V5.57"/><path class="c" d="M89.42,22.98V5.57"/><path class="c" d="M89.42,52.79V27.38"/><path class="c" d="M89.42,76v-18.41"/><path class="c" d="M6.88,76v-28.43"/><rect class="e" x="12.3" y="5.98" width="71.62" height="9.69"/><path class="d" d="M83.92,16.67s-11.46,1.07-27.63,11.61-27.1,16.32-35.66,17.97c-4.59,.88-8.34,.72-8.34,.72v5.31s3.75,.17,8.34-.72c8.56-1.65,19.49-7.42,35.66-17.97s27.63-11.61,27.63-11.61v-5.31Z"/><path class="e" d="M12.3,16.67s41.36,10.71,71.62,10.71v8.71s-31.58,1.88-71.62-10.71v-8.71Z"/><path class="d" d="M83.92,53.29s-13.18,1.49-35.77,9.06c0,0-18.05,7.37-35.85,7.37v-15.89c17.8,0,35.85-7.37,35.85-7.37,22.59-7.57,35.77-9.06,35.77-9.06v15.89Z"/><rect class="d" x="12.3" y="71" width="71.62" height="5.44"/><path class="e" d="M83.92,57.58l-8.82-2.38c-6.78-1.93-13.27-4.73-19.33-8.34l-14.69-8.76c-5.94-3.54-12.3-6.31-18.93-8.25l-9.85-2.88v12.14l9.85,2.88c6.64,1.94,13,4.71,18.93,8.25l14.69,8.76c6.05,3.61,12.55,6.41,19.33,8.34l8.82,2.38v-12.14Z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/></svg>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useMemo } from 'react'
|
|
2
|
+
import { timeParse } from 'd3-time-format'
|
|
2
3
|
|
|
3
4
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
4
5
|
import MediaControls from '@cdc/core/components/MediaControls'
|
|
@@ -8,7 +9,7 @@ import { customSort } from './helpers/customSort'
|
|
|
8
9
|
import ChartHeader from './components/ChartHeader'
|
|
9
10
|
import BoxplotHeader from './components/BoxplotHeader'
|
|
10
11
|
import MapHeader from './components/MapHeader'
|
|
11
|
-
import
|
|
12
|
+
import SkipTo from '../elements/SkipTo'
|
|
12
13
|
import ExpandCollapse from './components/ExpandCollapse'
|
|
13
14
|
import mapCellMatrix from './helpers/mapCellMatrix'
|
|
14
15
|
import Table from '../Table'
|
|
@@ -17,13 +18,12 @@ import regionCellMatrix from './helpers/regionCellMatrix'
|
|
|
17
18
|
import boxplotCellMatrix from './helpers/boxplotCellMatrix'
|
|
18
19
|
import customColumns from './helpers/customColumns'
|
|
19
20
|
import { TableConfig } from './types/TableConfig'
|
|
21
|
+
import { Column } from '../../types/Column'
|
|
20
22
|
|
|
21
23
|
export type DataTableProps = {
|
|
22
24
|
applyLegendToRow?: Function
|
|
23
25
|
colorScale?: Function
|
|
24
|
-
columns?:
|
|
25
|
-
// determines if columns should be wrapped in the table
|
|
26
|
-
wrapColumns?: boolean
|
|
26
|
+
columns?: Record<string, Column>
|
|
27
27
|
config: TableConfig
|
|
28
28
|
dataConfig?: Object
|
|
29
29
|
displayDataAsText?: Function
|
|
@@ -32,21 +32,27 @@ export type DataTableProps = {
|
|
|
32
32
|
formatLegendLocation?: Function
|
|
33
33
|
groupBy?: string
|
|
34
34
|
headerColor?: string
|
|
35
|
+
imageRef?: string
|
|
35
36
|
indexTitle?: string
|
|
37
|
+
isDebug?: boolean
|
|
38
|
+
isEditor?: boolean
|
|
36
39
|
navigationHandler?: Function
|
|
40
|
+
outerContainerRef?: Function
|
|
37
41
|
rawData: Object[]
|
|
38
|
-
runtimeData: Object[]
|
|
42
|
+
runtimeData: Object[] & Record<string, Object>
|
|
39
43
|
setFilteredCountryCode?: Function // used for Maps only
|
|
44
|
+
showDownloadButton?: boolean
|
|
40
45
|
tabbingId: string
|
|
41
46
|
tableTitle: string
|
|
42
47
|
viewport: string
|
|
43
48
|
vizTitle?: string
|
|
49
|
+
// determines if columns should be wrapped in the table
|
|
50
|
+
wrapColumns?: boolean
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
47
54
|
const DataTable = (props: DataTableProps) => {
|
|
48
55
|
const { config, dataConfig, tableTitle, vizTitle, rawData, runtimeData, headerColor, expandDataTable, columns, viewport, formatLegendLocation, tabbingId, wrapColumns } = props
|
|
49
|
-
|
|
50
56
|
const [expanded, setExpanded] = useState(expandDataTable)
|
|
51
57
|
|
|
52
58
|
const [sortBy, setSortBy] = useState<any>({ column: config.type === 'map' ? 'geo' : 'date', asc: false, colIndex: null })
|
|
@@ -84,7 +90,7 @@ const DataTable = (props: DataTableProps) => {
|
|
|
84
90
|
case 'Box Plot':
|
|
85
91
|
if (!config.boxplot) return <Loading />
|
|
86
92
|
break
|
|
87
|
-
case 'Line' || 'Bar' || 'Combo' || 'Pie' || 'Deviation Bar' || 'Paired Bar':
|
|
93
|
+
case 'Line' || 'Bar' || 'Combo' || 'Pie' || 'Deviation Bar' || 'Paired Bar' || 'Sankey' || 'Table':
|
|
88
94
|
if (!runtimeData) return <Loading />
|
|
89
95
|
break
|
|
90
96
|
default:
|
|
@@ -92,9 +98,9 @@ const DataTable = (props: DataTableProps) => {
|
|
|
92
98
|
break
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
const _runtimeData = config.table.customTableConfig ? customColumns(
|
|
101
|
+
const _runtimeData = config.table.customTableConfig ? customColumns(rawData, config.table.excludeColumns) : runtimeData
|
|
96
102
|
|
|
97
|
-
const rawRows = Object.keys(_runtimeData)
|
|
103
|
+
const rawRows = Object.keys(_runtimeData).filter(column => column != 'columns')
|
|
98
104
|
const rows = isVertical
|
|
99
105
|
? rawRows.sort((a, b) => {
|
|
100
106
|
let dataA
|
|
@@ -108,6 +114,10 @@ const DataTable = (props: DataTableProps) => {
|
|
|
108
114
|
dataA = _runtimeData[a][sortBy.column]
|
|
109
115
|
dataB = _runtimeData[b][sortBy.column]
|
|
110
116
|
}
|
|
117
|
+
if (!dataA && !dataB && config.type === 'chart' && config.xAxis && config.xAxis.type === 'date-time') {
|
|
118
|
+
dataA = timeParse(config.runtime.xAxis.dateParseFormat)(_runtimeData[a][config.xAxis.dataKey])
|
|
119
|
+
dataB = timeParse(config.runtime.xAxis.dateParseFormat)(_runtimeData[b][config.xAxis.dataKey])
|
|
120
|
+
}
|
|
111
121
|
return dataA && dataB ? customSort(dataA, dataB, sortBy, config) : 0
|
|
112
122
|
})
|
|
113
123
|
: rawRows
|
|
@@ -117,7 +127,7 @@ const DataTable = (props: DataTableProps) => {
|
|
|
117
127
|
OverflowY: 'scroll'
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
const hasRowType = !!Object.keys(rawData[0] || {}).find((v: string) => v.match(/row[_-]?type/i))
|
|
130
|
+
const hasRowType = !!Object.keys(rawData?.[0] || {}).find((v: string) => v.match(/row[_-]?type/i))
|
|
121
131
|
|
|
122
132
|
const caption = useMemo(() => {
|
|
123
133
|
if (config.type === 'map') {
|
|
@@ -131,12 +141,25 @@ const DataTable = (props: DataTableProps) => {
|
|
|
131
141
|
// If a relative region is found we don't want to display the data table.
|
|
132
142
|
// Takes backwards compatibility into consideration, ie !region.toType || !region.fromType
|
|
133
143
|
const noRelativeRegions = config?.regions?.every(region => {
|
|
134
|
-
|
|
144
|
+
const toTypeFixed = region.toType === 'Fixed'
|
|
145
|
+
const fromTypeFixed = region.fromType === 'Fixed'
|
|
146
|
+
const toIsNotSet = !region.toType
|
|
147
|
+
const fromIsNotSet = !region.fromType
|
|
148
|
+
const BothFixed = toTypeFixed && fromTypeFixed
|
|
149
|
+
const NeitherFixed = toIsNotSet && fromIsNotSet
|
|
150
|
+
const ToFixedFromNotSet = toTypeFixed && fromIsNotSet
|
|
151
|
+
const FromFixedToNotSet = fromTypeFixed && toIsNotSet
|
|
152
|
+
|
|
153
|
+
return BothFixed || NeitherFixed || ToFixedFromNotSet || FromFixedToNotSet
|
|
135
154
|
})
|
|
136
155
|
|
|
137
156
|
// prettier-ignore
|
|
138
157
|
const tableData = useMemo(() => (
|
|
139
|
-
|
|
158
|
+
config.data?.[0]?.tableData
|
|
159
|
+
? config.data?.[0]?.tableData
|
|
160
|
+
: config.visualizationType === 'Sankey'
|
|
161
|
+
? config.data?.[0]?.tableData
|
|
162
|
+
: config.visualizationType === 'Pie'
|
|
140
163
|
? [config.yAxis.dataKey]
|
|
141
164
|
: config.visualizationType === 'Box Plot'
|
|
142
165
|
? Object.entries(config.boxplot.tableData[0])
|
|
@@ -158,10 +181,10 @@ const DataTable = (props: DataTableProps) => {
|
|
|
158
181
|
<ErrorBoundary component='DataTable'>
|
|
159
182
|
<MediaControls.Section classes={['download-links']}>
|
|
160
183
|
<MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
|
|
161
|
-
{(config.table.download || config.general?.showDownloadButton) && <DownloadButton rawData={getDownloadData()} fileName={`${vizTitle || 'data-table'}.csv`} headerColor={headerColor}
|
|
184
|
+
{(config.table.download || config.general?.showDownloadButton) && <DownloadButton rawData={getDownloadData()} fileName={`${vizTitle || 'data-table'}.csv`} headerColor={headerColor} />}
|
|
162
185
|
</MediaControls.Section>
|
|
163
186
|
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
164
|
-
<
|
|
187
|
+
<SkipTo skipId={skipId} skipMessage='Skip Data Table' />
|
|
165
188
|
<ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
|
|
166
189
|
<div className='table-container' style={limitHeight}>
|
|
167
190
|
<Table
|
|
@@ -194,6 +217,9 @@ const DataTable = (props: DataTableProps) => {
|
|
|
194
217
|
)}
|
|
195
218
|
</div>
|
|
196
219
|
</section>
|
|
220
|
+
<div id={skipId} className='cdcdataviz-sr-only'>
|
|
221
|
+
Skipped data table.
|
|
222
|
+
</div>
|
|
197
223
|
</ErrorBoundary>
|
|
198
224
|
)
|
|
199
225
|
} else {
|
|
@@ -201,7 +227,7 @@ const DataTable = (props: DataTableProps) => {
|
|
|
201
227
|
return (
|
|
202
228
|
<ErrorBoundary component='DataTable'>
|
|
203
229
|
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
204
|
-
<
|
|
230
|
+
<SkipTo skipId={skipId} skipMessage='Skip Data Table' />
|
|
205
231
|
<ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
|
|
206
232
|
<div className='table-container' style={limitHeight}>
|
|
207
233
|
<Table
|
|
@@ -215,6 +241,9 @@ const DataTable = (props: DataTableProps) => {
|
|
|
215
241
|
/>
|
|
216
242
|
</div>
|
|
217
243
|
</section>
|
|
244
|
+
<div id={skipId} className='cdcdataviz-sr-only'>
|
|
245
|
+
Skipped data table.
|
|
246
|
+
</div>
|
|
218
247
|
</ErrorBoundary>
|
|
219
248
|
)
|
|
220
249
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ViewPort } from '../../types/ViewPort'
|
|
2
|
+
import { Visualization } from '../../types/Visualization'
|
|
3
|
+
import DataTable from './DataTable'
|
|
4
|
+
|
|
5
|
+
type StandAloneProps = {
|
|
6
|
+
visualizationKey: string
|
|
7
|
+
config: Visualization
|
|
8
|
+
viewport?: ViewPort
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DataTableStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, config, viewport }) => {
|
|
12
|
+
return <DataTable expandDataTable={true} config={config} rawData={config.data} runtimeData={config.formattedData} tabbingId={visualizationKey} tableTitle={config.table.label} viewport={viewport || 'lg'} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default DataTableStandAlone
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import ExternalIcon from '@cdc/core/assets/external-link.svg'
|
|
2
|
+
import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
|
|
3
|
+
|
|
2
4
|
// Optionally wrap cell with anchor if config defines a navigation url
|
|
3
5
|
const CellAnchor = ({ markup, row, columns, navigationHandler, mapZoomHandler }) => {
|
|
4
6
|
if (columns.navigate && row[columns.navigate.name]) {
|
|
@@ -16,7 +18,7 @@ const CellAnchor = ({ markup, row, columns, navigationHandler, mapZoomHandler })
|
|
|
16
18
|
}}
|
|
17
19
|
>
|
|
18
20
|
{markup}
|
|
19
|
-
<ExternalIcon className='inline-icon' />
|
|
21
|
+
{isDomainExternal(row[columns.navigate.name]) && <ExternalIcon className='inline-icon' />}
|
|
20
22
|
</span>
|
|
21
23
|
)
|
|
22
24
|
} else if (mapZoomHandler) {
|
|
@@ -2,6 +2,7 @@ import { getChartCellValue } from '../helpers/getChartCellValue'
|
|
|
2
2
|
import { getSeriesName } from '../helpers/getSeriesName'
|
|
3
3
|
import { getDataSeriesColumns } from '../helpers/getDataSeriesColumns'
|
|
4
4
|
import { DownIcon, UpIcon } from './Icons'
|
|
5
|
+
import ScreenReaderText from '@cdc/core/components/elements/ScreenReaderText'
|
|
5
6
|
|
|
6
7
|
type ChartHeaderProps = { data; isVertical; config; setSortBy; sortBy; groupBy?; hasRowType? }
|
|
7
8
|
|
|
@@ -16,6 +17,45 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
|
|
|
16
17
|
dataSeriesColumns = groupHeaderRemoved
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
const handleHeaderClasses = (sortBy, text) => {
|
|
22
|
+
let classes = ['sort']
|
|
23
|
+
if (sortBy.column === text && sortBy.asc) {
|
|
24
|
+
classes.push('sort-asc')
|
|
25
|
+
}
|
|
26
|
+
if (sortBy.column === text && sortBy.desc) {
|
|
27
|
+
classes.push('sort-desc')
|
|
28
|
+
}
|
|
29
|
+
return classes.join(' ')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ScreenReaderSortByText = ({ text, config, sortBy }) => {
|
|
33
|
+
const notApplicableText = 'Not Applicable'
|
|
34
|
+
let columnHeaderText = `${text} `
|
|
35
|
+
if (text !== '__series__' || text !== '') {
|
|
36
|
+
columnHeaderText = `${text} `
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if ((text === '__series__' || text === '') && !config.table.indexLabel) {
|
|
40
|
+
columnHeaderText = notApplicableText
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ((text === '__series__' || text === '') && config.table.indexLabel) {
|
|
44
|
+
columnHeaderText = config.table.indexLabel
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (columnHeaderText === notApplicableText) return
|
|
48
|
+
|
|
49
|
+
return <span className='cdcdataviz-sr-only'>{`Press command, modifier, or enter key to sort by ${columnHeaderText} in ${sortBy.column !== columnHeaderText ? 'ascending' : sortBy.column === 'desc' ? 'descending' : 'ascending'} order`}</span>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ColumnHeadingText = ({ column, text, config }) => {
|
|
53
|
+
let notApplicableText = 'Not Applicable'
|
|
54
|
+
if (text === '__series__' && config.table.indexLabel) return `${config.table.indexLabel} `
|
|
55
|
+
if (text === '__series__' && !config.table.indexLabel) return <ScreenReaderText as='span'>{notApplicableText}</ScreenReaderText>
|
|
56
|
+
return text
|
|
57
|
+
}
|
|
58
|
+
|
|
19
59
|
if (isVertical) {
|
|
20
60
|
if (hasRowType) {
|
|
21
61
|
// find the row type column and place it at the beginning of the array
|
|
@@ -25,6 +65,7 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
|
|
|
25
65
|
dataSeriesColumns.splice(rowTypeIndex, 1)
|
|
26
66
|
}
|
|
27
67
|
}
|
|
68
|
+
|
|
28
69
|
return (
|
|
29
70
|
<tr>
|
|
30
71
|
{dataSeriesColumns.map((column, index) => {
|
|
@@ -35,7 +76,6 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
|
|
|
35
76
|
style={{ minWidth: (config.table.cellMinWidth || 0) + 'px' }}
|
|
36
77
|
key={`col-header-${column}__${index}`}
|
|
37
78
|
tabIndex={0}
|
|
38
|
-
title={text}
|
|
39
79
|
role='columnheader'
|
|
40
80
|
scope='col'
|
|
41
81
|
onClick={() => {
|
|
@@ -48,14 +88,12 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
|
|
|
48
88
|
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false, colIndex: index })
|
|
49
89
|
}
|
|
50
90
|
}}
|
|
51
|
-
className={
|
|
91
|
+
className={handleHeaderClasses(sortBy, text)}
|
|
52
92
|
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
53
93
|
>
|
|
54
|
-
{text}
|
|
94
|
+
<ColumnHeadingText text={text} column={column} config={config} />
|
|
55
95
|
{column === sortBy.column && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
|
|
56
|
-
<
|
|
57
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
58
|
-
</button>
|
|
96
|
+
<ScreenReaderSortByText sortBy={sortBy} config={config} text={text} />
|
|
59
97
|
</th>
|
|
60
98
|
)
|
|
61
99
|
})}
|
|
@@ -68,12 +106,12 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
|
|
|
68
106
|
{['__series__', ...Object.keys(data)].slice(sliceVal).map((row, index) => {
|
|
69
107
|
let column = config.xAxis?.dataKey
|
|
70
108
|
let text = row !== '__series__' ? getChartCellValue(row, column, config, data) : '__series__'
|
|
109
|
+
|
|
71
110
|
return (
|
|
72
111
|
<th
|
|
73
112
|
style={{ minWidth: (config.table.cellMinWidth || 0) + 'px' }}
|
|
74
113
|
key={`col-header-${text}__${index}`}
|
|
75
114
|
tabIndex={0}
|
|
76
|
-
title={text}
|
|
77
115
|
role='columnheader'
|
|
78
116
|
scope='col'
|
|
79
117
|
onClick={() => {
|
|
@@ -84,14 +122,12 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
|
|
|
84
122
|
setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
|
|
85
123
|
}
|
|
86
124
|
}}
|
|
87
|
-
className={sortBy
|
|
125
|
+
className={handleHeaderClasses(sortBy, text)}
|
|
88
126
|
{...(sortBy.column === text ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
89
127
|
>
|
|
90
|
-
{text
|
|
128
|
+
<ColumnHeadingText text={text} column={column} config={config} />
|
|
91
129
|
{index === sortBy.colIndex && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
|
|
92
|
-
<
|
|
93
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === text ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
94
|
-
</button>
|
|
130
|
+
<ScreenReaderSortByText text={text} config={config} sortBy={sortBy} />
|
|
95
131
|
</th>
|
|
96
132
|
)
|
|
97
133
|
})}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
|
|
2
|
+
import DataTableEditor from '../../EditorPanel/DataTableEditor'
|
|
3
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
4
|
+
import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
|
|
5
|
+
import { useMemo } from 'react'
|
|
6
|
+
import ColumnsEditor from '../../EditorPanel/ColumnsEditor'
|
|
7
|
+
|
|
8
|
+
type DataTableEditorProps = {
|
|
9
|
+
config: Visualization
|
|
10
|
+
updateConfig: Function
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DataTableEditorPanel: React.FC<DataTableEditorProps> = ({ config, updateConfig }) => {
|
|
14
|
+
const updateField = useMemo(() => updateFieldFactory(config, updateConfig), [JSON.stringify(config)])
|
|
15
|
+
const deleteColumn = columnName => {
|
|
16
|
+
const newColumns = config.columns
|
|
17
|
+
|
|
18
|
+
delete newColumns[columnName]
|
|
19
|
+
|
|
20
|
+
updateConfig({
|
|
21
|
+
...config,
|
|
22
|
+
columns: newColumns
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const columns = Object.keys(config.columns || {})
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<ColumnsEditor config={config} updateField={updateField} deleteColumn={deleteColumn} />
|
|
30
|
+
<AccordionItem>
|
|
31
|
+
<AccordionItemHeading>
|
|
32
|
+
<AccordionItemButton>Data Table</AccordionItemButton>
|
|
33
|
+
</AccordionItemHeading>
|
|
34
|
+
<AccordionItemPanel>
|
|
35
|
+
<DataTableEditor config={config} columns={columns} updateField={updateField} isDashboard={true} />
|
|
36
|
+
</AccordionItemPanel>
|
|
37
|
+
</AccordionItem>
|
|
38
|
+
</>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default DataTableEditorPanel
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { DataTableProps } from '../DataTable'
|
|
2
2
|
import { DownIcon, UpIcon } from './Icons'
|
|
3
|
+
import ScreenReaderText from '../../elements/ScreenReaderText'
|
|
3
4
|
|
|
4
5
|
type MapHeaderProps = DataTableProps & {
|
|
5
6
|
sortBy: { column; asc }
|
|
6
7
|
setSortBy: Function
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
const ColumnHeadingText = ({ column, text, config }) => {
|
|
11
|
+
let notApplicableText = 'Not Applicable'
|
|
12
|
+
if (text === '__series__' && config.table.indexLabel) return `${config.table.indexLabel} `
|
|
13
|
+
if (text === '__series__' && !config.table.indexLabel) return <ScreenReaderText as='span'>{notApplicableText}</ScreenReaderText>
|
|
14
|
+
return text
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeaderProps) => {
|
|
10
18
|
return (
|
|
11
19
|
<tr>
|
|
@@ -27,7 +35,6 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeader
|
|
|
27
35
|
key={`col-header-${column}__${index}`}
|
|
28
36
|
id={column}
|
|
29
37
|
tabIndex={0}
|
|
30
|
-
title={text}
|
|
31
38
|
role='columnheader'
|
|
32
39
|
scope='col'
|
|
33
40
|
onClick={() => {
|
|
@@ -41,11 +48,9 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeader
|
|
|
41
48
|
className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
42
49
|
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
43
50
|
>
|
|
44
|
-
{text}
|
|
51
|
+
<ColumnHeadingText text={text} config={config} column={column} />
|
|
45
52
|
{sortBy.column === column && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
|
|
46
|
-
<
|
|
47
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
48
|
-
</button>
|
|
53
|
+
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} order`}</span>
|
|
49
54
|
</th>
|
|
50
55
|
)
|
|
51
56
|
})}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
type RuntimeData = Object[] & Record<string, Object>
|
|
2
|
+
|
|
1
3
|
// removes null and excluded columns
|
|
2
|
-
const customColumns = (runtimeData: Object[] |
|
|
4
|
+
const customColumns = (runtimeData: Object[] | RuntimeData, excludeColumns: string[] = []): RuntimeData => {
|
|
3
5
|
if (!Array.isArray(runtimeData)) {
|
|
4
6
|
// currently we don't support Record types
|
|
5
7
|
return runtimeData
|
|
@@ -18,7 +20,7 @@ const customColumns = (runtimeData: Object[] | Record<string, Object>, excludeCo
|
|
|
18
20
|
if (runtimeDataMemo[key] === true) row[key] = d[key]
|
|
19
21
|
})
|
|
20
22
|
return row
|
|
21
|
-
})
|
|
23
|
+
}) as RuntimeData
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -8,7 +8,16 @@ export const customSort = (a, b, sortBy, config) => {
|
|
|
8
8
|
if (config.type === 'map') {
|
|
9
9
|
valueA = standardizeStateName(a)
|
|
10
10
|
valueB = standardizeStateName(b)
|
|
11
|
+
// sort for Regions table for Map
|
|
12
|
+
if (String(valueA).toLowerCase().includes('region') && String(valueB).toLowerCase().includes('region')) {
|
|
13
|
+
const numberA = parseInt(a.match(/\d+$/)[0], 10)
|
|
14
|
+
const numberB = parseInt(b.match(/\d+$/)[0], 10)
|
|
15
|
+
|
|
16
|
+
// Compare the numeric parts
|
|
17
|
+
return !sortBy.asc ? Number(numberA) - Number(numberB) : Number(numberB) - Number(numberA)
|
|
18
|
+
}
|
|
11
19
|
}
|
|
20
|
+
|
|
12
21
|
// Treat booleans and nulls as an empty string
|
|
13
22
|
valueA = valueA === false || valueA === true || valueA === null ? '' : valueA
|
|
14
23
|
valueB = valueB === false || valueB === true || valueB === null ? '' : valueB
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseDate, formatDate } from '@cdc/core/helpers/cove/date'
|
|
2
2
|
import { formatNumber } from '@cdc/core/helpers/cove/number'
|
|
3
|
+
import { TableConfig } from '../types/TableConfig'
|
|
3
4
|
|
|
4
5
|
// if its additional column, return formatting params
|
|
5
6
|
const isAdditionalColumn = (column, config) => {
|
|
@@ -23,14 +24,15 @@ const isAdditionalColumn = (column, config) => {
|
|
|
23
24
|
return formattingParams
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export const getChartCellValue = (row, column, config, runtimeData) => {
|
|
27
|
-
if (config.table.customTableConfig) return runtimeData[row][column]
|
|
27
|
+
export const getChartCellValue = (row: string, column: string, config: TableConfig, runtimeData: Object[]) => {
|
|
28
|
+
if (config.table.customTableConfig || config.visualizationType === 'Sankey' || runtimeData?.[0]?.tableData) return runtimeData[row][column]
|
|
29
|
+
|
|
28
30
|
const rowObj = runtimeData[row]
|
|
29
31
|
let cellValue // placeholder for formatting below
|
|
30
32
|
let labelValue = rowObj[column] // just raw X axis string
|
|
31
33
|
if (column === config.xAxis?.dataKey) {
|
|
32
34
|
// not the prettiest, but helper functions work nicely here.
|
|
33
|
-
cellValue = config.xAxis?.type === 'date' ? formatDate(config.xAxis?.dateDisplayFormat, parseDate(config.xAxis?.dateParseFormat, labelValue)) : labelValue
|
|
35
|
+
cellValue = config.xAxis?.type === 'date' ? formatDate(config.table?.dateDisplayFormat || config.xAxis?.dateDisplayFormat, parseDate(config.xAxis?.dateParseFormat, labelValue)) : labelValue
|
|
34
36
|
} else {
|
|
35
37
|
let resolvedAxis = 'left'
|
|
36
38
|
let leftAxisItems = config.series ? config.series.filter(item => item?.axis === 'Left') : []
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { TableConfig } from '../types/TableConfig'
|
|
2
|
+
|
|
3
|
+
export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, runtimeData: Object[]): string[] => {
|
|
2
4
|
if (config.table.customTableConfig) return runtimeData[0] ? Object.keys(runtimeData[0]) : []
|
|
5
|
+
if (config.type === 'table') {
|
|
6
|
+
const excludeColumns = Object.values(config.columns)
|
|
7
|
+
.filter(column => column.dataTable === false)
|
|
8
|
+
.map(column => column.name)
|
|
9
|
+
return Object.keys(runtimeData[0]).filter(columnName => !excludeColumns.includes(columnName))
|
|
10
|
+
}
|
|
3
11
|
let tmpSeriesColumns
|
|
4
12
|
if (config.visualizationType !== 'Pie') {
|
|
5
13
|
tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey] : [] //, ...config.runtime.seriesLabelsAll
|
|
@@ -11,7 +19,7 @@ export const getDataSeriesColumns = (config, isVertical, runtimeData): string[]
|
|
|
11
19
|
tmpSeriesColumns = Object.keys(runtimeData[0])
|
|
12
20
|
}
|
|
13
21
|
} else {
|
|
14
|
-
tmpSeriesColumns = [config.xAxis?.dataKey, config.yAxis?.dataKey]
|
|
22
|
+
tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey, config.yAxis?.dataKey] : [config.yAxis?.dataKey]
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
// then add the additional Columns
|
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
custLabel = tmpColumn.label
|
|
9
|
-
}
|
|
10
|
-
})
|
|
11
|
-
return custLabel
|
|
1
|
+
import { TableConfig } from '../types/TableConfig'
|
|
2
|
+
|
|
3
|
+
const getLabel = (name: string, config: TableConfig) => {
|
|
4
|
+
const columns = Object.values(config.columns || {})
|
|
5
|
+
const matchingConfiguredColumn = columns.find(col => col.name === name)
|
|
6
|
+
if (matchingConfiguredColumn?.label) {
|
|
7
|
+
return matchingConfiguredColumn.label
|
|
12
8
|
}
|
|
9
|
+
return name
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
export const getSeriesName = (column, config) => {
|
|
12
|
+
export const getSeriesName = (column: string, config: TableConfig) => {
|
|
16
13
|
// If a user sets the name on a series use that.
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
14
|
+
const userDefinedSeries = config.series?.find(series => series.dataKey === column)
|
|
15
|
+
if (userDefinedSeries?.name) {
|
|
16
|
+
return userDefinedSeries.name
|
|
17
|
+
}
|
|
20
18
|
if (config.runtimeSeriesLabels && config.runtimeSeriesLabels[column]) return config.runtimeSeriesLabels[column]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let text = column === config.xAxis?.dataKey ? config.table.indexLabel : custLabel
|
|
24
|
-
|
|
25
|
-
return text
|
|
19
|
+
const columnIsDataKey = column === config.xAxis?.dataKey
|
|
20
|
+
return columnIsDataKey ? config.table.indexLabel : getLabel(column, config)
|
|
26
21
|
}
|
|
@@ -21,7 +21,11 @@ const mapCellArray = ({ rows, columns, runtimeData, config, applyLegendToRow, di
|
|
|
21
21
|
let labelValue
|
|
22
22
|
const mapZoomHandler = config.general.type === 'bubble' && config.general.allowMapZoom && config.general.geoType === 'world' ? () => setFilteredCountryCode(row) : undefined
|
|
23
23
|
if ((config.general.geoType !== 'single-state' && config.general.geoType !== 'us-county') || config.general.type === 'us-geocode') {
|
|
24
|
+
const capitalize = str => {
|
|
25
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
26
|
+
}
|
|
24
27
|
labelValue = displayGeoName(row)
|
|
28
|
+
labelValue = String(labelValue).startsWith('region') ? capitalize(labelValue) : labelValue
|
|
25
29
|
} else {
|
|
26
30
|
labelValue = formatLegendLocation(row)
|
|
27
31
|
}
|
|
@@ -2,51 +2,26 @@ import { Axis } from '@cdc/core/types/Axis'
|
|
|
2
2
|
import { Series } from '@cdc/core/types/Series'
|
|
3
3
|
import { Runtime } from '@cdc/core/types/Runtime'
|
|
4
4
|
import { Table } from '@cdc/core/types/Table'
|
|
5
|
+
import { Region } from '@cdc/core/types/Region'
|
|
6
|
+
import { BoxPlot } from '../../../types/BoxPlot'
|
|
7
|
+
import { General } from '../../../types/General'
|
|
8
|
+
import { Column } from '../../../types/Column'
|
|
9
|
+
import { Legend } from '@cdc/core/types/Legend'
|
|
5
10
|
|
|
6
11
|
export type TableConfig = {
|
|
7
12
|
type?: string
|
|
8
13
|
table: Table
|
|
9
14
|
xAxis?: Axis
|
|
10
15
|
yAxis?: Axis
|
|
11
|
-
boxplot?:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
minimum: string
|
|
17
|
-
iqr: string
|
|
18
|
-
median: string
|
|
19
|
-
q1: string
|
|
20
|
-
q3: string
|
|
21
|
-
outliers: string
|
|
22
|
-
values: string
|
|
23
|
-
total: string
|
|
24
|
-
lowerBounds: string
|
|
25
|
-
upperBounds: string
|
|
26
|
-
}
|
|
27
|
-
plots: []
|
|
28
|
-
categories: string[]
|
|
29
|
-
}
|
|
30
|
-
visualizationType?: string
|
|
31
|
-
general?: {
|
|
32
|
-
geoType: string
|
|
33
|
-
type: string
|
|
34
|
-
showDownloadButton: boolean
|
|
35
|
-
allowMapZoom?: boolean
|
|
36
|
-
}
|
|
37
|
-
columns?: {
|
|
38
|
-
geo: {
|
|
39
|
-
name: string
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
legend?: {
|
|
43
|
-
specialClasses?: { key: string; label: string; value: string }[]
|
|
44
|
-
hide?: boolean
|
|
45
|
-
}
|
|
16
|
+
boxplot?: BoxPlot
|
|
17
|
+
visualizationType: string
|
|
18
|
+
general?: General
|
|
19
|
+
columns?: Record<string, Column>
|
|
20
|
+
legend?: Legend
|
|
46
21
|
series?: Series
|
|
47
|
-
regions?:
|
|
22
|
+
regions?: Region[]
|
|
48
23
|
runtimeSeriesLabels?: Object
|
|
49
24
|
dataFormat?: Object
|
|
50
|
-
runtime
|
|
25
|
+
runtime?: Runtime
|
|
51
26
|
data: Object[]
|
|
52
27
|
}
|