@cdc/core 4.23.10-alpha → 4.23.11
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-deviation-bar.svg +1 -0
- package/components/DataTable/DataTable.tsx +205 -0
- package/components/DataTable/components/BoxplotHeader.tsx +16 -0
- package/components/DataTable/components/CellAnchor.tsx +44 -0
- package/components/DataTable/components/ChartHeader.tsx +91 -0
- package/components/DataTable/components/ExpandCollapse.tsx +21 -0
- package/components/DataTable/components/Icons.tsx +10 -0
- package/components/DataTable/components/MapHeader.tsx +56 -0
- package/components/DataTable/components/SkipNav.tsx +7 -0
- package/components/DataTable/helpers/boxplotCellMatrix.tsx +64 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +78 -0
- package/components/DataTable/helpers/customSort.ts +55 -0
- package/components/DataTable/helpers/getChartCellValue.ts +55 -0
- package/components/DataTable/helpers/getDataSeriesColumns.ts +28 -0
- package/components/DataTable/helpers/getSeriesName.ts +26 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +56 -0
- package/components/DataTable/helpers/regionCellMatrix.tsx +13 -0
- package/components/DataTable/helpers/standardizeState.js +76 -0
- package/components/DataTable/index.ts +1 -0
- package/components/DataTable/types/TableConfig.ts +57 -0
- package/components/DownloadButton.tsx +29 -0
- package/components/LegendCircle.jsx +2 -2
- package/components/Table/Table.tsx +49 -0
- package/components/Table/components/Cell.tsx +9 -0
- package/components/Table/components/GroupRow.tsx +16 -0
- package/components/Table/components/Row.tsx +19 -0
- package/components/Table/index.ts +1 -0
- package/components/Table/types/CellMatrix.ts +4 -0
- package/components/_stories/DataTable.stories.tsx +62 -0
- package/components/_stories/Table.stories.tsx +53 -0
- package/components/_stories/_mocks/dashboard_no_filter.json +121 -0
- package/components/_stories/_mocks/example-city-state.json +808 -0
- package/components/_stories/styles.scss +9 -0
- package/components/managers/{DataDesigner.jsx → DataDesigner.tsx} +96 -87
- package/components/ui/Title/Title.scss +95 -0
- package/components/ui/Title/index.tsx +34 -0
- package/components/ui/_stories/Title.stories.tsx +21 -0
- package/helpers/DataTransform.ts +41 -18
- package/helpers/cove/string.ts +11 -0
- package/package.json +2 -2
- package/styles/_data-table.scss +1 -0
- package/styles/heading-colors.scss +0 -3
- package/styles/v2/layout/_component.scss +0 -11
- package/types/Axis.ts +6 -0
- package/types/Color.ts +5 -0
- package/types/ComponentStyles.ts +7 -0
- package/types/ComponentThemes.ts +13 -0
- package/types/EditorColumnProperties.ts +8 -0
- package/types/Runtime.ts +9 -0
- package/types/Series.ts +1 -0
- package/types/Visualization.ts +21 -0
- package/components/DataTable.jsx +0 -759
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99.1 79.69"><defs><style>.cls-1{fill:#231f20;}</style></defs><rect class="cls-1" x="46.87" y="5.79" width="8.32" height="67.79" rx="1"/><rect class="cls-1" x="23.98" y="17.65" width="8.32" height="23.31" rx="1" transform="translate(57.44 1.16) rotate(90)"/><rect class="cls-1" x="19.53" y="25.99" width="8.32" height="32.21" rx="1" transform="translate(65.79 18.41) rotate(90)"/><rect class="cls-1" x="27.33" y="60.4" width="8.32" height="14.6" rx="1" transform="translate(99.19 36.2) rotate(90)"/><path class="cls-1" d="M81,52.74v4.32H64.55V52.74H81m1-2H63.55a1,1,0,0,0-1,1v6.32a1,1,0,0,0,1,1H82a1,1,0,0,0,1-1V51.74a1,1,0,0,0-1-1Z"/><path class="cls-1" d="M90,14.41v4.33H64.55V14.41H90m1-2H63.55a1,1,0,0,0-1,1v6.33a1,1,0,0,0,1,1H91a1,1,0,0,0,1-1V13.41a1,1,0,0,0-1-1Z"/></svg>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
4
|
+
import MediaControls from '@cdc/core/components/MediaControls'
|
|
5
|
+
import Loading from '@cdc/core/components/Loading'
|
|
6
|
+
import DownloadButton from '../DownloadButton'
|
|
7
|
+
import { customSort } from './helpers/customSort'
|
|
8
|
+
import ChartHeader from './components/ChartHeader'
|
|
9
|
+
import BoxplotHeader from './components/BoxplotHeader'
|
|
10
|
+
import MapHeader from './components/MapHeader'
|
|
11
|
+
import SkipNav from './components/SkipNav'
|
|
12
|
+
import ExpandCollapse from './components/ExpandCollapse'
|
|
13
|
+
import mapCellMatrix from './helpers/mapCellMatrix'
|
|
14
|
+
import Table from '../Table'
|
|
15
|
+
import chartCellMatrix from './helpers/chartCellMatrix'
|
|
16
|
+
import regionCellMatrix from './helpers/regionCellMatrix'
|
|
17
|
+
import boxplotCellMatrix from './helpers/boxplotCellMatrix'
|
|
18
|
+
import { TableConfig } from './types/TableConfig'
|
|
19
|
+
|
|
20
|
+
export type DataTableProps = {
|
|
21
|
+
applyLegendToRow?: Function
|
|
22
|
+
colorScale?: Function
|
|
23
|
+
columns?: { navigate: { name: string } }
|
|
24
|
+
config: TableConfig
|
|
25
|
+
dataConfig?: Object
|
|
26
|
+
displayDataAsText?: Function
|
|
27
|
+
displayGeoName?: Function
|
|
28
|
+
expandDataTable: boolean
|
|
29
|
+
formatLegendLocation?: Function
|
|
30
|
+
groupBy?: string
|
|
31
|
+
headerColor?: string
|
|
32
|
+
indexTitle?: string
|
|
33
|
+
navigationHandler?: Function
|
|
34
|
+
rawData: Object[]
|
|
35
|
+
runtimeData: Object[] | Record<string, Object> // UNSAFE
|
|
36
|
+
setFilteredCountryCode?: Function // used for Maps only
|
|
37
|
+
tabbingId: string
|
|
38
|
+
tableTitle: string
|
|
39
|
+
viewport: string
|
|
40
|
+
vizTitle?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
44
|
+
const DataTable = (props: DataTableProps) => {
|
|
45
|
+
const { config, dataConfig, tableTitle, vizTitle, rawData, runtimeData, headerColor, expandDataTable, columns, viewport, formatLegendLocation, tabbingId } = props
|
|
46
|
+
|
|
47
|
+
const [expanded, setExpanded] = useState(expandDataTable)
|
|
48
|
+
|
|
49
|
+
const [sortBy, setSortBy] = useState<any>({ column: config.type === 'map' ? 'geo' : 'date', asc: false, colIndex: null })
|
|
50
|
+
|
|
51
|
+
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
52
|
+
|
|
53
|
+
const isVertical = !(config.type === 'chart' && !config.table?.showVertical)
|
|
54
|
+
|
|
55
|
+
const rand = Math.random().toString(16).substr(2, 8)
|
|
56
|
+
const skipId = `btn__${rand}`
|
|
57
|
+
|
|
58
|
+
const mapLookup = {
|
|
59
|
+
'us-county': 'United States County Map',
|
|
60
|
+
'single-state': 'State Map',
|
|
61
|
+
us: 'United States Map',
|
|
62
|
+
world: 'World Map'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Change accessibility label depending on expanded status
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const expandedLabel = 'Accessible data table.'
|
|
68
|
+
const collapsedLabel = 'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.'
|
|
69
|
+
|
|
70
|
+
if (expanded === true && accessibilityLabel !== expandedLabel) {
|
|
71
|
+
setAccessibilityLabel(expandedLabel)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (expanded === false && accessibilityLabel !== collapsedLabel) {
|
|
75
|
+
setAccessibilityLabel(collapsedLabel)
|
|
76
|
+
}
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [expanded])
|
|
79
|
+
|
|
80
|
+
switch (config.visualizationType) {
|
|
81
|
+
case 'Box Plot':
|
|
82
|
+
if (!config.boxplot) return <Loading />
|
|
83
|
+
break
|
|
84
|
+
case 'Line' || 'Bar' || 'Combo' || 'Pie' || 'Deviation Bar' || 'Paired Bar':
|
|
85
|
+
if (!runtimeData) return <Loading />
|
|
86
|
+
break
|
|
87
|
+
default:
|
|
88
|
+
if (!runtimeData) return <Loading />
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rawRows = Object.keys(runtimeData)
|
|
93
|
+
const rows = isVertical
|
|
94
|
+
? rawRows.sort((a, b) => {
|
|
95
|
+
let dataA
|
|
96
|
+
let dataB
|
|
97
|
+
if (config.type === 'map' && config.columns) {
|
|
98
|
+
const sortByColName = config.columns[sortBy.column].name
|
|
99
|
+
dataA = runtimeData[a][sortByColName]
|
|
100
|
+
dataB = runtimeData[b][sortByColName]
|
|
101
|
+
}
|
|
102
|
+
if (config.type === 'chart' || config.type === 'dashboard') {
|
|
103
|
+
dataA = runtimeData[a][sortBy.column]
|
|
104
|
+
dataB = runtimeData[b][sortBy.column]
|
|
105
|
+
}
|
|
106
|
+
return dataA && dataB ? customSort(dataA, dataB, sortBy, config) : 0
|
|
107
|
+
})
|
|
108
|
+
: rawRows
|
|
109
|
+
|
|
110
|
+
const limitHeight = {
|
|
111
|
+
maxHeight: config.table.limitHeight && `${config.table.height}px`,
|
|
112
|
+
OverflowY: 'scroll'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const caption = useMemo(() => {
|
|
116
|
+
if (config.type === 'map') {
|
|
117
|
+
return config.table.caption ? config.table.caption : `Data table showing data for the ${mapLookup[config.general.geoType]} figure.`
|
|
118
|
+
} else {
|
|
119
|
+
return config.table.caption ? config.table.caption : `Data table showing data for the ${config.type} figure.`
|
|
120
|
+
}
|
|
121
|
+
}, [config.table.caption])
|
|
122
|
+
|
|
123
|
+
// prettier-ignore
|
|
124
|
+
const tableData = useMemo(() => (
|
|
125
|
+
config.visualizationType === 'Pie'
|
|
126
|
+
? [config.yAxis.dataKey]
|
|
127
|
+
: config.visualizationType === 'Box Plot'
|
|
128
|
+
? Object.entries(config.boxplot.tableData[0])
|
|
129
|
+
: config.runtime?.seriesKeys),
|
|
130
|
+
[config.runtime?.seriesKeys]) // eslint-disable-line
|
|
131
|
+
|
|
132
|
+
if (config.visualizationType !== 'Box Plot') {
|
|
133
|
+
const getDownloadData = () => {
|
|
134
|
+
// only use fullGeoName on County maps and no other
|
|
135
|
+
if (config.general?.geoType === 'us-county') {
|
|
136
|
+
// Add column for full Geo name along with State
|
|
137
|
+
return rawData.map(row => ({ FullGeoName: formatLegendLocation(row[config.columns.geo.name]), ...row }))
|
|
138
|
+
} else {
|
|
139
|
+
return rawData
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<ErrorBoundary component='DataTable'>
|
|
145
|
+
<MediaControls.Section classes={['download-links']}>
|
|
146
|
+
<MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
|
|
147
|
+
{(config.table.download || config.general?.showDownloadButton) && <DownloadButton rawData={getDownloadData()} fileName={`${vizTitle || 'data-table'}.csv`} headerColor={headerColor} skipId={skipId} />}
|
|
148
|
+
</MediaControls.Section>
|
|
149
|
+
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
150
|
+
<SkipNav skipId={skipId} />
|
|
151
|
+
<ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
|
|
152
|
+
<div className='table-container' style={limitHeight}>
|
|
153
|
+
<Table
|
|
154
|
+
childrenMatrix={config.type === 'map' ? mapCellMatrix({ rows, ...props }) : chartCellMatrix({ rows, ...props, isVertical, sortBy })}
|
|
155
|
+
tableName={config.type}
|
|
156
|
+
caption={caption}
|
|
157
|
+
stickyHeader
|
|
158
|
+
headContent={config.type === 'map' ? <MapHeader columns={columns} {...props} sortBy={sortBy} setSortBy={setSortBy} /> : <ChartHeader data={runtimeData} {...props} isVertical={isVertical} sortBy={sortBy} setSortBy={setSortBy} />}
|
|
159
|
+
tableOptions={{ className: `${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}${isVertical ? '' : ' horizontal'}`, 'aria-live': 'assertive', 'aria-rowcount': config?.data?.length ? config.data.length : -1, hidden: !expanded }}
|
|
160
|
+
/>
|
|
161
|
+
|
|
162
|
+
{/* REGION Data Table */}
|
|
163
|
+
{config.regions && config.regions.length > 0 && config.visualizationType !== 'Box Plot' && (
|
|
164
|
+
<Table
|
|
165
|
+
childrenMatrix={regionCellMatrix({ config })}
|
|
166
|
+
tableName={config.visualizationType}
|
|
167
|
+
caption='Table of the highlighted regions in the visualization'
|
|
168
|
+
headContent={
|
|
169
|
+
<tr>
|
|
170
|
+
<th>Region Name</th>
|
|
171
|
+
<th>Start Date</th>
|
|
172
|
+
<th>End Date</th>
|
|
173
|
+
</tr>
|
|
174
|
+
}
|
|
175
|
+
tableOptions={{ className: 'region-table data-table' }}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</section>
|
|
180
|
+
</ErrorBoundary>
|
|
181
|
+
)
|
|
182
|
+
} else {
|
|
183
|
+
// Render Data Table for Box Plots
|
|
184
|
+
return (
|
|
185
|
+
<ErrorBoundary component='DataTable'>
|
|
186
|
+
<section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
187
|
+
<SkipNav skipId={skipId} />
|
|
188
|
+
<ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
|
|
189
|
+
<div className='table-container' style={limitHeight}>
|
|
190
|
+
<Table
|
|
191
|
+
childrenMatrix={boxplotCellMatrix({ rows: tableData, config })}
|
|
192
|
+
tableName={config.visualizationType}
|
|
193
|
+
caption={caption}
|
|
194
|
+
stickyHeader
|
|
195
|
+
headContent={<BoxplotHeader categories={config.boxplot.categories} />}
|
|
196
|
+
tableOptions={{ className: `${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}`, 'aria-live': 'assertive', 'aria-rowcount': 11, hidden: !expanded }}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</section>
|
|
200
|
+
</ErrorBoundary>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default DataTable
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const BoxplotHeader = ({ categories }) => {
|
|
2
|
+
let columns = ['Measures', ...categories]
|
|
3
|
+
return (
|
|
4
|
+
<tr>
|
|
5
|
+
{columns.map(column => {
|
|
6
|
+
return (
|
|
7
|
+
<th key={`col-header-${column}`} tabIndex={0} title={column} role='columnheader' scope='col'>
|
|
8
|
+
{column}
|
|
9
|
+
</th>
|
|
10
|
+
)
|
|
11
|
+
})}
|
|
12
|
+
</tr>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default BoxplotHeader
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import ExternalIcon from '@cdc/core/assets/external-link.svg'
|
|
2
|
+
// Optionally wrap cell with anchor if config defines a navigation url
|
|
3
|
+
const CellAnchor = ({ markup, row, columns, navigationHandler, mapZoomHandler }) => {
|
|
4
|
+
if (columns.navigate && row[columns.navigate.name]) {
|
|
5
|
+
return (
|
|
6
|
+
<span
|
|
7
|
+
onClick={() => navigationHandler(row[columns.navigate.name])}
|
|
8
|
+
className='table-link'
|
|
9
|
+
title='Click for more information (Opens in a new window)'
|
|
10
|
+
role='link'
|
|
11
|
+
tabIndex={0}
|
|
12
|
+
onKeyDown={e => {
|
|
13
|
+
if (e.keyCode === 13) {
|
|
14
|
+
navigationHandler(row[columns.navigate.name])
|
|
15
|
+
}
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
{markup}
|
|
19
|
+
<ExternalIcon className='inline-icon' />
|
|
20
|
+
</span>
|
|
21
|
+
)
|
|
22
|
+
} else if (mapZoomHandler) {
|
|
23
|
+
return (
|
|
24
|
+
<span
|
|
25
|
+
onClick={mapZoomHandler}
|
|
26
|
+
className='table-link'
|
|
27
|
+
title='Click to view on map'
|
|
28
|
+
role='link'
|
|
29
|
+
tabIndex={0}
|
|
30
|
+
onKeyDown={e => {
|
|
31
|
+
if (e.keyCode === 13) {
|
|
32
|
+
mapZoomHandler()
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{markup}
|
|
37
|
+
</span>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return <>{markup}</>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default CellAnchor
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getChartCellValue } from '../helpers/getChartCellValue'
|
|
2
|
+
import { getSeriesName } from '../helpers/getSeriesName'
|
|
3
|
+
import { getDataSeriesColumns } from '../helpers/getDataSeriesColumns'
|
|
4
|
+
import { DownIcon, UpIcon } from './Icons'
|
|
5
|
+
|
|
6
|
+
type ChartHeaderProps = { data; isVertical; config; runtimeData; setSortBy; sortBy; groupBy? }
|
|
7
|
+
|
|
8
|
+
const ChartHeader = ({ data, isVertical, config, runtimeData, setSortBy, sortBy, groupBy }: ChartHeaderProps) => {
|
|
9
|
+
if (!data) return
|
|
10
|
+
let dataSeriesColumns = getDataSeriesColumns(config, isVertical, runtimeData)
|
|
11
|
+
if (groupBy) {
|
|
12
|
+
let groupHeaderRemoved = dataSeriesColumns.filter(col => col !== groupBy)
|
|
13
|
+
if (groupHeaderRemoved.length != dataSeriesColumns.length) {
|
|
14
|
+
// match was found
|
|
15
|
+
// assign headers with groupHeaderRemoved
|
|
16
|
+
dataSeriesColumns = groupHeaderRemoved
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (isVertical) {
|
|
20
|
+
return (
|
|
21
|
+
<tr>
|
|
22
|
+
{dataSeriesColumns.map((column, index) => {
|
|
23
|
+
const text = getSeriesName(column, config)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<th
|
|
27
|
+
key={`col-header-${column}__${index}`}
|
|
28
|
+
tabIndex={0}
|
|
29
|
+
title={text}
|
|
30
|
+
role='columnheader'
|
|
31
|
+
scope='col'
|
|
32
|
+
onClick={() => {
|
|
33
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false, colIndex: index })
|
|
34
|
+
}}
|
|
35
|
+
onKeyDown={e => {
|
|
36
|
+
if (e.keyCode === 13) {
|
|
37
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false, colIndex: index })
|
|
38
|
+
}
|
|
39
|
+
}}
|
|
40
|
+
className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
41
|
+
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
42
|
+
>
|
|
43
|
+
{text}
|
|
44
|
+
{column === sortBy.column && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
|
|
45
|
+
<button>
|
|
46
|
+
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
47
|
+
</button>
|
|
48
|
+
</th>
|
|
49
|
+
)
|
|
50
|
+
})}
|
|
51
|
+
</tr>
|
|
52
|
+
)
|
|
53
|
+
} else {
|
|
54
|
+
const sliceVal = config.visualizationType === 'Pie' ? 1 : 0
|
|
55
|
+
return (
|
|
56
|
+
<tr>
|
|
57
|
+
{['__series__', ...Object.keys(runtimeData)].slice(sliceVal).map((row, index) => {
|
|
58
|
+
let column = config.xAxis?.dataKey
|
|
59
|
+
let text = row !== '__series__' ? getChartCellValue(row, column, config, runtimeData) : '__series__'
|
|
60
|
+
return (
|
|
61
|
+
<th
|
|
62
|
+
key={`col-header-${text}__${index}`}
|
|
63
|
+
tabIndex={0}
|
|
64
|
+
title={text}
|
|
65
|
+
role='columnheader'
|
|
66
|
+
scope='col'
|
|
67
|
+
onClick={() => {
|
|
68
|
+
setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
|
|
69
|
+
}}
|
|
70
|
+
onKeyDown={e => {
|
|
71
|
+
if (e.keyCode === 13) {
|
|
72
|
+
setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
className={sortBy.column === text ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
76
|
+
{...(sortBy.column === text ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
77
|
+
>
|
|
78
|
+
{text === '__series__' ? '' : text}
|
|
79
|
+
{index === sortBy.colIndex && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
|
|
80
|
+
<button>
|
|
81
|
+
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === text ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
82
|
+
</button>
|
|
83
|
+
</th>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</tr>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default ChartHeader
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
2
|
+
|
|
3
|
+
const ExpandCollapse = ({ expanded, setExpanded, tableTitle }) => (
|
|
4
|
+
<div
|
|
5
|
+
className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
6
|
+
onClick={() => {
|
|
7
|
+
setExpanded(!expanded)
|
|
8
|
+
}}
|
|
9
|
+
tabIndex={0}
|
|
10
|
+
onKeyDown={e => {
|
|
11
|
+
if (e.keyCode === 13) {
|
|
12
|
+
setExpanded(!expanded)
|
|
13
|
+
}
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
<Icon display={expanded ? 'minus' : 'plus'} base />
|
|
17
|
+
{tableTitle}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export default ExpandCollapse
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const UpIcon = () => (
|
|
2
|
+
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
3
|
+
<path d='M0 5l5-5 5 5z' />
|
|
4
|
+
</svg>
|
|
5
|
+
)
|
|
6
|
+
export const DownIcon = () => (
|
|
7
|
+
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
8
|
+
<path d='M0 0l5 5 5-5z' />
|
|
9
|
+
</svg>
|
|
10
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DataTableProps } from '../DataTable'
|
|
2
|
+
import { DownIcon, UpIcon } from './Icons'
|
|
3
|
+
|
|
4
|
+
type MapHeaderProps = DataTableProps & {
|
|
5
|
+
sortBy: { column; asc }
|
|
6
|
+
setSortBy: Function
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeaderProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<tr>
|
|
12
|
+
{Object.keys(columns)
|
|
13
|
+
.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
14
|
+
.map((column, index) => {
|
|
15
|
+
let text
|
|
16
|
+
if (column !== 'geo') {
|
|
17
|
+
text = columns[column].label ? columns[column].label : columns[column].name
|
|
18
|
+
} else {
|
|
19
|
+
text = config.type === 'map' ? indexTitle : config.xAxis?.dataKey
|
|
20
|
+
}
|
|
21
|
+
if (config.type === 'map' && (text === undefined || text === '')) {
|
|
22
|
+
text = 'Location'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<th
|
|
27
|
+
key={`col-header-${column}__${index}`}
|
|
28
|
+
id={column}
|
|
29
|
+
tabIndex={0}
|
|
30
|
+
title={text}
|
|
31
|
+
role='columnheader'
|
|
32
|
+
scope='col'
|
|
33
|
+
onClick={() => {
|
|
34
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
35
|
+
}}
|
|
36
|
+
onKeyDown={e => {
|
|
37
|
+
if (e.keyCode === 13) {
|
|
38
|
+
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
39
|
+
}
|
|
40
|
+
}}
|
|
41
|
+
className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
|
|
42
|
+
{...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
|
|
43
|
+
>
|
|
44
|
+
{text}
|
|
45
|
+
{sortBy.column === column && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
|
|
46
|
+
<button>
|
|
47
|
+
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
|
|
48
|
+
</button>
|
|
49
|
+
</th>
|
|
50
|
+
)
|
|
51
|
+
})}
|
|
52
|
+
</tr>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default MapHeader
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CellMatrix } from '../../Table/types/CellMatrix'
|
|
2
|
+
|
|
3
|
+
const boxplotCellMatrix = ({ rows, config }): CellMatrix => {
|
|
4
|
+
const resolveName = key => {
|
|
5
|
+
let {
|
|
6
|
+
boxplot: { labels }
|
|
7
|
+
} = config
|
|
8
|
+
const columnLookup = {
|
|
9
|
+
columnMean: labels.mean,
|
|
10
|
+
columnMax: labels.maximum,
|
|
11
|
+
columnMin: labels.minimum,
|
|
12
|
+
columnIqr: labels.iqr,
|
|
13
|
+
columnCategory: 'Category',
|
|
14
|
+
columnMedian: labels.median,
|
|
15
|
+
columnFirstQuartile: labels.q1,
|
|
16
|
+
columnThirdQuartile: labels.q3,
|
|
17
|
+
columnOutliers: labels.outliers,
|
|
18
|
+
values: labels.values,
|
|
19
|
+
columnTotal: labels.total,
|
|
20
|
+
columnSd: 'Standard Deviation',
|
|
21
|
+
nonOutlierValues: 'Non Outliers',
|
|
22
|
+
columnLowerBounds: labels.lowerBounds,
|
|
23
|
+
columnUpperBounds: labels.upperBounds
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let resolvedName = columnLookup[key]
|
|
27
|
+
|
|
28
|
+
return resolvedName
|
|
29
|
+
}
|
|
30
|
+
let resolveCell = (rowid, plot) => {
|
|
31
|
+
if (Number(rowid) === 0) return true
|
|
32
|
+
if (Number(rowid) === 1) return plot.columnMax
|
|
33
|
+
if (Number(rowid) === 2) return plot.columnThirdQuartile
|
|
34
|
+
if (Number(rowid) === 3) return plot.columnMedian
|
|
35
|
+
if (Number(rowid) === 4) return plot.columnFirstQuartile
|
|
36
|
+
if (Number(rowid) === 5) return plot.columnMin
|
|
37
|
+
if (Number(rowid) === 6) return plot.columnTotal
|
|
38
|
+
if (Number(rowid) === 7) return plot.columnSd
|
|
39
|
+
if (Number(rowid) === 8) return plot.columnMean
|
|
40
|
+
if (Number(rowid) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
|
|
41
|
+
if (Number(rowid) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
|
|
42
|
+
return <p>-</p>
|
|
43
|
+
}
|
|
44
|
+
// get list of data keys for each row
|
|
45
|
+
let dataKeys = rows.map(row => {
|
|
46
|
+
return row[0]
|
|
47
|
+
})
|
|
48
|
+
let columns = ['Measures', ...config.boxplot.categories]
|
|
49
|
+
dataKeys.shift() // remove index 0 // we did header column separately
|
|
50
|
+
return dataKeys.map((rowkey, index) => {
|
|
51
|
+
return columns.map((column, colnum) => {
|
|
52
|
+
let cellValue
|
|
53
|
+
if (column === 'Measures') {
|
|
54
|
+
let labelValue = index > 0 ? resolveName(rowkey) : ''
|
|
55
|
+
cellValue = <>{labelValue}</>
|
|
56
|
+
} else {
|
|
57
|
+
cellValue = resolveCell(index, config.boxplot.plots[colnum - 1])
|
|
58
|
+
}
|
|
59
|
+
return cellValue
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default boxplotCellMatrix
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
2
|
+
import { customSort } from './customSort'
|
|
3
|
+
import { getSeriesName } from './getSeriesName'
|
|
4
|
+
import { DataTableProps } from '../DataTable'
|
|
5
|
+
import { getChartCellValue } from './getChartCellValue'
|
|
6
|
+
import { getDataSeriesColumns } from './getDataSeriesColumns'
|
|
7
|
+
import { ReactNode } from 'react'
|
|
8
|
+
import { CellMatrix, GroupCellMatrix } from '../../Table/types/CellMatrix'
|
|
9
|
+
|
|
10
|
+
type ChartRowsProps = DataTableProps & {
|
|
11
|
+
rows: string[]
|
|
12
|
+
isVertical: boolean
|
|
13
|
+
sortBy: { colIndex; column }
|
|
14
|
+
groupBy?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorScale, groupBy }: ChartRowsProps): CellMatrix | GroupCellMatrix => {
|
|
18
|
+
const dataSeriesColumns = getDataSeriesColumns(config, isVertical, runtimeData)
|
|
19
|
+
|
|
20
|
+
const dataSeriesColumnsSorted = () => {
|
|
21
|
+
if (!sortBy && sortBy.colIndex === null) return dataSeriesColumns
|
|
22
|
+
return dataSeriesColumns.sort((a, b) => {
|
|
23
|
+
if (sortBy.column === '__series__') return customSort(a, b, sortBy, config)
|
|
24
|
+
let row = runtimeData.find(d => d[config.xAxis?.dataKey] === sortBy.column)
|
|
25
|
+
|
|
26
|
+
const rowIndex = runtimeData[sortBy.colIndex - 1]
|
|
27
|
+
if (row) {
|
|
28
|
+
return customSort(row[a], row[b], sortBy, config)
|
|
29
|
+
}
|
|
30
|
+
if (row === undefined && rowIndex) {
|
|
31
|
+
return customSort(rowIndex[a], rowIndex[b], sortBy, config)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isVertical) {
|
|
37
|
+
if (groupBy) {
|
|
38
|
+
const cellMatrix: GroupCellMatrix = {}
|
|
39
|
+
rows.forEach(row => {
|
|
40
|
+
let groupKey: string
|
|
41
|
+
let groupValues = []
|
|
42
|
+
dataSeriesColumns.forEach((column, j) => {
|
|
43
|
+
if (groupBy === column) {
|
|
44
|
+
groupKey = getChartCellValue(row, column, config, runtimeData)
|
|
45
|
+
} else {
|
|
46
|
+
groupValues.push(getChartCellValue(row, column, config, runtimeData))
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
if (!cellMatrix[groupKey]) {
|
|
50
|
+
cellMatrix[groupKey] = [groupValues]
|
|
51
|
+
} else {
|
|
52
|
+
cellMatrix[groupKey].push(groupValues)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
return cellMatrix
|
|
56
|
+
} else {
|
|
57
|
+
return rows.map(row => {
|
|
58
|
+
return dataSeriesColumns.map((column, j) => getChartCellValue(row, column, config, runtimeData))
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
return dataSeriesColumnsSorted().map(column => {
|
|
63
|
+
const seriesName = getSeriesName(column, config)
|
|
64
|
+
let nodes: ReactNode[] =
|
|
65
|
+
config.visualizationType !== 'Pie'
|
|
66
|
+
? [
|
|
67
|
+
<>
|
|
68
|
+
{colorScale && colorScale(seriesName) && <LegendCircle fill={colorScale(seriesName)} />}
|
|
69
|
+
{seriesName}
|
|
70
|
+
</>
|
|
71
|
+
]
|
|
72
|
+
: []
|
|
73
|
+
return nodes.concat(rows.map((row, i) => getChartCellValue(row, column, config, runtimeData)))
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default chartCellArray
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parseDate } from '@cdc/core/helpers/cove/date'
|
|
2
|
+
import { standardizeStateName } from './standardizeState'
|
|
3
|
+
|
|
4
|
+
export const customSort = (a, b, sortBy, config) => {
|
|
5
|
+
let valueA = a
|
|
6
|
+
let valueB = b
|
|
7
|
+
|
|
8
|
+
if (config.type === 'map') {
|
|
9
|
+
valueA = standardizeStateName(a)
|
|
10
|
+
valueB = standardizeStateName(b)
|
|
11
|
+
}
|
|
12
|
+
// Treat booleans and nulls as an empty string
|
|
13
|
+
valueA = valueA === false || valueA === true || valueA === null ? '' : valueA
|
|
14
|
+
valueB = valueB === false || valueB === true || valueB === null ? '' : valueB
|
|
15
|
+
|
|
16
|
+
const trimmedA = String(valueA).trim()
|
|
17
|
+
const trimmedB = String(valueB).trim()
|
|
18
|
+
|
|
19
|
+
if (config.xAxis?.dataKey === sortBy.column && config.xAxis.type === 'date') {
|
|
20
|
+
let dateA = parseDate(config.xAxis.dateParseFormat, trimmedA)
|
|
21
|
+
|
|
22
|
+
let dateB = parseDate(config.xAxis.dateParseFormat, trimmedB)
|
|
23
|
+
|
|
24
|
+
if (dateA && dateA.getTime) dateA = dateA.getTime()
|
|
25
|
+
|
|
26
|
+
if (dateB && dateB.getTime) dateB = dateB.getTime()
|
|
27
|
+
|
|
28
|
+
return !sortBy.asc ? dateA - dateB : dateB - dateA
|
|
29
|
+
}
|
|
30
|
+
// Check if values are numbers
|
|
31
|
+
const isNumA = !isNaN(Number(valueA)) && valueA !== undefined && valueA !== null && trimmedA !== ''
|
|
32
|
+
const isNumB = !isNaN(Number(valueB)) && valueB !== undefined && valueB !== null && trimmedB !== ''
|
|
33
|
+
|
|
34
|
+
// Handle empty strings or spaces
|
|
35
|
+
if (trimmedA === '' && trimmedB !== '') return !sortBy.asc ? -1 : 1
|
|
36
|
+
if (trimmedA !== '' && trimmedB === '') return !sortBy.asc ? 1 : -1
|
|
37
|
+
|
|
38
|
+
// Both are numbers: Compare numerically
|
|
39
|
+
if (isNumA && isNumB) {
|
|
40
|
+
return !sortBy.asc ? Number(valueA) - Number(valueB) : Number(valueB) - Number(valueA)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Only A is a number
|
|
44
|
+
if (isNumA) {
|
|
45
|
+
return !sortBy.asc ? -1 : 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Only B is a number
|
|
49
|
+
if (isNumB) {
|
|
50
|
+
return !sortBy.asc ? 1 : -1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Neither are numbers: Compare as strings
|
|
54
|
+
return !sortBy.asc ? trimmedA.localeCompare(trimmedB) : trimmedB.localeCompare(trimmedA)
|
|
55
|
+
}
|