@cdc/map 2.6.4 → 9.22.9

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 (46) hide show
  1. package/dist/cdcmap.js +22 -16
  2. package/examples/default-county.json +64 -12
  3. package/examples/default-hex.json +3 -1
  4. package/examples/example-city-state.json +10 -1
  5. package/examples/gallery/categorical-qualitative.json +797 -0
  6. package/examples/gallery/categorical-scale-based.json +739 -0
  7. package/examples/gallery/city-state.json +479 -0
  8. package/examples/gallery/county.json +22731 -0
  9. package/examples/gallery/equal-interval.json +1027 -0
  10. package/examples/gallery/equal-number.json +1027 -0
  11. package/examples/gallery/filterable.json +909 -0
  12. package/examples/gallery/hex-filtered.json +420 -0
  13. package/examples/gallery/hex.json +413 -0
  14. package/examples/gallery/single-state.json +21402 -0
  15. package/examples/gallery/world.json +1592 -0
  16. package/examples/private/city-state.json +428 -0
  17. package/examples/private/cty-issue.json +42768 -0
  18. package/examples/private/default-usa.json +460 -0
  19. package/examples/private/legend-issue.json +1 -0
  20. package/examples/private/map-rounding-error.json +42759 -0
  21. package/examples/private/monkeypox.json +376 -0
  22. package/examples/private/valid-data-map.csv +59 -0
  23. package/examples/private/wcmsrd-14492-data.json +292 -0
  24. package/examples/private/wcmsrd-14492.json +114 -0
  25. package/package.json +3 -3
  26. package/src/CdcMap.js +204 -127
  27. package/src/components/BubbleList.js +9 -5
  28. package/src/components/CityList.js +22 -4
  29. package/src/components/CountyMap.js +13 -4
  30. package/src/components/DataTable.js +9 -9
  31. package/src/components/EditorPanel.js +239 -121
  32. package/src/components/Modal.js +2 -1
  33. package/src/components/NavigationMenu.js +4 -3
  34. package/src/components/Sidebar.js +14 -19
  35. package/src/components/SingleStateMap.js +10 -4
  36. package/src/components/UsaMap.js +82 -30
  37. package/src/components/UsaRegionMap.js +3 -2
  38. package/src/components/WorldMap.js +7 -2
  39. package/src/data/{dfc-map.json → county-map.json} +0 -0
  40. package/src/data/initial-state.js +2 -1
  41. package/src/data/supported-geos.js +5 -0
  42. package/src/index.html +3 -8
  43. package/src/scss/editor-panel.scss +2 -2
  44. package/src/scss/main.scss +1 -1
  45. package/src/scss/map.scss +4 -0
  46. package/src/scss/sidebar.scss +2 -1
package/src/CdcMap.js CHANGED
@@ -9,7 +9,6 @@ import ResizeObserver from 'resize-observer-polyfill';
9
9
  // Third party
10
10
  import ReactTooltip from 'react-tooltip';
11
11
  import chroma from 'chroma-js';
12
- import Papa from 'papaparse';
13
12
  import parse from 'html-react-parser';
14
13
  import html2pdf from 'html2pdf.js'
15
14
  import html2canvas from 'html2canvas';
@@ -36,15 +35,16 @@ import './scss/main.scss';
36
35
  import './scss/btn.scss'
37
36
 
38
37
  // Images
38
+ // TODO: Move to Icon component
39
39
  import DownloadImg from './images/icon-download-img.svg'
40
40
  import DownloadPdf from './images/icon-download-pdf.svg'
41
41
 
42
42
  // Core
43
43
  import Loading from '@cdc/core/components/Loading';
44
- import DataTransform from '@cdc/core/components/DataTransform';
44
+ import { DataTransform } from '@cdc/core/helpers/DataTransform';
45
45
  import getViewport from '@cdc/core/helpers/getViewport';
46
- import numberFromString from '@cdc/core/helpers/numberFromString'
47
- import validateFipsCodeLength from '@cdc/core/helpers/validateFipsCodeLength'
46
+ import numberFromString from '@cdc/core/helpers/numberFromString';
47
+ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData';
48
48
 
49
49
 
50
50
  // Child Components
@@ -113,8 +113,8 @@ const getUniqueValues = (data, columnName) => {
113
113
  return Object.keys(result)
114
114
  }
115
115
 
116
- const CdcMap = ({className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, hostname}) => {
117
-
116
+ const CdcMap = ({className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, setSharedFilter, setSharedFilterValue, hostname = "localhost:8080",link}) => {
117
+
118
118
  const [showLoadingMessage, setShowLoadingMessage] = useState(false)
119
119
  const transform = new DataTransform()
120
120
  const [state, setState] = useState( {...initialState} )
@@ -183,12 +183,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
183
183
  })
184
184
  };
185
185
 
186
-
187
-
188
186
  const resizeObserver = new ResizeObserver(entries => {
189
187
  for (let entry of entries) {
190
188
  let newViewport = getViewport(entry.contentRect.width)
191
-
189
+
192
190
  setCurrentViewport(newViewport)
193
191
  }
194
192
  });
@@ -309,7 +307,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
309
307
  let mapColorPalette = obj.customColors || colorPalettes[obj.color] || colorPalettes['bluegreen']
310
308
 
311
309
  // Handle Region Maps need for a 10th color
312
- if( general.geoType === 'us-region' && mapColorPalette.length < 10 ) {
310
+ if( general.geoType === 'us-region' && mapColorPalette.length < 10 && mapColorPalette.length > 8 ) {
313
311
  if(!general.palette.isReversed) {
314
312
  mapColorPalette.push( chroma(mapColorPalette[8]).darken(0.75).hex() )
315
313
  } else {
@@ -362,8 +360,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
362
360
  }
363
361
 
364
362
  let specialColor = '';
365
-
366
- // color the state if val is in row
363
+
364
+ // color the state if val is in row
367
365
  specialColor = result.findIndex(p => p.value === val)
368
366
 
369
367
  newLegendMemo.set( hashObj(row), specialColor)
@@ -393,10 +391,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
393
391
 
394
392
  specialClasses += 1;
395
393
  }
396
-
394
+
397
395
  let specialColor = '';
398
-
399
- // color the state if val is in row
396
+
397
+ // color the state if val is in row
400
398
  if ( Object.values(row).includes(val) ) {
401
399
  specialColor = result.findIndex(p => p.value === val)
402
400
  }
@@ -479,7 +477,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
479
477
  let legendNumber = Math.min(number, Object.keys(uniqueValues).length);
480
478
 
481
479
  // Separate zero
482
- if(true === obj.legend.separateZero) {
480
+ if(true === obj.legend.separateZero && !state.general.equalNumberOptIn) {
483
481
  let addLegendItem = false;
484
482
 
485
483
  for(let i = 0; i < dataSet.length; i++) {
@@ -562,74 +560,101 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
562
560
  // get nums
563
561
  let hasZeroInData = dataSet.filter(obj => obj[state.columns.primary.name] === 0).length > 0
564
562
  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
563
 
568
564
  domainNums = d3.extent(domainNums)
569
565
  let colors = colorPalettes[state.color]
570
- let colorRange = colors.slice(0, state.legend.separateZero ? state.legend.numberOfItems - 1 : state.legend.numberOfItems)
566
+
567
+ let colorRange = colors.slice(0, state.legend.numberOfItems )
568
+
571
569
  let scale = d3.scaleQuantile()
572
- .domain(dataSet.map(item => Math.round(item[state.columns.primary.name]))) // min/max values
573
- //.domain(domainNums)
570
+ .domain([... new Set(dataSet.map(item => Math.round(item[state.columns.primary.name])))]) // min/max values
574
571
  .range(colorRange) // set range to our colors array
575
572
 
576
573
  let breaks = scale.quantiles();
574
+
577
575
  breaks = breaks.map( item => Math.round(item))
576
+
577
+ // always start with zero for new quantile
578
+ // we can't start at the first break, because there will be items missing.
579
+ // if(d3.extent(domainNums)?.[0] !== 0 && Math.min.apply(null, domainNums) !== 0) {
580
+ // console.log(`Adding: ${d3.extent(domainNums)?.[0]}`)
581
+ // breaks.unshift(d3.extent(domainNums)?.[0])
582
+ // }
578
583
 
579
- console.log('breaks', breaks)
580
- console.log('d', domainNums)
581
584
 
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
-
585
+ // if seperating zero force it into breaks
586
+ if(breaks[0] !== 0) {
587
+ breaks.unshift(0)
588
+ }
589
+
590
590
  breaks.map( (item, index) => {
591
591
 
592
- let min = breaks[index];
593
- let max = breaks[index + 1] - 1;
592
+ const setMin = (index) => {
594
593
 
595
- const setMin = () => {
596
- // in starting position and zero in the data
597
- if(index === 0 && state.legend.separateZero) {
594
+ //debugger;
595
+ let min = breaks[index];
596
+
597
+ // if first break is a seperated zero, min is zero
598
+ if( index === 0 && state.legend.separateZero) {
598
599
  min = 0;
599
600
  }
600
601
 
601
- if(index === 0 && !state.legend.separateZero) {
602
- min = domainNums[0]
602
+ // if we're on the second break, and seperating out zero, increment min to 1.
603
+ if( index === 1 && state.legend.separateZero) {
604
+ min = 1
603
605
  }
604
606
 
607
+ // // in starting position and zero in the data
608
+ // if((index === state.legend.specialClasses?.length ) && (state.legend.specialClasses.length !== 0)) {
609
+ // min = breaks[index]
610
+ // }
611
+ return min;
612
+
605
613
  }
606
614
 
607
- const setMax = () => {
615
+ const setMax = (index, min) => {
616
+
617
+ let max = breaks[index + 1] - 1;
618
+
619
+ // check if min and max range are the same
620
+ // if (min === max + 1) {
621
+ // max = breaks[index + 1]
622
+ // }
623
+
608
624
  if(index === 0 && state.legend.separateZero) {
609
625
  max = 0;
610
626
  }
627
+ // if ((index === state.legend.specialClasses.length && state.legend.specialClasses.length !== 0) && !state.legend.separateZero && hasZeroInData) {
628
+ // max = 0;
629
+ // }
611
630
 
612
631
  if(index + 1 === breaks.length) {
613
632
  max = domainNums[1]
614
633
  }
634
+
635
+ return max;
615
636
  }
616
637
 
617
- setMin()
618
- setMax()
619
- console.log('res', result)
638
+ let min = setMin(index)
639
+ let max = setMax(index, min)
620
640
 
621
- if(index === 0 && result[index]?.max === 0 && state.legend.separateZero) return true;
622
641
  result.push({
623
642
  min,
624
643
  max,
625
644
  color: scale(item)
626
645
  })
627
-
646
+
628
647
 
629
648
  dataSet.forEach( (row, dataIndex) => {
630
649
  let number = row[state.columns.primary.name]
650
+
651
+ let updated = 0
631
652
 
632
- let updated = state.legend.separateZero ? index : index;
653
+ // check if we're seperating zero out
654
+ updated = state.legend.separateZero && hasZeroInData ? index : index;
655
+
656
+ // check for special classes
657
+ updated = state.legend.specialClasses ? updated + state.legend.specialClasses.length : index;
633
658
 
634
659
  if (result[updated]?.min === (null || undefined) || result[updated]?.max === (null || undefined)) return;
635
660
 
@@ -758,7 +783,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
758
783
  value : hash
759
784
  });
760
785
  }
761
-
786
+
762
787
 
763
788
  obj.data.forEach(row => {
764
789
 
@@ -784,7 +809,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
784
809
  // If this is a navigation only map, skip if it doesn't have a URL
785
810
  if("navigation" === obj.general.type ) {
786
811
  let navigateUrl = row[obj.columns.navigate.name] || "";
787
-
812
+
788
813
  if ( undefined !== navigateUrl && typeof navigateUrl === "string" ) {
789
814
  // Strip hidden characters before we check length
790
815
  navigateUrl = navigateUrl.replace( /(\r\n|\n|\r)/gm, '' );
@@ -932,7 +957,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
932
957
 
933
958
  filters[idx].active = activeValue
934
959
  const newData = generateRuntimeData(state, filters)
935
-
960
+
936
961
  // throw an error if newData is empty
937
962
  if (isEmpty(newData)) throw new Error('Cove Filter Error: No runtime data to set for this filter')
938
963
 
@@ -944,12 +969,14 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
944
969
  }
945
970
 
946
971
  }
947
-
948
972
  const displayDataAsText = (value, columnName) => {
949
- if(value === null) {
973
+ if(value === null || value === "" || value === undefined ) {
950
974
  return ""
951
975
  }
952
-
976
+ if(typeof value === 'string' && value.length > 0 && state.legend.type==='equalnumber'){
977
+ return value
978
+ }
979
+
953
980
  let formattedValue = value
954
981
 
955
982
  let columnObj = state.columns[columnName]
@@ -957,20 +984,18 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
957
984
  if (columnObj) {
958
985
  // If value is a number, apply specific formattings
959
986
  if (Number(value)) {
987
+ const decimalPoint = columnObj.roundToPlace ? Number(columnObj.roundToPlace) : 0
988
+
960
989
  // Rounding
961
990
  if(columnObj.hasOwnProperty('roundToPlace') && columnObj.roundToPlace !== "None") {
962
-
963
- const decimalPoint = columnObj.roundToPlace
964
-
965
991
  formattedValue = Number(value).toFixed(decimalPoint)
966
-
992
+
967
993
  }
968
994
 
969
995
  if(columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) {
970
-
971
- formattedValue = Number(value).toLocaleString('en-US', { style: 'decimal'})
972
-
996
+ formattedValue = Number(value).toLocaleString('en-US', { style: 'decimal', minimumFractionDigits: decimalPoint, maximumFractionDigits: decimalPoint })
973
997
  }
998
+
974
999
  }
975
1000
 
976
1001
  // Check if it's a special value. If it is not, apply the designated prefix and suffix
@@ -1084,45 +1109,6 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1084
1109
  setRuntimeLegend(newLegend)
1085
1110
  }
1086
1111
 
1087
- // Supports JSON or CSV
1088
- const fetchRemoteData = async (url) => {
1089
- try {
1090
- const urlObj = new URL(url);
1091
- const regex = /(?:\.([^.]+))?$/
1092
-
1093
- let data = []
1094
-
1095
- const ext = (regex.exec(urlObj.pathname)[1])
1096
- if ('csv' === ext) {
1097
- data = await fetch(url)
1098
- .then(response => response.text())
1099
- .then(responseText => {
1100
- const parsedCsv = Papa.parse(responseText, {
1101
- header: true,
1102
- dynamicTyping: true,
1103
- skipEmptyLines: true
1104
- })
1105
- return parsedCsv.data
1106
- })
1107
- }
1108
-
1109
- if ('json' === ext) {
1110
- data = await fetch(url)
1111
- .then(response => response.json())
1112
- }
1113
-
1114
- return data;
1115
- } catch {
1116
- // If we can't parse it, still attempt to fetch it
1117
- try {
1118
- let response = await (await fetch(configUrl)).json()
1119
- return response
1120
- } catch {
1121
- console.error(`Cannot parse URL: ${url}`);
1122
- }
1123
- }
1124
- }
1125
-
1126
1112
  const formatLegendLocation = (key) => {
1127
1113
  let value = key;
1128
1114
  var formattedName = '';
@@ -1161,7 +1147,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1161
1147
  }
1162
1148
 
1163
1149
  const dict = {
1164
- "District of Columbia" : "Washington D.C.",
1150
+ "Washington D.C." : "District of Columbia",
1151
+ "WASHINGTON DC":"District of Columbia",
1152
+ "DC":"District of Columbia",
1153
+ "WASHINGTON DC.":"District of Columbia",
1165
1154
  "Congo": "Republic of the Congo"
1166
1155
  }
1167
1156
 
@@ -1190,6 +1179,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1190
1179
  }
1191
1180
 
1192
1181
  const geoClickHandler = (key, value) => {
1182
+ if(setSharedFilter){
1183
+ setSharedFilter(state.uid, value);
1184
+ }
1185
+
1193
1186
  // If modals are set or we are on a mobile viewport, display modal
1194
1187
  if ('xs' === currentViewport || 'xxs' === currentViewport || 'click' === state.tooltips.appearanceType) {
1195
1188
  setModal({
@@ -1222,6 +1215,41 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1222
1215
  return newState;
1223
1216
  }
1224
1217
 
1218
+ const handleMapAriaLabels = (state = '', testing = false) => {
1219
+ if(testing) console.log(`handleMapAriaLabels Testing On: ${state}`);
1220
+ try {
1221
+ if(!state.general.geoType) throw Error('handleMapAriaLabels: no geoType found in state');
1222
+ let ariaLabel = '';
1223
+ switch(state.general.geoType) {
1224
+ case 'world':
1225
+ ariaLabel += 'World map'
1226
+ break;
1227
+ case 'us':
1228
+ ariaLabel += 'United States map'
1229
+ break;
1230
+ case 'us-county':
1231
+ ariaLabel += `United States county map`
1232
+ break;
1233
+ case 'single-state':
1234
+ ariaLabel += `${state.general.statePicked.stateName} county map`
1235
+ break;
1236
+ case 'us-region':
1237
+ ariaLabel += `United States HHS Region map`
1238
+ break;
1239
+ default:
1240
+ ariaLabel = 'Data visualization container'
1241
+ break;
1242
+ }
1243
+
1244
+ if(state.general.title) {
1245
+ ariaLabel += ` with the title: ${state.general.title}`
1246
+ }
1247
+ return ariaLabel;
1248
+ } catch(e) {
1249
+ console.error(e.message)
1250
+ }
1251
+ }
1252
+
1225
1253
  const loadConfig = async (configObj) => {
1226
1254
  // Set loading flag
1227
1255
  if(!loading) setLoading(true)
@@ -1239,10 +1267,13 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1239
1267
  // If a dataUrl property exists, always pull from that.
1240
1268
  if (newState.dataUrl) {
1241
1269
  if(newState.dataUrl[0] === '/') {
1242
- newState.dataUrl = 'https://' + hostname + newState.dataUrl + '?v=' + cacheBustingString
1270
+ newState.dataUrl = 'http://' + hostname + newState.dataUrl
1243
1271
  }
1244
1272
 
1245
- let newData = await fetchRemoteData(newState.dataUrl + '?v=' + cacheBustingString )
1273
+ // handle urls with spaces in the name.
1274
+ if (newState.dataUrl) newState.dataUrl = encodeURI(newState.dataUrl + '?v=' + cacheBustingString )
1275
+
1276
+ let newData = await fetchRemoteData(newState.dataUrl )
1246
1277
 
1247
1278
  if(newData && newState.dataDescription) {
1248
1279
  newData = transform.autoStandardize(newData);
@@ -1388,8 +1419,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1388
1419
  // Data
1389
1420
  let newRuntimeData;
1390
1421
  if(hashData !== runtimeData.fromHash && state.data?.fromColumn) {
1391
- const data = generateRuntimeData(state, filters || runtimeFilters, hashData)
1392
- setRuntimeData(data)
1422
+ const newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1423
+ setRuntimeData(newRuntimeData)
1393
1424
  }
1394
1425
 
1395
1426
  // Legend
@@ -1397,7 +1428,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1397
1428
  const legend = generateRuntimeLegend(state, newRuntimeData || runtimeData, hashLegend)
1398
1429
  setRuntimeLegend(legend)
1399
1430
  }
1400
-
1431
+
1401
1432
  }, [state])
1402
1433
 
1403
1434
  useEffect(() => {
@@ -1412,7 +1443,7 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1412
1443
  geoType: state.general.geoType,
1413
1444
  data: state.data
1414
1445
  })
1415
-
1446
+
1416
1447
  // Legend - Update when runtimeData does
1417
1448
  if(hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
1418
1449
  const legend = generateRuntimeLegend(state, runtimeData)
@@ -1429,8 +1460,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1429
1460
 
1430
1461
  // Destructuring for more readable JSX
1431
1462
  const { general, tooltips, dataTable } = state
1432
- const { title = '', subtext = ''} = general
1433
-
1463
+ const { title = '', subtext = '' } = general
1464
+
1434
1465
  // Outer container classes
1435
1466
  let outerContainerClasses = [
1436
1467
  'cdc-open-viz-module',
@@ -1447,7 +1478,8 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1447
1478
  'map-container',
1448
1479
  state.legend.position,
1449
1480
  state.general.type,
1450
- state.general.geoType
1481
+ state.general.geoType,
1482
+ 'outline-none'
1451
1483
  ]
1452
1484
 
1453
1485
  if(modal) {
@@ -1479,13 +1511,40 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1479
1511
  filteredCountryCode,
1480
1512
  position,
1481
1513
  setPosition,
1482
- hasZoom : state.general.allowMapZoom
1514
+ setSharedFilterValue,
1515
+ hasZoom : state.general.allowMapZoom,
1516
+ handleMapAriaLabels
1483
1517
  }
1484
1518
 
1485
1519
  if (!mapProps.data || !state.data) return <Loading />;
1486
1520
 
1487
- const handleMapTabbing = general.showSidebar ? `#legend` : state.general.title ? `#dataTableSection__${state.general.title.replace(/\s/g, '')}` : `#dataTableSection`
1488
-
1521
+ const hasDataTable = state.runtime.editorErrorMessage.length === 0 && true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading;
1522
+
1523
+ const handleMapTabbing = () => {
1524
+ let tabbingID;
1525
+
1526
+ // 1) skip to legend
1527
+ if (general.showSidebar) {
1528
+ tabbingID = '#legend'
1529
+ }
1530
+
1531
+ // 2) skip to data table if it exists and not a navigation map
1532
+ if (hasDataTable && !general.showSidebar) {
1533
+ tabbingID = `#dataTableSection__${Date.now()}`;
1534
+ }
1535
+
1536
+ // 3) if its a navigation map skip to the dropdown.
1537
+ if (state.general.type === 'navigation') {
1538
+ tabbingID = `#dropdown-${Date.now()}`;
1539
+ }
1540
+
1541
+ // 4) handle other options
1542
+ return tabbingID || '#!';
1543
+
1544
+ }
1545
+
1546
+ const tabId = handleMapTabbing()
1547
+
1489
1548
  return (
1490
1549
  <div className={outerContainerClasses.join(' ')} ref={outerContainerRef}>
1491
1550
  {isEditor && (
@@ -1510,12 +1569,20 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1510
1569
  html={true}
1511
1570
  className={tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}
1512
1571
  />
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>
1572
+ )}
1573
+ {state.general.title &&
1574
+ <header className={general.showTitle === true ? 'visible' : 'hidden'} {...(!general.showTitle || !state.general.title ? { "aria-hidden": true } : { "aria-hidden": false })}>
1575
+ <div role='heading' className={'map-title ' + general.headerColor} tabIndex="0" aria-level="2">
1576
+ <sup>{general.superTitle}</sup>
1577
+ <div>{parse(title)}</div>
1578
+ </div>
1579
+ </header>
1580
+ }
1581
+
1582
+ <div>
1583
+ {general.introText && <section className="introText">{parse(general.introText)}</section>}
1584
+ </div>
1585
+
1519
1586
  <section
1520
1587
  role="button"
1521
1588
  tabIndex="0"
@@ -1544,12 +1611,13 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1544
1611
  </div>
1545
1612
  )}
1546
1613
 
1547
- <a id='skip-geo-container' className='cdcdataviz-sr-only-focusable' href={handleMapTabbing}>
1614
+ <a id='skip-geo-container' className='cdcdataviz-sr-only-focusable' href={tabId}>
1548
1615
  Skip Over Map Container
1549
1616
  </a>
1550
- <section className='geography-container' aria-hidden='true' ref={mapSvg}>
1617
+
1618
+ <section className='geography-container outline-none' ref={mapSvg} tabIndex="0">
1551
1619
  {currentViewport && (
1552
- <section className='geography-container' aria-hidden='true' ref={mapSvg}>
1620
+ <section className='geography-container' ref={mapSvg}>
1553
1621
  {modal && (
1554
1622
  <Modal
1555
1623
  type={general.type}
@@ -1580,10 +1648,10 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1580
1648
  )}
1581
1649
  {'data' === general.type && logo && <img src={logo} alt='' className='map-logo' />}
1582
1650
  </section>
1583
-
1651
+
1584
1652
  )}
1585
1653
  </section>
1586
-
1654
+
1587
1655
  {general.showSidebar && 'navigation' !== general.type && (
1588
1656
  <Sidebar
1589
1657
  viewport={currentViewport}
@@ -1599,18 +1667,24 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1599
1667
  resetLegendToggles={resetLegendToggles}
1600
1668
  changeFilterActive={changeFilterActive}
1601
1669
  setAccessibleStatus={setAccessibleStatus}
1670
+ displayDataAsText={displayDataAsText}
1602
1671
  />
1603
1672
  )}
1604
1673
  </section>
1605
1674
  {'navigation' === general.type && (
1606
- <NavigationMenu
1675
+ <NavigationMenu
1676
+ mapTabbingID={tabId}
1607
1677
  displayGeoName={displayGeoName}
1608
1678
  data={runtimeData}
1609
1679
  options={general}
1610
1680
  columns={state.columns}
1611
1681
  navigationHandler={(val) => navigationHandler(val)}
1612
1682
  />
1613
- )}
1683
+ )}
1684
+ {link && link}
1685
+
1686
+ {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1687
+
1614
1688
  {state.runtime.editorErrorMessage.length === 0 && true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading && (
1615
1689
  <DataTable
1616
1690
  state={state}
@@ -1626,15 +1700,18 @@ const CdcMap = ({className, config, navigationHandler: customNavigationHandler,
1626
1700
  displayGeoName={displayGeoName}
1627
1701
  applyLegendToRow={applyLegendToRow}
1628
1702
  tableTitle={dataTable.title}
1629
- indexTitle={dataTable.indexTitle}
1703
+ indexTitle={dataTable.indexLabel}
1630
1704
  mapTitle={general.title}
1631
1705
  viewport={currentViewport}
1632
1706
  formatLegendLocation={formatLegendLocation}
1633
1707
  setFilteredCountryCode={setFilteredCountryCode}
1708
+ tabbingId={tabId}
1634
1709
  />
1635
- )}
1636
- {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1637
- </section>}
1710
+ )}
1711
+
1712
+ {general.footnotes && <section className="footnotes">{parse(general.footnotes)}</section>}
1713
+ </section>}
1714
+
1638
1715
  <div aria-live='assertive' className='cdcdataviz-sr-only'>
1639
1716
  {accessibleStatus}
1640
1717
  </div>
@@ -2,6 +2,7 @@ import React, {memo, useState, useEffect} from 'react'
2
2
  import { scaleLinear } from 'd3-scale';
3
3
  import { countryCoordinates } from '../data/country-coordinates';
4
4
  import stateCoordinates from '../data/state-coordinates';
5
+ import ReactTooltip from 'react-tooltip'
5
6
 
6
7
  export const BubbleList = (
7
8
  {
@@ -15,6 +16,9 @@ export const BubbleList = (
15
16
  displayGeoName
16
17
  }) => {
17
18
 
19
+ useEffect(() => {
20
+ ReactTooltip.hide()
21
+ }, [runtimeData]);
18
22
 
19
23
  const maxDataValue = Math.max(...dataImport.map(d => d[state.columns.primary.name]))
20
24
  const hasBubblesWithZeroOnMap = state.visual.showBubbleZeros ? 0 : 1;
@@ -56,7 +60,7 @@ export const BubbleList = (
56
60
  key={`circle-${countryName.replace(' ', '')}`}
57
61
  data-tip={toolTip}
58
62
  data-for="tooltip"
59
- className="bubble"
63
+ className={`bubble country--${countryName}`}
60
64
  cx={ Number(projection(coordinates[1], coordinates[0])[0]) || 0 } // || 0 handles error on loads where the data isn't ready
61
65
  cy={ Number(projection(coordinates[1], coordinates[0])[1]) || 0 }
62
66
  r={ Number(size(country[primaryKey])) }
@@ -197,9 +201,9 @@ export const BubbleList = (
197
201
  data-tip={toolTip}
198
202
  data-for="tooltip"
199
203
  className="bubble"
200
- cx={projection(coordinates)[0] || 0} // || 0 handles error on loads where the data isn't ready
201
- cy={projection(coordinates)[1] || 0}
202
- r={Number(size(item[primaryKey]) + 1)}
204
+ cx={ projection(coordinates)[0] || 0 } // || 0 handles error on loads where the data isn't ready
205
+ cy={ projection(coordinates)[1] || 0 }
206
+ r={ Number(size(item[primaryKey])) + 1 }
203
207
  fill={"transparent"}
204
208
  stroke={"white"}
205
209
  strokeWidth={.5}
@@ -237,4 +241,4 @@ export const BubbleList = (
237
241
  return bubbles;
238
242
  }
239
243
  }
240
- export default memo(BubbleList)
244
+ export default BubbleList