@cdc/map 2.6.2 → 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/dist/cdcmap.js +27 -27
- package/examples/default-county.json +105 -0
- package/examples/default-single-state.json +109 -0
- package/examples/default-usa.json +744 -603
- 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 -47
- package/src/CdcMap.js +422 -159
- package/src/components/CityList.js +3 -2
- package/src/components/CountyMap.js +556 -0
- package/src/components/DataTable.js +73 -19
- package/src/components/EditorPanel.js +2088 -1230
- package/src/components/Sidebar.js +5 -5
- package/src/components/SingleStateMap.js +326 -0
- package/src/components/UsaMap.js +20 -3
- package/src/data/abbreviations.js +57 -0
- package/src/data/color-palettes.js +10 -1
- 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 +2 -2
- package/src/data/newtest.json +1 -0
- package/src/data/state-abbreviations.js +60 -0
- package/src/data/supported-geos.js +3504 -151
- package/src/data/test.json +1 -0
- package/src/hooks/useActiveElement.js +19 -0
- package/src/index.html +27 -20
- package/src/index.js +8 -4
- package/src/scss/datatable.scss +2 -1
- package/src/scss/main.scss +10 -1
- package/src/scss/map.scss +153 -123
- package/src/scss/sidebar.scss +0 -1
- package/uploads/upload-example-city-state.json +392 -0
- package/uploads/upload-example-world-data.json +1490 -0
- package/LICENSE +0 -201
package/src/CdcMap.js
CHANGED
|
@@ -16,7 +16,7 @@ import Canvg from 'canvg';
|
|
|
16
16
|
|
|
17
17
|
// Data
|
|
18
18
|
import ExternalIcon from './images/external-link.svg';
|
|
19
|
-
import { supportedStates, supportedTerritories, supportedCountries, supportedCities } from './data/supported-geos';
|
|
19
|
+
import { supportedStates, supportedTerritories, supportedCountries, supportedCounties, supportedCities, supportedStatesFipsCodes } from './data/supported-geos';
|
|
20
20
|
import colorPalettes from './data/color-palettes';
|
|
21
21
|
import initialState from './data/initial-state';
|
|
22
22
|
|
|
@@ -34,19 +34,23 @@ import DataTransform from '@cdc/core/components/DataTransform';
|
|
|
34
34
|
import getViewport from '@cdc/core/helpers/getViewport';
|
|
35
35
|
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
36
36
|
|
|
37
|
+
|
|
37
38
|
// Child Components
|
|
38
39
|
import Sidebar from './components/Sidebar';
|
|
39
40
|
import Modal from './components/Modal';
|
|
40
41
|
import EditorPanel from './components/EditorPanel'; // Future: Lazy
|
|
41
42
|
import UsaMap from './components/UsaMap'; // Future: Lazy
|
|
43
|
+
import CountyMap from './components/CountyMap'; // Future: Lazy
|
|
42
44
|
import DataTable from './components/DataTable'; // Future: Lazy
|
|
43
45
|
import NavigationMenu from './components/NavigationMenu'; // Future: Lazy
|
|
44
46
|
import WorldMap from './components/WorldMap'; // Future: Lazy
|
|
47
|
+
import SingleStateMap from './components/SingleStateMap'; // Future: Lazy
|
|
45
48
|
|
|
46
49
|
// Data props
|
|
47
50
|
const stateKeys = Object.keys(supportedStates)
|
|
48
51
|
const territoryKeys = Object.keys(supportedTerritories)
|
|
49
52
|
const countryKeys = Object.keys(supportedCountries)
|
|
53
|
+
const countyKeys = Object.keys(supportedCounties)
|
|
50
54
|
const cityKeys = Object.keys(supportedCities)
|
|
51
55
|
|
|
52
56
|
const generateColorsArray = (color = '#000000', special = false) => {
|
|
@@ -65,7 +69,7 @@ const hashObj = (row) => {
|
|
|
65
69
|
let str = JSON.stringify(row)
|
|
66
70
|
|
|
67
71
|
let hash = 0;
|
|
68
|
-
if (str.length
|
|
72
|
+
if (str.length === 0) return hash;
|
|
69
73
|
for (let i = 0; i < str.length; i++) {
|
|
70
74
|
let char = str.charCodeAt(i);
|
|
71
75
|
hash = ((hash<<5)-hash) + char;
|
|
@@ -93,30 +97,44 @@ const getUniqueValues = (data, columnName) => {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
const CdcMap = ({className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, hostname}) => {
|
|
100
|
+
|
|
101
|
+
const [showLoadingMessage, setShowLoadingMessage] = useState(false)
|
|
96
102
|
const transform = new DataTransform()
|
|
97
|
-
|
|
98
103
|
const [state, setState] = useState( {...initialState} )
|
|
99
104
|
const [loading, setLoading] = useState(true)
|
|
100
|
-
const [currentViewport, setCurrentViewport] = useState(
|
|
105
|
+
const [currentViewport, setCurrentViewport] = useState()
|
|
101
106
|
const [runtimeFilters, setRuntimeFilters] = useState([])
|
|
102
107
|
const [runtimeLegend, setRuntimeLegend] = useState([])
|
|
103
108
|
const [runtimeData, setRuntimeData] = useState({init: true})
|
|
104
109
|
const [modal, setModal] = useState(null)
|
|
105
110
|
const [accessibleStatus, setAccessibleStatus] = useState('')
|
|
106
|
-
|
|
107
111
|
let legendMemo = useRef(new Map())
|
|
108
112
|
|
|
113
|
+
|
|
114
|
+
|
|
109
115
|
const resizeObserver = new ResizeObserver(entries => {
|
|
110
116
|
for (let entry of entries) {
|
|
111
117
|
let newViewport = getViewport(entry.contentRect.width)
|
|
112
|
-
|
|
118
|
+
|
|
113
119
|
setCurrentViewport(newViewport)
|
|
114
120
|
}
|
|
115
121
|
});
|
|
116
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
|
+
|
|
117
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
|
|
118
135
|
// We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
|
|
119
136
|
const addUIDs = useCallback((obj, fromColumn) => {
|
|
137
|
+
|
|
120
138
|
obj.data.forEach(row => {
|
|
121
139
|
let uid = null
|
|
122
140
|
|
|
@@ -124,7 +142,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
124
142
|
|
|
125
143
|
// United States check
|
|
126
144
|
if("us" === obj.general.geoType) {
|
|
127
|
-
const geoName = row[obj.columns.geo.name]
|
|
145
|
+
const geoName = row[obj.columns.geo.name] ? row[obj.columns.geo.name].toUpperCase() : '';
|
|
128
146
|
|
|
129
147
|
// States
|
|
130
148
|
uid = stateKeys.find( (key) => supportedStates[key].includes(geoName) )
|
|
@@ -147,8 +165,13 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
147
165
|
uid = countryKeys.find( (key) => supportedCountries[key].includes(geoName) )
|
|
148
166
|
}
|
|
149
167
|
|
|
150
|
-
//
|
|
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
|
+
}
|
|
151
173
|
|
|
174
|
+
// TODO: Points
|
|
152
175
|
if(uid) {
|
|
153
176
|
Object.defineProperty(row, 'uid', {
|
|
154
177
|
value: uid,
|
|
@@ -161,6 +184,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
161
184
|
})
|
|
162
185
|
|
|
163
186
|
const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
|
|
187
|
+
|
|
164
188
|
const newLegendMemo = new Map(); // Reset memoization
|
|
165
189
|
|
|
166
190
|
const
|
|
@@ -191,7 +215,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
191
215
|
|
|
192
216
|
const applyColorToLegend = (legendIdx) => {
|
|
193
217
|
// Default to "bluegreen" color scheme if the passed color isn't valid
|
|
194
|
-
let mapColorPalette = colorPalettes[obj.color] || colorPalettes['bluegreen']
|
|
218
|
+
let mapColorPalette = obj.customColors || colorPalettes[obj.color] || colorPalettes['bluegreen']
|
|
195
219
|
|
|
196
220
|
let colorIdx = legendIdx - specialClasses
|
|
197
221
|
|
|
@@ -217,30 +241,73 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
217
241
|
|
|
218
242
|
// Special classes
|
|
219
243
|
if (obj.legend.specialClasses.length) {
|
|
220
|
-
|
|
221
|
-
|
|
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]);
|
|
222
248
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
249
|
+
if(specialClass.value === val){
|
|
250
|
+
if(undefined === specialClassesHash[val]) {
|
|
251
|
+
specialClassesHash[val] = true;
|
|
226
252
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
253
|
+
result.push({
|
|
254
|
+
special: true,
|
|
255
|
+
value: val,
|
|
256
|
+
label: specialClass.label
|
|
257
|
+
});
|
|
231
258
|
|
|
232
|
-
|
|
259
|
+
result[result.length - 1].color = applyColorToLegend(result.length - 1);
|
|
233
260
|
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
specialClasses += 1;
|
|
262
|
+
}
|
|
236
263
|
|
|
237
|
-
|
|
264
|
+
let specialColor = '';
|
|
265
|
+
|
|
266
|
+
// color the state if val is in row
|
|
267
|
+
specialColor = result.findIndex(p => p.value === val)
|
|
238
268
|
|
|
239
|
-
|
|
240
|
-
}
|
|
269
|
+
newLegendMemo.set( hashObj(row), specialColor)
|
|
241
270
|
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
}
|
|
244
311
|
}
|
|
245
312
|
|
|
246
313
|
// Category
|
|
@@ -305,7 +372,12 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
305
372
|
return result
|
|
306
373
|
}
|
|
307
374
|
|
|
308
|
-
let
|
|
375
|
+
let uniqueValues = {};
|
|
376
|
+
dataSet.forEach(datum => {
|
|
377
|
+
uniqueValues[datum[primaryCol]] = true;
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
let legendNumber = Math.min(number, Object.keys(uniqueValues).length);
|
|
309
381
|
|
|
310
382
|
// Separate zero
|
|
311
383
|
if(true === obj.legend.separateZero) {
|
|
@@ -388,7 +460,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
388
460
|
|
|
389
461
|
// Equal Interval
|
|
390
462
|
if(type === 'equalinterval') {
|
|
391
|
-
dataSet = dataSet.filter(row => row[primaryCol])
|
|
463
|
+
dataSet = dataSet.filter(row => row[primaryCol] !== undefined)
|
|
392
464
|
let dataMin = dataSet[0][primaryCol]
|
|
393
465
|
let dataMax = dataSet[dataSet.length - 1][primaryCol]
|
|
394
466
|
|
|
@@ -435,11 +507,34 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
435
507
|
|
|
436
508
|
if(hash) filters.fromHash = hash
|
|
437
509
|
|
|
438
|
-
obj
|
|
510
|
+
obj?.filters.forEach(({columnName, label, active, values}, idx) => {
|
|
439
511
|
if(undefined === columnName) return
|
|
440
512
|
|
|
441
513
|
let newFilter = runtimeFilters[idx]
|
|
442
|
-
|
|
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
|
+
}
|
|
443
538
|
|
|
444
539
|
if(undefined === newFilter) {
|
|
445
540
|
newFilter = {}
|
|
@@ -448,7 +543,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
448
543
|
newFilter.label = label ?? ''
|
|
449
544
|
newFilter.columnName = columnName
|
|
450
545
|
newFilter.values = values
|
|
451
|
-
newFilter.active = values[0] // Default to first found value
|
|
546
|
+
newFilter.active = active || values[0] // Default to first found value
|
|
452
547
|
|
|
453
548
|
filters.push(newFilter)
|
|
454
549
|
})
|
|
@@ -466,10 +561,20 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
466
561
|
value : hash
|
|
467
562
|
});
|
|
468
563
|
}
|
|
564
|
+
|
|
469
565
|
|
|
470
566
|
obj.data.forEach(row => {
|
|
471
567
|
if(undefined === row.uid) return false // No UID for this row, we can't use for mapping
|
|
472
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
|
+
|
|
473
578
|
if(row[obj.columns.primary.name]) {
|
|
474
579
|
row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name])
|
|
475
580
|
}
|
|
@@ -477,7 +582,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
477
582
|
// If this is a navigation only map, skip if it doesn't have a URL
|
|
478
583
|
if("navigation" === obj.general.type ) {
|
|
479
584
|
let navigateUrl = row[obj.columns.navigate.name] || "";
|
|
480
|
-
|
|
585
|
+
|
|
586
|
+
if ( undefined !== navigateUrl && typeof navigateUrl === "string" ) {
|
|
481
587
|
// Strip hidden characters before we check length
|
|
482
588
|
navigateUrl = navigateUrl.replace( /(\r\n|\n|\r)/gm, '' );
|
|
483
589
|
}
|
|
@@ -487,11 +593,11 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
487
593
|
}
|
|
488
594
|
|
|
489
595
|
// Filters
|
|
490
|
-
if(filters
|
|
596
|
+
if(filters?.length) {
|
|
491
597
|
for(let i = 0; i < filters.length; i++) {
|
|
492
598
|
const {columnName, active} = filters[i]
|
|
493
599
|
|
|
494
|
-
if (row[columnName]
|
|
600
|
+
if (String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
|
|
495
601
|
}
|
|
496
602
|
}
|
|
497
603
|
|
|
@@ -611,16 +717,29 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
611
717
|
// Reset active legend toggles
|
|
612
718
|
resetLegendToggles()
|
|
613
719
|
|
|
614
|
-
|
|
720
|
+
try {
|
|
721
|
+
|
|
722
|
+
const isEmpty = (obj) => {
|
|
723
|
+
return Object.keys(obj).length === 0;
|
|
724
|
+
}
|
|
615
725
|
|
|
616
|
-
|
|
726
|
+
let filters = [...runtimeFilters]
|
|
617
727
|
|
|
618
|
-
|
|
728
|
+
filters[idx] = { ...filters[idx] }
|
|
619
729
|
|
|
620
|
-
|
|
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
|
+
}
|
|
621
742
|
|
|
622
|
-
setRuntimeData(newData)
|
|
623
|
-
setRuntimeFilters(filters)
|
|
624
743
|
}
|
|
625
744
|
|
|
626
745
|
const displayDataAsText = (value, columnName) => {
|
|
@@ -652,7 +771,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
652
771
|
}
|
|
653
772
|
|
|
654
773
|
// Check if it's a special value. If it is not, apply the designated prefix and suffix
|
|
655
|
-
if (false === state.legend.specialClasses.includes(value)) {
|
|
774
|
+
if (false === state.legend.specialClasses.includes(String(value))) {
|
|
656
775
|
formattedValue = columnObj.prefix + formattedValue + columnObj.suffix
|
|
657
776
|
}
|
|
658
777
|
}
|
|
@@ -683,9 +802,22 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
683
802
|
}
|
|
684
803
|
|
|
685
804
|
const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
|
|
686
|
-
let toolTipText =
|
|
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>`
|
|
687
819
|
|
|
688
|
-
if('data' === state.general.type) {
|
|
820
|
+
if('data' === state.general.type && undefined !== row) {
|
|
689
821
|
toolTipText += `<dl>`
|
|
690
822
|
|
|
691
823
|
Object.keys(state.columns).forEach((columnKey) => {
|
|
@@ -695,7 +827,20 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
695
827
|
|
|
696
828
|
let label = column.label.length > 0 ? column.label : '';
|
|
697
829
|
|
|
698
|
-
let value
|
|
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
|
+
}
|
|
699
844
|
|
|
700
845
|
if(0 < value.length) { // Only spit out the tooltip if there's a value there
|
|
701
846
|
toolTipText += `<div><dt>${label}</dt><dd>${value}</dd></div>`
|
|
@@ -720,6 +865,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
720
865
|
|
|
721
866
|
}
|
|
722
867
|
|
|
868
|
+
const titleCase = (string) => {
|
|
869
|
+
return string.split(' ').map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()).join(' ');
|
|
870
|
+
}
|
|
871
|
+
|
|
723
872
|
// This resets all active legend toggles.
|
|
724
873
|
const resetLegendToggles = async () => {
|
|
725
874
|
let newLegend = [...runtimeLegend]
|
|
@@ -746,7 +895,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
746
895
|
.then(responseText => {
|
|
747
896
|
const parsedCsv = Papa.parse(responseText, {
|
|
748
897
|
header: true,
|
|
749
|
-
dynamicTyping: true
|
|
898
|
+
dynamicTyping: true,
|
|
899
|
+
skipEmptyLines: true
|
|
750
900
|
})
|
|
751
901
|
return parsedCsv.data
|
|
752
902
|
})
|
|
@@ -775,15 +925,19 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
775
925
|
|
|
776
926
|
// Map to first item in values array which is the preferred label
|
|
777
927
|
if(stateKeys.includes(value)) {
|
|
778
|
-
value = supportedStates[key][0]
|
|
928
|
+
value = titleCase(supportedStates[key][0])
|
|
779
929
|
}
|
|
780
930
|
|
|
781
931
|
if(territoryKeys.includes(value)) {
|
|
782
|
-
value = supportedTerritories[key][0]
|
|
932
|
+
value = titleCase(supportedTerritories[key][0])
|
|
783
933
|
}
|
|
784
934
|
|
|
785
935
|
if(countryKeys.includes(value)) {
|
|
786
|
-
value = supportedCountries[key][0]
|
|
936
|
+
value = titleCase(supportedCountries[key][0])
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if(countyKeys.includes(value)) {
|
|
940
|
+
value = titleCase(supportedCounties[key])
|
|
787
941
|
}
|
|
788
942
|
|
|
789
943
|
const dict = {
|
|
@@ -794,7 +948,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
794
948
|
value = dict[value]
|
|
795
949
|
}
|
|
796
950
|
|
|
797
|
-
return value
|
|
951
|
+
return titleCase(value);
|
|
798
952
|
}
|
|
799
953
|
|
|
800
954
|
const navigationHandler = (urlString) => {
|
|
@@ -832,6 +986,20 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
832
986
|
}
|
|
833
987
|
}
|
|
834
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
|
+
|
|
835
1003
|
const loadConfig = async (configObj) => {
|
|
836
1004
|
// Set loading flag
|
|
837
1005
|
if(!loading) setLoading(true)
|
|
@@ -851,8 +1019,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
851
1019
|
let newData = await fetchRemoteData(newState.dataUrl)
|
|
852
1020
|
|
|
853
1021
|
if(newData && newState.dataDescription) {
|
|
854
|
-
newData = transform.autoStandardize(
|
|
855
|
-
newData = transform.developerStandardize(
|
|
1022
|
+
newData = transform.autoStandardize(newData);
|
|
1023
|
+
newData = transform.developerStandardize(newData, newState.dataDescription);
|
|
856
1024
|
}
|
|
857
1025
|
|
|
858
1026
|
if(newData) {
|
|
@@ -876,14 +1044,16 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
876
1044
|
})
|
|
877
1045
|
|
|
878
1046
|
// If there's a name for the geo, add UIDs
|
|
879
|
-
if(newState.columns.geo.name) {
|
|
880
|
-
addUIDs(newState, newState.columns.geo.name)
|
|
1047
|
+
if(newState.columns.geo.name || newState.columns.geo.fips) {
|
|
1048
|
+
addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
|
|
881
1049
|
}
|
|
882
1050
|
|
|
883
1051
|
if(newState.dataTable.forceDisplay === undefined){
|
|
884
1052
|
newState.dataTable.forceDisplay = !isDashboard;
|
|
885
1053
|
}
|
|
886
1054
|
|
|
1055
|
+
|
|
1056
|
+
validateFipsCodeLength(newState);
|
|
887
1057
|
setState(newState)
|
|
888
1058
|
|
|
889
1059
|
// Done loading
|
|
@@ -915,6 +1085,26 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
915
1085
|
}, [])
|
|
916
1086
|
|
|
917
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
|
+
|
|
918
1108
|
// UID
|
|
919
1109
|
if(state.data && state.columns.geo.name && state.columns.geo.name !== state.data.fromColumn) {
|
|
920
1110
|
addUIDs(state, state.columns.geo.name)
|
|
@@ -922,9 +1112,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
922
1112
|
|
|
923
1113
|
// Filters
|
|
924
1114
|
const hashFilters = hashObj(state.filters)
|
|
1115
|
+
let filters;
|
|
925
1116
|
|
|
926
1117
|
if(state.filters && hashFilters !== runtimeFilters.fromHash) {
|
|
927
|
-
|
|
1118
|
+
filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
|
|
928
1119
|
|
|
929
1120
|
if(filters) {
|
|
930
1121
|
setRuntimeFilters(filters)
|
|
@@ -933,42 +1124,56 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
933
1124
|
|
|
934
1125
|
const hashLegend = hashObj({
|
|
935
1126
|
color: state.color,
|
|
1127
|
+
customColors: state.customColors,
|
|
936
1128
|
numberOfItems: state.legend.numberOfItems,
|
|
937
1129
|
type: state.legend.type,
|
|
938
1130
|
separateZero: state.legend.separateZero ?? false,
|
|
939
1131
|
categoryValuesOrder: state.legend.categoryValuesOrder,
|
|
940
1132
|
specialClasses: state.legend.specialClasses,
|
|
941
|
-
geoType: state.general.geoType
|
|
1133
|
+
geoType: state.general.geoType,
|
|
1134
|
+
data: state.data
|
|
942
1135
|
})
|
|
943
1136
|
|
|
944
|
-
// Legend
|
|
945
|
-
if(hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
|
|
946
|
-
const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
|
|
947
|
-
|
|
948
|
-
setRuntimeLegend(legend)
|
|
949
|
-
}
|
|
950
|
-
|
|
951
1137
|
const hashData = hashObj({
|
|
1138
|
+
columns: state.columns,
|
|
952
1139
|
geoType: state.general.geoType,
|
|
953
1140
|
type: state.general.type,
|
|
954
1141
|
geo: state.columns.geo.name,
|
|
955
1142
|
primary: state.columns.primary.name,
|
|
1143
|
+
data: state.data,
|
|
956
1144
|
...runtimeFilters
|
|
957
1145
|
})
|
|
958
1146
|
|
|
959
1147
|
// Data
|
|
1148
|
+
let newRuntimeData;
|
|
960
1149
|
if(hashData !== runtimeData.fromHash && state.data?.fromColumn) {
|
|
961
|
-
|
|
1150
|
+
newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
|
|
1151
|
+
setRuntimeData(newRuntimeData)
|
|
1152
|
+
}
|
|
962
1153
|
|
|
963
|
-
|
|
1154
|
+
// Legend
|
|
1155
|
+
if (hashLegend !== runtimeLegend.fromHash && (undefined === runtimeData.init || newRuntimeData)) {
|
|
1156
|
+
const legend = generateRuntimeLegend(state, newRuntimeData || runtimeData, hashLegend)
|
|
1157
|
+
setRuntimeLegend(legend)
|
|
964
1158
|
}
|
|
965
1159
|
}, [state])
|
|
966
1160
|
|
|
967
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
|
+
|
|
968
1174
|
// Legend - Update when runtimeData does
|
|
969
|
-
if(undefined === runtimeData.init) {
|
|
1175
|
+
if(hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
|
|
970
1176
|
const legend = generateRuntimeLegend(state, runtimeData)
|
|
971
|
-
|
|
972
1177
|
setRuntimeLegend(legend)
|
|
973
1178
|
}
|
|
974
1179
|
}, [runtimeData])
|
|
@@ -1010,8 +1215,6 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
1010
1215
|
mapContainerClasses.push('full-border')
|
|
1011
1216
|
}
|
|
1012
1217
|
|
|
1013
|
-
if(loading) return <Loading />
|
|
1014
|
-
|
|
1015
1218
|
// Props passed to all map types
|
|
1016
1219
|
const mapProps = {
|
|
1017
1220
|
state,
|
|
@@ -1022,100 +1225,160 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
1022
1225
|
navigationHandler,
|
|
1023
1226
|
geoClickHandler,
|
|
1024
1227
|
applyLegendToRow,
|
|
1025
|
-
displayGeoName
|
|
1228
|
+
displayGeoName,
|
|
1229
|
+
runtimeLegend,
|
|
1230
|
+
generateColorsArray,
|
|
1231
|
+
titleCase
|
|
1026
1232
|
}
|
|
1027
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
|
+
|
|
1028
1239
|
return (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
+
);
|
|
1119
1382
|
}
|
|
1120
1383
|
|
|
1121
1384
|
export default memo(CdcMap)
|