@cdc/map 2.6.2 → 2.6.4
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 +37 -29
- package/examples/bubble-us.json +363 -0
- package/examples/bubble-world.json +427 -0
- package/examples/default-county.json +105 -0
- package/examples/default-hex.json +475 -0
- package/examples/default-single-state.json +109 -0
- package/examples/default-usa-regions.json +118 -0
- package/examples/default-usa.json +744 -603
- package/examples/default-world-data.json +1450 -0
- package/examples/default-world.json +5 -3
- package/examples/example-city-state.json +510 -0
- package/examples/example-world-map.json +1596 -0
- package/examples/gender-rate-map.json +1 -0
- package/examples/private/atsdr.json +439 -0
- package/examples/private/atsdr_new.json +436 -0
- package/examples/private/bubble.json +285 -0
- package/examples/private/default-world-data.json +1444 -0
- package/examples/private/default.json +968 -0
- package/examples/private/map.csv +60 -0
- package/examples/private/mdx.json +210 -0
- package/examples/private/regions.json +52 -0
- package/examples/private/wcmsrd-13881-data.json +2858 -0
- package/examples/private/wcmsrd-13881.json +5823 -0
- package/examples/private/wcmsrd-test.json +268 -0
- package/examples/private/world.json +1580 -0
- package/examples/private/worldmap.json +1490 -0
- package/package.json +11 -7
- package/src/CdcMap.js +726 -202
- package/src/components/BubbleList.js +240 -0
- package/src/components/CityList.js +22 -3
- package/src/components/CountyMap.js +557 -0
- package/src/components/DataTable.js +85 -24
- package/src/components/EditorPanel.js +2455 -1204
- package/src/components/Geo.js +1 -1
- package/src/components/Sidebar.js +5 -5
- package/src/components/SingleStateMap.js +326 -0
- package/src/components/UsaMap.js +41 -9
- package/src/components/UsaRegionMap.js +319 -0
- package/src/components/WorldMap.js +112 -35
- package/src/data/abbreviations.js +57 -0
- package/src/data/country-coordinates.js +250 -0
- package/src/data/county-map-halfquality.json +58453 -0
- package/src/data/county-map-quarterquality.json +1 -0
- package/src/data/county-topo.json +1 -0
- package/src/data/dfc-map.json +1 -0
- package/src/data/initial-state.js +21 -4
- package/src/data/newtest.json +1 -0
- package/src/data/state-abbreviations.js +60 -0
- package/src/data/state-coordinates.js +55 -0
- package/src/data/supported-geos.js +3592 -163
- package/src/data/test.json +1 -0
- package/src/data/us-regions-topo-2.json +360525 -0
- package/src/data/us-regions-topo.json +37894 -0
- package/src/hooks/useActiveElement.js +19 -0
- package/src/hooks/useColorPalette.ts +96 -0
- package/src/index.html +35 -20
- package/src/index.js +8 -4
- package/src/scss/datatable.scss +2 -1
- package/src/scss/editor-panel.scss +76 -55
- package/src/scss/main.scss +10 -1
- package/src/scss/map.scss +257 -121
- 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/data/color-palettes.js +0 -191
- package/src/images/map-folded.svg +0 -1
package/src/CdcMap.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, memo, useCallback } from 'react';
|
|
2
|
+
import * as d3 from 'd3';
|
|
2
3
|
|
|
3
4
|
// IE11
|
|
4
5
|
import 'core-js/stable'
|
|
@@ -15,10 +16,20 @@ import html2canvas from 'html2canvas';
|
|
|
15
16
|
import Canvg from 'canvg';
|
|
16
17
|
|
|
17
18
|
// Data
|
|
19
|
+
import colorPalettes from '../../core/data/colorPalettes';
|
|
18
20
|
import ExternalIcon from './images/external-link.svg';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
+
import {
|
|
22
|
+
supportedStates,
|
|
23
|
+
supportedTerritories,
|
|
24
|
+
supportedCountries,
|
|
25
|
+
supportedCounties,
|
|
26
|
+
supportedCities,
|
|
27
|
+
supportedStatesFipsCodes,
|
|
28
|
+
stateFipsToTwoDigit,
|
|
29
|
+
supportedRegions
|
|
30
|
+
} from './data/supported-geos';
|
|
21
31
|
import initialState from './data/initial-state';
|
|
32
|
+
import { countryCoordinates } from './data/country-coordinates';
|
|
22
33
|
|
|
23
34
|
// Sass
|
|
24
35
|
import './scss/main.scss';
|
|
@@ -33,22 +44,32 @@ import Loading from '@cdc/core/components/Loading';
|
|
|
33
44
|
import DataTransform from '@cdc/core/components/DataTransform';
|
|
34
45
|
import getViewport from '@cdc/core/helpers/getViewport';
|
|
35
46
|
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
47
|
+
import validateFipsCodeLength from '@cdc/core/helpers/validateFipsCodeLength'
|
|
48
|
+
|
|
36
49
|
|
|
37
50
|
// Child Components
|
|
38
51
|
import Sidebar from './components/Sidebar';
|
|
39
52
|
import Modal from './components/Modal';
|
|
40
53
|
import EditorPanel from './components/EditorPanel'; // Future: Lazy
|
|
41
54
|
import UsaMap from './components/UsaMap'; // Future: Lazy
|
|
55
|
+
import UsaRegionMap from './components/UsaRegionMap'; // Future: Lazy
|
|
56
|
+
import CountyMap from './components/CountyMap'; // Future: Lazy
|
|
42
57
|
import DataTable from './components/DataTable'; // Future: Lazy
|
|
43
58
|
import NavigationMenu from './components/NavigationMenu'; // Future: Lazy
|
|
44
59
|
import WorldMap from './components/WorldMap'; // Future: Lazy
|
|
60
|
+
import SingleStateMap from './components/SingleStateMap'; // Future: Lazy
|
|
61
|
+
|
|
62
|
+
import { publish } from '@cdc/core/helpers/events';
|
|
45
63
|
|
|
46
64
|
// Data props
|
|
47
65
|
const stateKeys = Object.keys(supportedStates)
|
|
48
66
|
const territoryKeys = Object.keys(supportedTerritories)
|
|
67
|
+
const regionKeys = Object.keys(supportedRegions)
|
|
49
68
|
const countryKeys = Object.keys(supportedCountries)
|
|
69
|
+
const countyKeys = Object.keys(supportedCounties)
|
|
50
70
|
const cityKeys = Object.keys(supportedCities)
|
|
51
71
|
|
|
72
|
+
|
|
52
73
|
const generateColorsArray = (color = '#000000', special = false) => {
|
|
53
74
|
let colorObj = chroma(color)
|
|
54
75
|
|
|
@@ -65,7 +86,7 @@ const hashObj = (row) => {
|
|
|
65
86
|
let str = JSON.stringify(row)
|
|
66
87
|
|
|
67
88
|
let hash = 0;
|
|
68
|
-
if (str.length
|
|
89
|
+
if (str.length === 0) return hash;
|
|
69
90
|
for (let i = 0; i < str.length; i++) {
|
|
70
91
|
let char = str.charCodeAt(i);
|
|
71
92
|
hash = ((hash<<5)-hash) + char;
|
|
@@ -93,23 +114,81 @@ const getUniqueValues = (data, columnName) => {
|
|
|
93
114
|
}
|
|
94
115
|
|
|
95
116
|
const CdcMap = ({className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, hostname}) => {
|
|
117
|
+
|
|
118
|
+
const [showLoadingMessage, setShowLoadingMessage] = useState(false)
|
|
96
119
|
const transform = new DataTransform()
|
|
97
|
-
|
|
98
120
|
const [state, setState] = useState( {...initialState} )
|
|
99
121
|
const [loading, setLoading] = useState(true)
|
|
100
|
-
const [currentViewport, setCurrentViewport] = useState(
|
|
122
|
+
const [currentViewport, setCurrentViewport] = useState()
|
|
101
123
|
const [runtimeFilters, setRuntimeFilters] = useState([])
|
|
102
124
|
const [runtimeLegend, setRuntimeLegend] = useState([])
|
|
103
125
|
const [runtimeData, setRuntimeData] = useState({init: true})
|
|
104
126
|
const [modal, setModal] = useState(null)
|
|
105
127
|
const [accessibleStatus, setAccessibleStatus] = useState('')
|
|
128
|
+
const [filteredCountryCode, setFilteredCountryCode] = useState()
|
|
129
|
+
const [position, setPosition] = useState(state.mapPosition);
|
|
130
|
+
const [coveLoadedHasRan, setCoveLoadedHasRan] = useState(false)
|
|
131
|
+
const [container, setContainer] = useState()
|
|
106
132
|
|
|
133
|
+
|
|
107
134
|
let legendMemo = useRef(new Map())
|
|
108
135
|
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
try {
|
|
138
|
+
if (filteredCountryCode) {
|
|
139
|
+
const filteredCountryObj = runtimeData[filteredCountryCode]
|
|
140
|
+
const coordinates = countryCoordinates[filteredCountryCode]
|
|
141
|
+
const long = coordinates[1]
|
|
142
|
+
const lat = coordinates[0]
|
|
143
|
+
const reversedCoordinates = [long, lat];
|
|
144
|
+
|
|
145
|
+
setState({
|
|
146
|
+
...state,
|
|
147
|
+
mapPosition: { coordinates: reversedCoordinates, zoom: 3 }
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
} catch(e) {
|
|
152
|
+
console.error('Failed to set world map zoom.')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
}, [filteredCountryCode]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
|
|
159
|
+
setTimeout( () => {
|
|
160
|
+
if (filteredCountryCode) {
|
|
161
|
+
const filteredCountryObj = runtimeData[filteredCountryCode]
|
|
162
|
+
|
|
163
|
+
const tmpData = {
|
|
164
|
+
[filteredCountryCode]: filteredCountryObj
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setRuntimeData(tmpData)
|
|
168
|
+
}
|
|
169
|
+
}, 100)
|
|
170
|
+
|
|
171
|
+
}, [filteredCountryCode]);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (state.mapPosition) {
|
|
175
|
+
setPosition(state.mapPosition)
|
|
176
|
+
}
|
|
177
|
+
}, [state.mapPosition, setPosition]);
|
|
178
|
+
|
|
179
|
+
const setZoom = (reversedCoordinates) => {
|
|
180
|
+
setState({
|
|
181
|
+
...state,
|
|
182
|
+
mapPosition: { coordinates: reversedCoordinates, zoom: 3 }
|
|
183
|
+
})
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
109
188
|
const resizeObserver = new ResizeObserver(entries => {
|
|
110
189
|
for (let entry of entries) {
|
|
111
190
|
let newViewport = getViewport(entry.contentRect.width)
|
|
112
|
-
|
|
191
|
+
|
|
113
192
|
setCurrentViewport(newViewport)
|
|
114
193
|
}
|
|
115
194
|
});
|
|
@@ -117,14 +196,23 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
117
196
|
// 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
197
|
// We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
|
|
119
198
|
const addUIDs = useCallback((obj, fromColumn) => {
|
|
199
|
+
|
|
120
200
|
obj.data.forEach(row => {
|
|
121
201
|
let uid = null
|
|
122
202
|
|
|
123
203
|
if(row.uid) row.uid = null // Wipe existing UIDs
|
|
124
204
|
|
|
125
205
|
// United States check
|
|
126
|
-
if("us" === obj.general.geoType) {
|
|
127
|
-
|
|
206
|
+
if("us" === obj.general.geoType && obj.columns.geo.name) {
|
|
207
|
+
|
|
208
|
+
// const geoName = row[obj.columns.geo.name] && typeof row[obj.columns.geo.name] === "string" ? row[obj.columns.geo.name].toUpperCase() : '';
|
|
209
|
+
|
|
210
|
+
let geoName = '';
|
|
211
|
+
if (row[obj.columns.geo.name] !== undefined && row[obj.columns.geo.name] !== null) {
|
|
212
|
+
|
|
213
|
+
geoName = String(row[obj.columns.geo.name])
|
|
214
|
+
geoName = geoName.toUpperCase()
|
|
215
|
+
}
|
|
128
216
|
|
|
129
217
|
// States
|
|
130
218
|
uid = stateKeys.find( (key) => supportedStates[key].includes(geoName) )
|
|
@@ -140,6 +228,23 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
140
228
|
}
|
|
141
229
|
}
|
|
142
230
|
|
|
231
|
+
if("us-region" === obj.general.geoType && obj.columns.geo.name) {
|
|
232
|
+
|
|
233
|
+
// const geoName = row[obj.columns.geo.name] && typeof row[obj.columns.geo.name] === "string" ? row[obj.columns.geo.name].toUpperCase() : '';
|
|
234
|
+
|
|
235
|
+
let geoName = '';
|
|
236
|
+
if (row[obj.columns.geo.name] !== undefined && row[obj.columns.geo.name] !== null) {
|
|
237
|
+
|
|
238
|
+
geoName = String(row[obj.columns.geo.name])
|
|
239
|
+
geoName = geoName.toUpperCase()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Regions
|
|
243
|
+
uid = regionKeys.find( (key) => supportedRegions[key].includes(geoName) )
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
}
|
|
247
|
+
|
|
143
248
|
// World Check
|
|
144
249
|
if("world" === obj.general.geoType) {
|
|
145
250
|
const geoName = row[obj.columns.geo.name]
|
|
@@ -147,8 +252,13 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
147
252
|
uid = countryKeys.find( (key) => supportedCountries[key].includes(geoName) )
|
|
148
253
|
}
|
|
149
254
|
|
|
150
|
-
//
|
|
255
|
+
// County Check
|
|
256
|
+
if("us-county" === obj.general.geoType || "single-state" === obj.general.geoType) {
|
|
257
|
+
const fips = row[obj.columns.geo.name]
|
|
258
|
+
uid = countyKeys.find( (key) => key === fips )
|
|
259
|
+
}
|
|
151
260
|
|
|
261
|
+
// TODO: Points
|
|
152
262
|
if(uid) {
|
|
153
263
|
Object.defineProperty(row, 'uid', {
|
|
154
264
|
value: uid,
|
|
@@ -161,10 +271,14 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
161
271
|
})
|
|
162
272
|
|
|
163
273
|
const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
|
|
274
|
+
|
|
164
275
|
const newLegendMemo = new Map(); // Reset memoization
|
|
165
276
|
|
|
166
277
|
const
|
|
167
278
|
primaryCol = obj.columns.primary.name,
|
|
279
|
+
isData = obj.general.type === 'data',
|
|
280
|
+
isBubble = obj.general.type === 'bubble',
|
|
281
|
+
categoricalCol = obj.columns.categorical ? obj.columns.categorical.name : undefined,
|
|
168
282
|
type = obj.legend.type,
|
|
169
283
|
number = obj.legend.numberOfItems,
|
|
170
284
|
result = [];
|
|
@@ -186,12 +300,22 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
186
300
|
6: [ 0, 2, 3, 4, 5, 7 ],
|
|
187
301
|
7: [ 0, 2, 3, 4, 5, 6, 7 ],
|
|
188
302
|
8: [ 0, 2, 3, 4, 5, 6, 7, 8 ],
|
|
189
|
-
9: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
|
|
303
|
+
9: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ],
|
|
304
|
+
10: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
|
190
305
|
}
|
|
191
306
|
|
|
192
307
|
const applyColorToLegend = (legendIdx) => {
|
|
193
308
|
// Default to "bluegreen" color scheme if the passed color isn't valid
|
|
194
|
-
let mapColorPalette = colorPalettes[obj.color] || colorPalettes['bluegreen']
|
|
309
|
+
let mapColorPalette = obj.customColors || colorPalettes[obj.color] || colorPalettes['bluegreen']
|
|
310
|
+
|
|
311
|
+
// Handle Region Maps need for a 10th color
|
|
312
|
+
if( general.geoType === 'us-region' && mapColorPalette.length < 10 ) {
|
|
313
|
+
if(!general.palette.isReversed) {
|
|
314
|
+
mapColorPalette.push( chroma(mapColorPalette[8]).darken(0.75).hex() )
|
|
315
|
+
} else {
|
|
316
|
+
mapColorPalette.unshift( chroma(mapColorPalette[0]).darken(0.75).hex() )
|
|
317
|
+
}
|
|
318
|
+
}
|
|
195
319
|
|
|
196
320
|
let colorIdx = legendIdx - specialClasses
|
|
197
321
|
|
|
@@ -217,30 +341,73 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
217
341
|
|
|
218
342
|
// Special classes
|
|
219
343
|
if (obj.legend.specialClasses.length) {
|
|
220
|
-
|
|
221
|
-
|
|
344
|
+
if(typeof obj.legend.specialClasses[0] === 'object'){
|
|
345
|
+
obj.legend.specialClasses.forEach(specialClass => {
|
|
346
|
+
dataSet = dataSet.filter(row => {
|
|
347
|
+
const val = String(row[specialClass.key]);
|
|
222
348
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
349
|
+
if(specialClass.value === val){
|
|
350
|
+
if(undefined === specialClassesHash[val]) {
|
|
351
|
+
specialClassesHash[val] = true;
|
|
226
352
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
353
|
+
result.push({
|
|
354
|
+
special: true,
|
|
355
|
+
value: val,
|
|
356
|
+
label: specialClass.label
|
|
357
|
+
});
|
|
231
358
|
|
|
232
|
-
|
|
359
|
+
result[result.length - 1].color = applyColorToLegend(result.length - 1);
|
|
233
360
|
|
|
234
|
-
|
|
235
|
-
|
|
361
|
+
specialClasses += 1;
|
|
362
|
+
}
|
|
236
363
|
|
|
237
|
-
|
|
364
|
+
let specialColor = '';
|
|
365
|
+
|
|
366
|
+
// color the state if val is in row
|
|
367
|
+
specialColor = result.findIndex(p => p.value === val)
|
|
238
368
|
|
|
239
|
-
|
|
240
|
-
}
|
|
369
|
+
newLegendMemo.set( hashObj(row), specialColor)
|
|
241
370
|
|
|
242
|
-
|
|
243
|
-
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return true;
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
dataSet = dataSet.filter(row => {
|
|
379
|
+
const val = row[primaryCol]
|
|
380
|
+
|
|
381
|
+
if( obj.legend.specialClasses.includes(val) ) {
|
|
382
|
+
|
|
383
|
+
// apply the special color to the legend
|
|
384
|
+
if(undefined === specialClassesHash[val]) {
|
|
385
|
+
specialClassesHash[val] = true;
|
|
386
|
+
|
|
387
|
+
result.push({
|
|
388
|
+
special: true,
|
|
389
|
+
value: val
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
result[result.length - 1].color = applyColorToLegend(result.length - 1);
|
|
393
|
+
|
|
394
|
+
specialClasses += 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let specialColor = '';
|
|
398
|
+
|
|
399
|
+
// color the state if val is in row
|
|
400
|
+
if ( Object.values(row).includes(val) ) {
|
|
401
|
+
specialColor = result.findIndex(p => p.value === val)
|
|
402
|
+
}
|
|
403
|
+
newLegendMemo.set( hashObj(row), specialColor)
|
|
404
|
+
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return true
|
|
409
|
+
})
|
|
410
|
+
}
|
|
244
411
|
}
|
|
245
412
|
|
|
246
413
|
// Category
|
|
@@ -250,8 +417,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
250
417
|
|
|
251
418
|
for(let i = 0; i < dataSet.length; i++) {
|
|
252
419
|
let row = dataSet[i]
|
|
253
|
-
let value = row[primaryCol]
|
|
254
|
-
|
|
420
|
+
let value = isBubble && categoricalCol && row[categoricalCol] ? row[categoricalCol] : row[primaryCol]
|
|
255
421
|
if(undefined === value) continue
|
|
256
422
|
|
|
257
423
|
if(false === uniqueValues.has(value)) {
|
|
@@ -261,7 +427,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
261
427
|
uniqueValues.get(value).push(hashObj(row))
|
|
262
428
|
}
|
|
263
429
|
|
|
264
|
-
if(count ===
|
|
430
|
+
if(count === 10) break // Can only have 10 categorical items for now
|
|
265
431
|
}
|
|
266
432
|
|
|
267
433
|
let sorted = [...uniqueValues.keys()]
|
|
@@ -305,7 +471,12 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
305
471
|
return result
|
|
306
472
|
}
|
|
307
473
|
|
|
308
|
-
let
|
|
474
|
+
let uniqueValues = {};
|
|
475
|
+
dataSet.forEach(datum => {
|
|
476
|
+
uniqueValues[datum[primaryCol]] = true;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
let legendNumber = Math.min(number, Object.keys(uniqueValues).length);
|
|
309
480
|
|
|
310
481
|
// Separate zero
|
|
311
482
|
if(true === obj.legend.separateZero) {
|
|
@@ -347,48 +518,146 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
347
518
|
})
|
|
348
519
|
|
|
349
520
|
// Equal Number
|
|
350
|
-
if(type === 'equalnumber') {
|
|
351
|
-
|
|
521
|
+
if (type === 'equalnumber') {
|
|
522
|
+
// start work on changing legend functionality
|
|
523
|
+
// FALSE === ignore old version for now.
|
|
524
|
+
if (!state.general.equalNumberOptIn) {
|
|
525
|
+
let numberOfRows = dataSet.length
|
|
526
|
+
|
|
527
|
+
let remainder
|
|
528
|
+
let changingNumber = legendNumber
|
|
352
529
|
|
|
353
|
-
|
|
354
|
-
let changingNumber = legendNumber
|
|
530
|
+
let chunkAmt
|
|
355
531
|
|
|
356
|
-
|
|
532
|
+
// Loop through the array until it has been split into equal subarrays
|
|
533
|
+
while (numberOfRows > 0) {
|
|
534
|
+
remainder = numberOfRows % changingNumber
|
|
535
|
+
|
|
536
|
+
chunkAmt = Math.floor(numberOfRows / changingNumber)
|
|
537
|
+
|
|
538
|
+
if (remainder > 0) {
|
|
539
|
+
chunkAmt += 1
|
|
540
|
+
}
|
|
357
541
|
|
|
358
|
-
|
|
359
|
-
while ( numberOfRows > 0 ) {
|
|
360
|
-
remainder = numberOfRows % changingNumber
|
|
542
|
+
let removedRows = dataSet.splice(0, chunkAmt);
|
|
361
543
|
|
|
362
|
-
|
|
544
|
+
let min = removedRows[0][primaryCol],
|
|
545
|
+
max = removedRows[removedRows.length - 1][primaryCol]
|
|
363
546
|
|
|
364
|
-
|
|
365
|
-
|
|
547
|
+
removedRows.forEach(row => {
|
|
548
|
+
newLegendMemo.set(hashObj(row), result.length)
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
result.push({
|
|
552
|
+
min,
|
|
553
|
+
max
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
result[result.length - 1].color = applyColorToLegend(result.length - 1)
|
|
557
|
+
|
|
558
|
+
changingNumber -= 1
|
|
559
|
+
numberOfRows -= chunkAmt
|
|
366
560
|
}
|
|
561
|
+
} else {
|
|
562
|
+
// get nums
|
|
563
|
+
let hasZeroInData = dataSet.filter(obj => obj[state.columns.primary.name] === 0).length > 0
|
|
564
|
+
let domainNums = new Set(dataSet.map(item => item[state.columns.primary.name]))
|
|
565
|
+
console.log('hasZeroInData', hasZeroInData)
|
|
566
|
+
if(hasZeroInData && state.legend.separateZero) { domainNums.add(0) }
|
|
567
|
+
|
|
568
|
+
domainNums = d3.extent(domainNums)
|
|
569
|
+
let colors = colorPalettes[state.color]
|
|
570
|
+
let colorRange = colors.slice(0, state.legend.separateZero ? state.legend.numberOfItems - 1 : state.legend.numberOfItems)
|
|
571
|
+
let scale = d3.scaleQuantile()
|
|
572
|
+
.domain(dataSet.map(item => Math.round(item[state.columns.primary.name]))) // min/max values
|
|
573
|
+
//.domain(domainNums)
|
|
574
|
+
.range(colorRange) // set range to our colors array
|
|
575
|
+
|
|
576
|
+
let breaks = scale.quantiles();
|
|
577
|
+
breaks = breaks.map( item => Math.round(item))
|
|
578
|
+
|
|
579
|
+
console.log('breaks', breaks)
|
|
580
|
+
console.log('d', domainNums)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
// always start with domain beginning breakpoint
|
|
584
|
+
breaks.unshift(d3.extent(domainNums)?.[0])
|
|
585
|
+
|
|
586
|
+
if (state.legend.separateZero && !hasZeroInData) {
|
|
587
|
+
breaks.splice(1, 0, 1);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
breaks.map( (item, index) => {
|
|
591
|
+
|
|
592
|
+
let min = breaks[index];
|
|
593
|
+
let max = breaks[index + 1] - 1;
|
|
594
|
+
|
|
595
|
+
const setMin = () => {
|
|
596
|
+
// in starting position and zero in the data
|
|
597
|
+
if(index === 0 && state.legend.separateZero) {
|
|
598
|
+
min = 0;
|
|
599
|
+
}
|
|
367
600
|
|
|
368
|
-
|
|
601
|
+
if(index === 0 && !state.legend.separateZero) {
|
|
602
|
+
min = domainNums[0]
|
|
603
|
+
}
|
|
369
604
|
|
|
370
|
-
|
|
371
|
-
max = removedRows[removedRows.length - 1][primaryCol]
|
|
605
|
+
}
|
|
372
606
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
607
|
+
const setMax = () => {
|
|
608
|
+
if(index === 0 && state.legend.separateZero) {
|
|
609
|
+
max = 0;
|
|
610
|
+
}
|
|
376
611
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
612
|
+
if(index + 1 === breaks.length) {
|
|
613
|
+
max = domainNums[1]
|
|
614
|
+
}
|
|
615
|
+
}
|
|
381
616
|
|
|
382
|
-
|
|
617
|
+
setMin()
|
|
618
|
+
setMax()
|
|
619
|
+
console.log('res', result)
|
|
620
|
+
|
|
621
|
+
if(index === 0 && result[index]?.max === 0 && state.legend.separateZero) return true;
|
|
622
|
+
result.push({
|
|
623
|
+
min,
|
|
624
|
+
max,
|
|
625
|
+
color: scale(item)
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
dataSet.forEach( (row, dataIndex) => {
|
|
630
|
+
let number = row[state.columns.primary.name]
|
|
631
|
+
|
|
632
|
+
let updated = state.legend.separateZero ? index : index;
|
|
633
|
+
|
|
634
|
+
if (result[updated]?.min === (null || undefined) || result[updated]?.max === (null || undefined)) return;
|
|
635
|
+
|
|
636
|
+
if(number >= result[updated].min && number <= result[updated].max) {
|
|
637
|
+
newLegendMemo.set(hashObj(row), updated)
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
})
|
|
383
643
|
|
|
384
|
-
changingNumber -= 1
|
|
385
|
-
numberOfRows -= chunkAmt
|
|
386
644
|
}
|
|
387
645
|
}
|
|
388
646
|
|
|
389
647
|
// Equal Interval
|
|
390
|
-
|
|
391
|
-
|
|
648
|
+
|
|
649
|
+
if(type === 'equalinterval' && dataSet?.length !== 0) {
|
|
650
|
+
if(!dataSet || dataSet.length === 0) {
|
|
651
|
+
setState({
|
|
652
|
+
...state,
|
|
653
|
+
runtime: {
|
|
654
|
+
...state.runtime,
|
|
655
|
+
editorErrorMessage: 'Error setting equal interval legend type'
|
|
656
|
+
}
|
|
657
|
+
})
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
dataSet = dataSet.filter(row => row[primaryCol] !== undefined)
|
|
392
661
|
let dataMin = dataSet[0][primaryCol]
|
|
393
662
|
let dataMax = dataSet[dataSet.length - 1][primaryCol]
|
|
394
663
|
|
|
@@ -435,11 +704,34 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
435
704
|
|
|
436
705
|
if(hash) filters.fromHash = hash
|
|
437
706
|
|
|
438
|
-
obj
|
|
707
|
+
obj?.filters.forEach(({columnName, label, active, values}, idx) => {
|
|
439
708
|
if(undefined === columnName) return
|
|
440
709
|
|
|
441
710
|
let newFilter = runtimeFilters[idx]
|
|
442
|
-
|
|
711
|
+
|
|
712
|
+
const sortAsc = (a, b) => {
|
|
713
|
+
return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const sortDesc = (a, b) => {
|
|
717
|
+
return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
values = getUniqueValues(state.data, columnName)
|
|
721
|
+
|
|
722
|
+
if(obj.filters[idx].order === 'asc') {
|
|
723
|
+
values = values.sort(sortAsc)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if(obj.filters[idx].order === 'desc') {
|
|
727
|
+
values = values.sort(sortDesc)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if(obj.filters[idx].order === 'cust') {
|
|
731
|
+
if(obj.filters[idx]?.values.length > 0) {
|
|
732
|
+
values = obj.filters[idx].values
|
|
733
|
+
}
|
|
734
|
+
}
|
|
443
735
|
|
|
444
736
|
if(undefined === newFilter) {
|
|
445
737
|
newFilter = {}
|
|
@@ -448,7 +740,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
448
740
|
newFilter.label = label ?? ''
|
|
449
741
|
newFilter.columnName = columnName
|
|
450
742
|
newFilter.values = values
|
|
451
|
-
newFilter.active = values[0] // Default to first found value
|
|
743
|
+
newFilter.active = active || values[0] // Default to first found value
|
|
452
744
|
|
|
453
745
|
filters.push(newFilter)
|
|
454
746
|
})
|
|
@@ -457,7 +749,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
457
749
|
})
|
|
458
750
|
|
|
459
751
|
// Calculates what's going to be displayed on the map and data table at render.
|
|
460
|
-
const generateRuntimeData = useCallback((obj, filters, hash) => {
|
|
752
|
+
const generateRuntimeData = useCallback((obj, filters, hash, test) => {
|
|
461
753
|
const result = {}
|
|
462
754
|
|
|
463
755
|
if(hash) {
|
|
@@ -466,18 +758,34 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
466
758
|
value : hash
|
|
467
759
|
});
|
|
468
760
|
}
|
|
761
|
+
|
|
469
762
|
|
|
470
763
|
obj.data.forEach(row => {
|
|
764
|
+
|
|
765
|
+
if(test) {
|
|
766
|
+
console.log('object', obj)
|
|
767
|
+
console.log('row', row)
|
|
768
|
+
}
|
|
471
769
|
if(undefined === row.uid) return false // No UID for this row, we can't use for mapping
|
|
472
770
|
|
|
771
|
+
// When on a single state map filter runtime data by state
|
|
772
|
+
if (
|
|
773
|
+
!(String(row[obj.columns.geo.name]).substring(0, 2) === obj.general?.statePicked?.fipsCode) &&
|
|
774
|
+
obj.general.geoType === 'single-state'
|
|
775
|
+
) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
|
|
473
780
|
if(row[obj.columns.primary.name]) {
|
|
474
|
-
row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name])
|
|
781
|
+
row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name], state)
|
|
475
782
|
}
|
|
476
783
|
|
|
477
784
|
// If this is a navigation only map, skip if it doesn't have a URL
|
|
478
785
|
if("navigation" === obj.general.type ) {
|
|
479
786
|
let navigateUrl = row[obj.columns.navigate.name] || "";
|
|
480
|
-
|
|
787
|
+
|
|
788
|
+
if ( undefined !== navigateUrl && typeof navigateUrl === "string" ) {
|
|
481
789
|
// Strip hidden characters before we check length
|
|
482
790
|
navigateUrl = navigateUrl.replace( /(\r\n|\n|\r)/gm, '' );
|
|
483
791
|
}
|
|
@@ -487,11 +795,11 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
487
795
|
}
|
|
488
796
|
|
|
489
797
|
// Filters
|
|
490
|
-
if(filters
|
|
798
|
+
if(filters?.length) {
|
|
491
799
|
for(let i = 0; i < filters.length; i++) {
|
|
492
800
|
const {columnName, active} = filters[i]
|
|
493
801
|
|
|
494
|
-
if (row[columnName]
|
|
802
|
+
if (String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
|
|
495
803
|
}
|
|
496
804
|
}
|
|
497
805
|
|
|
@@ -508,6 +816,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
508
816
|
if (node !== null) {
|
|
509
817
|
resizeObserver.observe(node);
|
|
510
818
|
}
|
|
819
|
+
setContainer(node)
|
|
511
820
|
},[]);
|
|
512
821
|
|
|
513
822
|
const mapSvg = useRef(null);
|
|
@@ -611,16 +920,29 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
611
920
|
// Reset active legend toggles
|
|
612
921
|
resetLegendToggles()
|
|
613
922
|
|
|
614
|
-
|
|
923
|
+
try {
|
|
924
|
+
|
|
925
|
+
const isEmpty = (obj) => {
|
|
926
|
+
return Object.keys(obj).length === 0;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
let filters = [...runtimeFilters]
|
|
615
930
|
|
|
616
|
-
|
|
931
|
+
filters[idx] = { ...filters[idx] }
|
|
617
932
|
|
|
618
|
-
|
|
933
|
+
filters[idx].active = activeValue
|
|
934
|
+
const newData = generateRuntimeData(state, filters)
|
|
935
|
+
|
|
936
|
+
// throw an error if newData is empty
|
|
937
|
+
if (isEmpty(newData)) throw new Error('Cove Filter Error: No runtime data to set for this filter')
|
|
619
938
|
|
|
620
|
-
|
|
939
|
+
// set the runtime filters and data
|
|
940
|
+
setRuntimeData(newData)
|
|
941
|
+
setRuntimeFilters(filters)
|
|
942
|
+
} catch(e) {
|
|
943
|
+
console.error(e.message)
|
|
944
|
+
}
|
|
621
945
|
|
|
622
|
-
setRuntimeData(newData)
|
|
623
|
-
setRuntimeFilters(filters)
|
|
624
946
|
}
|
|
625
947
|
|
|
626
948
|
const displayDataAsText = (value, columnName) => {
|
|
@@ -652,8 +974,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
652
974
|
}
|
|
653
975
|
|
|
654
976
|
// 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)) {
|
|
656
|
-
formattedValue = columnObj.prefix + formattedValue + columnObj.suffix
|
|
977
|
+
if (false === state.legend.specialClasses.includes(String(value))) {
|
|
978
|
+
formattedValue = (columnObj.prefix || '') + formattedValue + (columnObj.suffix || '')
|
|
657
979
|
}
|
|
658
980
|
}
|
|
659
981
|
|
|
@@ -683,22 +1005,49 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
683
1005
|
}
|
|
684
1006
|
|
|
685
1007
|
const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
|
|
686
|
-
let toolTipText =
|
|
1008
|
+
let toolTipText = '';
|
|
1009
|
+
|
|
1010
|
+
// Adds geo label, ie State: Georgia
|
|
1011
|
+
let stateOrCounty =
|
|
1012
|
+
state.general.geoType === 'us' ? 'State: ' :
|
|
1013
|
+
(state.general.geoType === 'us-county' || state.general.geoType === 'single-state') ? 'County: ':
|
|
1014
|
+
'';
|
|
1015
|
+
|
|
1016
|
+
if (state.general.geoType === 'us-county') {
|
|
1017
|
+
let stateFipsCode = row[state.columns.geo.name].substring(0,2)
|
|
1018
|
+
const stateName = supportedStatesFipsCodes[stateFipsCode];
|
|
1019
|
+
|
|
1020
|
+
toolTipText += !state.general.hideGeoColumnInTooltip ? `<strong>State: ${stateName}</strong><br/>` : `<strong>${stateName}</strong><br/>` ;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
toolTipText += !state.general.hideGeoColumnInTooltip ? `<strong>${stateOrCounty}${displayGeoName(geoName)}</strong>` : `<strong>${displayGeoName(geoName)}</strong>`
|
|
687
1024
|
|
|
688
|
-
if('data' === state.general.type) {
|
|
1025
|
+
if( ('data' === state.general.type || state.general.type === 'bubble') && undefined !== row) {
|
|
689
1026
|
toolTipText += `<dl>`
|
|
690
1027
|
|
|
691
1028
|
Object.keys(state.columns).forEach((columnKey) => {
|
|
692
1029
|
const column = state.columns[columnKey]
|
|
693
1030
|
|
|
694
1031
|
if (true === column.tooltip) {
|
|
695
|
-
|
|
696
1032
|
let label = column.label.length > 0 ? column.label : '';
|
|
697
1033
|
|
|
698
|
-
let value
|
|
1034
|
+
let value;
|
|
1035
|
+
|
|
1036
|
+
if(state.legend.specialClasses && state.legend.specialClasses.length && typeof state.legend.specialClasses[0] === 'object'){
|
|
1037
|
+
for(let i = 0; i < state.legend.specialClasses.length; i++){
|
|
1038
|
+
if(String(row[state.legend.specialClasses[i].key]) === state.legend.specialClasses[i].value){
|
|
1039
|
+
value = displayDataAsText(state.legend.specialClasses[i].label, columnKey);
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if(!value){
|
|
1046
|
+
value = displayDataAsText(row[column.name], columnKey);
|
|
1047
|
+
}
|
|
699
1048
|
|
|
700
1049
|
if(0 < value.length) { // Only spit out the tooltip if there's a value there
|
|
701
|
-
toolTipText += `<div><dt>${label}</dt><dd>${value}</dd></div>`
|
|
1050
|
+
toolTipText += state.general.hidePrimaryColumnInTooltip ? `<div><dd>${value}</dd></div>` : `<div><dt>${label}</dt><dd>${value}</dd></div>`
|
|
702
1051
|
}
|
|
703
1052
|
|
|
704
1053
|
}
|
|
@@ -720,6 +1069,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
720
1069
|
|
|
721
1070
|
}
|
|
722
1071
|
|
|
1072
|
+
const titleCase = (string) => {
|
|
1073
|
+
return string.split(' ').map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()).join(' ');
|
|
1074
|
+
}
|
|
1075
|
+
|
|
723
1076
|
// This resets all active legend toggles.
|
|
724
1077
|
const resetLegendToggles = async () => {
|
|
725
1078
|
let newLegend = [...runtimeLegend]
|
|
@@ -746,7 +1099,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
746
1099
|
.then(responseText => {
|
|
747
1100
|
const parsedCsv = Papa.parse(responseText, {
|
|
748
1101
|
header: true,
|
|
749
|
-
dynamicTyping: true
|
|
1102
|
+
dynamicTyping: true,
|
|
1103
|
+
skipEmptyLines: true
|
|
750
1104
|
})
|
|
751
1105
|
return parsedCsv.data
|
|
752
1106
|
})
|
|
@@ -769,32 +1123,52 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
769
1123
|
}
|
|
770
1124
|
}
|
|
771
1125
|
|
|
1126
|
+
const formatLegendLocation = (key) => {
|
|
1127
|
+
let value = key;
|
|
1128
|
+
var formattedName = '';
|
|
1129
|
+
let stateName = stateFipsToTwoDigit[key.substring(0, 2)]
|
|
1130
|
+
|
|
1131
|
+
if(stateName) {
|
|
1132
|
+
formattedName += stateName
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (countyKeys.includes(value)) {
|
|
1136
|
+
formattedName += ', ' + titleCase(supportedCounties[key])
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return formattedName;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
772
1142
|
// Attempts to find the corresponding value
|
|
773
1143
|
const displayGeoName = (key) => {
|
|
774
1144
|
let value = key
|
|
775
1145
|
|
|
776
1146
|
// Map to first item in values array which is the preferred label
|
|
777
1147
|
if(stateKeys.includes(value)) {
|
|
778
|
-
value = supportedStates[key][0]
|
|
1148
|
+
value = titleCase(supportedStates[key][0])
|
|
779
1149
|
}
|
|
780
1150
|
|
|
781
1151
|
if(territoryKeys.includes(value)) {
|
|
782
|
-
value = supportedTerritories[key][0]
|
|
1152
|
+
value = titleCase(supportedTerritories[key][0])
|
|
783
1153
|
}
|
|
784
1154
|
|
|
785
1155
|
if(countryKeys.includes(value)) {
|
|
786
|
-
value = supportedCountries[key][0]
|
|
1156
|
+
value = titleCase(supportedCountries[key][0])
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if(countyKeys.includes(value)) {
|
|
1160
|
+
value = titleCase(supportedCounties[key])
|
|
787
1161
|
}
|
|
788
1162
|
|
|
789
1163
|
const dict = {
|
|
790
|
-
"District of Columbia" : "Washington D.C."
|
|
1164
|
+
"District of Columbia" : "Washington D.C.",
|
|
1165
|
+
"Congo": "Republic of the Congo"
|
|
791
1166
|
}
|
|
792
1167
|
|
|
793
1168
|
if(true === Object.keys(dict).includes(value)) {
|
|
794
1169
|
value = dict[value]
|
|
795
1170
|
}
|
|
796
|
-
|
|
797
|
-
return value
|
|
1171
|
+
return titleCase(value);
|
|
798
1172
|
}
|
|
799
1173
|
|
|
800
1174
|
const navigationHandler = (urlString) => {
|
|
@@ -832,7 +1206,23 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
832
1206
|
}
|
|
833
1207
|
}
|
|
834
1208
|
|
|
835
|
-
const
|
|
1209
|
+
const validateFipsCodeLength = (newState) => {
|
|
1210
|
+
if(newState.general.geoType === 'us-county' || newState.general.geoType === 'single-state' || newState.general.geoType === 'us' && newState?.data) {
|
|
1211
|
+
|
|
1212
|
+
newState?.data.forEach(dataPiece => {
|
|
1213
|
+
if(dataPiece[newState.columns.geo.name]) {
|
|
1214
|
+
|
|
1215
|
+
if(!isNaN(parseInt(dataPiece[newState.columns.geo.name])) && dataPiece[newState.columns.geo.name].length === 4) {
|
|
1216
|
+
dataPiece[newState.columns.geo.name] = 0 + dataPiece[newState.columns.geo.name]
|
|
1217
|
+
}
|
|
1218
|
+
dataPiece[newState.columns.geo.name] = dataPiece[newState.columns.geo.name].toString()
|
|
1219
|
+
}
|
|
1220
|
+
})
|
|
1221
|
+
}
|
|
1222
|
+
return newState;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const loadConfig = async (configObj) => {
|
|
836
1226
|
// Set loading flag
|
|
837
1227
|
if(!loading) setLoading(true)
|
|
838
1228
|
|
|
@@ -842,17 +1232,21 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
842
1232
|
...configObj
|
|
843
1233
|
}
|
|
844
1234
|
|
|
1235
|
+
const round = 1000 * 60 * 15;
|
|
1236
|
+
const date = new Date();
|
|
1237
|
+
let cacheBustingString = new Date(date.getTime() - (date.getTime() % round)).toISOString();
|
|
1238
|
+
|
|
845
1239
|
// If a dataUrl property exists, always pull from that.
|
|
846
1240
|
if (newState.dataUrl) {
|
|
847
1241
|
if(newState.dataUrl[0] === '/') {
|
|
848
|
-
newState.dataUrl = 'https://' + hostname + newState.dataUrl
|
|
1242
|
+
newState.dataUrl = 'https://' + hostname + newState.dataUrl + '?v=' + cacheBustingString
|
|
849
1243
|
}
|
|
850
1244
|
|
|
851
|
-
let newData = await fetchRemoteData(newState.dataUrl)
|
|
1245
|
+
let newData = await fetchRemoteData(newState.dataUrl + '?v=' + cacheBustingString )
|
|
852
1246
|
|
|
853
1247
|
if(newData && newState.dataDescription) {
|
|
854
|
-
newData = transform.autoStandardize(
|
|
855
|
-
newData = transform.developerStandardize(
|
|
1248
|
+
newData = transform.autoStandardize(newData);
|
|
1249
|
+
newData = transform.developerStandardize(newData, newState.dataDescription);
|
|
856
1250
|
}
|
|
857
1251
|
|
|
858
1252
|
if(newData) {
|
|
@@ -876,17 +1270,16 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
876
1270
|
})
|
|
877
1271
|
|
|
878
1272
|
// If there's a name for the geo, add UIDs
|
|
879
|
-
if(newState.columns.geo.name) {
|
|
880
|
-
addUIDs(newState, newState.columns.geo.name)
|
|
1273
|
+
if(newState.columns.geo.name || newState.columns.geo.fips) {
|
|
1274
|
+
addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
|
|
881
1275
|
}
|
|
882
1276
|
|
|
883
1277
|
if(newState.dataTable.forceDisplay === undefined){
|
|
884
1278
|
newState.dataTable.forceDisplay = !isDashboard;
|
|
885
1279
|
}
|
|
886
1280
|
|
|
1281
|
+
validateFipsCodeLength(newState);
|
|
887
1282
|
setState(newState)
|
|
888
|
-
|
|
889
|
-
// Done loading
|
|
890
1283
|
setLoading(false)
|
|
891
1284
|
}
|
|
892
1285
|
|
|
@@ -914,6 +1307,42 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
914
1307
|
init()
|
|
915
1308
|
}, [])
|
|
916
1309
|
|
|
1310
|
+
useEffect(() => {
|
|
1311
|
+
if (state && !coveLoadedHasRan && container) {
|
|
1312
|
+
publish('cove_loaded', { config: state })
|
|
1313
|
+
setCoveLoadedHasRan(true)
|
|
1314
|
+
}
|
|
1315
|
+
}, [state, container]);
|
|
1316
|
+
|
|
1317
|
+
// useEffect(() => {
|
|
1318
|
+
// if(state.focusedCountry && state.data) {
|
|
1319
|
+
// let newRuntimeData = state.data.filter(item => item[state.columns.geo.name] === state.focusedCountry[state.columns.geo.name])
|
|
1320
|
+
// let temp = {
|
|
1321
|
+
// ...state,
|
|
1322
|
+
// data: newRuntimeData
|
|
1323
|
+
// }
|
|
1324
|
+
// setRuntimeData(temp)
|
|
1325
|
+
// }
|
|
1326
|
+
|
|
1327
|
+
// }, [state.focusedCountry]);
|
|
1328
|
+
|
|
1329
|
+
useEffect(() => {
|
|
1330
|
+
if (state.data) {
|
|
1331
|
+
let newData = generateRuntimeData(state);
|
|
1332
|
+
setRuntimeData(newData);
|
|
1333
|
+
}
|
|
1334
|
+
}, [state.general.statePicked]);
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
// When geotype changes
|
|
1338
|
+
useEffect(() => {
|
|
1339
|
+
// UID
|
|
1340
|
+
if(state.data && state.columns.geo.name) {
|
|
1341
|
+
addUIDs(state, state.columns.geo.name)
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
}, [state]);
|
|
1345
|
+
|
|
917
1346
|
useEffect(() => {
|
|
918
1347
|
// UID
|
|
919
1348
|
if(state.data && state.columns.geo.name && state.columns.geo.name !== state.data.fromColumn) {
|
|
@@ -922,9 +1351,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
922
1351
|
|
|
923
1352
|
// Filters
|
|
924
1353
|
const hashFilters = hashObj(state.filters)
|
|
1354
|
+
let filters;
|
|
925
1355
|
|
|
926
1356
|
if(state.filters && hashFilters !== runtimeFilters.fromHash) {
|
|
927
|
-
|
|
1357
|
+
filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
|
|
928
1358
|
|
|
929
1359
|
if(filters) {
|
|
930
1360
|
setRuntimeFilters(filters)
|
|
@@ -933,44 +1363,62 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
933
1363
|
|
|
934
1364
|
const hashLegend = hashObj({
|
|
935
1365
|
color: state.color,
|
|
1366
|
+
customColors: state.customColors,
|
|
936
1367
|
numberOfItems: state.legend.numberOfItems,
|
|
937
1368
|
type: state.legend.type,
|
|
938
1369
|
separateZero: state.legend.separateZero ?? false,
|
|
939
1370
|
categoryValuesOrder: state.legend.categoryValuesOrder,
|
|
940
1371
|
specialClasses: state.legend.specialClasses,
|
|
941
|
-
geoType: state.general.geoType
|
|
1372
|
+
geoType: state.general.geoType,
|
|
1373
|
+
data: state.data,
|
|
1374
|
+
...runtimeLegend
|
|
942
1375
|
})
|
|
943
1376
|
|
|
944
|
-
// Legend
|
|
945
|
-
if(hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
|
|
946
|
-
const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
|
|
947
|
-
|
|
948
|
-
setRuntimeLegend(legend)
|
|
949
|
-
}
|
|
950
|
-
|
|
951
1377
|
const hashData = hashObj({
|
|
1378
|
+
columns: state.columns,
|
|
952
1379
|
geoType: state.general.geoType,
|
|
953
1380
|
type: state.general.type,
|
|
954
1381
|
geo: state.columns.geo.name,
|
|
955
1382
|
primary: state.columns.primary.name,
|
|
956
|
-
|
|
1383
|
+
data: state.data,
|
|
1384
|
+
...runtimeFilters,
|
|
1385
|
+
mapPosition: state.mapPosition
|
|
957
1386
|
})
|
|
958
1387
|
|
|
959
1388
|
// Data
|
|
1389
|
+
let newRuntimeData;
|
|
960
1390
|
if(hashData !== runtimeData.fromHash && state.data?.fromColumn) {
|
|
961
|
-
const data = generateRuntimeData(state, runtimeFilters, hashData)
|
|
962
|
-
|
|
1391
|
+
const data = generateRuntimeData(state, filters || runtimeFilters, hashData)
|
|
963
1392
|
setRuntimeData(data)
|
|
964
1393
|
}
|
|
1394
|
+
|
|
1395
|
+
// Legend
|
|
1396
|
+
if (hashLegend !== runtimeLegend.fromHash && (undefined === runtimeData.init || newRuntimeData)) {
|
|
1397
|
+
const legend = generateRuntimeLegend(state, newRuntimeData || runtimeData, hashLegend)
|
|
1398
|
+
setRuntimeLegend(legend)
|
|
1399
|
+
}
|
|
1400
|
+
|
|
965
1401
|
}, [state])
|
|
966
1402
|
|
|
967
1403
|
useEffect(() => {
|
|
1404
|
+
const hashLegend = hashObj({
|
|
1405
|
+
color: state.color,
|
|
1406
|
+
customColors: state.customColors,
|
|
1407
|
+
numberOfItems: state.legend.numberOfItems,
|
|
1408
|
+
type: state.legend.type,
|
|
1409
|
+
separateZero: state.legend.separateZero ?? false,
|
|
1410
|
+
categoryValuesOrder: state.legend.categoryValuesOrder,
|
|
1411
|
+
specialClasses: state.legend.specialClasses,
|
|
1412
|
+
geoType: state.general.geoType,
|
|
1413
|
+
data: state.data
|
|
1414
|
+
})
|
|
1415
|
+
|
|
968
1416
|
// Legend - Update when runtimeData does
|
|
969
|
-
if(undefined === runtimeData.init) {
|
|
1417
|
+
if(hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
|
|
970
1418
|
const legend = generateRuntimeLegend(state, runtimeData)
|
|
971
|
-
|
|
972
1419
|
setRuntimeLegend(legend)
|
|
973
1420
|
}
|
|
1421
|
+
|
|
974
1422
|
}, [runtimeData])
|
|
975
1423
|
|
|
976
1424
|
if(config) {
|
|
@@ -1010,8 +1458,6 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
1010
1458
|
mapContainerClasses.push('full-border')
|
|
1011
1459
|
}
|
|
1012
1460
|
|
|
1013
|
-
if(loading) return <Loading />
|
|
1014
|
-
|
|
1015
1461
|
// Props passed to all map types
|
|
1016
1462
|
const mapProps = {
|
|
1017
1463
|
state,
|
|
@@ -1022,100 +1468,178 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
|
|
|
1022
1468
|
navigationHandler,
|
|
1023
1469
|
geoClickHandler,
|
|
1024
1470
|
applyLegendToRow,
|
|
1025
|
-
displayGeoName
|
|
1471
|
+
displayGeoName,
|
|
1472
|
+
runtimeLegend,
|
|
1473
|
+
generateColorsArray,
|
|
1474
|
+
titleCase,
|
|
1475
|
+
setState,
|
|
1476
|
+
setRuntimeData,
|
|
1477
|
+
generateRuntimeData,
|
|
1478
|
+
setFilteredCountryCode,
|
|
1479
|
+
filteredCountryCode,
|
|
1480
|
+
position,
|
|
1481
|
+
setPosition,
|
|
1482
|
+
hasZoom : state.general.allowMapZoom
|
|
1026
1483
|
}
|
|
1027
1484
|
|
|
1485
|
+
if (!mapProps.data || !state.data) return <Loading />;
|
|
1486
|
+
|
|
1487
|
+
const handleMapTabbing = general.showSidebar ? `#legend` : state.general.title ? `#dataTableSection__${state.general.title.replace(/\s/g, '')}` : `#dataTableSection`
|
|
1488
|
+
|
|
1028
1489
|
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
|
-
|
|
1490
|
+
<div className={outerContainerClasses.join(' ')} ref={outerContainerRef}>
|
|
1491
|
+
{isEditor && (
|
|
1492
|
+
<EditorPanel
|
|
1493
|
+
isDashboard={isDashboard}
|
|
1494
|
+
state={state}
|
|
1495
|
+
setState={setState}
|
|
1496
|
+
loadConfig={loadConfig}
|
|
1497
|
+
setParentConfig={setConfig}
|
|
1498
|
+
setRuntimeFilters={setRuntimeFilters}
|
|
1499
|
+
runtimeFilters={runtimeFilters}
|
|
1500
|
+
runtimeLegend={runtimeLegend}
|
|
1501
|
+
columnsInData={Object.keys(state.data[0])}
|
|
1502
|
+
/>
|
|
1503
|
+
)}
|
|
1504
|
+
{!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && <section className={`cdc-map-inner-container ${currentViewport}`} aria-label={'Map: ' + title}>
|
|
1505
|
+
{['lg', 'md'].includes(currentViewport) && 'hover' === tooltips.appearanceType && (
|
|
1506
|
+
<ReactTooltip
|
|
1507
|
+
id='tooltip'
|
|
1508
|
+
place='right'
|
|
1509
|
+
type='light'
|
|
1510
|
+
html={true}
|
|
1511
|
+
className={tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}
|
|
1512
|
+
/>
|
|
1513
|
+
)}
|
|
1514
|
+
<header className={general.showTitle === true ? 'visible' : 'hidden'} {...(!general.showTitle || !state.general.title ? { "aria-hidden": true } : { "aria-hidden": false } )}>
|
|
1515
|
+
<div role='heading' className={'map-title ' + general.headerColor} tabIndex="0" aria-level="2">
|
|
1516
|
+
{parse(title)}
|
|
1517
|
+
</div>
|
|
1518
|
+
</header>
|
|
1519
|
+
<section
|
|
1520
|
+
role="button"
|
|
1521
|
+
tabIndex="0"
|
|
1522
|
+
className={mapContainerClasses.join(' ')}
|
|
1523
|
+
onClick={(e) => closeModal(e)}
|
|
1524
|
+
onKeyDown={(e) => { if (e.keyCode === 13) { closeModal(e) } }}
|
|
1525
|
+
>
|
|
1526
|
+
{general.showDownloadMediaButton === true && (
|
|
1527
|
+
<div className='map-downloads' data-html2canvas-ignore>
|
|
1528
|
+
<div className='map-downloads__ui btn-group'>
|
|
1529
|
+
<button
|
|
1530
|
+
className='btn'
|
|
1531
|
+
title='Download Map as Image'
|
|
1532
|
+
onClick={() => generateMedia(outerContainerRef.current, 'image')}
|
|
1533
|
+
>
|
|
1534
|
+
<DownloadImg className='btn__icon' title='Download Map as Image' />
|
|
1535
|
+
</button>
|
|
1536
|
+
<button
|
|
1537
|
+
className='btn'
|
|
1538
|
+
title='Download Map as PDF'
|
|
1539
|
+
onClick={() => generateMedia(outerContainerRef.current, 'pdf')}
|
|
1540
|
+
>
|
|
1541
|
+
<DownloadPdf className='btn__icon' title='Download Map as PDF' />
|
|
1542
|
+
</button>
|
|
1543
|
+
</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
)}
|
|
1546
|
+
|
|
1547
|
+
<a id='skip-geo-container' className='cdcdataviz-sr-only-focusable' href={handleMapTabbing}>
|
|
1548
|
+
Skip Over Map Container
|
|
1549
|
+
</a>
|
|
1550
|
+
<section className='geography-container' aria-hidden='true' ref={mapSvg}>
|
|
1551
|
+
{currentViewport && (
|
|
1552
|
+
<section className='geography-container' aria-hidden='true' ref={mapSvg}>
|
|
1553
|
+
{modal && (
|
|
1554
|
+
<Modal
|
|
1555
|
+
type={general.type}
|
|
1556
|
+
viewport={currentViewport}
|
|
1557
|
+
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
1558
|
+
applyLegendToRow={applyLegendToRow}
|
|
1559
|
+
capitalize={state.tooltips.capitalizeLabels}
|
|
1560
|
+
content={modal}
|
|
1561
|
+
/>
|
|
1562
|
+
)}
|
|
1563
|
+
{'single-state' === general.geoType && (
|
|
1564
|
+
<SingleStateMap supportedTerritories={supportedTerritories} {...mapProps} />
|
|
1565
|
+
)}
|
|
1566
|
+
{'us' === general.geoType && (
|
|
1567
|
+
<UsaMap supportedTerritories={supportedTerritories} {...mapProps} />
|
|
1568
|
+
)}
|
|
1569
|
+
{'us-region' === general.geoType && (
|
|
1570
|
+
<UsaRegionMap supportedTerritories={supportedTerritories} {...mapProps} />
|
|
1571
|
+
)}
|
|
1572
|
+
{'world' === general.geoType && (
|
|
1573
|
+
<WorldMap supportedCountries={supportedCountries} {...mapProps} />
|
|
1574
|
+
)}
|
|
1575
|
+
{'us-county' === general.geoType && (
|
|
1576
|
+
<CountyMap
|
|
1577
|
+
supportedCountries={supportedCountries}
|
|
1578
|
+
{...mapProps}
|
|
1579
|
+
/>
|
|
1580
|
+
)}
|
|
1581
|
+
{'data' === general.type && logo && <img src={logo} alt='' className='map-logo' />}
|
|
1582
|
+
</section>
|
|
1583
|
+
|
|
1584
|
+
)}
|
|
1585
|
+
</section>
|
|
1586
|
+
|
|
1587
|
+
{general.showSidebar && 'navigation' !== general.type && (
|
|
1588
|
+
<Sidebar
|
|
1589
|
+
viewport={currentViewport}
|
|
1590
|
+
legend={state.legend}
|
|
1591
|
+
runtimeLegend={runtimeLegend}
|
|
1592
|
+
setRuntimeLegend={setRuntimeLegend}
|
|
1593
|
+
runtimeFilters={runtimeFilters}
|
|
1594
|
+
columns={state.columns}
|
|
1595
|
+
sharing={state.sharing}
|
|
1596
|
+
prefix={state.columns.primary.prefix}
|
|
1597
|
+
suffix={state.columns.primary.suffix}
|
|
1598
|
+
setState={setState}
|
|
1599
|
+
resetLegendToggles={resetLegendToggles}
|
|
1600
|
+
changeFilterActive={changeFilterActive}
|
|
1601
|
+
setAccessibleStatus={setAccessibleStatus}
|
|
1602
|
+
/>
|
|
1603
|
+
)}
|
|
1604
|
+
</section>
|
|
1605
|
+
{'navigation' === general.type && (
|
|
1606
|
+
<NavigationMenu
|
|
1607
|
+
displayGeoName={displayGeoName}
|
|
1608
|
+
data={runtimeData}
|
|
1609
|
+
options={general}
|
|
1610
|
+
columns={state.columns}
|
|
1611
|
+
navigationHandler={(val) => navigationHandler(val)}
|
|
1612
|
+
/>
|
|
1613
|
+
)}
|
|
1614
|
+
{state.runtime.editorErrorMessage.length === 0 && true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading && (
|
|
1615
|
+
<DataTable
|
|
1616
|
+
state={state}
|
|
1617
|
+
rawData={state.data}
|
|
1618
|
+
navigationHandler={navigationHandler}
|
|
1619
|
+
expandDataTable={general.expandDataTable}
|
|
1620
|
+
headerColor={general.headerColor}
|
|
1621
|
+
columns={state.columns}
|
|
1622
|
+
showDownloadButton={general.showDownloadButton}
|
|
1623
|
+
runtimeLegend={runtimeLegend}
|
|
1624
|
+
runtimeData={runtimeData}
|
|
1625
|
+
displayDataAsText={displayDataAsText}
|
|
1626
|
+
displayGeoName={displayGeoName}
|
|
1627
|
+
applyLegendToRow={applyLegendToRow}
|
|
1628
|
+
tableTitle={dataTable.title}
|
|
1629
|
+
indexTitle={dataTable.indexTitle}
|
|
1630
|
+
mapTitle={general.title}
|
|
1631
|
+
viewport={currentViewport}
|
|
1632
|
+
formatLegendLocation={formatLegendLocation}
|
|
1633
|
+
setFilteredCountryCode={setFilteredCountryCode}
|
|
1634
|
+
/>
|
|
1635
|
+
)}
|
|
1636
|
+
{subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
|
|
1637
|
+
</section>}
|
|
1638
|
+
<div aria-live='assertive' className='cdcdataviz-sr-only'>
|
|
1639
|
+
{accessibleStatus}
|
|
1640
|
+
</div>
|
|
1641
|
+
</div>
|
|
1642
|
+
);
|
|
1119
1643
|
}
|
|
1120
1644
|
|
|
1121
1645
|
export default memo(CdcMap)
|