@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.
Files changed (37) hide show
  1. package/dist/cdcmap.js +27 -27
  2. package/examples/default-county.json +105 -0
  3. package/examples/default-single-state.json +109 -0
  4. package/examples/default-usa.json +744 -603
  5. package/examples/example-city-state.json +474 -0
  6. package/examples/example-world-map.json +1596 -0
  7. package/examples/gender-rate-map.json +1 -0
  8. package/package.json +50 -47
  9. package/src/CdcMap.js +422 -159
  10. package/src/components/CityList.js +3 -2
  11. package/src/components/CountyMap.js +556 -0
  12. package/src/components/DataTable.js +73 -19
  13. package/src/components/EditorPanel.js +2088 -1230
  14. package/src/components/Sidebar.js +5 -5
  15. package/src/components/SingleStateMap.js +326 -0
  16. package/src/components/UsaMap.js +20 -3
  17. package/src/data/abbreviations.js +57 -0
  18. package/src/data/color-palettes.js +10 -1
  19. package/src/data/county-map-halfquality.json +58453 -0
  20. package/src/data/county-map-quarterquality.json +1 -0
  21. package/src/data/county-topo.json +1 -0
  22. package/src/data/dfc-map.json +1 -0
  23. package/src/data/initial-state.js +2 -2
  24. package/src/data/newtest.json +1 -0
  25. package/src/data/state-abbreviations.js +60 -0
  26. package/src/data/supported-geos.js +3504 -151
  27. package/src/data/test.json +1 -0
  28. package/src/hooks/useActiveElement.js +19 -0
  29. package/src/index.html +27 -20
  30. package/src/index.js +8 -4
  31. package/src/scss/datatable.scss +2 -1
  32. package/src/scss/main.scss +10 -1
  33. package/src/scss/map.scss +153 -123
  34. package/src/scss/sidebar.scss +0 -1
  35. package/uploads/upload-example-city-state.json +392 -0
  36. package/uploads/upload-example-world-data.json +1490 -0
  37. package/LICENSE +0 -201
@@ -10,7 +10,8 @@ const CityList = (({
10
10
  applyTooltipsToGeo,
11
11
  displayGeoName,
12
12
  applyLegendToRow,
13
- projection
13
+ projection,
14
+ titleCase
14
15
  }) => {
15
16
  const [citiesData, setCitiesData] = useState({});
16
17
 
@@ -27,7 +28,7 @@ const CityList = (({
27
28
  const cityList = Object.keys(citiesData).filter((c) => undefined !== data[c]);
28
29
 
29
30
  const cities = cityList.map((city, i) => {
30
- const cityDisplayName = displayGeoName(city);
31
+ const cityDisplayName = titleCase( displayGeoName(city) );
31
32
 
32
33
  const legendColors = applyLegendToRow(data[city]);
33
34
 
@@ -0,0 +1,556 @@
1
+ import React, { useState, useEffect, memo, useRef } from 'react';
2
+ /** @jsx jsx */
3
+ import { jsx } from '@emotion/react';
4
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
5
+ import { geoCentroid, geoPath } from 'd3-geo';
6
+ import { feature, mesh } from 'topojson-client';
7
+ import { CustomProjection } from '@visx/geo';
8
+ import colorPalettes from '../data/color-palettes';
9
+ import { geoAlbersUsaTerritories } from 'd3-composite-projections';
10
+ import testJSON from '../data/dfc-map.json';
11
+ import { abbrs } from '../data/abbreviations';
12
+
13
+ const offsets = {
14
+ Vermont: [50, -8],
15
+ 'New Hampshire': [34, 5],
16
+ Massachusetts: [30, -5],
17
+ 'Rhode Island': [28, 4],
18
+ Connecticut: [35, 16],
19
+ 'New Jersey': [42, 0],
20
+ Delaware: [33, 0],
21
+ Maryland: [47, 10],
22
+ 'District of Columbia': [30, 20],
23
+ 'Puerto Rico': [10, -20],
24
+ 'Virgin Islands': [10, -10],
25
+ Guam: [10, -5],
26
+ 'American Samoa': [10, 0],
27
+ };
28
+
29
+ // SVG ITEMS
30
+ const WIDTH = 880;
31
+ const HEIGHT = 500;
32
+ const PADDING = 25;
33
+
34
+ // DATA
35
+ let { features: counties } = feature(testJSON, testJSON.objects.counties);
36
+ let { features: states } = feature(testJSON, testJSON.objects.states);
37
+
38
+ // CONSTANTS
39
+ const STATE_BORDER = '#c0cad4';
40
+ const STATE_INACTIVE_FILL = '#F4F7FA';
41
+
42
+ // CREATE STATE LINES
43
+ const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]);
44
+ const path = geoPath().projection(projection);
45
+ const stateLines = path(mesh(testJSON, testJSON.objects.states));
46
+
47
+ const nudges = {
48
+ 'US-FL': [15, 3],
49
+ 'US-AK': [0, -8],
50
+ 'US-CA': [-10, 0],
51
+ 'US-NY': [5, 0],
52
+ 'US-MI': [13, 20],
53
+ 'US-LA': [-10, -3],
54
+ 'US-HI': [-10, 10],
55
+ 'US-ID': [0, 10],
56
+ 'US-WV': [-2, 2],
57
+ };
58
+
59
+ function CountyMapChecks(prevState, nextState) {
60
+ const equalColumnName = prevState.state.general.type && nextState.state.general.type;
61
+ const equalNavColumn = prevState.state.columns.navigate && nextState.state.columns.navigate;
62
+ const equalLegend = prevState.runtimeLegend === nextState.runtimeLegend;
63
+ const equalBorderColors = prevState.state.general.geoBorderColor === nextState.state.general.geoBorderColor; // update when geoborder color changes
64
+ const equalMapColors = prevState.state.color === nextState.state.color; // update when map colors change
65
+ const equalData = prevState.data === nextState.data; // update when data changes
66
+ return equalMapColors && equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn ? true : false;
67
+ }
68
+
69
+ const CountyMap = (props) => {
70
+
71
+ let mapData = states.concat(counties);
72
+
73
+ const {
74
+ state,
75
+ applyTooltipsToGeo,
76
+ data,
77
+ geoClickHandler,
78
+ applyLegendToRow,
79
+ displayGeoName,
80
+ rebuildTooltips,
81
+ containerEl,
82
+ } = props;
83
+
84
+ useEffect(() => {
85
+ if(containerEl) {
86
+ if (containerEl.className.indexOf('loaded') === -1) {
87
+ containerEl.className += ' loaded';
88
+ }
89
+ }
90
+ });
91
+
92
+ // Use State
93
+ const [scale, setScale] = useState(0.85);
94
+ const [startingLineWidth, setStartingLineWidth] = useState(1.3);
95
+ const [translate, setTranslate] = useState([0, 0]);
96
+ const [mapColorPalette, setMapColorPalette] = useState(colorPalettes[state.color] || '#fff');
97
+ const [focusedState, setFocusedState] = useState(null);
98
+ const [showLabel, setShowLabels] = useState(true);
99
+
100
+ const resetButton = useRef();
101
+ const focusedBorderPath = useRef();
102
+ const stateLinesPath = useRef();
103
+ const mapGroup = useRef();
104
+
105
+ let focusedBorderColor = mapColorPalette[3];
106
+ let geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)';
107
+
108
+ // Use Effect
109
+ useEffect(() => rebuildTooltips());
110
+
111
+ const geoLabel = (geo, projection) => {
112
+ let [x, y] = projection(geoCentroid(geo));
113
+ let abbr = abbrs[geo.properties.name];
114
+ if (abbr === 'NJ') x += 3;
115
+ if (undefined === abbr) return null;
116
+ let [dx, dy] = offsets[geo.properties.name];
117
+
118
+ return (
119
+ <>
120
+ <line className='abbrLine' x1={x} y1={y} x2={x + dx} y2={y + dy} stroke='black' strokeWidth={0.85} />
121
+ <text
122
+ className='abbrText'
123
+ x={4}
124
+ strokeWidth='0'
125
+ fontSize={13}
126
+ style={{ fill: '#202020' }}
127
+ alignmentBaseline='middle'
128
+ transform={`translate(${x + dx}, ${y + dy})`}
129
+ >
130
+ {abbr}
131
+ </text>
132
+ </>
133
+ );
134
+ };
135
+
136
+ const focusGeo = (geoKey, geo) => {
137
+ if (!geoKey) {
138
+ console.log('County Map: no geoKey provided to focusGeo');
139
+ return;
140
+ }
141
+
142
+ // 1) Get the state the county is in.
143
+ let myState = states.find((s) => s.id === geoKey);
144
+
145
+ // 2) Set projections translation & scale to the geographic center of the passed geo.
146
+ const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]);
147
+ const newProjection = projection.fitExtent(
148
+ [
149
+ [PADDING, PADDING],
150
+ [WIDTH - PADDING, HEIGHT - PADDING],
151
+ ],
152
+ myState
153
+ );
154
+
155
+ // 3) Gets the new scale
156
+ const newScale = newProjection.scale();
157
+ const hypot = Math.hypot(880, 500);
158
+ const newScaleWithHypot = newScale / 1070;
159
+
160
+ // 4) Pull the x & y out, divide by half the viewport for some reason
161
+ let [x, y] = newProjection.translate();
162
+ x = x - WIDTH / 2;
163
+ y = y - HEIGHT / 2;
164
+
165
+ // 5) Debug if needed
166
+ const debug = {
167
+ width: WIDTH,
168
+ height: HEIGHT,
169
+ beginX: 0,
170
+ beginY: 0,
171
+ hypot: hypot,
172
+ x: x,
173
+ y: y,
174
+ newScale: newScale,
175
+ newScaleWithHypot: newScaleWithHypot,
176
+ geoKey: geoKey,
177
+ geo: geo,
178
+ };
179
+ //console.table(debug)
180
+
181
+ mapGroup.current.setAttribute('transform', `translate(${[x, y]}) scale(${newScaleWithHypot})`);
182
+ resetButton.current.style.display = 'block';
183
+
184
+ // set the states border
185
+ let allStates = document.querySelectorAll('.state path');
186
+ let allCounties = document.querySelectorAll('.county path');
187
+ let currentState = document.querySelector(`.state--${myState.id}`);
188
+ let otherStates = document.querySelectorAll(`.state:not(.state--${myState.id})`);
189
+ let svgContainer = document.querySelector('.svg-container')
190
+ svgContainer.setAttribute('data-scaleZoom', newScaleWithHypot)
191
+
192
+ const state = testJSON.objects.states.geometries.filter((el, index) => {
193
+ return el.id === myState.id;
194
+ });
195
+
196
+ const focusedStateLine = path(mesh(testJSON, state[0]));
197
+
198
+ currentState.style.display = 'none';
199
+
200
+ allStates.forEach((state) => (state.style.strokeWidth = 0.75 / newScaleWithHypot));
201
+ allCounties.forEach((county) => (county.style.strokeWidth = 0.75 / newScaleWithHypot));
202
+ otherStates.forEach((el) => (el.style.display = 'block'));
203
+
204
+ // State Line Updates
205
+ stateLinesPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot);
206
+ stateLinesPath.current.setAttribute('stroke', geoStrokeColor);
207
+
208
+ // Set Focus Border
209
+ focusedBorderPath.current.style.display = 'block';
210
+ focusedBorderPath.current.setAttribute('d', focusedStateLine);
211
+ //focusedBorderPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot);
212
+ //focusedBorderPath.current.setAttribute('stroke', focusedBorderColor)
213
+ };
214
+
215
+ const onReset = (e) => {
216
+ e.preventDefault();
217
+ const svg = document.querySelector('.svg-container')
218
+
219
+ svg.setAttribute('data-scaleZoom', 0)
220
+
221
+
222
+ const allStates = document.querySelectorAll('.state path');
223
+ const allCounties = document.querySelectorAll('.county path');
224
+
225
+ stateLinesPath.current.setAttribute('stroke', geoStrokeColor);
226
+ stateLinesPath.current.setAttribute('stroke-width', startingLineWidth);
227
+
228
+ let otherStates = document.querySelectorAll(`.state--inactive`);
229
+ otherStates.forEach((el) => (el.style.display = 'none'));
230
+ allCounties.forEach((el) => (el.style.strokeWidth = 0.85));
231
+ allStates.forEach((state) => state.setAttribute('stroke-width', .75 / .85 ));
232
+
233
+ mapGroup.current.setAttribute('transform', `translate(${[0, 0]}) scale(${0.85})`);
234
+
235
+ // reset button
236
+ resetButton.current.style.display = 'none';
237
+ };
238
+
239
+ function setStateLeave() {
240
+ focusedBorderPath.current.setAttribute('d', '');
241
+ focusedBorderPath.current.setAttribute('stroke', '');
242
+ focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale);
243
+ }
244
+
245
+ function setStateEnter(id) {
246
+ const svg = document.querySelector('.svg-container')
247
+ const scale = svg.getAttribute('data-scaleZoom');
248
+
249
+ let myState = id.substring(0, 2);
250
+ const allStates = document.querySelectorAll('.state path');
251
+
252
+
253
+ let state = testJSON.objects.states.geometries.filter((el, index) => {
254
+ return el.id === myState;
255
+ });
256
+
257
+ let focusedStateLine = path(mesh(testJSON, state[0]));
258
+ focusedBorderPath.current.style.display = 'block';
259
+ focusedBorderPath.current.setAttribute('d', focusedStateLine);
260
+ focusedBorderPath.current.setAttribute('stroke', '#000');
261
+
262
+ if(scale) {
263
+ allStates.forEach( state => state.setAttribute('stroke-width', 0.75 / scale))
264
+ focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale );
265
+ }
266
+
267
+ }
268
+
269
+ const StateLines = memo(({ stateLines, lineWidth, geoStrokeColor }) => {
270
+ return (
271
+ <g className='stateLines' key='state-line'>
272
+ <path
273
+ id='stateLinesPath'
274
+ ref={stateLinesPath}
275
+ d={stateLines}
276
+ strokeWidth={lineWidth}
277
+ stroke={geoStrokeColor}
278
+ fill='none'
279
+ fillOpacity='1'
280
+ />
281
+ </g>
282
+ );
283
+ });
284
+
285
+ const FocusedStateBorder = memo(() => {
286
+ return (
287
+ <g id='focusedBorder' key='focusedStateBorder'>
288
+ <path
289
+ ref={focusedBorderPath}
290
+ d=''
291
+ strokeWidth=''
292
+ stroke={focusedBorderColor}
293
+ fill='none'
294
+ fillOpacity='1'
295
+ />
296
+ </g>
297
+ );
298
+ });
299
+
300
+ const CountyOutput = memo(({ geographies, counties }) => {
301
+ let output = [];
302
+ output.push(
303
+ counties.map(({ feature: geo, path = '' }) => {
304
+ const key = geo.id + '-group';
305
+
306
+ // COUNTY GROUPS
307
+ let styles = {
308
+ fillOpacity: '1',
309
+ fill: '#E6E6E6',
310
+ cursor: 'default',
311
+ };
312
+
313
+ // Map the name from the geo data with the appropriate key for the processed data
314
+ let geoKey = geo.id;
315
+
316
+ if (!geoKey) return null;
317
+
318
+ const geoData = data[geoKey];
319
+
320
+ let legendColors;
321
+
322
+ // Once we receive data for this geographic item, setup variables.
323
+ if (geoData !== undefined) {
324
+ legendColors = applyLegendToRow(geoData);
325
+ }
326
+
327
+ const geoDisplayName = displayGeoName(geoKey);
328
+
329
+ // For some reason, these two geos are breaking the display.
330
+ if (geoDisplayName === 'Franklin City' || geoDisplayName === 'Waynesboro') return null;
331
+
332
+ // If a legend applies, return it with appropriate information.
333
+ if (legendColors && legendColors[0] !== '#000000') {
334
+ const tooltip = applyTooltipsToGeo(geoDisplayName, geoData);
335
+
336
+ styles = {
337
+ fill: legendColors[0],
338
+ cursor: 'default',
339
+ '&:hover': {
340
+ fill: legendColors[1],
341
+ },
342
+ '&:active': {
343
+ fill: legendColors[2],
344
+ },
345
+ };
346
+
347
+ // When to add pointer cursor
348
+ if (
349
+ (state.columns.navigate && geoData[state.columns.navigate.name]) ||
350
+ state.tooltips.appearanceType === 'hover'
351
+ ) {
352
+ styles.cursor = 'pointer';
353
+ }
354
+ let stateFipsCode = geoData[state.columns.geo.name].substring(0, 2);
355
+
356
+ return (
357
+ <g
358
+ tabIndex="-1"
359
+ data-for='tooltip'
360
+ data-tip={tooltip}
361
+ key={`county--${key}`}
362
+ className={`county county--${geoDisplayName.split(' ').join('')} county--${
363
+ geoData[state.columns.geo.name]
364
+ }`}
365
+ css={styles}
366
+ onMouseEnter={() => {
367
+ setStateEnter(geo.id);
368
+ }}
369
+ onMouseLeave={() => {
370
+ setStateLeave();
371
+ }}
372
+ onClick={
373
+ // default
374
+ (e) => {
375
+ e.stopPropagation();
376
+ e.nativeEvent.stopImmediatePropagation();
377
+ geoClickHandler(geoDisplayName, geoData);
378
+ focusGeo(stateFipsCode, geo);
379
+ }
380
+ }
381
+ >
382
+ <path
383
+ tabIndex={-1}
384
+ className={`county county--${geoDisplayName}`}
385
+ stroke={geoStrokeColor}
386
+ d={path}
387
+ strokeWidth='.5'
388
+ />
389
+ </g>
390
+ );
391
+ }
392
+
393
+ // default county
394
+ return (
395
+ <g
396
+ key={`county--default-${key}`}
397
+ className={`county county--${geoDisplayName}`}
398
+ css={styles}
399
+ strokeWidth=''
400
+ onMouseEnter={() => {
401
+ setStateEnter(geo.id);
402
+ }}
403
+ onMouseLeave={() => {
404
+ setStateLeave();
405
+ }}
406
+ onClick={
407
+ // default
408
+ (e) => {
409
+ e.stopPropagation();
410
+ e.nativeEvent.stopImmediatePropagation();
411
+ let countyFipsCode = geo.id;
412
+ let stateFipsCode = countyFipsCode.substring(0, 2);
413
+ focusGeo(stateFipsCode, geo);
414
+ }
415
+ }
416
+ >
417
+ <path tabIndex={-1} className='single-geo' stroke={geoStrokeColor} d={path} strokeWidth='.85' />
418
+ </g>
419
+ );
420
+ })
421
+ );
422
+ return output;
423
+ });
424
+
425
+ const StateOutput = memo(({ geographies, states }) => {
426
+ let output = [];
427
+ output.push(
428
+ states.map(({ feature: geo, path = '' }) => {
429
+ const key = geo.id + '-group';
430
+
431
+ // Map the name from the geo data with the appropriate key for the processed data
432
+ let geoKey = geo.id;
433
+
434
+ if (!geoKey) return;
435
+
436
+ const geoData = data[geoKey];
437
+
438
+ let legendColors;
439
+
440
+ // Once we receive data for this geographic item, setup variables.
441
+ if (geoData !== undefined) {
442
+ legendColors = applyLegendToRow(geoData);
443
+ }
444
+
445
+ const geoDisplayName = displayGeoName(geoKey);
446
+
447
+ let stateStyles = {
448
+ cursor: 'default',
449
+ stroke: STATE_BORDER,
450
+ strokeWidth: 0.75 / scale,
451
+ display: !focusedState ? 'none' : focusedState && focusedState !== geo.id ? 'block' : 'none',
452
+ fill: focusedState && focusedState !== geo.id ? STATE_INACTIVE_FILL : 'none',
453
+ };
454
+
455
+ let stateSelectedStyles = {
456
+ fillOpacity: 1,
457
+ cursor: 'default',
458
+ };
459
+
460
+ let stateClasses = ['state', `state--${geo.properties.name}`, `state--${geo.id}`];
461
+ focusedState === geo.id ? stateClasses.push('state--focused') : stateClasses.push('state--inactive');
462
+
463
+ return (
464
+ <React.Fragment key={`state--${key}`}>
465
+ <g key={`state--${key}`} className={stateClasses.join(' ')} style={stateStyles} tabIndex="-1">
466
+ <>
467
+ <path
468
+ tabIndex={-1}
469
+ className='state-path'
470
+ d={path}
471
+ fillOpacity={`${focusedState !== geo.id ? '1' : '0'}`}
472
+ fill={STATE_INACTIVE_FILL}
473
+ css={stateSelectedStyles}
474
+ onClick={(e) => {
475
+ e.stopPropagation();
476
+ e.nativeEvent.stopImmediatePropagation();
477
+ focusGeo(geo.id, geo);
478
+ }}
479
+ onMouseEnter={(e) => {
480
+ e.target.attributes.fill.value = colorPalettes[state.color][3];
481
+ }}
482
+ onMouseLeave={(e) => {
483
+ e.target.attributes.fill.value = STATE_INACTIVE_FILL;
484
+ }}
485
+ />
486
+ </>
487
+ </g>
488
+ <g key={`label--${key}`}>
489
+ {offsets[geo.properties.name] &&
490
+ geoLabel(geo, geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]))}
491
+ </g>
492
+ </React.Fragment>
493
+ );
494
+ })
495
+ );
496
+ return output;
497
+ });
498
+
499
+ // Constructs and displays markup for all geos on the map (except territories right now)
500
+ const constructGeoJsx = (geographies, projection) => {
501
+ const states = geographies.slice(0, 56);
502
+ const counties = geographies.slice(56);
503
+ let geosJsx = [];
504
+ geosJsx.push(<CountyOutput geographies={geographies} counties={counties} key="county-key" />);
505
+ geosJsx.push(<StateOutput geographies={geographies} states={states} key="state-key" />);
506
+ geosJsx.push(
507
+ <StateLines
508
+ key='stateLines'
509
+ lineWidth={startingLineWidth}
510
+ geoStrokeColor={geoStrokeColor}
511
+ stateLines={stateLines}
512
+ />
513
+ );
514
+ geosJsx.push(<FocusedStateBorder key="focused-border-key" />);
515
+ return geosJsx;
516
+ };
517
+
518
+ return (
519
+ <ErrorBoundary component='CountyMap'>
520
+ <svg viewBox={`0 0 ${WIDTH} ${HEIGHT}`} preserveAspectRatio='xMinYMin' className='svg-container' data-scale={scale ? scale : ''} data-translate={translate ? translate : ''}>
521
+ <rect
522
+ className='background center-container ocean'
523
+ width={WIDTH}
524
+ height={HEIGHT}
525
+ fillOpacity={1}
526
+ fill='white'
527
+ onClick={(e) => onReset(e)}
528
+ tabIndex="0"
529
+ ></rect>
530
+ <CustomProjection
531
+ data={mapData}
532
+ translate={[WIDTH / 2, HEIGHT / 2]}
533
+ projection={geoAlbersUsaTerritories}
534
+ >
535
+ {({ features, projection }) => {
536
+ return (
537
+ <g
538
+ ref={mapGroup}
539
+ className='countyMapGroup'
540
+ transform={`translate(${translate}) scale(${scale})`}
541
+ key='countyMapGroup'
542
+ >
543
+ {constructGeoJsx(features, geoAlbersUsaTerritories)}
544
+ </g>
545
+ );
546
+ }}
547
+ </CustomProjection>
548
+ </svg>
549
+ <button className={`btn btn--reset`} onClick={onReset} ref={resetButton} style={{ display: 'none' }} tabIndex="0">
550
+ Reset Zoom
551
+ </button>
552
+ </ErrorBoundary>
553
+ );
554
+ };
555
+
556
+ export default memo(CountyMap, CountyMapChecks);