@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.
Files changed (61) hide show
  1. package/convert-topojson.js +70 -0
  2. package/dist/cdcmap.js +190 -0
  3. package/examples/default-county.json +105 -0
  4. package/examples/default-single-state.json +109 -0
  5. package/examples/default-usa.json +968 -0
  6. package/examples/default-world.json +1495 -0
  7. package/examples/example-city-state.json +474 -0
  8. package/examples/example-world-map.json +1596 -0
  9. package/examples/gender-rate-map.json +1 -0
  10. package/package.json +50 -50
  11. package/src/CdcMap.js +1384 -0
  12. package/src/components/CityList.js +93 -0
  13. package/src/components/CountyMap.js +556 -0
  14. package/src/components/DataTable.js +357 -0
  15. package/src/components/EditorPanel.js +2111 -0
  16. package/src/components/Geo.js +21 -0
  17. package/src/components/Modal.js +31 -0
  18. package/src/components/NavigationMenu.js +66 -0
  19. package/src/components/Sidebar.js +167 -0
  20. package/src/components/SingleStateMap.js +326 -0
  21. package/src/components/UsaMap.js +342 -0
  22. package/src/components/WorldMap.js +175 -0
  23. package/src/components/ZoomableGroup.js +47 -0
  24. package/src/data/abbreviations.js +57 -0
  25. package/src/data/color-palettes.js +200 -0
  26. package/src/data/county-map-halfquality.json +58453 -0
  27. package/src/data/county-map-quarterquality.json +1 -0
  28. package/src/data/county-topo.json +1 -0
  29. package/src/data/dfc-map.json +1 -0
  30. package/src/data/initial-state.js +60 -0
  31. package/src/data/newtest.json +1 -0
  32. package/src/data/state-abbreviations.js +60 -0
  33. package/src/data/supported-geos.js +3775 -0
  34. package/src/data/test.json +1 -0
  35. package/src/data/us-hex-topo.json +1 -0
  36. package/src/data/us-topo.json +1 -0
  37. package/src/data/world-topo.json +1 -0
  38. package/src/hooks/useActiveElement.js +19 -0
  39. package/src/hooks/useZoomPan.js +110 -0
  40. package/src/images/active-checkmark.svg +1 -0
  41. package/src/images/asc.svg +1 -0
  42. package/src/images/close.svg +1 -0
  43. package/src/images/desc.svg +1 -0
  44. package/src/images/external-link.svg +1 -0
  45. package/src/images/icon-download-img.svg +1 -0
  46. package/src/images/icon-download-pdf.svg +1 -0
  47. package/src/images/inactive-checkmark.svg +1 -0
  48. package/src/images/map-folded.svg +1 -0
  49. package/src/index.html +29 -0
  50. package/src/index.js +20 -0
  51. package/src/scss/btn.scss +69 -0
  52. package/src/scss/datatable.scss +7 -0
  53. package/src/scss/editor-panel.scss +654 -0
  54. package/src/scss/main.scss +224 -0
  55. package/src/scss/map.scss +188 -0
  56. package/src/scss/sidebar.scss +146 -0
  57. package/src/scss/tooltips.scss +30 -0
  58. package/src/scss/variables.scss +1 -0
  59. package/uploads/upload-example-city-state.json +392 -0
  60. package/uploads/upload-example-world-data.json +1490 -0
  61. 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;