@cdc/map 2.6.0 → 2.6.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/convert-topojson.js +70 -0
- package/dist/cdcmap.js +190 -0
- package/examples/default-county.json +105 -0
- package/examples/default-single-state.json +109 -0
- package/examples/default-usa.json +968 -0
- package/examples/default-world.json +1495 -0
- package/examples/example-city-state.json +474 -0
- package/examples/example-world-map.json +1596 -0
- package/examples/gender-rate-map.json +1 -0
- package/package.json +50 -50
- package/src/CdcMap.js +1384 -0
- package/src/components/CityList.js +93 -0
- package/src/components/CountyMap.js +556 -0
- package/src/components/DataTable.js +357 -0
- package/src/components/EditorPanel.js +2111 -0
- package/src/components/Geo.js +21 -0
- package/src/components/Modal.js +31 -0
- package/src/components/NavigationMenu.js +66 -0
- package/src/components/Sidebar.js +167 -0
- package/src/components/SingleStateMap.js +326 -0
- package/src/components/UsaMap.js +342 -0
- package/src/components/WorldMap.js +175 -0
- package/src/components/ZoomableGroup.js +47 -0
- package/src/data/abbreviations.js +57 -0
- package/src/data/color-palettes.js +200 -0
- package/src/data/county-map-halfquality.json +58453 -0
- package/src/data/county-map-quarterquality.json +1 -0
- package/src/data/county-topo.json +1 -0
- package/src/data/dfc-map.json +1 -0
- package/src/data/initial-state.js +60 -0
- package/src/data/newtest.json +1 -0
- package/src/data/state-abbreviations.js +60 -0
- package/src/data/supported-geos.js +3775 -0
- package/src/data/test.json +1 -0
- package/src/data/us-hex-topo.json +1 -0
- package/src/data/us-topo.json +1 -0
- package/src/data/world-topo.json +1 -0
- package/src/hooks/useActiveElement.js +19 -0
- package/src/hooks/useZoomPan.js +110 -0
- package/src/images/active-checkmark.svg +1 -0
- package/src/images/asc.svg +1 -0
- package/src/images/close.svg +1 -0
- package/src/images/desc.svg +1 -0
- package/src/images/external-link.svg +1 -0
- package/src/images/icon-download-img.svg +1 -0
- package/src/images/icon-download-pdf.svg +1 -0
- package/src/images/inactive-checkmark.svg +1 -0
- package/src/images/map-folded.svg +1 -0
- package/src/index.html +29 -0
- package/src/index.js +20 -0
- package/src/scss/btn.scss +69 -0
- package/src/scss/datatable.scss +7 -0
- package/src/scss/editor-panel.scss +654 -0
- package/src/scss/main.scss +224 -0
- package/src/scss/map.scss +188 -0
- package/src/scss/sidebar.scss +146 -0
- package/src/scss/tooltips.scss +30 -0
- package/src/scss/variables.scss +1 -0
- package/uploads/upload-example-city-state.json +392 -0
- package/uploads/upload-example-world-data.json +1490 -0
- package/LICENSE +0 -201
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect, useState, useMemo, memo, useCallback
|
|
3
|
+
} from 'react';
|
|
4
|
+
import {
|
|
5
|
+
useTable, useSortBy, useResizeColumns, useBlockLayout
|
|
6
|
+
} from 'react-table';
|
|
7
|
+
import Papa from 'papaparse';
|
|
8
|
+
import ExternalIcon from '../images/external-link.svg';
|
|
9
|
+
|
|
10
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
|
|
11
|
+
import LegendCircle from '@cdc/core/components/LegendCircle';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import Loading from '@cdc/core/components/Loading';
|
|
15
|
+
|
|
16
|
+
const DataTable = (props) => {
|
|
17
|
+
const {
|
|
18
|
+
state,
|
|
19
|
+
tableTitle,
|
|
20
|
+
indexTitle,
|
|
21
|
+
mapTitle,
|
|
22
|
+
rawData,
|
|
23
|
+
showDownloadButton,
|
|
24
|
+
runtimeData,
|
|
25
|
+
runtimeLegend,
|
|
26
|
+
headerColor,
|
|
27
|
+
expandDataTable,
|
|
28
|
+
columns,
|
|
29
|
+
displayDataAsText,
|
|
30
|
+
applyLegendToRow,
|
|
31
|
+
displayGeoName,
|
|
32
|
+
navigationHandler,
|
|
33
|
+
viewport
|
|
34
|
+
} = props;
|
|
35
|
+
|
|
36
|
+
const [expanded, setExpanded] = useState(expandDataTable);
|
|
37
|
+
|
|
38
|
+
const [accessibilityLabel, setAccessibilityLabel] = useState('');
|
|
39
|
+
|
|
40
|
+
const [ready, setReady] = useState(false)
|
|
41
|
+
|
|
42
|
+
const fileName = `${mapTitle}.csv`;
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// Catch all sorting method used on load by default but also on user click
|
|
46
|
+
// Having a custom method means we can add in any business logic we want going forward
|
|
47
|
+
const customSort = useCallback((a, b) => {
|
|
48
|
+
const digitRegex = /\d+/;
|
|
49
|
+
|
|
50
|
+
const hasNumber = (value) => digitRegex.test(value);
|
|
51
|
+
|
|
52
|
+
// force null and undefined to the bottom
|
|
53
|
+
a = a === null || a === undefined ? '' : a;
|
|
54
|
+
b = b === null || b === undefined ? '' : b;
|
|
55
|
+
|
|
56
|
+
// convert any strings that are actually numbers to proper data type
|
|
57
|
+
const aNum = Number(a);
|
|
58
|
+
|
|
59
|
+
if (!Number.isNaN(aNum)) {
|
|
60
|
+
a = aNum;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bNum = Number(b);
|
|
64
|
+
|
|
65
|
+
if (!Number.isNaN(bNum)) {
|
|
66
|
+
b = bNum;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// remove iso code prefixes
|
|
70
|
+
if (typeof a === 'string') {
|
|
71
|
+
a = a.replace('us-', '');
|
|
72
|
+
a = displayGeoName(a);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof b === 'string') {
|
|
76
|
+
b = b.replace('us-', '');
|
|
77
|
+
b = displayGeoName(b);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// force any string values to lowercase
|
|
81
|
+
a = typeof a === 'string' ? a.toLowerCase() : a;
|
|
82
|
+
b = typeof b === 'string' ? b.toLowerCase() : b;
|
|
83
|
+
|
|
84
|
+
// If the string contains a number, remove the text from the value and only sort by the number. Only uses the first number it finds.
|
|
85
|
+
if (typeof a === 'string' && hasNumber(a) === true) {
|
|
86
|
+
a = a.match(digitRegex)[0];
|
|
87
|
+
|
|
88
|
+
a = Number(a);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof b === 'string' && hasNumber(b) === true) {
|
|
92
|
+
b = b.match(digitRegex)[0];
|
|
93
|
+
|
|
94
|
+
b = Number(b);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// When comparing a number to a string, always send string to bottom
|
|
98
|
+
if (typeof a === 'number' && typeof b === 'string') {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof b === 'number' && typeof a === 'string') {
|
|
103
|
+
return -1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Return either 1 or -1 to indicate a sort priority
|
|
107
|
+
if (a > b) {
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
if (a < b) {
|
|
111
|
+
return -1;
|
|
112
|
+
}
|
|
113
|
+
// returning 0, undefined or any falsey value will use subsequent sorts or
|
|
114
|
+
// the index as a tiebreaker
|
|
115
|
+
return 0;
|
|
116
|
+
}, [displayGeoName]);
|
|
117
|
+
|
|
118
|
+
// Optionally wrap cell with anchor if config defines a navigation url
|
|
119
|
+
const getCellAnchor = useCallback((markup, row) => {
|
|
120
|
+
if (columns.navigate && row[columns.navigate.name]) {
|
|
121
|
+
markup = (
|
|
122
|
+
<span
|
|
123
|
+
onClick={() => navigationHandler(row[columns.navigate.name])}
|
|
124
|
+
className="table-link"
|
|
125
|
+
title="Click for more information (Opens in a new window)"
|
|
126
|
+
role="link"
|
|
127
|
+
tabIndex="0"
|
|
128
|
+
onKeyDown={(e) => {
|
|
129
|
+
if (e.keyCode === 13) {
|
|
130
|
+
navigationHandler(row[columns.navigate.name]);
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{markup}
|
|
135
|
+
<ExternalIcon className="inline-icon" />
|
|
136
|
+
</span>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return markup;
|
|
141
|
+
}, [columns.navigate, navigationHandler]);
|
|
142
|
+
|
|
143
|
+
const DownloadButton = memo(() => {
|
|
144
|
+
const csvData = Papa.unparse(rawData);
|
|
145
|
+
|
|
146
|
+
const blob = new Blob([csvData], {type: "text/csv;charset=utf-8;"});
|
|
147
|
+
|
|
148
|
+
const saveBlob = () => {
|
|
149
|
+
//@ts-ignore
|
|
150
|
+
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
151
|
+
//@ts-ignore
|
|
152
|
+
navigator.msSaveBlob(blob, fileName);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<a
|
|
158
|
+
download={fileName}
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={saveBlob}
|
|
161
|
+
href={URL.createObjectURL(blob)}
|
|
162
|
+
aria-label="Download this data in a CSV file format."
|
|
163
|
+
className={`${headerColor} btn btn-download no-border`}
|
|
164
|
+
id={`${skipId}`}
|
|
165
|
+
data-html2canvas-ignore
|
|
166
|
+
role="button"
|
|
167
|
+
tabIndex="-1"
|
|
168
|
+
>
|
|
169
|
+
Download Data (CSV)
|
|
170
|
+
</a>
|
|
171
|
+
)
|
|
172
|
+
}, [rawData]);
|
|
173
|
+
|
|
174
|
+
// Creates columns structure for the table
|
|
175
|
+
const tableColumns = useMemo(() => {
|
|
176
|
+
const newTableColumns = [];
|
|
177
|
+
|
|
178
|
+
Object.keys(columns).forEach((column) => {
|
|
179
|
+
if (columns[column].dataTable === true && '' !== columns[column].name) {
|
|
180
|
+
const newCol = {
|
|
181
|
+
Header: columns[column].label || columns[column].name,
|
|
182
|
+
id: column,
|
|
183
|
+
accessor: (row) => {
|
|
184
|
+
if (runtimeData) {
|
|
185
|
+
if(state.legend.specialClasses && state.legend.specialClasses.length && typeof state.legend.specialClasses[0] === 'object'){
|
|
186
|
+
for(let i = 0; i < state.legend.specialClasses.length; i++){
|
|
187
|
+
if(String(runtimeData[row][state.legend.specialClasses[i].key]) === state.legend.specialClasses[i].value){
|
|
188
|
+
return state.legend.specialClasses[i].label;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return runtimeData[row][columns[column].name] ?? null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
},
|
|
197
|
+
sortType: (a, b) => customSort(a.values[column], b.values[column])
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (column === 'geo') {
|
|
201
|
+
newCol.Header = indexTitle || 'Location';
|
|
202
|
+
newCol.Cell = ({ row, value }) => {
|
|
203
|
+
const rowObj = runtimeData[row.original];
|
|
204
|
+
|
|
205
|
+
const legendColor = applyLegendToRow(rowObj);
|
|
206
|
+
|
|
207
|
+
let labelValue = displayGeoName(row.original);
|
|
208
|
+
|
|
209
|
+
labelValue = getCellAnchor(labelValue, rowObj);
|
|
210
|
+
|
|
211
|
+
const cellMarkup = (
|
|
212
|
+
<>
|
|
213
|
+
<LegendCircle fill={legendColor[0]} />
|
|
214
|
+
{labelValue}
|
|
215
|
+
</>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return cellMarkup;
|
|
219
|
+
};
|
|
220
|
+
} else {
|
|
221
|
+
newCol.Cell = ({ value }) => {
|
|
222
|
+
const cellMarkup = displayDataAsText(value, column);
|
|
223
|
+
|
|
224
|
+
return (cellMarkup);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
newTableColumns.push(newCol);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return newTableColumns;
|
|
233
|
+
}, [indexTitle, columns, runtimeData, runtimeLegend]);
|
|
234
|
+
|
|
235
|
+
const tableData = useMemo(
|
|
236
|
+
() => Object.keys(runtimeData).filter((key) => applyLegendToRow(runtimeData[key])).sort((a, b) => customSort(a, b)),
|
|
237
|
+
[runtimeLegend, runtimeData, applyLegendToRow, customSort]
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Change accessibility label depending on expanded status
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
const expandedLabel = 'Accessible data table.';
|
|
243
|
+
const collapsedLabel = 'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.';
|
|
244
|
+
|
|
245
|
+
if (expanded === true && accessibilityLabel !== expandedLabel) {
|
|
246
|
+
setAccessibilityLabel(expandedLabel);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (expanded === false && accessibilityLabel !== collapsedLabel) {
|
|
250
|
+
setAccessibilityLabel(collapsedLabel);
|
|
251
|
+
}
|
|
252
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
253
|
+
}, [expanded, applyLegendToRow, customSort]);
|
|
254
|
+
|
|
255
|
+
const defaultColumn = useMemo(
|
|
256
|
+
() => ({
|
|
257
|
+
minWidth: 150,
|
|
258
|
+
width: 200,
|
|
259
|
+
maxWidth: 400,
|
|
260
|
+
}),
|
|
261
|
+
[]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const mapLookup = {
|
|
265
|
+
'us-county': 'United States County Map',
|
|
266
|
+
'single-state': 'State Map',
|
|
267
|
+
'us': 'United States Map',
|
|
268
|
+
'world': 'World Map'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const {
|
|
272
|
+
getTableProps,
|
|
273
|
+
getTableBodyProps,
|
|
274
|
+
headerGroups,
|
|
275
|
+
rows,
|
|
276
|
+
prepareRow,
|
|
277
|
+
} = useTable({ columns: tableColumns, data: tableData, defaultColumn }, useSortBy, useBlockLayout, useResizeColumns);
|
|
278
|
+
|
|
279
|
+
const rand = Math.random().toString(16).substr(2, 8);
|
|
280
|
+
const skipId = `btn__${rand}`
|
|
281
|
+
|
|
282
|
+
if(!state.data) return <Loading />
|
|
283
|
+
return (
|
|
284
|
+
<ErrorBoundary component="DataTable">
|
|
285
|
+
<section id={state.general.title ? `dataTableSection__${state.general.title.replace(/\s/g, '')}` : `dataTableSection`} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
|
|
286
|
+
<a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
|
|
287
|
+
Skip Navigation or Skip to Content
|
|
288
|
+
</a>
|
|
289
|
+
<div
|
|
290
|
+
className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
291
|
+
onClick={() => { setExpanded(!expanded); }}
|
|
292
|
+
tabIndex="0"
|
|
293
|
+
onKeyDown={(e) => { if (e.keyCode === 13) { setExpanded(!expanded); } }}
|
|
294
|
+
>
|
|
295
|
+
|
|
296
|
+
{tableTitle}
|
|
297
|
+
</div>
|
|
298
|
+
<div
|
|
299
|
+
className="table-container"
|
|
300
|
+
style={ { maxHeight: state.dataTable.limitHeight && `${state.dataTable.height}px`, overflowY: 'scroll' } }
|
|
301
|
+
>
|
|
302
|
+
<table
|
|
303
|
+
height={expanded ? null : 0} {...getTableProps()}
|
|
304
|
+
aria-live="assertive"
|
|
305
|
+
className={expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}
|
|
306
|
+
hidden={!expanded}
|
|
307
|
+
aria-rowcount={state?.data.length ? state.data.length : '-1' }
|
|
308
|
+
>
|
|
309
|
+
<caption className='cdcdataviz-sr-only'>{state.dataTable.caption ? state.dataTable.caption : `Datatable showing data for the ${mapLookup[state.general.geoType]} figure.`}</caption>
|
|
310
|
+
<thead style={{position: 'sticky', top: 0, zIndex: 999}}>
|
|
311
|
+
{headerGroups.map((headerGroup) => (
|
|
312
|
+
<tr {...headerGroup.getHeaderGroupProps()}>
|
|
313
|
+
{headerGroup.headers.map((column) => (
|
|
314
|
+
<th tabIndex="0"
|
|
315
|
+
title={column.Header}
|
|
316
|
+
role="columnheader"
|
|
317
|
+
scope="col"
|
|
318
|
+
{...column.getHeaderProps(column.getSortByToggleProps())}
|
|
319
|
+
className={column.isSorted ? column.isSortedDesc ? 'sort sort-desc' : 'sort sort-asc' : 'sort'}
|
|
320
|
+
onKeyDown={(e) => { if (e.keyCode === 13) { column.toggleSortBy(); } }}
|
|
321
|
+
//aria-sort={column.isSorted ? column.isSortedDesc ? 'descending' : 'ascending' : 'none' }
|
|
322
|
+
{...(column.isSorted ? column.isSortedDesc ? { 'aria-sort': 'descending' } : { 'aria-sort': 'ascending' } : null)}
|
|
323
|
+
|
|
324
|
+
>
|
|
325
|
+
{column.render('Header')}
|
|
326
|
+
<button>
|
|
327
|
+
<span className="cdcdataviz-sr-only">{`Sort by ${(column.render('Header')).toLowerCase() } in ${ column.isSorted ? column.isSortedDesc ? 'descending' : 'ascending' : 'no'} `} order</span>
|
|
328
|
+
</button>
|
|
329
|
+
<div {...column.getResizerProps()} className="resizer" />
|
|
330
|
+
</th>
|
|
331
|
+
))}
|
|
332
|
+
</tr>
|
|
333
|
+
))}
|
|
334
|
+
</thead>
|
|
335
|
+
<tbody {...getTableBodyProps()}>
|
|
336
|
+
{rows.map((row) => {
|
|
337
|
+
prepareRow(row);
|
|
338
|
+
return (
|
|
339
|
+
<tr {...row.getRowProps()} role="row">
|
|
340
|
+
{row.cells.map((cell) => (
|
|
341
|
+
<td tabIndex="0" {...cell.getCellProps()} role="gridcell">
|
|
342
|
+
{cell.render('Cell')}
|
|
343
|
+
</td>
|
|
344
|
+
))}
|
|
345
|
+
</tr>
|
|
346
|
+
);
|
|
347
|
+
})}
|
|
348
|
+
</tbody>
|
|
349
|
+
</table>
|
|
350
|
+
</div>
|
|
351
|
+
{showDownloadButton === true && <DownloadButton />}
|
|
352
|
+
</section>
|
|
353
|
+
</ErrorBoundary>
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export default DataTable;
|