@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.
Files changed (52) hide show
  1. package/assets/icon-deviation-bar.svg +1 -0
  2. package/components/DataTable/DataTable.tsx +205 -0
  3. package/components/DataTable/components/BoxplotHeader.tsx +16 -0
  4. package/components/DataTable/components/CellAnchor.tsx +44 -0
  5. package/components/DataTable/components/ChartHeader.tsx +91 -0
  6. package/components/DataTable/components/ExpandCollapse.tsx +21 -0
  7. package/components/DataTable/components/Icons.tsx +10 -0
  8. package/components/DataTable/components/MapHeader.tsx +56 -0
  9. package/components/DataTable/components/SkipNav.tsx +7 -0
  10. package/components/DataTable/helpers/boxplotCellMatrix.tsx +64 -0
  11. package/components/DataTable/helpers/chartCellMatrix.tsx +78 -0
  12. package/components/DataTable/helpers/customSort.ts +55 -0
  13. package/components/DataTable/helpers/getChartCellValue.ts +55 -0
  14. package/components/DataTable/helpers/getDataSeriesColumns.ts +28 -0
  15. package/components/DataTable/helpers/getSeriesName.ts +26 -0
  16. package/components/DataTable/helpers/mapCellMatrix.tsx +56 -0
  17. package/components/DataTable/helpers/regionCellMatrix.tsx +13 -0
  18. package/components/DataTable/helpers/standardizeState.js +76 -0
  19. package/components/DataTable/index.ts +1 -0
  20. package/components/DataTable/types/TableConfig.ts +57 -0
  21. package/components/DownloadButton.tsx +29 -0
  22. package/components/LegendCircle.jsx +2 -2
  23. package/components/Table/Table.tsx +49 -0
  24. package/components/Table/components/Cell.tsx +9 -0
  25. package/components/Table/components/GroupRow.tsx +16 -0
  26. package/components/Table/components/Row.tsx +19 -0
  27. package/components/Table/index.ts +1 -0
  28. package/components/Table/types/CellMatrix.ts +4 -0
  29. package/components/_stories/DataTable.stories.tsx +62 -0
  30. package/components/_stories/Table.stories.tsx +53 -0
  31. package/components/_stories/_mocks/dashboard_no_filter.json +121 -0
  32. package/components/_stories/_mocks/example-city-state.json +808 -0
  33. package/components/_stories/styles.scss +9 -0
  34. package/components/managers/{DataDesigner.jsx → DataDesigner.tsx} +96 -87
  35. package/components/ui/Title/Title.scss +95 -0
  36. package/components/ui/Title/index.tsx +34 -0
  37. package/components/ui/_stories/Title.stories.tsx +21 -0
  38. package/helpers/DataTransform.ts +41 -18
  39. package/helpers/cove/string.ts +11 -0
  40. package/package.json +2 -2
  41. package/styles/_data-table.scss +1 -0
  42. package/styles/heading-colors.scss +0 -3
  43. package/styles/v2/layout/_component.scss +0 -11
  44. package/types/Axis.ts +6 -0
  45. package/types/Color.ts +5 -0
  46. package/types/ComponentStyles.ts +7 -0
  47. package/types/ComponentThemes.ts +13 -0
  48. package/types/EditorColumnProperties.ts +8 -0
  49. package/types/Runtime.ts +9 -0
  50. package/types/Series.ts +1 -0
  51. package/types/Visualization.ts +21 -0
  52. 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,7 @@
1
+ const SkipNav = skipId => (
2
+ <a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
3
+ Skip Navigation or Skip to Content
4
+ </a>
5
+ )
6
+
7
+ export default SkipNav
@@ -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
+ }