@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
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 == 0) return hash;
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('lg')
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
- // TODO: Points
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
- dataSet = dataSet.filter(row => {
221
- const val = row[primaryCol]
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
- if( obj.legend.specialClasses.includes(val) ) {
224
- if(undefined === specialClassesHash[val]) {
225
- specialClassesHash[val] = true
249
+ if(specialClass.value === val){
250
+ if(undefined === specialClassesHash[val]) {
251
+ specialClassesHash[val] = true;
226
252
 
227
- result.push({
228
- special: true,
229
- value: val
230
- })
253
+ result.push({
254
+ special: true,
255
+ value: val,
256
+ label: specialClass.label
257
+ });
231
258
 
232
- result[result.length - 1].color = applyColorToLegend(result.length - 1)
259
+ result[result.length - 1].color = applyColorToLegend(result.length - 1);
233
260
 
234
- specialClasses += 1
235
- }
261
+ specialClasses += 1;
262
+ }
236
263
 
237
- newLegendMemo.set( hashObj(row), result.length - 1)
264
+ let specialColor = '';
265
+
266
+ // color the state if val is in row
267
+ specialColor = result.findIndex(p => p.value === val)
238
268
 
239
- return false
240
- }
269
+ newLegendMemo.set( hashObj(row), specialColor)
241
270
 
242
- return true
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 legendNumber = number
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.filters.forEach(({columnName, label}, idx) => {
510
+ obj?.filters.forEach(({columnName, label, active, values}, idx) => {
439
511
  if(undefined === columnName) return
440
512
 
441
513
  let newFilter = runtimeFilters[idx]
442
- let values = getUniqueValues(state.data, columnName)
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
- if ( undefined !== navigateUrl ) {
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.length) {
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] != active) return false // Bail out, not part of filter
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
- let filters = [...runtimeFilters]
720
+ try {
721
+
722
+ const isEmpty = (obj) => {
723
+ return Object.keys(obj).length === 0;
724
+ }
615
725
 
616
- filters[idx] = {...filters[idx]}
726
+ let filters = [...runtimeFilters]
617
727
 
618
- filters[idx].active = activeValue
728
+ filters[idx] = { ...filters[idx] }
619
729
 
620
- const newData = generateRuntimeData(state, filters)
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 = `<strong>${displayGeoName(geoName)}</strong>`
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 = displayDataAsText(row[column.name], columnKey);
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(data);
855
- newData = transform.developerStandardize(data, newState.dataDescription);
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
- const filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
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
- const data = generateRuntimeData(state, runtimeFilters, hashData)
1150
+ newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1151
+ setRuntimeData(newRuntimeData)
1152
+ }
962
1153
 
963
- setRuntimeData(data)
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
- <div className={outerContainerClasses.join(' ')} ref={outerContainerRef}>
1030
- {isEditor && <EditorPanel isDashboard={isDashboard} state={state} setState={setState} loadConfig={loadConfig} setParentConfig={setConfig} runtimeFilters={runtimeFilters} runtimeLegend={runtimeLegend} columnsInData={Object.keys(state.data[0])} />}
1031
- <section className={`cdc-map-inner-container ${currentViewport}`} aria-label={'Map: ' + title}>
1032
- {['lg', 'md'].includes(currentViewport) && 'hover' === tooltips.appearanceType &&
1033
- <ReactTooltip
1034
- id="tooltip"
1035
- place="right"
1036
- type="light"
1037
- html={true}
1038
- className={tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}
1039
- />
1040
- }
1041
- <header className={general.showTitle === true ? '' : 'hidden'} aria-hidden="true">
1042
- <div role="heading" className={'map-title ' + general.headerColor}>
1043
- { parse(title) }
1044
- </div>
1045
- </header>
1046
- <section className={mapContainerClasses.join(' ')} onClick={(e) => closeModal(e)}>
1047
- {general.showDownloadMediaButton === true &&
1048
- <div className="map-downloads" data-html2canvas-ignore>
1049
- <div className="map-downloads__ui btn-group">
1050
- <button className="btn" title="Download Map as Image"
1051
- onClick={() => generateMedia(outerContainerRef.current, 'image')}>
1052
- <DownloadImg className="btn__icon" title='Download Map as Image'/>
1053
- </button>
1054
- <button className="btn" title="Download Map as PDF"
1055
- onClick={() => generateMedia(outerContainerRef.current, 'pdf')}>
1056
- <DownloadPdf className="btn__icon" title='Download Map as PDF'/>
1057
- </button>
1058
- </div>
1059
- </div>
1060
- }
1061
- <section className="geography-container" aria-hidden="true" ref={mapSvg}>
1062
- {modal && <Modal type={general.type} viewport={currentViewport} applyTooltipsToGeo={applyTooltipsToGeo} applyLegendToRow={applyLegendToRow} capitalize={state.tooltips.capitalizeLabels} content={modal} />}
1063
- {'us' === general.geoType && <UsaMap supportedTerritories={supportedTerritories} {...mapProps} />}
1064
- {'world' === general.geoType && <WorldMap supportedCountries={supportedCountries} {...mapProps} />}
1065
- {"data" === general.type && logo && <img src={logo} alt="" className="map-logo"/>}
1066
- </section>
1067
- {general.showSidebar && 'navigation' !== general.type && false === loading &&
1068
- <Sidebar
1069
- viewport={currentViewport}
1070
- legend={state.legend}
1071
- runtimeLegend={runtimeLegend}
1072
- setRuntimeLegend={setRuntimeLegend}
1073
- runtimeFilters={runtimeFilters}
1074
- columns={state.columns}
1075
- sharing={state.sharing}
1076
- prefix={state.columns.primary.prefix}
1077
- suffix={state.columns.primary.suffix}
1078
- setState={setState}
1079
- resetLegendToggles={resetLegendToggles}
1080
- changeFilterActive={changeFilterActive}
1081
- setAccessibleStatus={setAccessibleStatus}
1082
- />
1083
- }
1084
- </section>
1085
- {"navigation" === general.type &&
1086
- <NavigationMenu
1087
- displayGeoName={displayGeoName}
1088
- data={runtimeData}
1089
- options={general}
1090
- columns={state.columns}
1091
- navigationHandler={(val) => navigationHandler(val)}
1092
- />
1093
- }
1094
- {true === dataTable.forceDisplay && general.type !== "navigation" && false === loading &&
1095
- <DataTable
1096
- state={state}
1097
- rawData={state.data}
1098
- navigationHandler={navigationHandler}
1099
- expandDataTable={general.expandDataTable}
1100
- headerColor={general.headerColor}
1101
- columns={state.columns}
1102
- showDownloadButton={general.showDownloadButton}
1103
- runtimeLegend={runtimeLegend}
1104
- runtimeData={runtimeData}
1105
- displayDataAsText={displayDataAsText}
1106
- displayGeoName={displayGeoName}
1107
- applyLegendToRow={applyLegendToRow}
1108
- tableTitle={dataTable.title}
1109
- indexTitle={dataTable.indexTitle}
1110
- mapTitle={general.title}
1111
- viewport={currentViewport}
1112
- />
1113
- }
1114
- {subtext.length > 0 && <p className="subtext">{ parse(subtext) }</p>}
1115
- </section>
1116
- <div aria-live="assertive" className="cdcdataviz-sr-only">{ accessibleStatus }</div>
1117
- </div>
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)