@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
package/src/CdcMap.js ADDED
@@ -0,0 +1,1384 @@
1
+ import React, { useState, useEffect, useRef, memo, useCallback } from 'react';
2
+
3
+ // IE11
4
+ import 'core-js/stable'
5
+ import 'whatwg-fetch'
6
+ import ResizeObserver from 'resize-observer-polyfill';
7
+
8
+ // Third party
9
+ import ReactTooltip from 'react-tooltip';
10
+ import chroma from 'chroma-js';
11
+ import Papa from 'papaparse';
12
+ import parse from 'html-react-parser';
13
+ import html2pdf from 'html2pdf.js'
14
+ import html2canvas from 'html2canvas';
15
+ import Canvg from 'canvg';
16
+
17
+ // Data
18
+ import ExternalIcon from './images/external-link.svg';
19
+ import { supportedStates, supportedTerritories, supportedCountries, supportedCounties, supportedCities, supportedStatesFipsCodes } from './data/supported-geos';
20
+ import colorPalettes from './data/color-palettes';
21
+ import initialState from './data/initial-state';
22
+
23
+ // Sass
24
+ import './scss/main.scss';
25
+ import './scss/btn.scss'
26
+
27
+ // Images
28
+ import DownloadImg from './images/icon-download-img.svg'
29
+ import DownloadPdf from './images/icon-download-pdf.svg'
30
+
31
+ // Core
32
+ import Loading from '@cdc/core/components/Loading';
33
+ import DataTransform from '@cdc/core/components/DataTransform';
34
+ import getViewport from '@cdc/core/helpers/getViewport';
35
+ import numberFromString from '@cdc/core/helpers/numberFromString'
36
+
37
+
38
+ // Child Components
39
+ import Sidebar from './components/Sidebar';
40
+ import Modal from './components/Modal';
41
+ import EditorPanel from './components/EditorPanel'; // Future: Lazy
42
+ import UsaMap from './components/UsaMap'; // Future: Lazy
43
+ import CountyMap from './components/CountyMap'; // Future: Lazy
44
+ import DataTable from './components/DataTable'; // Future: Lazy
45
+ import NavigationMenu from './components/NavigationMenu'; // Future: Lazy
46
+ import WorldMap from './components/WorldMap'; // Future: Lazy
47
+ import SingleStateMap from './components/SingleStateMap'; // Future: Lazy
48
+
49
+ // Data props
50
+ const stateKeys = Object.keys(supportedStates)
51
+ const territoryKeys = Object.keys(supportedTerritories)
52
+ const countryKeys = Object.keys(supportedCountries)
53
+ const countyKeys = Object.keys(supportedCounties)
54
+ const cityKeys = Object.keys(supportedCities)
55
+
56
+ const generateColorsArray = (color = '#000000', special = false) => {
57
+ let colorObj = chroma(color)
58
+
59
+ let hoverColor = special ? colorObj.brighten(0.5).hex() : colorObj.saturate(1.3).hex()
60
+
61
+ return [
62
+ color,
63
+ hoverColor,
64
+ colorObj.darken(0.3).hex()
65
+ ]
66
+ }
67
+
68
+ const hashObj = (row) => {
69
+ let str = JSON.stringify(row)
70
+
71
+ let hash = 0;
72
+ if (str.length === 0) return hash;
73
+ for (let i = 0; i < str.length; i++) {
74
+ let char = str.charCodeAt(i);
75
+ hash = ((hash<<5)-hash) + char;
76
+ hash = hash & hash;
77
+ }
78
+
79
+ return hash;
80
+ }
81
+
82
+ // returns string[]
83
+ const getUniqueValues = (data, columnName) => {
84
+ let result = {};
85
+
86
+ for(let i = 0; i < data.length; i++) {
87
+ let val = data[i][columnName]
88
+
89
+ if(undefined === val) continue
90
+
91
+ if(undefined === result[val]) {
92
+ result[val] = true
93
+ }
94
+ }
95
+
96
+ return Object.keys(result)
97
+ }
98
+
99
+ const CdcMap = ({className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, hostname}) => {
100
+
101
+ const [showLoadingMessage, setShowLoadingMessage] = useState(false)
102
+ const transform = new DataTransform()
103
+ const [state, setState] = useState( {...initialState} )
104
+ const [loading, setLoading] = useState(true)
105
+ const [currentViewport, setCurrentViewport] = useState()
106
+ const [runtimeFilters, setRuntimeFilters] = useState([])
107
+ const [runtimeLegend, setRuntimeLegend] = useState([])
108
+ const [runtimeData, setRuntimeData] = useState({init: true})
109
+ const [modal, setModal] = useState(null)
110
+ const [accessibleStatus, setAccessibleStatus] = useState('')
111
+ let legendMemo = useRef(new Map())
112
+
113
+
114
+
115
+ const resizeObserver = new ResizeObserver(entries => {
116
+ for (let entry of entries) {
117
+ let newViewport = getViewport(entry.contentRect.width)
118
+
119
+ setCurrentViewport(newViewport)
120
+ }
121
+ });
122
+
123
+ // *******START SCREEN READER DEBUG*******
124
+ // const focusedElement = useActiveElement();
125
+
126
+ // useEffect(() => {
127
+ // if (focusedElement) {
128
+ // focusedElement.value && console.log(focusedElement.value);
129
+ // }
130
+ // console.log(focusedElement);
131
+ // }, [focusedElement])
132
+ // *******END SCREEN READER DEBUG*******
133
+
134
+ // Tag each row with a UID. Helps with filtering/placing geos. Not enumerable so doesn't show up in loops/console logs except when directly addressed ex row.uid
135
+ // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
136
+ const addUIDs = useCallback((obj, fromColumn) => {
137
+
138
+ obj.data.forEach(row => {
139
+ let uid = null
140
+
141
+ if(row.uid) row.uid = null // Wipe existing UIDs
142
+
143
+ // United States check
144
+ if("us" === obj.general.geoType) {
145
+ const geoName = row[obj.columns.geo.name] ? row[obj.columns.geo.name].toUpperCase() : '';
146
+
147
+ // States
148
+ uid = stateKeys.find( (key) => supportedStates[key].includes(geoName) )
149
+
150
+ // Territories
151
+ if(!uid) {
152
+ uid = territoryKeys.find( (key) => supportedTerritories[key].includes(geoName) )
153
+ }
154
+
155
+ // Cities
156
+ if(!uid) {
157
+ uid = cityKeys.find( (key) => key === geoName)
158
+ }
159
+ }
160
+
161
+ // World Check
162
+ if("world" === obj.general.geoType) {
163
+ const geoName = row[obj.columns.geo.name]
164
+
165
+ uid = countryKeys.find( (key) => supportedCountries[key].includes(geoName) )
166
+ }
167
+
168
+ // County Check
169
+ if("us-county" === obj.general.geoType || "single-state" === obj.general.geoType) {
170
+ const fips = row[obj.columns.geo.name]
171
+ uid = countyKeys.find( (key) => key === fips )
172
+ }
173
+
174
+ // TODO: Points
175
+ if(uid) {
176
+ Object.defineProperty(row, 'uid', {
177
+ value: uid,
178
+ writable: true
179
+ });
180
+ }
181
+ })
182
+
183
+ obj.data.fromColumn = fromColumn
184
+ })
185
+
186
+ const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
187
+
188
+ const newLegendMemo = new Map(); // Reset memoization
189
+
190
+ const
191
+ primaryCol = obj.columns.primary.name,
192
+ type = obj.legend.type,
193
+ number = obj.legend.numberOfItems,
194
+ result = [];
195
+
196
+ // Add a hash for what we're working from if passed
197
+ if(hash) {
198
+ result.fromHash = hash
199
+ }
200
+
201
+ // Unified will based the legend off ALL of the data maps received. Otherwise, it will use
202
+ let dataSet = obj.legend.unified ? obj.data : Object.values(runtimeData);
203
+
204
+ const colorDistributions = {
205
+ 1: [ 1 ],
206
+ 2: [ 1, 3 ],
207
+ 3: [ 1, 3, 5 ],
208
+ 4: [ 0, 2, 4, 6 ],
209
+ 5: [ 0, 2, 4, 6, 7 ],
210
+ 6: [ 0, 2, 3, 4, 5, 7 ],
211
+ 7: [ 0, 2, 3, 4, 5, 6, 7 ],
212
+ 8: [ 0, 2, 3, 4, 5, 6, 7, 8 ],
213
+ 9: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
214
+ }
215
+
216
+ const applyColorToLegend = (legendIdx) => {
217
+ // Default to "bluegreen" color scheme if the passed color isn't valid
218
+ let mapColorPalette = obj.customColors || colorPalettes[obj.color] || colorPalettes['bluegreen']
219
+
220
+ let colorIdx = legendIdx - specialClasses
221
+
222
+ // Special Classes (No Data)
223
+ if (result[legendIdx].special) {
224
+ const specialClassColors = chroma.scale(['#D4D4D4', '#939393']).colors(specialClasses)
225
+
226
+ return specialClassColors[ legendIdx ]
227
+ }
228
+
229
+ if ( obj.color.includes( 'qualitative' ) ) return mapColorPalette[colorIdx]
230
+
231
+ let amt = Math.max( result.length - specialClasses, 1 )
232
+ let distributionArray = colorDistributions[ amt ]
233
+
234
+ const specificColor = distributionArray[ colorIdx ]
235
+
236
+ return mapColorPalette[specificColor]
237
+ }
238
+
239
+ let specialClasses = 0
240
+ let specialClassesHash = {}
241
+
242
+ // Special classes
243
+ if (obj.legend.specialClasses.length) {
244
+ if(typeof obj.legend.specialClasses[0] === 'object'){
245
+ obj.legend.specialClasses.forEach(specialClass => {
246
+ dataSet = dataSet.filter(row => {
247
+ const val = String(row[specialClass.key]);
248
+
249
+ if(specialClass.value === val){
250
+ if(undefined === specialClassesHash[val]) {
251
+ specialClassesHash[val] = true;
252
+
253
+ result.push({
254
+ special: true,
255
+ value: val,
256
+ label: specialClass.label
257
+ });
258
+
259
+ result[result.length - 1].color = applyColorToLegend(result.length - 1);
260
+
261
+ specialClasses += 1;
262
+ }
263
+
264
+ let specialColor = '';
265
+
266
+ // color the state if val is in row
267
+ specialColor = result.findIndex(p => p.value === val)
268
+
269
+ newLegendMemo.set( hashObj(row), specialColor)
270
+
271
+ return false;
272
+ }
273
+
274
+ return true;
275
+ });
276
+ });
277
+ } else {
278
+ dataSet = dataSet.filter(row => {
279
+ const val = row[primaryCol]
280
+
281
+ if( obj.legend.specialClasses.includes(val) ) {
282
+
283
+ // apply the special color to the legend
284
+ if(undefined === specialClassesHash[val]) {
285
+ specialClassesHash[val] = true;
286
+
287
+ result.push({
288
+ special: true,
289
+ value: val
290
+ });
291
+
292
+ result[result.length - 1].color = applyColorToLegend(result.length - 1);
293
+
294
+ specialClasses += 1;
295
+ }
296
+
297
+ let specialColor = '';
298
+
299
+ // color the state if val is in row
300
+ if ( Object.values(row).includes(val) ) {
301
+ specialColor = result.findIndex(p => p.value === val)
302
+ }
303
+ newLegendMemo.set( hashObj(row), specialColor)
304
+
305
+ return false
306
+ }
307
+
308
+ return true
309
+ })
310
+ }
311
+ }
312
+
313
+ // Category
314
+ if('category' === type) {
315
+ let uniqueValues = new Map()
316
+ let count = 0
317
+
318
+ for(let i = 0; i < dataSet.length; i++) {
319
+ let row = dataSet[i]
320
+ let value = row[primaryCol]
321
+
322
+ if(undefined === value) continue
323
+
324
+ if(false === uniqueValues.has(value)) {
325
+ uniqueValues.set(value, [hashObj(row)]);
326
+ count++
327
+ } else {
328
+ uniqueValues.get(value).push(hashObj(row))
329
+ }
330
+
331
+ if(count === 9) break // Can only have 9 categorical items for now
332
+ }
333
+
334
+ let sorted = [...uniqueValues.keys()]
335
+
336
+ // Apply custom sorting or regular sorting
337
+ let configuredOrder = obj.legend.categoryValuesOrder ?? []
338
+
339
+ // Coerce strings to numbers inside configuredOrder property
340
+ for(let i = 0; i < configuredOrder.length; i++) {
341
+ configuredOrder[i] = numberFromString(configuredOrder[i])
342
+ }
343
+
344
+ if(configuredOrder.length) {
345
+ sorted.sort( (a, b) => {
346
+ return configuredOrder.indexOf(a) - configuredOrder.indexOf(b);
347
+ })
348
+ } else {
349
+ sorted.sort((a, b) => a - b)
350
+ }
351
+
352
+ // Add legend item for each
353
+ sorted.forEach((val) => {
354
+ result.push({
355
+ value: val,
356
+ })
357
+
358
+ let lastIdx = result.length - 1
359
+ let arr = uniqueValues.get(val)
360
+
361
+ if(arr) {
362
+ arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
363
+ }
364
+ })
365
+
366
+
367
+ // Add color to new legend item
368
+ for(let i = 0; i < result.length; i++) {
369
+ result[i].color = applyColorToLegend(i)
370
+ }
371
+ legendMemo.current = newLegendMemo
372
+ return result
373
+ }
374
+
375
+ let uniqueValues = {};
376
+ dataSet.forEach(datum => {
377
+ uniqueValues[datum[primaryCol]] = true;
378
+ });
379
+
380
+ let legendNumber = Math.min(number, Object.keys(uniqueValues).length);
381
+
382
+ // Separate zero
383
+ if(true === obj.legend.separateZero) {
384
+ let addLegendItem = false;
385
+
386
+ for(let i = 0; i < dataSet.length; i++) {
387
+ if (dataSet[i][primaryCol] === 0) {
388
+ addLegendItem = true
389
+
390
+ let row = dataSet.splice(i, 1)[0]
391
+
392
+ newLegendMemo.set( hashObj(row), result.length)
393
+ i--
394
+ }
395
+ }
396
+
397
+ if(addLegendItem) {
398
+ legendNumber -= 1 // This zero takes up one legend item
399
+
400
+ // Add new legend item
401
+ result.push({
402
+ min: 0,
403
+ max: 0
404
+ })
405
+
406
+ let lastIdx = result.length - 1
407
+
408
+ // Add color to new legend item
409
+ result[lastIdx].color = applyColorToLegend(lastIdx)
410
+ }
411
+ }
412
+
413
+ // Sort data for use in equalnumber or equalinterval
414
+ dataSet = dataSet.filter(row => typeof row[primaryCol] === 'number').sort((a, b) => {
415
+ let aNum = a[primaryCol]
416
+ let bNum = b[primaryCol]
417
+
418
+ return aNum - bNum
419
+ })
420
+
421
+ // Equal Number
422
+ if(type === 'equalnumber') {
423
+ let numberOfRows = dataSet.length
424
+
425
+ let remainder
426
+ let changingNumber = legendNumber
427
+
428
+ let chunkAmt
429
+
430
+ // Loop through the array until it has been split into equal subarrays
431
+ while ( numberOfRows > 0 ) {
432
+ remainder = numberOfRows % changingNumber
433
+
434
+ chunkAmt = Math.floor(numberOfRows / changingNumber)
435
+
436
+ if (remainder > 0) {
437
+ chunkAmt += 1
438
+ }
439
+
440
+ let removedRows = dataSet.splice(0, chunkAmt);
441
+
442
+ let min = removedRows[0][primaryCol],
443
+ max = removedRows[removedRows.length - 1][primaryCol]
444
+
445
+ removedRows.forEach(row => {
446
+ newLegendMemo.set( hashObj(row), result.length )
447
+ })
448
+
449
+ result.push({
450
+ min,
451
+ max
452
+ })
453
+
454
+ result[result.length - 1].color = applyColorToLegend(result.length - 1)
455
+
456
+ changingNumber -= 1
457
+ numberOfRows -= chunkAmt
458
+ }
459
+ }
460
+
461
+ // Equal Interval
462
+ if(type === 'equalinterval') {
463
+ dataSet = dataSet.filter(row => row[primaryCol] !== undefined)
464
+ let dataMin = dataSet[0][primaryCol]
465
+ let dataMax = dataSet[dataSet.length - 1][primaryCol]
466
+
467
+ let pointer = 0 // Start at beginning of dataSet
468
+
469
+ for (let i = 0; i < legendNumber; i++) {
470
+ let interval = Math.abs(dataMax - dataMin) / legendNumber
471
+
472
+ let min = dataMin + (interval * i)
473
+ let max = min + interval
474
+
475
+ // If this is the last loop, assign actual max of data as the end point
476
+ if (i === legendNumber - 1) max = dataMax
477
+
478
+ // Add rows in dataSet that belong to this new legend item since we've got the data sorted
479
+ while(pointer < dataSet.length && dataSet[pointer][primaryCol] <= max) {
480
+ newLegendMemo.set(hashObj(dataSet[pointer]), result.length )
481
+ pointer += 1
482
+ }
483
+
484
+ let range = {
485
+ min: Math.round(min * 100) / 100,
486
+ max: Math.round(max * 100) / 100,
487
+ }
488
+
489
+ result.push(range)
490
+
491
+ result[result.length - 1].color = applyColorToLegend(result.length - 1)
492
+ }
493
+ }
494
+
495
+ result.forEach((legendItem, idx) => {
496
+ legendItem.color = applyColorToLegend(idx, specialClasses, result)
497
+ })
498
+
499
+ legendMemo.current = newLegendMemo
500
+ return result
501
+ })
502
+
503
+ const generateRuntimeFilters = useCallback((obj, hash, runtimeFilters) => {
504
+ if(undefined === obj.filters || obj.filters.length === 0) return []
505
+
506
+ let filters = []
507
+
508
+ if(hash) filters.fromHash = hash
509
+
510
+ obj?.filters.forEach(({columnName, label, active, values}, idx) => {
511
+ if(undefined === columnName) return
512
+
513
+ let newFilter = runtimeFilters[idx]
514
+
515
+ const sortAsc = (a, b) => {
516
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
517
+ };
518
+
519
+ const sortDesc = (a, b) => {
520
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
521
+ };
522
+
523
+ values = getUniqueValues(state.data, columnName)
524
+
525
+ if(obj.filters[idx].order === 'asc') {
526
+ values = values.sort(sortAsc)
527
+ }
528
+
529
+ if(obj.filters[idx].order === 'desc') {
530
+ values = values.sort(sortDesc)
531
+ }
532
+
533
+ if(obj.filters[idx].order === 'cust') {
534
+ if(obj.filters[idx]?.values.length > 0) {
535
+ values = obj.filters[idx].values
536
+ }
537
+ }
538
+
539
+ if(undefined === newFilter) {
540
+ newFilter = {}
541
+ }
542
+
543
+ newFilter.label = label ?? ''
544
+ newFilter.columnName = columnName
545
+ newFilter.values = values
546
+ newFilter.active = active || values[0] // Default to first found value
547
+
548
+ filters.push(newFilter)
549
+ })
550
+
551
+ return filters
552
+ })
553
+
554
+ // Calculates what's going to be displayed on the map and data table at render.
555
+ const generateRuntimeData = useCallback((obj, filters, hash) => {
556
+ const result = {}
557
+
558
+ if(hash) {
559
+ // Adding property this way prevents it from being enumerated
560
+ Object.defineProperty(result, 'fromHash', {
561
+ value : hash
562
+ });
563
+ }
564
+
565
+
566
+ obj.data.forEach(row => {
567
+ if(undefined === row.uid) return false // No UID for this row, we can't use for mapping
568
+
569
+ // When on a single state map filter runtime data by state
570
+ if (
571
+ !(row[obj.columns.geo.name].substring(0, 2) === obj.general?.statePicked?.fipsCode) &&
572
+ obj.general.geoType === 'single-state'
573
+ ) {
574
+ return false;
575
+ }
576
+
577
+
578
+ if(row[obj.columns.primary.name]) {
579
+ row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name])
580
+ }
581
+
582
+ // If this is a navigation only map, skip if it doesn't have a URL
583
+ if("navigation" === obj.general.type ) {
584
+ let navigateUrl = row[obj.columns.navigate.name] || "";
585
+
586
+ if ( undefined !== navigateUrl && typeof navigateUrl === "string" ) {
587
+ // Strip hidden characters before we check length
588
+ navigateUrl = navigateUrl.replace( /(\r\n|\n|\r)/gm, '' );
589
+ }
590
+ if ( 0 === navigateUrl.length ) {
591
+ return false
592
+ }
593
+ }
594
+
595
+ // Filters
596
+ if(filters?.length) {
597
+ for(let i = 0; i < filters.length; i++) {
598
+ const {columnName, active} = filters[i]
599
+
600
+ if (String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
601
+ }
602
+ }
603
+
604
+ // Don't add additional rows with same UID
605
+ if(undefined === result[row.uid]) {
606
+ result[row.uid] = row
607
+ }
608
+ })
609
+
610
+ return result
611
+ })
612
+
613
+ const outerContainerRef = useCallback(node => {
614
+ if (node !== null) {
615
+ resizeObserver.observe(node);
616
+ }
617
+ },[]);
618
+
619
+ const mapSvg = useRef(null);
620
+
621
+ const closeModal = ({target}) => {
622
+ if('string' === typeof target.className && (target.className.includes('modal-close') || target.className.includes('modal-background') ) && null !== modal) {
623
+ setModal(null)
624
+ }
625
+ }
626
+
627
+ const saveImageAs = (uri, filename) => {
628
+ const ie = navigator.userAgent.match(/MSIE\s([\d.]+)/)
629
+ const ie11 = navigator.userAgent.match(/Trident\/7.0/) && navigator.userAgent.match(/rv:11/)
630
+ const ieEdge = navigator.userAgent.match(/Edge/g)
631
+ const ieVer=(ie ? ie[1] : (ie11 ? 11 : (ieEdge ? 12 : -1)));
632
+
633
+ if (ieVer>-1) {
634
+ const fileAsBlob = new Blob([uri], {
635
+ type: 'image/png'
636
+ });
637
+ window.navigator.msSaveBlob(fileAsBlob, filename);
638
+ } else {
639
+ const link = document.createElement('a')
640
+ if (typeof link.download === 'string') {
641
+ link.href = uri
642
+ link.download = filename
643
+ link.onclick = (e) => document.body.removeChild(e.target);
644
+ document.body.appendChild(link)
645
+ link.click()
646
+ } else {
647
+ window.open(uri)
648
+ }
649
+ }
650
+ }
651
+
652
+ const generateMedia = (target, type) => {
653
+ // Convert SVG to canvas
654
+ const baseSvg = mapSvg.current.querySelector('.rsm-svg')
655
+
656
+ const ratio = baseSvg.getBoundingClientRect().height / baseSvg.getBoundingClientRect().width
657
+ const calcHeight = ratio * 1440
658
+ const xmlSerializer = new XMLSerializer()
659
+ const svgStr = xmlSerializer.serializeToString(baseSvg)
660
+ const options = { log: false, ignoreMouse: true }
661
+ const canvas = document.createElement('canvas')
662
+ const ctx = canvas.getContext('2d')
663
+ ctx.canvas.width = 1440
664
+ ctx.canvas.height = calcHeight
665
+ const canvg = Canvg.fromString(ctx, svgStr, options)
666
+ canvg.start()
667
+
668
+ // Generate DOM <img> from svg data
669
+ const generatedImage = document.createElement('img')
670
+ generatedImage.src = canvas.toDataURL('image/png')
671
+ generatedImage.style.width = '100%'
672
+ generatedImage.style.height = 'auto'
673
+
674
+ baseSvg.style.display = 'none' // Hide default SVG during media generation
675
+ baseSvg.parentNode.insertBefore(generatedImage, baseSvg.nextSibling) // Insert png generated from canvas of svg
676
+
677
+ // Construct filename with timestamp
678
+ const date = new Date()
679
+ const filename = state.general.title.replace(/\s+/g, '-').toLowerCase() + '-' + date.getDate() + date.getMonth() + date.getFullYear()
680
+
681
+ switch (type) {
682
+ case 'image':
683
+ return html2canvas(target, {
684
+ allowTaint: true,
685
+ backgroundColor: '#ffffff',
686
+ width: 1440,
687
+ windowWidth: 1440,
688
+ scale: 1,
689
+ logging: false
690
+ }).then(canvas => {
691
+ saveImageAs(canvas.toDataURL(), filename + '.png')
692
+ }).then(() => {
693
+ generatedImage.remove() // Remove generated png
694
+ baseSvg.style.display = null // Re-display initial svg map
695
+ })
696
+ case 'pdf':
697
+ let opt = {
698
+ margin: 0.2,
699
+ filename: filename + '.pdf',
700
+ image: { type: 'png' },
701
+ html2canvas: { scale: 2, logging: false },
702
+ jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
703
+ };
704
+
705
+ html2pdf().set(opt).from(target).save().then(() => {
706
+ generatedImage.remove() // Remove generated png
707
+ baseSvg.style.display = null // Re-display initial svg map
708
+ })
709
+ break
710
+ default:
711
+ console.warn('generateMedia param 2 type must be \'image\' or \'pdf\'')
712
+ break
713
+ }
714
+ }
715
+
716
+ const changeFilterActive = async (idx, activeValue) => {
717
+ // Reset active legend toggles
718
+ resetLegendToggles()
719
+
720
+ try {
721
+
722
+ const isEmpty = (obj) => {
723
+ return Object.keys(obj).length === 0;
724
+ }
725
+
726
+ let filters = [...runtimeFilters]
727
+
728
+ filters[idx] = { ...filters[idx] }
729
+
730
+ filters[idx].active = activeValue
731
+ const newData = generateRuntimeData(state, filters)
732
+
733
+ // throw an error if newData is empty
734
+ if (isEmpty(newData)) throw new Error('Cove Filter Error: No runtime data to set for this filter')
735
+
736
+ // set the runtime filters and data
737
+ setRuntimeData(newData)
738
+ setRuntimeFilters(filters)
739
+ } catch(e) {
740
+ console.error(e.message)
741
+ }
742
+
743
+ }
744
+
745
+ const displayDataAsText = (value, columnName) => {
746
+ if(value === null) {
747
+ return ""
748
+ }
749
+
750
+ let formattedValue = value
751
+
752
+ let columnObj = state.columns[columnName]
753
+
754
+ if (columnObj) {
755
+ // If value is a number, apply specific formattings
756
+ if (Number(value)) {
757
+ // Rounding
758
+ if(columnObj.hasOwnProperty('roundToPlace') && columnObj.roundToPlace !== "None") {
759
+
760
+ const decimalPoint = columnObj.roundToPlace
761
+
762
+ formattedValue = Number(value).toFixed(decimalPoint)
763
+
764
+ }
765
+
766
+ if(columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) {
767
+
768
+ formattedValue = Number(value).toLocaleString('en-US', { style: 'decimal'})
769
+
770
+ }
771
+ }
772
+
773
+ // Check if it's a special value. If it is not, apply the designated prefix and suffix
774
+ if (false === state.legend.specialClasses.includes(String(value))) {
775
+ formattedValue = columnObj.prefix + formattedValue + columnObj.suffix
776
+ }
777
+ }
778
+
779
+ return formattedValue
780
+ }
781
+
782
+ const applyLegendToRow = (rowObj) => {
783
+ // Navigation map
784
+ if("navigation" === state.general.type) {
785
+ let mapColorPalette = colorPalettes[ state.color ] || colorPalettes[ 'bluegreenreverse' ]
786
+
787
+ return generateColorsArray( mapColorPalette[ 3 ] )
788
+ }
789
+
790
+ let hash = hashObj(rowObj)
791
+
792
+ if( legendMemo.current.has(hash) ) {
793
+ let idx = legendMemo.current.get(hash)
794
+
795
+ if(runtimeLegend[idx]?.disabled) return false
796
+
797
+ return generateColorsArray(runtimeLegend[idx]?.color, runtimeLegend[idx]?.special)
798
+ }
799
+
800
+ // Fail state
801
+ return generateColorsArray()
802
+ }
803
+
804
+ const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
805
+ let toolTipText = '';
806
+ let stateOrCounty =
807
+ state.general.geoType === 'us' ? 'State: ' :
808
+ (state.general.geoType === 'us-county' || state.general.geoType === 'single-state') ? 'County: ':
809
+ '';
810
+ if (state.general.geoType === 'us-county') {
811
+ let stateFipsCode = row[state.columns.geo.name].substring(0,2)
812
+ const stateName = supportedStatesFipsCodes[stateFipsCode];
813
+
814
+ //supportedStatesFipsCodes[]
815
+ toolTipText += `<strong>State: ${stateName}</strong><br/>`;
816
+ }
817
+
818
+ toolTipText += `<strong>${stateOrCounty}${displayGeoName(geoName)}</strong>`
819
+
820
+ if('data' === state.general.type && undefined !== row) {
821
+ toolTipText += `<dl>`
822
+
823
+ Object.keys(state.columns).forEach((columnKey) => {
824
+ const column = state.columns[columnKey]
825
+
826
+ if (true === column.tooltip) {
827
+
828
+ let label = column.label.length > 0 ? column.label : '';
829
+
830
+ let value;
831
+
832
+ if(state.legend.specialClasses && state.legend.specialClasses.length && typeof state.legend.specialClasses[0] === 'object'){
833
+ for(let i = 0; i < state.legend.specialClasses.length; i++){
834
+ if(String(row[state.legend.specialClasses[i].key]) === state.legend.specialClasses[i].value){
835
+ value = displayDataAsText(state.legend.specialClasses[i].label, columnKey);
836
+ break;
837
+ }
838
+ }
839
+ }
840
+
841
+ if(!value){
842
+ value = displayDataAsText(row[column.name], columnKey);
843
+ }
844
+
845
+ if(0 < value.length) { // Only spit out the tooltip if there's a value there
846
+ toolTipText += `<div><dt>${label}</dt><dd>${value}</dd></div>`
847
+ }
848
+
849
+ }
850
+ })
851
+
852
+ toolTipText += `</dl>`
853
+ }
854
+
855
+ // We convert the markup into JSX and add a navigation link if it's going into a modal.
856
+ if('jsx' === returnType) {
857
+ toolTipText = [(<div key="modal-content">{parse(toolTipText)}</div>)]
858
+
859
+ if(state.columns.hasOwnProperty('navigate') && row[state.columns.navigate.name]) {
860
+ toolTipText.push( (<span className="navigation-link" key="modal-navigation-link" onClick={() => navigationHandler(row[state.columns.navigate.name])}>{state.tooltips.linkLabel}<ExternalIcon className="inline-icon ml-1" /></span>) )
861
+ }
862
+ }
863
+
864
+ return toolTipText
865
+
866
+ }
867
+
868
+ const titleCase = (string) => {
869
+ return string.split(' ').map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()).join(' ');
870
+ }
871
+
872
+ // This resets all active legend toggles.
873
+ const resetLegendToggles = async () => {
874
+ let newLegend = [...runtimeLegend]
875
+
876
+ newLegend.forEach(legendItem => {
877
+ delete legendItem.disabled
878
+ })
879
+
880
+ setRuntimeLegend(newLegend)
881
+ }
882
+
883
+ // Supports JSON or CSV
884
+ const fetchRemoteData = async (url) => {
885
+ try {
886
+ const urlObj = new URL(url);
887
+ const regex = /(?:\.([^.]+))?$/
888
+
889
+ let data = []
890
+
891
+ const ext = (regex.exec(urlObj.pathname)[1])
892
+ if ('csv' === ext) {
893
+ data = await fetch(url)
894
+ .then(response => response.text())
895
+ .then(responseText => {
896
+ const parsedCsv = Papa.parse(responseText, {
897
+ header: true,
898
+ dynamicTyping: true,
899
+ skipEmptyLines: true
900
+ })
901
+ return parsedCsv.data
902
+ })
903
+ }
904
+
905
+ if ('json' === ext) {
906
+ data = await fetch(url)
907
+ .then(response => response.json())
908
+ }
909
+
910
+ return data;
911
+ } catch {
912
+ // If we can't parse it, still attempt to fetch it
913
+ try {
914
+ let response = await (await fetch(configUrl)).json()
915
+ return response
916
+ } catch {
917
+ console.error(`Cannot parse URL: ${url}`);
918
+ }
919
+ }
920
+ }
921
+
922
+ // Attempts to find the corresponding value
923
+ const displayGeoName = (key) => {
924
+ let value = key
925
+
926
+ // Map to first item in values array which is the preferred label
927
+ if(stateKeys.includes(value)) {
928
+ value = titleCase(supportedStates[key][0])
929
+ }
930
+
931
+ if(territoryKeys.includes(value)) {
932
+ value = titleCase(supportedTerritories[key][0])
933
+ }
934
+
935
+ if(countryKeys.includes(value)) {
936
+ value = titleCase(supportedCountries[key][0])
937
+ }
938
+
939
+ if(countyKeys.includes(value)) {
940
+ value = titleCase(supportedCounties[key])
941
+ }
942
+
943
+ const dict = {
944
+ "District of Columbia" : "Washington D.C."
945
+ }
946
+
947
+ if(true === Object.keys(dict).includes(value)) {
948
+ value = dict[value]
949
+ }
950
+
951
+ return titleCase(value);
952
+ }
953
+
954
+ const navigationHandler = (urlString) => {
955
+ // Call custom navigation method if passed
956
+ if(customNavigationHandler) {
957
+ customNavigationHandler(urlString);
958
+ return;
959
+ }
960
+
961
+ // Abort if value is blank
962
+ if(0 === urlString.length) {
963
+ throw Error("Blank string passed as URL. Navigation aborted.");
964
+ }
965
+
966
+ const urlObj = new URL(urlString);
967
+
968
+ // Open constructed link in new tab/window
969
+ window.open(urlObj.toString(), '_blank');
970
+ }
971
+
972
+ const geoClickHandler = (key, value) => {
973
+ // If modals are set or we are on a mobile viewport, display modal
974
+ if ('xs' === currentViewport || 'xxs' === currentViewport || 'click' === state.tooltips.appearanceType) {
975
+ setModal({
976
+ geoName: key,
977
+ keyedData: value
978
+ })
979
+
980
+ return;
981
+ }
982
+
983
+ // Otherwise if this item has a link specified for it, do regular navigation.
984
+ if (state.columns.navigate && value[state.columns.navigate.name]) {
985
+ navigationHandler(value[state.columns.navigate.name])
986
+ }
987
+ }
988
+
989
+ const validateFipsCodeLength = (newState) => {
990
+ if(newState.general.geoType === 'us-county' || newState.general.geoType === 'single-state' || newState.general.geoType === 'us' && newState?.data) {
991
+
992
+ newState?.data.forEach(dataPiece => {
993
+ if(dataPiece[newState.columns.geo.name]) {
994
+ if(!isNaN(parseInt(dataPiece[newState.columns.geo.name])) && dataPiece[newState.columns.geo.name].length === 4) {
995
+ dataPiece[newState.columns.geo.name] = 0 + dataPiece[newState.columns.geo.name]
996
+ }
997
+ }
998
+ })
999
+ }
1000
+ return newState;
1001
+ }
1002
+
1003
+ const loadConfig = async (configObj) => {
1004
+ // Set loading flag
1005
+ if(!loading) setLoading(true)
1006
+
1007
+ // Create new config object the same way each time no matter when this method is called.
1008
+ let newState = {
1009
+ ...initialState,
1010
+ ...configObj
1011
+ }
1012
+
1013
+ // If a dataUrl property exists, always pull from that.
1014
+ if (newState.dataUrl) {
1015
+ if(newState.dataUrl[0] === '/') {
1016
+ newState.dataUrl = 'https://' + hostname + newState.dataUrl
1017
+ }
1018
+
1019
+ let newData = await fetchRemoteData(newState.dataUrl)
1020
+
1021
+ if(newData && newState.dataDescription) {
1022
+ newData = transform.autoStandardize(newData);
1023
+ newData = transform.developerStandardize(newData, newState.dataDescription);
1024
+ }
1025
+
1026
+ if(newData) {
1027
+ newState.data = newData
1028
+ }
1029
+ }
1030
+
1031
+ // This code goes through and adds the defaults for every property declaring in the initial state at the top.
1032
+ // This allows you to easily add new properties to the config without having to worry about accounting for backwards compatibility.
1033
+ // Right now this does not work recursively -- only on first and second level properties. So state -> prop1 -> childprop1
1034
+ Object.keys(newState).forEach( (key) => {
1035
+ if("object" === typeof newState[key] && false === Array.isArray(newState[key])) {
1036
+ if(initialState[key] ) {
1037
+ Object.keys(initialState[key]).forEach( (property) => {
1038
+ if(undefined === newState[key][property]) {
1039
+ newState[key][property] = initialState[key][property]
1040
+ }
1041
+ })
1042
+ }
1043
+ }
1044
+ })
1045
+
1046
+ // If there's a name for the geo, add UIDs
1047
+ if(newState.columns.geo.name || newState.columns.geo.fips) {
1048
+ addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
1049
+ }
1050
+
1051
+ if(newState.dataTable.forceDisplay === undefined){
1052
+ newState.dataTable.forceDisplay = !isDashboard;
1053
+ }
1054
+
1055
+
1056
+ validateFipsCodeLength(newState);
1057
+ setState(newState)
1058
+
1059
+ // Done loading
1060
+ setLoading(false)
1061
+ }
1062
+
1063
+ const init = async () => {
1064
+ let configData = null
1065
+
1066
+ // Load the configuration data passed to this component if it exists
1067
+ if(config) {
1068
+ configData = config
1069
+ }
1070
+
1071
+ // If the config passed is a string, try to load it as an ajax
1072
+ if(configUrl) {
1073
+ configData = await fetchRemoteData(configUrl)
1074
+ }
1075
+
1076
+ // Once we have a config verify that it is an object and load it
1077
+ if('object' === typeof configData) {
1078
+ loadConfig(configData)
1079
+ }
1080
+ }
1081
+
1082
+ // Initial load
1083
+ useEffect(() => {
1084
+ init()
1085
+ }, [])
1086
+
1087
+ useEffect(() => {
1088
+ if (state.data) {
1089
+ let newData = generateRuntimeData(state);
1090
+ setRuntimeData(newData);
1091
+ }
1092
+ }, [state.general.statePicked]);
1093
+
1094
+
1095
+
1096
+ // When geotype changes
1097
+ useEffect(() => {
1098
+
1099
+ // UID
1100
+ if(state.data && state.columns.geo.name) {
1101
+ addUIDs(state, state.columns.geo.name)
1102
+ }
1103
+
1104
+ }, [state.general.geoType]);
1105
+
1106
+ useEffect(() => {
1107
+
1108
+ // UID
1109
+ if(state.data && state.columns.geo.name && state.columns.geo.name !== state.data.fromColumn) {
1110
+ addUIDs(state, state.columns.geo.name)
1111
+ }
1112
+
1113
+ // Filters
1114
+ const hashFilters = hashObj(state.filters)
1115
+ let filters;
1116
+
1117
+ if(state.filters && hashFilters !== runtimeFilters.fromHash) {
1118
+ filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
1119
+
1120
+ if(filters) {
1121
+ setRuntimeFilters(filters)
1122
+ }
1123
+ }
1124
+
1125
+ const hashLegend = hashObj({
1126
+ color: state.color,
1127
+ customColors: state.customColors,
1128
+ numberOfItems: state.legend.numberOfItems,
1129
+ type: state.legend.type,
1130
+ separateZero: state.legend.separateZero ?? false,
1131
+ categoryValuesOrder: state.legend.categoryValuesOrder,
1132
+ specialClasses: state.legend.specialClasses,
1133
+ geoType: state.general.geoType,
1134
+ data: state.data
1135
+ })
1136
+
1137
+ const hashData = hashObj({
1138
+ columns: state.columns,
1139
+ geoType: state.general.geoType,
1140
+ type: state.general.type,
1141
+ geo: state.columns.geo.name,
1142
+ primary: state.columns.primary.name,
1143
+ data: state.data,
1144
+ ...runtimeFilters
1145
+ })
1146
+
1147
+ // Data
1148
+ let newRuntimeData;
1149
+ if(hashData !== runtimeData.fromHash && state.data?.fromColumn) {
1150
+ newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1151
+ setRuntimeData(newRuntimeData)
1152
+ }
1153
+
1154
+ // Legend
1155
+ if (hashLegend !== runtimeLegend.fromHash && (undefined === runtimeData.init || newRuntimeData)) {
1156
+ const legend = generateRuntimeLegend(state, newRuntimeData || runtimeData, hashLegend)
1157
+ setRuntimeLegend(legend)
1158
+ }
1159
+ }, [state])
1160
+
1161
+ useEffect(() => {
1162
+ const hashLegend = hashObj({
1163
+ color: state.color,
1164
+ customColors: state.customColors,
1165
+ numberOfItems: state.legend.numberOfItems,
1166
+ type: state.legend.type,
1167
+ separateZero: state.legend.separateZero ?? false,
1168
+ categoryValuesOrder: state.legend.categoryValuesOrder,
1169
+ specialClasses: state.legend.specialClasses,
1170
+ geoType: state.general.geoType,
1171
+ data: state.data
1172
+ })
1173
+
1174
+ // Legend - Update when runtimeData does
1175
+ if(hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
1176
+ const legend = generateRuntimeLegend(state, runtimeData)
1177
+ setRuntimeLegend(legend)
1178
+ }
1179
+ }, [runtimeData])
1180
+
1181
+ if(config) {
1182
+ useEffect(() => {
1183
+ loadConfig(config)
1184
+ }, [config.data])
1185
+ }
1186
+
1187
+ // Destructuring for more readable JSX
1188
+ const { general, tooltips, dataTable } = state
1189
+ const { title = '', subtext = ''} = general
1190
+
1191
+ // Outer container classes
1192
+ let outerContainerClasses = [
1193
+ 'cdc-open-viz-module',
1194
+ 'cdc-map-outer-container',
1195
+ currentViewport
1196
+ ]
1197
+
1198
+ if(className) {
1199
+ outerContainerClasses.push(className)
1200
+ }
1201
+
1202
+ // Map container classes
1203
+ let mapContainerClasses = [
1204
+ 'map-container',
1205
+ state.legend.position,
1206
+ state.general.type,
1207
+ state.general.geoType
1208
+ ]
1209
+
1210
+ if(modal) {
1211
+ mapContainerClasses.push('modal-background')
1212
+ }
1213
+
1214
+ if(general.type === 'navigation' && true === general.fullBorder) {
1215
+ mapContainerClasses.push('full-border')
1216
+ }
1217
+
1218
+ // Props passed to all map types
1219
+ const mapProps = {
1220
+ state,
1221
+ data: runtimeData,
1222
+ rebuildTooltips : ReactTooltip.rebuild,
1223
+ applyTooltipsToGeo,
1224
+ closeModal,
1225
+ navigationHandler,
1226
+ geoClickHandler,
1227
+ applyLegendToRow,
1228
+ displayGeoName,
1229
+ runtimeLegend,
1230
+ generateColorsArray,
1231
+ titleCase
1232
+ }
1233
+
1234
+ if (!mapProps.data || !state.data) return <Loading />;
1235
+
1236
+ const handleMapTabbing = general.showSidebar ? `#legend` : state.general.title ? `#dataTableSection__${state.general.title.replace(/\s/g, '')}` : `#dataTableSection`
1237
+
1238
+
1239
+ return (
1240
+ <div className={outerContainerClasses.join(' ')} ref={outerContainerRef}>
1241
+ {isEditor && (
1242
+ <EditorPanel
1243
+ isDashboard={isDashboard}
1244
+ state={state}
1245
+ setState={setState}
1246
+ loadConfig={loadConfig}
1247
+ setParentConfig={setConfig}
1248
+ setRuntimeFilters={setRuntimeFilters}
1249
+ runtimeFilters={runtimeFilters}
1250
+ runtimeLegend={runtimeLegend}
1251
+ columnsInData={Object.keys(state.data[0])}
1252
+ />
1253
+ )}
1254
+ {!runtimeData.init && (general.type === 'navigation' || runtimeLegend.length !== 0) && <section className={`cdc-map-inner-container ${currentViewport}`} aria-label={'Map: ' + title}>
1255
+ {['lg', 'md'].includes(currentViewport) && 'hover' === tooltips.appearanceType && (
1256
+ <ReactTooltip
1257
+ id='tooltip'
1258
+ place='right'
1259
+ type='light'
1260
+ html={true}
1261
+ className={tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}
1262
+ />
1263
+ )}
1264
+ <header className={general.showTitle === true ? '' : 'hidden'} aria-hidden='true'>
1265
+ <div role='heading' className={'map-title ' + general.headerColor} tabIndex="0">
1266
+ {parse(title)}
1267
+ </div>
1268
+ </header>
1269
+ <section className={mapContainerClasses.join(' ')} onClick={(e) => closeModal(e)}>
1270
+ {general.showDownloadMediaButton === true && (
1271
+ <div className='map-downloads' data-html2canvas-ignore>
1272
+ <div className='map-downloads__ui btn-group'>
1273
+ <button
1274
+ className='btn'
1275
+ title='Download Map as Image'
1276
+ onClick={() => generateMedia(outerContainerRef.current, 'image')}
1277
+ >
1278
+ <DownloadImg className='btn__icon' title='Download Map as Image' />
1279
+ </button>
1280
+ <button
1281
+ className='btn'
1282
+ title='Download Map as PDF'
1283
+ onClick={() => generateMedia(outerContainerRef.current, 'pdf')}
1284
+ >
1285
+ <DownloadPdf className='btn__icon' title='Download Map as PDF' />
1286
+ </button>
1287
+ </div>
1288
+ </div>
1289
+ )}
1290
+
1291
+ <a id='skip-geo-container' className='cdcdataviz-sr-only-focusable' href={handleMapTabbing}>
1292
+ Skip Over Map Container
1293
+ </a>
1294
+ <section className='geography-container' aria-hidden='true' ref={mapSvg}>
1295
+ {currentViewport && (
1296
+ <section className='geography-container' aria-hidden='true' ref={mapSvg}>
1297
+ {modal && (
1298
+ <Modal
1299
+ type={general.type}
1300
+ viewport={currentViewport}
1301
+ applyTooltipsToGeo={applyTooltipsToGeo}
1302
+ applyLegendToRow={applyLegendToRow}
1303
+ capitalize={state.tooltips.capitalizeLabels}
1304
+ content={modal}
1305
+ />
1306
+ )}
1307
+ {'single-state' === general.geoType && (
1308
+ <SingleStateMap supportedTerritories={supportedTerritories} {...mapProps} />
1309
+ )}
1310
+ {'us' === general.geoType && (
1311
+ <UsaMap supportedTerritories={supportedTerritories} {...mapProps} />
1312
+ )}
1313
+ {'world' === general.geoType && (
1314
+ <WorldMap supportedCountries={supportedCountries} {...mapProps} />
1315
+ )}
1316
+ {'us-county' === general.geoType && (
1317
+ <CountyMap
1318
+ supportedCountries={supportedCountries}
1319
+ {...mapProps}
1320
+ />
1321
+ )}
1322
+ {'data' === general.type && logo && <img src={logo} alt='' className='map-logo' />}
1323
+ </section>
1324
+
1325
+ )}
1326
+ </section>
1327
+
1328
+ {general.showSidebar && 'navigation' !== general.type && (
1329
+ <Sidebar
1330
+ viewport={currentViewport}
1331
+ legend={state.legend}
1332
+ runtimeLegend={runtimeLegend}
1333
+ setRuntimeLegend={setRuntimeLegend}
1334
+ runtimeFilters={runtimeFilters}
1335
+ columns={state.columns}
1336
+ sharing={state.sharing}
1337
+ prefix={state.columns.primary.prefix}
1338
+ suffix={state.columns.primary.suffix}
1339
+ setState={setState}
1340
+ resetLegendToggles={resetLegendToggles}
1341
+ changeFilterActive={changeFilterActive}
1342
+ setAccessibleStatus={setAccessibleStatus}
1343
+ />
1344
+ )}
1345
+ </section>
1346
+ {'navigation' === general.type && (
1347
+ <NavigationMenu
1348
+ displayGeoName={displayGeoName}
1349
+ data={runtimeData}
1350
+ options={general}
1351
+ columns={state.columns}
1352
+ navigationHandler={(val) => navigationHandler(val)}
1353
+ />
1354
+ )}
1355
+ {true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading && (
1356
+ <DataTable
1357
+ state={state}
1358
+ rawData={state.data}
1359
+ navigationHandler={navigationHandler}
1360
+ expandDataTable={general.expandDataTable}
1361
+ headerColor={general.headerColor}
1362
+ columns={state.columns}
1363
+ showDownloadButton={general.showDownloadButton}
1364
+ runtimeLegend={runtimeLegend}
1365
+ runtimeData={runtimeData}
1366
+ displayDataAsText={displayDataAsText}
1367
+ displayGeoName={displayGeoName}
1368
+ applyLegendToRow={applyLegendToRow}
1369
+ tableTitle={dataTable.title}
1370
+ indexTitle={dataTable.indexTitle}
1371
+ mapTitle={general.title}
1372
+ viewport={currentViewport}
1373
+ />
1374
+ )}
1375
+ {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1376
+ </section>}
1377
+ <div aria-live='assertive' className='cdcdataviz-sr-only'>
1378
+ {accessibleStatus}
1379
+ </div>
1380
+ </div>
1381
+ );
1382
+ }
1383
+
1384
+ export default memo(CdcMap)