@cdc/map 2.6.3 → 4.22.10-alpha.1

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 (76) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcmap.js +32 -18
  3. package/examples/bubble-us.json +363 -0
  4. package/examples/bubble-world.json +427 -0
  5. package/examples/default-county.json +64 -12
  6. package/examples/default-geocode.json +746 -0
  7. package/examples/default-hex.json +477 -0
  8. package/examples/default-usa-regions.json +118 -0
  9. package/examples/default-usa.json +1 -1
  10. package/examples/default-world-data.json +1450 -0
  11. package/examples/default-world.json +5 -3
  12. package/examples/example-city-state.json +46 -1
  13. package/examples/gallery/categorical-qualitative.json +797 -0
  14. package/examples/gallery/categorical-scale-based.json +739 -0
  15. package/examples/gallery/city-state.json +479 -0
  16. package/examples/gallery/county.json +22731 -0
  17. package/examples/gallery/equal-interval.json +1027 -0
  18. package/examples/gallery/equal-number.json +1027 -0
  19. package/examples/gallery/filterable.json +909 -0
  20. package/examples/gallery/hex-filtered.json +420 -0
  21. package/examples/gallery/hex.json +413 -0
  22. package/examples/gallery/single-state.json +21402 -0
  23. package/examples/gallery/world.json +1592 -0
  24. package/examples/private/atsdr.json +439 -0
  25. package/examples/private/atsdr_new.json +436 -0
  26. package/examples/private/bubble.json +285 -0
  27. package/examples/private/city-state.json +428 -0
  28. package/examples/private/city-state2.json +434 -0
  29. package/examples/private/cty-issue.json +42768 -0
  30. package/examples/private/default-usa.json +460 -0
  31. package/examples/private/default-world-data.json +1444 -0
  32. package/examples/private/default.json +968 -0
  33. package/examples/private/legend-issue.json +1 -0
  34. package/examples/private/map-rounding-error.json +42759 -0
  35. package/examples/private/map.csv +60 -0
  36. package/examples/private/mdx.json +210 -0
  37. package/examples/private/monkeypox.json +376 -0
  38. package/examples/private/regions.json +52 -0
  39. package/examples/private/valid-data-map.csv +59 -0
  40. package/examples/private/wcmsrd-13881-data.json +2858 -0
  41. package/examples/private/wcmsrd-13881.json +5823 -0
  42. package/examples/private/wcmsrd-14492-data.json +292 -0
  43. package/examples/private/wcmsrd-14492.json +114 -0
  44. package/examples/private/wcmsrd-test.json +268 -0
  45. package/examples/private/world.json +1580 -0
  46. package/examples/private/worldmap.json +1490 -0
  47. package/package.json +51 -50
  48. package/src/CdcMap.js +1384 -1075
  49. package/src/components/BubbleList.js +244 -0
  50. package/src/components/CityList.js +79 -17
  51. package/src/components/CountyMap.js +104 -44
  52. package/src/components/DataTable.js +32 -22
  53. package/src/components/EditorPanel.js +977 -414
  54. package/src/components/Geo.js +1 -1
  55. package/src/components/Modal.js +2 -1
  56. package/src/components/NavigationMenu.js +4 -3
  57. package/src/components/Sidebar.js +14 -19
  58. package/src/components/SingleStateMap.js +178 -249
  59. package/src/components/UsaMap.js +104 -36
  60. package/src/components/UsaRegionMap.js +320 -0
  61. package/src/components/WorldMap.js +117 -34
  62. package/src/data/country-coordinates.js +250 -0
  63. package/src/data/{dfc-map.json → county-map.json} +0 -0
  64. package/src/data/initial-state.js +23 -3
  65. package/src/data/state-coordinates.js +55 -0
  66. package/src/data/supported-geos.js +101 -15
  67. package/src/data/us-regions-topo-2.json +360525 -0
  68. package/src/data/us-regions-topo.json +37894 -0
  69. package/src/hooks/useColorPalette.ts +96 -0
  70. package/src/index.html +8 -4
  71. package/src/scss/editor-panel.scss +78 -57
  72. package/src/scss/main.scss +1 -6
  73. package/src/scss/map.scss +126 -2
  74. package/src/scss/sidebar.scss +2 -1
  75. package/src/data/color-palettes.js +0 -200
  76. package/src/images/map-folded.svg +0 -1
package/src/CdcMap.js CHANGED
@@ -1,1384 +1,1693 @@
1
- import React, { useState, useEffect, useRef, memo, useCallback } from 'react';
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'
5
6
  import 'whatwg-fetch'
6
- import ResizeObserver from 'resize-observer-polyfill';
7
+ import ResizeObserver from 'resize-observer-polyfill'
7
8
 
8
9
  // Third party
9
- import ReactTooltip from 'react-tooltip';
10
- import chroma from 'chroma-js';
11
- import Papa from 'papaparse';
12
- import parse from 'html-react-parser';
10
+ import ReactTooltip from 'react-tooltip'
11
+ import chroma from 'chroma-js'
12
+ import parse from 'html-react-parser'
13
13
  import html2pdf from 'html2pdf.js'
14
- import html2canvas from 'html2canvas';
15
- import Canvg from 'canvg';
14
+ import html2canvas from 'html2canvas'
15
+ import Canvg from 'canvg'
16
16
 
17
17
  // Data
18
- import ExternalIcon from './images/external-link.svg';
19
- import { supportedStates, supportedTerritories, supportedCountries, supportedCounties, supportedCities, supportedStatesFipsCodes } from './data/supported-geos';
20
- import colorPalettes from './data/color-palettes';
21
- import initialState from './data/initial-state';
18
+ import colorPalettes from '../../core/data/colorPalettes'
19
+ import ExternalIcon from './images/external-link.svg'
20
+ import {
21
+ supportedStates,
22
+ supportedTerritories,
23
+ supportedCountries,
24
+ supportedCounties,
25
+ supportedCities,
26
+ supportedStatesFipsCodes,
27
+ stateFipsToTwoDigit,
28
+ supportedRegions
29
+ } from './data/supported-geos'
30
+ import initialState from './data/initial-state'
31
+ import { countryCoordinates } from './data/country-coordinates'
22
32
 
23
33
  // Sass
24
- import './scss/main.scss';
34
+ import './scss/main.scss'
25
35
  import './scss/btn.scss'
26
36
 
27
37
  // Images
38
+ // TODO: Move to Icon component
28
39
  import DownloadImg from './images/icon-download-img.svg'
29
40
  import DownloadPdf from './images/icon-download-pdf.svg'
30
41
 
31
42
  // Core
32
43
  import Loading from '@cdc/core/components/Loading';
33
- import DataTransform from '@cdc/core/components/DataTransform';
44
+ import { DataTransform } from '@cdc/core/helpers/DataTransform';
34
45
  import getViewport from '@cdc/core/helpers/getViewport';
35
- import numberFromString from '@cdc/core/helpers/numberFromString'
36
-
46
+ import numberFromString from '@cdc/core/helpers/numberFromString';
47
+ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData';
48
+ import cacheBustingString from '@cdc/core/helpers/cacheBustingString';
37
49
 
38
50
  // Child Components
39
- import Sidebar from './components/Sidebar';
40
- import Modal from './components/Modal';
41
- import EditorPanel from './components/EditorPanel'; // Future: Lazy
42
- import UsaMap from './components/UsaMap'; // Future: Lazy
43
- import CountyMap from './components/CountyMap'; // Future: Lazy
44
- import DataTable from './components/DataTable'; // Future: Lazy
45
- import NavigationMenu from './components/NavigationMenu'; // Future: Lazy
46
- import WorldMap from './components/WorldMap'; // Future: Lazy
47
- import SingleStateMap from './components/SingleStateMap'; // Future: Lazy
51
+ import Sidebar from './components/Sidebar'
52
+ import Modal from './components/Modal'
53
+ import EditorPanel from './components/EditorPanel' // Future: Lazy
54
+ import UsaMap from './components/UsaMap' // Future: Lazy
55
+ import UsaRegionMap from './components/UsaRegionMap' // Future: Lazy
56
+ import CountyMap from './components/CountyMap' // Future: Lazy
57
+ import DataTable from './components/DataTable' // Future: Lazy
58
+ import NavigationMenu from './components/NavigationMenu' // Future: Lazy
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'
48
63
 
49
64
  // Data props
50
65
  const stateKeys = Object.keys(supportedStates)
51
66
  const territoryKeys = Object.keys(supportedTerritories)
67
+ const regionKeys = Object.keys(supportedRegions)
52
68
  const countryKeys = Object.keys(supportedCountries)
53
69
  const countyKeys = Object.keys(supportedCounties)
54
70
  const cityKeys = Object.keys(supportedCities)
55
71
 
56
72
  const generateColorsArray = (color = '#000000', special = false) => {
57
- let colorObj = chroma(color)
58
-
59
- let hoverColor = special ? colorObj.brighten(0.5).hex() : colorObj.saturate(1.3).hex()
60
-
61
- return [
62
- color,
63
- hoverColor,
64
- colorObj.darken(0.3).hex()
65
- ]
73
+ let colorObj = chroma(color)
74
+ let hoverColor = special ? colorObj.brighten(0.5).hex() : colorObj.saturate(1.3).hex()
75
+
76
+ return [
77
+ color,
78
+ hoverColor,
79
+ colorObj.darken(0.3).hex()
80
+ ]
66
81
  }
67
82
 
68
83
  const hashObj = (row) => {
84
+ try {
85
+ if (!row) throw new Error('No row supplied to hashObj')
86
+
69
87
  let str = JSON.stringify(row)
88
+ let hash = 0
70
89
 
71
- let hash = 0;
72
- if (str.length === 0) return hash;
73
- for (let i = 0; i < str.length; i++) {
74
- let char = str.charCodeAt(i);
75
- hash = ((hash<<5)-hash) + char;
76
- hash = hash & hash;
77
- }
90
+ if (str.length === 0) return hash
78
91
 
79
- return hash;
92
+ for (let i = 0; i < str.length; i++) {
93
+ let char = str.charCodeAt(i)
94
+ hash = ((hash << 5) - hash) + char
95
+ hash = hash & hash
96
+ }
97
+
98
+ return hash
99
+ } catch (e) {
100
+ console.error(e)
101
+ }
80
102
  }
81
103
 
82
104
  // returns string[]
83
105
  const getUniqueValues = (data, columnName) => {
84
- let result = {};
106
+ let result = {}
85
107
 
86
- for(let i = 0; i < data.length; i++) {
87
- let val = data[i][columnName]
108
+ for (let i = 0; i < data.length; i++) {
109
+ let val = data[i][columnName]
88
110
 
89
- if(undefined === val) continue
111
+ if (undefined === val) continue
90
112
 
91
- if(undefined === result[val]) {
92
- result[val] = true
93
- }
113
+ if (undefined === result[val]) {
114
+ result[val] = true
94
115
  }
116
+ }
95
117
 
96
- return Object.keys(result)
118
+ return Object.keys(result)
97
119
  }
98
120
 
99
- const CdcMap = ({className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, hostname}) => {
100
-
101
- const [showLoadingMessage, setShowLoadingMessage] = useState(false)
102
- const transform = new DataTransform()
103
- const [state, setState] = useState( {...initialState} )
104
- const [loading, setLoading] = useState(true)
105
- const [currentViewport, setCurrentViewport] = useState()
106
- const [runtimeFilters, setRuntimeFilters] = useState([])
107
- const [runtimeLegend, setRuntimeLegend] = useState([])
108
- const [runtimeData, setRuntimeData] = useState({init: true})
109
- const [modal, setModal] = useState(null)
110
- const [accessibleStatus, setAccessibleStatus] = useState('')
111
- let legendMemo = useRef(new Map())
112
-
113
-
114
-
115
- const resizeObserver = new ResizeObserver(entries => {
116
- for (let entry of entries) {
117
- let newViewport = getViewport(entry.contentRect.width)
118
-
119
- setCurrentViewport(newViewport)
121
+ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, setSharedFilter, setSharedFilterValue, hostname = 'localhost:8080', link }) => {
122
+ const [ showLoadingMessage, setShowLoadingMessage ] = useState(false)
123
+ const transform = new DataTransform()
124
+ const [ state, setState ] = useState({ ...initialState })
125
+ const [ loading, setLoading ] = useState(true)
126
+ const [ currentViewport, setCurrentViewport ] = useState()
127
+ const [ runtimeFilters, setRuntimeFilters ] = useState([])
128
+ const [ runtimeLegend, setRuntimeLegend ] = useState([])
129
+ const [ runtimeData, setRuntimeData ] = useState({ init: true })
130
+ const [ modal, setModal ] = useState(null)
131
+ const [ accessibleStatus, setAccessibleStatus ] = useState('')
132
+ const [ filteredCountryCode, setFilteredCountryCode ] = useState()
133
+ const [ position, setPosition ] = useState(state.mapPosition)
134
+ const [ coveLoadedHasRan, setCoveLoadedHasRan ] = useState(false)
135
+ const [ container, setContainer ] = useState()
136
+
137
+ let legendMemo = useRef(new Map())
138
+
139
+ useEffect(() => {
140
+ try {
141
+ if (filteredCountryCode) {
142
+ const filteredCountryObj = runtimeData[filteredCountryCode]
143
+ const coordinates = countryCoordinates[filteredCountryCode]
144
+ const long = coordinates[1]
145
+ const lat = coordinates[0]
146
+ const reversedCoordinates = [ long, lat ]
147
+
148
+ setState({
149
+ ...state,
150
+ mapPosition: { coordinates: reversedCoordinates, zoom: 3 }
151
+ })
152
+ }
153
+ } catch (e) {
154
+ console.error('Failed to set world map zoom.')
155
+ }
156
+ }, [ filteredCountryCode ])
157
+
158
+ useEffect(() => {
159
+ setTimeout(() => {
160
+ if (filteredCountryCode) {
161
+ const filteredCountryObj = runtimeData[filteredCountryCode]
162
+ const tmpData = {
163
+ [filteredCountryCode]: filteredCountryObj
120
164
  }
121
- });
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
-
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
135
- // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
136
- const addUIDs = useCallback((obj, fromColumn) => {
137
-
138
- obj.data.forEach(row => {
139
- let uid = null
140
-
141
- if(row.uid) row.uid = null // Wipe existing UIDs
142
-
143
- // United States check
144
- if("us" === obj.general.geoType) {
145
- const geoName = row[obj.columns.geo.name] ? row[obj.columns.geo.name].toUpperCase() : '';
165
+ setRuntimeData(tmpData)
166
+ }
167
+ }, 100)
168
+ }, [ filteredCountryCode ])
169
+
170
+ useEffect(() => {
171
+ if (state.mapPosition) {
172
+ setPosition(state.mapPosition)
173
+ }
174
+ }, [ state.mapPosition, setPosition ])
146
175
 
147
- // States
148
- uid = stateKeys.find( (key) => supportedStates[key].includes(geoName) )
176
+ const setZoom = (reversedCoordinates) => {
177
+ setState({
178
+ ...state,
179
+ mapPosition: { coordinates: reversedCoordinates, zoom: 3 }
180
+ })
181
+ }
149
182
 
150
- // Territories
151
- if(!uid) {
152
- uid = territoryKeys.find( (key) => supportedTerritories[key].includes(geoName) )
153
- }
183
+ const resizeObserver = new ResizeObserver(entries => {
184
+ for (let entry of entries) {
185
+ let newViewport = getViewport(entry.contentRect.width)
154
186
 
155
- // Cities
156
- if(!uid) {
157
- uid = cityKeys.find( (key) => key === geoName)
158
- }
159
- }
187
+ setCurrentViewport(newViewport)
188
+ }
189
+ })
160
190
 
161
- // World Check
162
- if("world" === obj.general.geoType) {
163
- const geoName = row[obj.columns.geo.name]
191
+ // 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
192
+ // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
193
+ const addUIDs = useCallback((obj, fromColumn) => {
194
+ obj.data.forEach(row => {
195
+ let uid = null
164
196
 
165
- uid = countryKeys.find( (key) => supportedCountries[key].includes(geoName) )
166
- }
197
+ if (row.uid) row.uid = null // Wipe existing UIDs
167
198
 
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
- }
199
+ // United States check
200
+ if ('us' === obj.general.geoType && obj.columns.geo.name) {
201
+ // const geoName = row[obj.columns.geo.name] && typeof row[obj.columns.geo.name] === "string" ? row[obj.columns.geo.name].toUpperCase() : '';
202
+ let geoName = ''
173
203
 
174
- // TODO: Points
175
- if(uid) {
176
- Object.defineProperty(row, 'uid', {
177
- value: uid,
178
- writable: true
179
- });
180
- }
181
- })
204
+ if (row[obj.columns.geo.name] !== undefined && row[obj.columns.geo.name] !== null) {
205
+ geoName = String(row[obj.columns.geo.name])
206
+ geoName = geoName.toUpperCase()
207
+ }
182
208
 
183
- obj.data.fromColumn = fromColumn
184
- })
209
+ // States
210
+ uid = stateKeys.find((key) => supportedStates[key].includes(geoName))
185
211
 
186
- const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
212
+ // Territories
213
+ if (!uid) {
214
+ uid = territoryKeys.find((key) => supportedTerritories[key].includes(geoName))
215
+ }
187
216
 
188
- const newLegendMemo = new Map(); // Reset memoization
217
+ // Cities
218
+ if (!uid) {
219
+ uid = cityKeys.find((key) => key === geoName)
220
+ }
221
+ }
189
222
 
190
- const
191
- primaryCol = obj.columns.primary.name,
192
- type = obj.legend.type,
193
- number = obj.legend.numberOfItems,
194
- result = [];
223
+ if ('us-region' === obj.general.geoType && obj.columns.geo.name) {
224
+ // const geoName = row[obj.columns.geo.name] && typeof row[obj.columns.geo.name] === "string" ? row[obj.columns.geo.name].toUpperCase() : '';
225
+ let geoName = ''
195
226
 
196
- // Add a hash for what we're working from if passed
197
- if(hash) {
198
- result.fromHash = hash
227
+ if (row[obj.columns.geo.name] !== undefined && row[obj.columns.geo.name] !== null) {
228
+ geoName = String(row[obj.columns.geo.name])
229
+ geoName = geoName.toUpperCase()
199
230
  }
200
231
 
201
- // Unified will based the legend off ALL of the data maps received. Otherwise, it will use
202
- let dataSet = obj.legend.unified ? obj.data : Object.values(runtimeData);
203
-
204
- const colorDistributions = {
205
- 1: [ 1 ],
206
- 2: [ 1, 3 ],
207
- 3: [ 1, 3, 5 ],
208
- 4: [ 0, 2, 4, 6 ],
209
- 5: [ 0, 2, 4, 6, 7 ],
210
- 6: [ 0, 2, 3, 4, 5, 7 ],
211
- 7: [ 0, 2, 3, 4, 5, 6, 7 ],
212
- 8: [ 0, 2, 3, 4, 5, 6, 7, 8 ],
213
- 9: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
214
- }
232
+ // Regions
233
+ uid = regionKeys.find((key) => supportedRegions[key].includes(geoName))
234
+ }
215
235
 
216
- const applyColorToLegend = (legendIdx) => {
217
- // Default to "bluegreen" color scheme if the passed color isn't valid
218
- let mapColorPalette = obj.customColors || colorPalettes[obj.color] || colorPalettes['bluegreen']
236
+ // World Check
237
+ if ('world' === obj.general.geoType) {
238
+ const geoName = row[obj.columns.geo.name]
219
239
 
220
- let colorIdx = legendIdx - specialClasses
240
+ uid = countryKeys.find((key) => supportedCountries[key].includes(geoName))
241
+ }
221
242
 
222
- // Special Classes (No Data)
223
- if (result[legendIdx].special) {
224
- const specialClassColors = chroma.scale(['#D4D4D4', '#939393']).colors(specialClasses)
243
+ // County Check
244
+ if (('us-county' === obj.general.geoType || 'single-state' === obj.general.geoType) && 'us-geocode' !== obj.general.type) {
245
+ const fips = row[obj.columns.geo.name]
246
+ uid = countyKeys.find((key) => key === fips)
247
+ }
225
248
 
226
- return specialClassColors[ legendIdx ]
227
- }
249
+ if ('us-geocode' === state.general.type) {
250
+ uid = row[state.columns.geo.name]
251
+ }
228
252
 
229
- if ( obj.color.includes( 'qualitative' ) ) return mapColorPalette[colorIdx]
253
+ if (uid) {
254
+ Object.defineProperty(row, 'uid', {
255
+ value: uid,
256
+ writable: true
257
+ })
258
+ }
259
+ })
260
+
261
+ obj.data.fromColumn = fromColumn
262
+ })
263
+
264
+ const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
265
+ const newLegendMemo = new Map() // Reset memoization
266
+ const
267
+ primaryCol = obj.columns.primary.name,
268
+ isData = obj.general.type === 'data',
269
+ isBubble = obj.general.type === 'bubble',
270
+ categoricalCol = obj.columns.categorical ? obj.columns.categorical.name : undefined,
271
+ type = obj.legend.type,
272
+ number = obj.legend.numberOfItems,
273
+ result = []
274
+
275
+ // Add a hash for what we're working from if passed
276
+ if (hash) {
277
+ result.fromHash = hash
278
+ }
230
279
 
231
- let amt = Math.max( result.length - specialClasses, 1 )
232
- let distributionArray = colorDistributions[ amt ]
280
+ // Unified will based the legend off ALL of the data maps received. Otherwise, it will use
281
+ let dataSet = obj.legend.unified ? obj.data : Object.values(runtimeData)
282
+
283
+ const colorDistributions = {
284
+ 1: [ 1 ],
285
+ 2: [ 1, 3 ],
286
+ 3: [ 1, 3, 5 ],
287
+ 4: [ 0, 2, 4, 6 ],
288
+ 5: [ 0, 2, 4, 6, 7 ],
289
+ 6: [ 0, 2, 3, 4, 5, 7 ],
290
+ 7: [ 0, 2, 3, 4, 5, 6, 7 ],
291
+ 8: [ 0, 2, 3, 4, 5, 6, 7, 8 ],
292
+ 9: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ],
293
+ 10: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
294
+ }
233
295
 
234
- const specificColor = distributionArray[ colorIdx ]
296
+ const applyColorToLegend = (legendIdx) => {
297
+ // Default to "bluegreen" color scheme if the passed color isn't valid
298
+ let mapColorPalette = obj.customColors || colorPalettes[obj.color] || colorPalettes['bluegreen']
235
299
 
236
- return mapColorPalette[specificColor]
300
+ // Handle Region Maps need for a 10th color
301
+ if (general.geoType === 'us-region' && mapColorPalette.length < 10 && mapColorPalette.length > 8) {
302
+ if (!general.palette.isReversed) {
303
+ mapColorPalette.push(chroma(mapColorPalette[8]).darken(0.75).hex())
304
+ } else {
305
+ mapColorPalette.unshift(chroma(mapColorPalette[0]).darken(0.75).hex())
237
306
  }
307
+ }
238
308
 
239
- let specialClasses = 0
240
- let specialClassesHash = {}
309
+ let colorIdx = legendIdx - specialClasses
241
310
 
242
- // Special classes
243
- if (obj.legend.specialClasses.length) {
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]);
311
+ // Special Classes (No Data)
312
+ if (result[legendIdx].special) {
313
+ const specialClassColors = chroma.scale([ '#D4D4D4', '#939393' ]).colors(specialClasses)
248
314
 
249
- if(specialClass.value === val){
250
- if(undefined === specialClassesHash[val]) {
251
- specialClassesHash[val] = true;
315
+ return specialClassColors[legendIdx]
316
+ }
252
317
 
253
- result.push({
254
- special: true,
255
- value: val,
256
- label: specialClass.label
257
- });
318
+ if (obj.color.includes('qualitative')) return mapColorPalette[colorIdx]
258
319
 
259
- result[result.length - 1].color = applyColorToLegend(result.length - 1);
320
+ let amt = Math.max(result.length - specialClasses, 1)
321
+ let distributionArray = colorDistributions[amt]
260
322
 
261
- specialClasses += 1;
262
- }
323
+ const specificColor = distributionArray[colorIdx]
263
324
 
264
- let specialColor = '';
265
-
266
- // color the state if val is in row
267
- specialColor = result.findIndex(p => p.value === val)
325
+ return mapColorPalette[specificColor]
326
+ }
268
327
 
269
- newLegendMemo.set( hashObj(row), specialColor)
328
+ let specialClasses = 0
329
+ let specialClassesHash = {}
270
330
 
271
- return false;
272
- }
331
+ // Special classes
332
+ if (obj.legend.specialClasses.length) {
333
+ if (typeof obj.legend.specialClasses[0] === 'object') {
334
+ obj.legend.specialClasses.forEach(specialClass => {
335
+ dataSet = dataSet.filter(row => {
336
+ const val = String(row[specialClass.key])
273
337
 
274
- return true;
275
- });
276
- });
277
- } else {
278
- dataSet = dataSet.filter(row => {
279
- const val = row[primaryCol]
338
+ if (specialClass.value === val) {
339
+ if (undefined === specialClassesHash[val]) {
340
+ specialClassesHash[val] = true
280
341
 
281
- if( obj.legend.specialClasses.includes(val) ) {
342
+ result.push({
343
+ special: true,
344
+ value: val,
345
+ label: specialClass.label
346
+ })
282
347
 
283
- // apply the special color to the legend
284
- if(undefined === specialClassesHash[val]) {
285
- specialClassesHash[val] = true;
348
+ result[result.length - 1].color = applyColorToLegend(result.length - 1)
286
349
 
287
- result.push({
288
- special: true,
289
- value: val
290
- });
350
+ specialClasses += 1
351
+ }
291
352
 
292
- result[result.length - 1].color = applyColorToLegend(result.length - 1);
353
+ let specialColor = ''
293
354
 
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)
355
+ // color the state if val is in row
356
+ specialColor = result.findIndex(p => p.value === val)
304
357
 
305
- return false
306
- }
358
+ newLegendMemo.set(hashObj(row), specialColor)
307
359
 
308
- return true
309
- })
360
+ return false
310
361
  }
311
- }
312
362
 
313
- // Category
314
- if('category' === type) {
315
- let uniqueValues = new Map()
316
- let count = 0
363
+ return true
364
+ })
365
+ })
366
+ } else {
367
+ dataSet = dataSet.filter(row => {
368
+ const val = row[primaryCol]
317
369
 
318
- for(let i = 0; i < dataSet.length; i++) {
319
- let row = dataSet[i]
320
- let value = row[primaryCol]
370
+ if (obj.legend.specialClasses.includes(val)) {
371
+ // apply the special color to the legend
372
+ if (undefined === specialClassesHash[val]) {
373
+ specialClassesHash[val] = true
321
374
 
322
- if(undefined === value) continue
375
+ result.push({
376
+ special: true,
377
+ value: val
378
+ })
323
379
 
324
- if(false === uniqueValues.has(value)) {
325
- uniqueValues.set(value, [hashObj(row)]);
326
- count++
327
- } else {
328
- uniqueValues.get(value).push(hashObj(row))
329
- }
380
+ result[result.length - 1].color = applyColorToLegend(result.length - 1)
330
381
 
331
- if(count === 9) break // Can only have 9 categorical items for now
382
+ specialClasses += 1
332
383
  }
333
384
 
334
- let sorted = [...uniqueValues.keys()]
335
-
336
- // Apply custom sorting or regular sorting
337
- let configuredOrder = obj.legend.categoryValuesOrder ?? []
385
+ let specialColor = ''
338
386
 
339
- // Coerce strings to numbers inside configuredOrder property
340
- for(let i = 0; i < configuredOrder.length; i++) {
341
- configuredOrder[i] = numberFromString(configuredOrder[i])
387
+ // color the state if val is in row
388
+ if (Object.values(row).includes(val)) {
389
+ specialColor = result.findIndex(p => p.value === val)
342
390
  }
343
391
 
344
- if(configuredOrder.length) {
345
- sorted.sort( (a, b) => {
346
- return configuredOrder.indexOf(a) - configuredOrder.indexOf(b);
347
- })
348
- } else {
349
- sorted.sort((a, b) => a - b)
350
- }
392
+ newLegendMemo.set(hashObj(row), specialColor)
351
393
 
352
- // Add legend item for each
353
- sorted.forEach((val) => {
354
- result.push({
355
- value: val,
356
- })
394
+ return false
395
+ }
357
396
 
358
- let lastIdx = result.length - 1
359
- let arr = uniqueValues.get(val)
397
+ return true
398
+ })
399
+ }
400
+ }
360
401
 
361
- if(arr) {
362
- arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
363
- }
364
- })
402
+ // Category
403
+ if ('category' === type) {
404
+ let uniqueValues = new Map()
405
+ let count = 0
365
406
 
407
+ for (let i = 0; i < dataSet.length; i++) {
408
+ let row = dataSet[i]
409
+ let value = isBubble && categoricalCol && row[categoricalCol] ? row[categoricalCol] : row[primaryCol]
410
+ if (undefined === value) continue
366
411
 
367
- // Add color to new legend item
368
- for(let i = 0; i < result.length; i++) {
369
- result[i].color = applyColorToLegend(i)
370
- }
371
- legendMemo.current = newLegendMemo
372
- return result
412
+ if (false === uniqueValues.has(value)) {
413
+ uniqueValues.set(value, [ hashObj(row) ])
414
+ count++
415
+ } else {
416
+ uniqueValues.get(value).push(hashObj(row))
373
417
  }
374
418
 
375
- let uniqueValues = {};
376
- dataSet.forEach(datum => {
377
- uniqueValues[datum[primaryCol]] = true;
378
- });
419
+ if (count === 10) break // Can only have 10 categorical items for now
420
+ }
379
421
 
380
- let legendNumber = Math.min(number, Object.keys(uniqueValues).length);
422
+ let sorted = [ ...uniqueValues.keys() ]
381
423
 
382
- // Separate zero
383
- if(true === obj.legend.separateZero) {
384
- let addLegendItem = false;
424
+ // Apply custom sorting or regular sorting
425
+ let configuredOrder = obj.legend.categoryValuesOrder ?? []
385
426
 
386
- for(let i = 0; i < dataSet.length; i++) {
387
- if (dataSet[i][primaryCol] === 0) {
388
- addLegendItem = true
427
+ // Coerce strings to numbers inside configuredOrder property
428
+ for(let i = 0; i < configuredOrder.length; i++) {
429
+ configuredOrder[i] = numberFromString(configuredOrder[i])
430
+ }
389
431
 
390
- let row = dataSet.splice(i, 1)[0]
391
-
392
- newLegendMemo.set( hashObj(row), result.length)
393
- i--
394
- }
395
- }
432
+ if (configuredOrder.length) {
433
+ sorted.sort((a, b) => {
434
+ return configuredOrder.indexOf(a) - configuredOrder.indexOf(b)
435
+ })
436
+ } else {
437
+ sorted.sort((a, b) => a - b)
438
+ }
439
+
440
+ // Add legend item for each
441
+ sorted.forEach((val) => {
442
+ result.push({
443
+ value: val,
444
+ })
396
445
 
397
- if(addLegendItem) {
398
- legendNumber -= 1 // This zero takes up one legend item
446
+ let lastIdx = result.length - 1
447
+ let arr = uniqueValues.get(val)
399
448
 
400
- // Add new legend item
401
- result.push({
402
- min: 0,
403
- max: 0
404
- })
449
+ if (arr) {
450
+ arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
451
+ }
452
+ })
405
453
 
406
- let lastIdx = result.length - 1
454
+ // Add color to new legend item
455
+ for (let i = 0; i < result.length; i++) {
456
+ result[i].color = applyColorToLegend(i)
457
+ }
407
458
 
408
- // Add color to new legend item
409
- result[lastIdx].color = applyColorToLegend(lastIdx)
410
- }
411
- }
459
+ legendMemo.current = newLegendMemo
460
+ return result
461
+ }
412
462
 
413
- // Sort data for use in equalnumber or equalinterval
414
- dataSet = dataSet.filter(row => typeof row[primaryCol] === 'number').sort((a, b) => {
415
- let aNum = a[primaryCol]
416
- let bNum = b[primaryCol]
463
+ let uniqueValues = {}
464
+ dataSet.forEach(datum => {
465
+ uniqueValues[datum[primaryCol]] = true
466
+ })
417
467
 
418
- return aNum - bNum
419
- })
468
+ let legendNumber = Math.min(number, Object.keys(uniqueValues).length)
420
469
 
421
- // Equal Number
422
- if(type === 'equalnumber') {
423
- let numberOfRows = dataSet.length
470
+ // Separate zero
471
+ if (true === obj.legend.separateZero && !state.general.equalNumberOptIn) {
472
+ let addLegendItem = false
424
473
 
425
- let remainder
426
- let changingNumber = legendNumber
474
+ for (let i = 0; i < dataSet.length; i++) {
475
+ if (dataSet[i][primaryCol] === 0) {
476
+ addLegendItem = true
427
477
 
428
- let chunkAmt
478
+ let row = dataSet.splice(i, 1)[0]
429
479
 
430
- // Loop through the array until it has been split into equal subarrays
431
- while ( numberOfRows > 0 ) {
432
- remainder = numberOfRows % changingNumber
480
+ newLegendMemo.set(hashObj(row), result.length)
481
+ i--
482
+ }
483
+ }
433
484
 
434
- chunkAmt = Math.floor(numberOfRows / changingNumber)
485
+ if (addLegendItem) {
486
+ legendNumber -= 1 // This zero takes up one legend item
435
487
 
436
- if (remainder > 0) {
437
- chunkAmt += 1
438
- }
488
+ // Add new legend item
489
+ result.push({
490
+ min: 0,
491
+ max: 0
492
+ })
439
493
 
440
- let removedRows = dataSet.splice(0, chunkAmt);
494
+ let lastIdx = result.length - 1
441
495
 
442
- let min = removedRows[0][primaryCol],
443
- max = removedRows[removedRows.length - 1][primaryCol]
496
+ // Add color to new legend item
497
+ result[lastIdx].color = applyColorToLegend(lastIdx)
498
+ }
499
+ }
444
500
 
445
- removedRows.forEach(row => {
446
- newLegendMemo.set( hashObj(row), result.length )
447
- })
501
+ // Sort data for use in equalnumber or equalinterval
502
+ if (state.general.type !== 'us-geocode') {
503
+ dataSet = dataSet.filter(row => typeof row[primaryCol] === 'number').sort((a, b) => {
504
+ let aNum = a[primaryCol]
505
+ let bNum = b[primaryCol]
448
506
 
449
- result.push({
450
- min,
451
- max
452
- })
507
+ return aNum - bNum
508
+ })
509
+ }
453
510
 
454
- result[result.length - 1].color = applyColorToLegend(result.length - 1)
511
+ // Equal Number
512
+ if (type === 'equalnumber') {
513
+ // start work on changing legend functionality
514
+ // FALSE === ignore old version for now.
515
+ if (!state.general.equalNumberOptIn) {
516
+ let numberOfRows = dataSet.length
455
517
 
456
- changingNumber -= 1
457
- numberOfRows -= chunkAmt
458
- }
459
- }
518
+ let remainder
519
+ let changingNumber = legendNumber
460
520
 
461
- // Equal Interval
462
- if(type === 'equalinterval') {
463
- dataSet = dataSet.filter(row => row[primaryCol] !== undefined)
464
- let dataMin = dataSet[0][primaryCol]
465
- let dataMax = dataSet[dataSet.length - 1][primaryCol]
521
+ let chunkAmt
466
522
 
467
- let pointer = 0 // Start at beginning of dataSet
523
+ // Loop through the array until it has been split into equal subarrays
524
+ while (numberOfRows > 0) {
525
+ remainder = numberOfRows % changingNumber
526
+ chunkAmt = Math.floor(numberOfRows / changingNumber)
468
527
 
469
- for (let i = 0; i < legendNumber; i++) {
470
- let interval = Math.abs(dataMax - dataMin) / legendNumber
528
+ if (remainder > 0) {
529
+ chunkAmt += 1
530
+ }
471
531
 
472
- let min = dataMin + (interval * i)
473
- let max = min + interval
532
+ let removedRows = dataSet.splice(0, chunkAmt)
474
533
 
475
- // If this is the last loop, assign actual max of data as the end point
476
- if (i === legendNumber - 1) max = dataMax
534
+ let min = removedRows[0][primaryCol],
535
+ max = removedRows[removedRows.length - 1][primaryCol]
477
536
 
478
- // Add rows in dataSet that belong to this new legend item since we've got the data sorted
479
- while(pointer < dataSet.length && dataSet[pointer][primaryCol] <= max) {
480
- newLegendMemo.set(hashObj(dataSet[pointer]), result.length )
481
- pointer += 1
482
- }
537
+ removedRows.forEach(row => {
538
+ newLegendMemo.set(hashObj(row), result.length)
539
+ })
483
540
 
484
- let range = {
485
- min: Math.round(min * 100) / 100,
486
- max: Math.round(max * 100) / 100,
487
- }
541
+ result.push({
542
+ min,
543
+ max
544
+ })
488
545
 
489
- result.push(range)
546
+ result[result.length - 1].color = applyColorToLegend(result.length - 1)
490
547
 
491
- result[result.length - 1].color = applyColorToLegend(result.length - 1)
492
- }
548
+ changingNumber -= 1
549
+ numberOfRows -= chunkAmt
493
550
  }
551
+ } else {
552
+ // get nums
553
+ let hasZeroInData = dataSet.filter(obj => obj[state.columns.primary.name] === 0).length > 0
554
+ let domainNums = new Set(dataSet.map(item => item[state.columns.primary.name]))
494
555
 
495
- result.forEach((legendItem, idx) => {
496
- legendItem.color = applyColorToLegend(idx, specialClasses, result)
497
- })
556
+ domainNums = d3.extent(domainNums)
498
557
 
499
- legendMemo.current = newLegendMemo
500
- return result
501
- })
558
+ let colors = colorPalettes[state.color]
559
+ let colorRange = colors.slice(0, state.legend.numberOfItems)
502
560
 
503
- const generateRuntimeFilters = useCallback((obj, hash, runtimeFilters) => {
504
- if(undefined === obj.filters || obj.filters.length === 0) return []
561
+ let scale = d3.scaleQuantile()
562
+ .domain([ ...new Set(dataSet.map(item => Math.round(item[state.columns.primary.name]))) ]) // min/max values
563
+ .range(colorRange) // set range to our colors array
505
564
 
506
- let filters = []
565
+ let breaks = scale.quantiles()
507
566
 
508
- if(hash) filters.fromHash = hash
567
+ breaks = breaks.map(item => Math.round(item))
509
568
 
510
- obj?.filters.forEach(({columnName, label, active, values}, idx) => {
511
- if(undefined === columnName) return
569
+ // if seperating zero force it into breaks
570
+ if (breaks[0] !== 0) {
571
+ breaks.unshift(0)
572
+ }
512
573
 
513
- let newFilter = runtimeFilters[idx]
574
+ breaks.map((item, index) => {
575
+ const setMin = (index) => {
576
+ //debugger;
577
+ let min = breaks[index]
514
578
 
515
- const sortAsc = (a, b) => {
516
- return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
517
- };
579
+ // if first break is a seperated zero, min is zero
580
+ if (index === 0 && state.legend.separateZero) {
581
+ min = 0
582
+ }
518
583
 
519
- const sortDesc = (a, b) => {
520
- return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
521
- };
584
+ // if we're on the second break, and seperating out zero, increment min to 1.
585
+ if (index === 1 && state.legend.separateZero) {
586
+ min = 1
587
+ }
522
588
 
523
- values = getUniqueValues(state.data, columnName)
589
+ // // in starting position and zero in the data
590
+ // if((index === state.legend.specialClasses?.length ) && (state.legend.specialClasses.length !== 0)) {
591
+ // min = breaks[index]
592
+ // }
593
+ return min
594
+ }
524
595
 
525
- if(obj.filters[idx].order === 'asc') {
526
- values = values.sort(sortAsc)
527
- }
596
+ const setMax = (index, min) => {
597
+ let max = breaks[index + 1] - 1
528
598
 
529
- if(obj.filters[idx].order === 'desc') {
530
- values = values.sort(sortDesc)
531
- }
599
+ // check if min and max range are the same
600
+ // if (min === max + 1) {
601
+ // max = breaks[index + 1]
602
+ // }
532
603
 
533
- if(obj.filters[idx].order === 'cust') {
534
- if(obj.filters[idx]?.values.length > 0) {
535
- values = obj.filters[idx].values
536
- }
604
+ if (index === 0 && state.legend.separateZero) {
605
+ max = 0
537
606
  }
607
+ // if ((index === state.legend.specialClasses.length && state.legend.specialClasses.length !== 0) && !state.legend.separateZero && hasZeroInData) {
608
+ // max = 0;
609
+ // }
538
610
 
539
- if(undefined === newFilter) {
540
- newFilter = {}
611
+ if (index + 1 === breaks.length) {
612
+ max = domainNums[1]
541
613
  }
542
614
 
543
- newFilter.label = label ?? ''
544
- newFilter.columnName = columnName
545
- newFilter.values = values
546
- newFilter.active = active || values[0] // Default to first found value
615
+ return max
616
+ }
547
617
 
548
- filters.push(newFilter)
549
- })
618
+ let min = setMin(index)
619
+ let max = setMax(index, min)
550
620
 
551
- return filters
552
- })
621
+ result.push({
622
+ min,
623
+ max,
624
+ color: scale(item)
625
+ })
553
626
 
554
- // Calculates what's going to be displayed on the map and data table at render.
555
- const generateRuntimeData = useCallback((obj, filters, hash) => {
556
- const result = {}
627
+ dataSet.forEach((row, dataIndex) => {
628
+ let number = row[state.columns.primary.name]
629
+ let updated = 0
557
630
 
558
- if(hash) {
559
- // Adding property this way prevents it from being enumerated
560
- Object.defineProperty(result, 'fromHash', {
561
- value : hash
562
- });
563
- }
564
-
631
+ // check if we're seperating zero out
632
+ updated = state.legend.separateZero && hasZeroInData ? index : index
633
+ // check for special classes
634
+ updated = state.legend.specialClasses ? updated + state.legend.specialClasses.length : index
565
635
 
566
- obj.data.forEach(row => {
567
- if(undefined === row.uid) return false // No UID for this row, we can't use for mapping
636
+ if (result[updated]?.min === (null || undefined) || result[updated]?.max === (null || undefined)) return
568
637
 
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;
638
+ if (number >= result[updated].min && number <= result[updated].max) {
639
+ newLegendMemo.set(hashObj(row), updated)
575
640
  }
641
+ })
642
+ })
643
+ }
644
+ }
576
645
 
646
+ // Equal Interval
577
647
 
578
- if(row[obj.columns.primary.name]) {
579
- row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name])
580
- }
648
+ if (type === 'equalinterval' && dataSet?.length !== 0) {
649
+ if (!dataSet || dataSet.length === 0) {
650
+ setState({
651
+ ...state,
652
+ runtime: {
653
+ ...state.runtime,
654
+ editorErrorMessage: 'Error setting equal interval legend type'
655
+ }
656
+ })
657
+ return
658
+ }
659
+ dataSet = dataSet.filter(row => row[primaryCol] !== undefined)
660
+ let dataMin = dataSet[0][primaryCol]
661
+ let dataMax = dataSet[dataSet.length - 1][primaryCol]
581
662
 
582
- // If this is a navigation only map, skip if it doesn't have a URL
583
- if("navigation" === obj.general.type ) {
584
- let navigateUrl = row[obj.columns.navigate.name] || "";
585
-
586
- if ( undefined !== navigateUrl && typeof navigateUrl === "string" ) {
587
- // Strip hidden characters before we check length
588
- navigateUrl = navigateUrl.replace( /(\r\n|\n|\r)/gm, '' );
589
- }
590
- if ( 0 === navigateUrl.length ) {
591
- return false
592
- }
593
- }
663
+ let pointer = 0 // Start at beginning of dataSet
594
664
 
595
- // Filters
596
- if(filters?.length) {
597
- for(let i = 0; i < filters.length; i++) {
598
- const {columnName, active} = filters[i]
665
+ for (let i = 0; i < legendNumber; i++) {
666
+ let interval = Math.abs(dataMax - dataMin) / legendNumber
599
667
 
600
- if (String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
601
- }
602
- }
668
+ let min = dataMin + (interval * i)
669
+ let max = min + interval
603
670
 
604
- // Don't add additional rows with same UID
605
- if(undefined === result[row.uid]) {
606
- result[row.uid] = row
607
- }
608
- })
671
+ // If this is the last loop, assign actual max of data as the end point
672
+ if (i === legendNumber - 1) max = dataMax
609
673
 
610
- return result
611
- })
674
+ // Add rows in dataSet that belong to this new legend item since we've got the data sorted
675
+ while (pointer < dataSet.length && dataSet[pointer][primaryCol] <= max) {
676
+ newLegendMemo.set(hashObj(dataSet[pointer]), result.length)
677
+ pointer += 1
678
+ }
612
679
 
613
- const outerContainerRef = useCallback(node => {
614
- if (node !== null) {
615
- resizeObserver.observe(node);
680
+ let range = {
681
+ min: Math.round(min * 100) / 100,
682
+ max: Math.round(max * 100) / 100,
616
683
  }
617
- },[]);
618
684
 
619
- const mapSvg = useRef(null);
685
+ result.push(range)
620
686
 
621
- const closeModal = ({target}) => {
622
- if('string' === typeof target.className && (target.className.includes('modal-close') || target.className.includes('modal-background') ) && null !== modal) {
623
- setModal(null)
624
- }
687
+ result[result.length - 1].color = applyColorToLegend(result.length - 1)
688
+ }
625
689
  }
626
690
 
627
- const saveImageAs = (uri, filename) => {
628
- const ie = navigator.userAgent.match(/MSIE\s([\d.]+)/)
629
- const ie11 = navigator.userAgent.match(/Trident\/7.0/) && navigator.userAgent.match(/rv:11/)
630
- const ieEdge = navigator.userAgent.match(/Edge/g)
631
- const ieVer=(ie ? ie[1] : (ie11 ? 11 : (ieEdge ? 12 : -1)));
632
-
633
- if (ieVer>-1) {
634
- const fileAsBlob = new Blob([uri], {
635
- type: 'image/png'
636
- });
637
- window.navigator.msSaveBlob(fileAsBlob, filename);
638
- } else {
639
- const link = document.createElement('a')
640
- if (typeof link.download === 'string') {
641
- link.href = uri
642
- link.download = filename
643
- link.onclick = (e) => document.body.removeChild(e.target);
644
- document.body.appendChild(link)
645
- link.click()
646
- } else {
647
- window.open(uri)
648
- }
649
- }
650
- }
691
+ result.forEach((legendItem, idx) => {
692
+ legendItem.color = applyColorToLegend(idx, specialClasses, result)
693
+ })
651
694
 
652
- const generateMedia = (target, type) => {
653
- // Convert SVG to canvas
654
- const baseSvg = mapSvg.current.querySelector('.rsm-svg')
655
-
656
- const ratio = baseSvg.getBoundingClientRect().height / baseSvg.getBoundingClientRect().width
657
- const calcHeight = ratio * 1440
658
- const xmlSerializer = new XMLSerializer()
659
- const svgStr = xmlSerializer.serializeToString(baseSvg)
660
- const options = { log: false, ignoreMouse: true }
661
- const canvas = document.createElement('canvas')
662
- const ctx = canvas.getContext('2d')
663
- ctx.canvas.width = 1440
664
- ctx.canvas.height = calcHeight
665
- const canvg = Canvg.fromString(ctx, svgStr, options)
666
- canvg.start()
667
-
668
- // Generate DOM <img> from svg data
669
- const generatedImage = document.createElement('img')
670
- generatedImage.src = canvas.toDataURL('image/png')
671
- generatedImage.style.width = '100%'
672
- generatedImage.style.height = 'auto'
673
-
674
- baseSvg.style.display = 'none' // Hide default SVG during media generation
675
- baseSvg.parentNode.insertBefore(generatedImage, baseSvg.nextSibling) // Insert png generated from canvas of svg
676
-
677
- // Construct filename with timestamp
678
- const date = new Date()
679
- const filename = state.general.title.replace(/\s+/g, '-').toLowerCase() + '-' + date.getDate() + date.getMonth() + date.getFullYear()
680
-
681
- switch (type) {
682
- case 'image':
683
- return html2canvas(target, {
684
- allowTaint: true,
685
- backgroundColor: '#ffffff',
686
- width: 1440,
687
- windowWidth: 1440,
688
- scale: 1,
689
- logging: false
690
- }).then(canvas => {
691
- saveImageAs(canvas.toDataURL(), filename + '.png')
692
- }).then(() => {
693
- generatedImage.remove() // Remove generated png
694
- baseSvg.style.display = null // Re-display initial svg map
695
- })
696
- case 'pdf':
697
- let opt = {
698
- margin: 0.2,
699
- filename: filename + '.pdf',
700
- image: { type: 'png' },
701
- html2canvas: { scale: 2, logging: false },
702
- jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
703
- };
704
-
705
- html2pdf().set(opt).from(target).save().then(() => {
706
- generatedImage.remove() // Remove generated png
707
- baseSvg.style.display = null // Re-display initial svg map
708
- })
709
- break
710
- default:
711
- console.warn('generateMedia param 2 type must be \'image\' or \'pdf\'')
712
- break
713
- }
714
- }
695
+ legendMemo.current = newLegendMemo
696
+ return result
697
+ })
715
698
 
716
- const changeFilterActive = async (idx, activeValue) => {
717
- // Reset active legend toggles
718
- resetLegendToggles()
699
+ const generateRuntimeFilters = useCallback((obj, hash, runtimeFilters) => {
700
+ if (undefined === obj.filters || obj.filters.length === 0) return []
719
701
 
720
- try {
702
+ let filters = []
721
703
 
722
- const isEmpty = (obj) => {
723
- return Object.keys(obj).length === 0;
724
- }
704
+ if (hash) filters.fromHash = hash
725
705
 
726
- let filters = [...runtimeFilters]
706
+ obj?.filters.forEach(({ columnName, label, active, values }, idx) => {
707
+ if (undefined === columnName) return
727
708
 
728
- filters[idx] = { ...filters[idx] }
709
+ let newFilter = runtimeFilters[idx]
729
710
 
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')
711
+ const sortAsc = (a, b) => {
712
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
713
+ }
735
714
 
736
- // set the runtime filters and data
737
- setRuntimeData(newData)
738
- setRuntimeFilters(filters)
739
- } catch(e) {
740
- console.error(e.message)
741
- }
715
+ const sortDesc = (a, b) => {
716
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
717
+ }
742
718
 
743
- }
719
+ values = getUniqueValues(state.data, columnName)
720
+
721
+ if (obj.filters[idx].order === 'asc') {
722
+ values = values.sort(sortAsc)
723
+ }
744
724
 
745
- const displayDataAsText = (value, columnName) => {
746
- if(value === null) {
747
- return ""
725
+ if (obj.filters[idx].order === 'desc') {
726
+ values = values.sort(sortDesc)
727
+ }
728
+
729
+ if (obj.filters[idx].order === 'cust') {
730
+ if (obj.filters[idx]?.values.length > 0) {
731
+ values = obj.filters[idx].values
748
732
  }
733
+ }
749
734
 
750
- let formattedValue = value
735
+ if (undefined === newFilter) {
736
+ newFilter = {}
737
+ }
751
738
 
752
- let columnObj = state.columns[columnName]
739
+ newFilter.label = label ?? ''
740
+ newFilter.columnName = columnName
741
+ newFilter.values = values
742
+ newFilter.active = active || values[0] // Default to first found value
753
743
 
754
- if (columnObj) {
755
- // If value is a number, apply specific formattings
756
- if (Number(value)) {
757
- // Rounding
758
- if(columnObj.hasOwnProperty('roundToPlace') && columnObj.roundToPlace !== "None") {
744
+ filters.push(newFilter)
745
+ })
759
746
 
760
- const decimalPoint = columnObj.roundToPlace
747
+ return filters
748
+ })
761
749
 
762
- formattedValue = Number(value).toFixed(decimalPoint)
750
+ // Calculates what's going to be displayed on the map and data table at render.
751
+ const generateRuntimeData = useCallback((obj, filters, hash, test) => {
752
+ try {
753
+ const result = {}
763
754
 
764
- }
755
+ if (hash) {
756
+ // Adding property this way prevents it from being enumerated
757
+ Object.defineProperty(result, 'fromHash', {
758
+ value: hash
759
+ })
760
+ }
765
761
 
766
- if(columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) {
762
+ obj.data.forEach(row => {
763
+ if (test) {
764
+ console.log('object', obj)
765
+ console.log('row', row)
766
+ }
767
767
 
768
- formattedValue = Number(value).toLocaleString('en-US', { style: 'decimal'})
768
+ if (undefined === row.uid) return false // No UID for this row, we can't use for mapping
769
769
 
770
- }
771
- }
770
+ // When on a single state map filter runtime data by state
771
+ if (
772
+ !(String(row[obj.columns.geo.name]).substring(0, 2) === obj.general?.statePicked?.fipsCode) &&
773
+ obj.general.geoType === 'single-state' &&
774
+ obj.general.type !== 'us-geocode'
775
+ ) {
776
+ return false
777
+ }
772
778
 
773
- // Check if it's a special value. If it is not, apply the designated prefix and suffix
774
- if (false === state.legend.specialClasses.includes(String(value))) {
775
- formattedValue = columnObj.prefix + formattedValue + columnObj.suffix
776
- }
779
+ if (row[obj.columns.primary.name]) {
780
+ row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name], state)
777
781
  }
778
782
 
779
- return formattedValue
780
- }
783
+ // If this is a navigation only map, skip if it doesn't have a URL
784
+ if ('navigation' === obj.general.type) {
785
+ let navigateUrl = row[obj.columns.navigate.name] || ''
786
+
787
+ if (undefined !== navigateUrl && typeof navigateUrl === 'string') {
788
+ // Strip hidden characters before we check length
789
+ navigateUrl = navigateUrl.replace(/(\r\n|\n|\r)/gm, '')
790
+ }
791
+ if (0 === navigateUrl.length) {
792
+ return false
793
+ }
794
+ }
781
795
 
782
- const applyLegendToRow = (rowObj) => {
783
- // Navigation map
784
- if("navigation" === state.general.type) {
785
- let mapColorPalette = colorPalettes[ state.color ] || colorPalettes[ 'bluegreenreverse' ]
796
+ // Filters
797
+ if (filters?.length) {
798
+ for (let i = 0; i < filters.length; i++) {
799
+ const { columnName, active } = filters[i]
800
+ if (String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
801
+ }
802
+ }
786
803
 
787
- return generateColorsArray( mapColorPalette[ 3 ] )
804
+ // Don't add additional rows with same UID
805
+ if (undefined === result[row.uid]) {
806
+ result[row.uid] = row
788
807
  }
808
+ })
789
809
 
790
- let hash = hashObj(rowObj)
810
+ return result
811
+ } catch (e) {
812
+ console.error(e)
813
+ }
814
+ })
791
815
 
792
- if( legendMemo.current.has(hash) ) {
793
- let idx = legendMemo.current.get(hash)
816
+ const outerContainerRef = useCallback(node => {
817
+ if (node !== null) {
818
+ resizeObserver.observe(node)
819
+ }
820
+ setContainer(node)
821
+ }, [])
794
822
 
795
- if(runtimeLegend[idx]?.disabled) return false
823
+ const mapSvg = useRef(null)
796
824
 
797
- return generateColorsArray(runtimeLegend[idx]?.color, runtimeLegend[idx]?.special)
825
+ const closeModal = ({ target }) => {
826
+ if ('string' === typeof target.className && (target.className.includes('modal-close') || target.className.includes('modal-background')) && null !== modal) {
827
+ setModal(null)
828
+ }
829
+ }
830
+
831
+ const saveImageAs = (uri, filename) => {
832
+ const ie = navigator.userAgent.match(/MSIE\s([\d.]+)/)
833
+ const ie11 = navigator.userAgent.match(/Trident\/7.0/) && navigator.userAgent.match(/rv:11/)
834
+ const ieEdge = navigator.userAgent.match(/Edge/g)
835
+ const ieVer = (ie ? ie[1] : (ie11 ? 11 : (ieEdge ? 12 : -1)))
836
+
837
+ if (ieVer > -1) {
838
+ const fileAsBlob = new Blob([ uri ], {
839
+ type: 'image/png'
840
+ })
841
+ window.navigator.msSaveBlob(fileAsBlob, filename)
842
+ } else {
843
+ const link = document.createElement('a')
844
+ if (typeof link.download === 'string') {
845
+ link.href = uri
846
+ link.download = filename
847
+ link.onclick = (e) => document.body.removeChild(e.target)
848
+ document.body.appendChild(link)
849
+ link.click()
850
+ } else {
851
+ window.open(uri)
852
+ }
853
+ }
854
+ }
855
+
856
+ const generateMedia = (target, type) => {
857
+ // Convert SVG to canvas
858
+ const baseSvg = mapSvg.current.querySelector('.rsm-svg')
859
+
860
+ const ratio = baseSvg.getBoundingClientRect().height / baseSvg.getBoundingClientRect().width
861
+ const calcHeight = ratio * 1440
862
+ const xmlSerializer = new XMLSerializer()
863
+ const svgStr = xmlSerializer.serializeToString(baseSvg)
864
+ const options = { log: false, ignoreMouse: true }
865
+ const canvas = document.createElement('canvas')
866
+ const ctx = canvas.getContext('2d')
867
+ ctx.canvas.width = 1440
868
+ ctx.canvas.height = calcHeight
869
+ const canvg = Canvg.fromString(ctx, svgStr, options)
870
+ canvg.start()
871
+
872
+ // Generate DOM <img> from svg data
873
+ const generatedImage = document.createElement('img')
874
+ generatedImage.src = canvas.toDataURL('image/png')
875
+ generatedImage.style.width = '100%'
876
+ generatedImage.style.height = 'auto'
877
+
878
+ baseSvg.style.display = 'none' // Hide default SVG during media generation
879
+ baseSvg.parentNode.insertBefore(generatedImage, baseSvg.nextSibling) // Insert png generated from canvas of svg
880
+
881
+ // Construct filename with timestamp
882
+ const date = new Date()
883
+ const filename = state.general.title.replace(/\s+/g, '-').toLowerCase() + '-' + date.getDate() + date.getMonth() + date.getFullYear()
884
+
885
+ switch (type) {
886
+ case 'image':
887
+ return html2canvas(target, {
888
+ allowTaint: true,
889
+ backgroundColor: '#ffffff',
890
+ width: 1440,
891
+ windowWidth: 1440,
892
+ scale: 1,
893
+ logging: false
894
+ }).then(canvas => {
895
+ saveImageAs(canvas.toDataURL(), filename + '.png')
896
+ }).then(() => {
897
+ generatedImage.remove() // Remove generated png
898
+ baseSvg.style.display = null // Re-display initial svg map
899
+ })
900
+ case 'pdf':
901
+ let opt = {
902
+ margin: 0.2,
903
+ filename: filename + '.pdf',
904
+ image: { type: 'png' },
905
+ html2canvas: { scale: 2, logging: false },
906
+ jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
798
907
  }
799
908
 
800
- // Fail state
801
- return generateColorsArray()
909
+ html2pdf().set(opt).from(target).save().then(() => {
910
+ generatedImage.remove() // Remove generated png
911
+ baseSvg.style.display = null // Re-display initial svg map
912
+ })
913
+ break
914
+ default:
915
+ console.warn('generateMedia param 2 type must be \'image\' or \'pdf\'')
916
+ break
802
917
  }
918
+ }
803
919
 
804
- const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
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>`
920
+ const changeFilterActive = async (idx, activeValue) => {
921
+ // Reset active legend toggles
922
+ resetLegendToggles()
819
923
 
820
- if('data' === state.general.type && undefined !== row) {
821
- toolTipText += `<dl>`
924
+ try {
925
+ const isEmpty = (obj) => {
926
+ return Object.keys(obj).length === 0
927
+ }
822
928
 
823
- Object.keys(state.columns).forEach((columnKey) => {
824
- const column = state.columns[columnKey]
929
+ let filters = [ ...runtimeFilters ]
825
930
 
826
- if (true === column.tooltip) {
931
+ filters[idx] = { ...filters[idx] }
932
+ filters[idx].active = activeValue
827
933
 
828
- let label = column.label.length > 0 ? column.label : '';
934
+ const newData = generateRuntimeData(state, filters)
829
935
 
830
- let value;
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')
831
938
 
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
- }
939
+ // set the runtime filters and data
940
+ setRuntimeData(newData)
941
+ setRuntimeFilters(filters)
942
+ } catch (e) {
943
+ console.error(e.message)
944
+ }
945
+ }
840
946
 
841
- if(!value){
842
- value = displayDataAsText(row[column.name], columnKey);
843
- }
947
+ const displayDataAsText = (value, columnName) => {
948
+ if (value === null || value === '' || value === undefined) {
949
+ return ''
950
+ }
844
951
 
845
- if(0 < value.length) { // Only spit out the tooltip if there's a value there
846
- toolTipText += `<div><dt>${label}</dt><dd>${value}</dd></div>`
847
- }
952
+ if (typeof value === 'string' && value.length > 0 && state.legend.type === 'equalnumber') {
953
+ return value
954
+ }
848
955
 
849
- }
850
- })
956
+ let formattedValue = value
851
957
 
852
- toolTipText += `</dl>`
853
- }
958
+ let columnObj = state.columns[columnName]
854
959
 
855
- // We convert the markup into JSX and add a navigation link if it's going into a modal.
856
- if('jsx' === returnType) {
857
- toolTipText = [(<div key="modal-content">{parse(toolTipText)}</div>)]
960
+ if (columnObj) {
961
+ // If value is a number, apply specific formattings
962
+ if (Number(value)) {
963
+ const hasDecimal = columnObj.roundToPlace && (columnObj.roundToPlace !== '' || columnObj.roundToPlace !== null)
964
+ const decimalPoint = columnObj.roundToPlace ? Number(columnObj.roundToPlace) : 0
858
965
 
859
- if(state.columns.hasOwnProperty('navigate') && row[state.columns.navigate.name]) {
860
- toolTipText.push( (<span className="navigation-link" key="modal-navigation-link" onClick={() => navigationHandler(row[state.columns.navigate.name])}>{state.tooltips.linkLabel}<ExternalIcon className="inline-icon ml-1" /></span>) )
861
- }
966
+ // Rounding
967
+ if (columnObj.hasOwnProperty('roundToPlace') && hasDecimal) {
968
+ formattedValue = Number(value).toFixed(decimalPoint)
862
969
  }
863
970
 
864
- return toolTipText
971
+ if (columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) {
972
+ // Formats number to string with commas - allows up to 5 decimal places, if rounding is not defined.
973
+ // Otherwise, uses the rounding value set at 'columnObj.roundToPlace'.
974
+ formattedValue = Number(value).toLocaleString('en-US', {
975
+ style: 'decimal',
976
+ minimumFractionDigits: hasDecimal ? decimalPoint : 0,
977
+ maximumFractionDigits: hasDecimal ? decimalPoint : 5
978
+ })
979
+ }
980
+ }
865
981
 
982
+ // Check if it's a special value. If it is not, apply the designated prefix and suffix
983
+ if (false === state.legend.specialClasses.includes(String(value))) {
984
+ formattedValue = (columnObj.prefix || '') + formattedValue + (columnObj.suffix || '')
985
+ }
866
986
  }
867
987
 
868
- const titleCase = (string) => {
869
- return string.split(' ').map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()).join(' ');
988
+ return formattedValue
989
+ }
990
+
991
+ const applyLegendToRow = (rowObj) => {
992
+ try {
993
+ if (!rowObj) throw new Error('COVE: No rowObj in applyLegendToRow')
994
+ // Navigation map
995
+ if ('navigation' === state.general.type) {
996
+ let mapColorPalette = colorPalettes[state.color] || colorPalettes['bluegreenreverse']
997
+ return generateColorsArray(mapColorPalette[3])
998
+ }
999
+
1000
+ let hash = hashObj(rowObj)
1001
+
1002
+ if (legendMemo.current.has(hash)) {
1003
+ let idx = legendMemo.current.get(hash)
1004
+ if (runtimeLegend[idx]?.disabled) return false
1005
+ return generateColorsArray(runtimeLegend[idx]?.color, runtimeLegend[idx]?.special)
1006
+ }
1007
+
1008
+ // Fail state
1009
+ return generateColorsArray()
1010
+ } catch (e) {
1011
+ console.error(e)
870
1012
  }
1013
+ }
871
1014
 
872
- // This resets all active legend toggles.
873
- const resetLegendToggles = async () => {
874
- let newLegend = [...runtimeLegend]
1015
+ const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
875
1016
 
876
- newLegend.forEach(legendItem => {
877
- delete legendItem.disabled
878
- })
1017
+ if(!row) return;
1018
+ let toolTipText = ''
1019
+
1020
+ // Adds geo label, ie State: Georgia
1021
+ let stateOrCounty = state.general.geoType === 'us'
1022
+ ? 'State: '
1023
+ : (state.general.geoType === 'us-county' || state.general.geoType === 'single-state')
1024
+ ? 'County: '
1025
+ : ''
1026
+
1027
+ if (state.general.geoType === 'us-county' && state.general.type !== 'us-geocode') {
1028
+ let stateFipsCode = row[state.columns.geo.name].substring(0, 2)
1029
+ const stateName = supportedStatesFipsCodes[stateFipsCode]
879
1030
 
880
- setRuntimeLegend(newLegend)
1031
+ toolTipText += !state.general.hideGeoColumnInTooltip ? `<strong>State: ${stateName}</strong><br/>` : `<strong>${stateName}</strong><br/>`
881
1032
  }
882
1033
 
883
- // Supports JSON or CSV
884
- const fetchRemoteData = async (url) => {
885
- try {
886
- const urlObj = new URL(url);
887
- const regex = /(?:\.([^.]+))?$/
888
-
889
- let data = []
890
-
891
- const ext = (regex.exec(urlObj.pathname)[1])
892
- if ('csv' === ext) {
893
- data = await fetch(url)
894
- .then(response => response.text())
895
- .then(responseText => {
896
- const parsedCsv = Papa.parse(responseText, {
897
- header: true,
898
- dynamicTyping: true,
899
- skipEmptyLines: true
900
- })
901
- return parsedCsv.data
902
- })
903
- }
1034
+ toolTipText += !state.general.hideGeoColumnInTooltip ? `<strong>${stateOrCounty}${displayGeoName(geoName)}</strong>` : `<strong>${displayGeoName(geoName)}</strong>`
904
1035
 
905
- if ('json' === ext) {
906
- data = await fetch(url)
907
- .then(response => response.json())
908
- }
1036
+ if (('data' === state.general.type || state.general.type === 'bubble' || state.general.type === 'us-geocode') && undefined !== row) {
1037
+ toolTipText += `<dl>`
1038
+
1039
+ Object.keys(state.columns).forEach((columnKey) => {
1040
+ const column = state.columns[columnKey]
1041
+
1042
+ if (true === column.tooltip) {
1043
+ let label = column.label.length > 0 ? column.label : ''
909
1044
 
910
- return data;
911
- } catch {
912
- // If we can't parse it, still attempt to fetch it
913
- try {
914
- let response = await (await fetch(configUrl)).json()
915
- return response
916
- } catch {
917
- console.error(`Cannot parse URL: ${url}`);
1045
+ let value
1046
+
1047
+ if (state.legend.specialClasses && state.legend.specialClasses.length && typeof state.legend.specialClasses[0] === 'object') {
1048
+ for (let i = 0; i < state.legend.specialClasses.length; i++) {
1049
+ if (String(row[state.legend.specialClasses[i].key]) === state.legend.specialClasses[i].value) {
1050
+ value = displayDataAsText(state.legend.specialClasses[i].label, columnKey)
1051
+ break
1052
+ }
918
1053
  }
1054
+ }
1055
+
1056
+ if (!value) {
1057
+ value = displayDataAsText(row[column.name], columnKey)
1058
+ }
1059
+
1060
+ if (0 < value.length) { // Only spit out the tooltip if there's a value there
1061
+ toolTipText += state.general.hidePrimaryColumnInTooltip ? `<div><dd>${value}</dd></div>` : `<div><dt>${label}</dt><dd>${value}</dd></div>`
1062
+ }
919
1063
  }
1064
+ })
1065
+ toolTipText += `</dl>`
920
1066
  }
921
1067
 
922
- // Attempts to find the corresponding value
923
- const displayGeoName = (key) => {
924
- let value = key
1068
+ // We convert the markup into JSX and add a navigation link if it's going into a modal.
1069
+ if ('jsx' === returnType) {
1070
+ toolTipText = [ (<div key="modal-content">{parse(toolTipText)}</div>) ]
925
1071
 
926
- // Map to first item in values array which is the preferred label
927
- if(stateKeys.includes(value)) {
928
- value = titleCase(supportedStates[key][0])
929
- }
1072
+ if (state.columns.hasOwnProperty('navigate') && row[state.columns.navigate.name]) {
1073
+ toolTipText.push((<span className="navigation-link" key="modal-navigation-link" onClick={() => navigationHandler(row[state.columns.navigate.name])}>{state.tooltips.linkLabel}<ExternalIcon className="inline-icon ml-1"/></span>))
1074
+ }
1075
+ }
930
1076
 
931
- if(territoryKeys.includes(value)) {
932
- value = titleCase(supportedTerritories[key][0])
933
- }
1077
+ return toolTipText
1078
+ }
934
1079
 
935
- if(countryKeys.includes(value)) {
936
- value = titleCase(supportedCountries[key][0])
937
- }
1080
+ const titleCase = (string) => {
1081
+ return string.split(' ').map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()).join(' ')
1082
+ }
938
1083
 
939
- if(countyKeys.includes(value)) {
940
- value = titleCase(supportedCounties[key])
941
- }
1084
+ // This resets all active legend toggles.
1085
+ const resetLegendToggles = async () => {
1086
+ let newLegend = [ ...runtimeLegend ]
942
1087
 
943
- const dict = {
944
- "District of Columbia" : "Washington D.C."
945
- }
1088
+ newLegend.forEach(legendItem => {
1089
+ delete legendItem.disabled
1090
+ })
946
1091
 
947
- if(true === Object.keys(dict).includes(value)) {
948
- value = dict[value]
949
- }
1092
+ setRuntimeLegend(newLegend)
1093
+ }
1094
+
1095
+ const formatLegendLocation = (key) => {
1096
+ let value = key
1097
+ let formattedName = ''
1098
+ let stateName = stateFipsToTwoDigit[key.substring(0, 2)]
950
1099
 
951
- return titleCase(value);
1100
+ if (stateName) {
1101
+ formattedName += stateName
952
1102
  }
953
1103
 
954
- const navigationHandler = (urlString) => {
955
- // Call custom navigation method if passed
956
- if(customNavigationHandler) {
957
- customNavigationHandler(urlString);
958
- return;
959
- }
1104
+ if (countyKeys.includes(value)) {
1105
+ formattedName += ', ' + titleCase(supportedCounties[key])
1106
+ }
960
1107
 
961
- // Abort if value is blank
962
- if(0 === urlString.length) {
963
- throw Error("Blank string passed as URL. Navigation aborted.");
964
- }
1108
+ return formattedName
1109
+ }
965
1110
 
966
- const urlObj = new URL(urlString);
1111
+ // Attempts to find the corresponding value
1112
+ const displayGeoName = (key) => {
1113
+ let value = key
967
1114
 
968
- // Open constructed link in new tab/window
969
- window.open(urlObj.toString(), '_blank');
1115
+ // Map to first item in values array which is the preferred label
1116
+ if (stateKeys.includes(value)) {
1117
+ value = titleCase(supportedStates[key][0])
970
1118
  }
971
1119
 
972
- const geoClickHandler = (key, value) => {
973
- // If modals are set or we are on a mobile viewport, display modal
974
- if ('xs' === currentViewport || 'xxs' === currentViewport || 'click' === state.tooltips.appearanceType) {
975
- setModal({
976
- geoName: key,
977
- keyedData: value
978
- })
1120
+ if (territoryKeys.includes(value)) {
1121
+ value = titleCase(supportedTerritories[key][0])
1122
+ }
979
1123
 
980
- return;
981
- }
1124
+ if (countryKeys.includes(value)) {
1125
+ value = titleCase(supportedCountries[key][0])
1126
+ }
982
1127
 
983
- // Otherwise if this item has a link specified for it, do regular navigation.
984
- if (state.columns.navigate && value[state.columns.navigate.name]) {
985
- navigationHandler(value[state.columns.navigate.name])
986
- }
1128
+ if (countyKeys.includes(value)) {
1129
+ value = titleCase(supportedCounties[key])
987
1130
  }
988
1131
 
989
- const validateFipsCodeLength = (newState) => {
990
- if(newState.general.geoType === 'us-county' || newState.general.geoType === 'single-state' || newState.general.geoType === 'us' && newState?.data) {
1132
+ const dict = {
1133
+ 'Washington D.C.': 'District of Columbia',
1134
+ 'WASHINGTON DC': 'District of Columbia',
1135
+ 'DC': 'District of Columbia',
1136
+ 'WASHINGTON DC.': 'District of Columbia',
1137
+ 'Congo': 'Republic of the Congo'
1138
+ }
991
1139
 
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;
1140
+ if (true === Object.keys(dict).includes(value)) {
1141
+ value = dict[value]
1142
+ }
1143
+ return titleCase(value)
1144
+ }
1145
+
1146
+ const navigationHandler = (urlString) => {
1147
+ // Call custom navigation method if passed
1148
+ if (customNavigationHandler) {
1149
+ customNavigationHandler(urlString)
1150
+ return
1001
1151
  }
1002
1152
 
1003
- const loadConfig = async (configObj) => {
1004
- // Set loading flag
1005
- if(!loading) setLoading(true)
1153
+ // Abort if value is blank
1154
+ if (0 === urlString.length) {
1155
+ throw Error('Blank string passed as URL. Navigation aborted.')
1156
+ }
1006
1157
 
1007
- // Create new config object the same way each time no matter when this method is called.
1008
- let newState = {
1009
- ...initialState,
1010
- ...configObj
1011
- }
1158
+ const urlObj = new URL(urlString)
1012
1159
 
1013
- // If a dataUrl property exists, always pull from that.
1014
- if (newState.dataUrl) {
1015
- if(newState.dataUrl[0] === '/') {
1016
- newState.dataUrl = 'https://' + hostname + newState.dataUrl
1017
- }
1160
+ // Open constructed link in new tab/window
1161
+ window.open(urlObj.toString(), '_blank')
1162
+ }
1018
1163
 
1019
- let newData = await fetchRemoteData(newState.dataUrl)
1164
+ const geoClickHandler = (key, value) => {
1165
+ if (setSharedFilter) {
1166
+ setSharedFilter(state.uid, value)
1167
+ }
1020
1168
 
1021
- if(newData && newState.dataDescription) {
1022
- newData = transform.autoStandardize(newData);
1023
- newData = transform.developerStandardize(newData, newState.dataDescription);
1024
- }
1169
+ // If modals are set or we are on a mobile viewport, display modal
1170
+ if (window.matchMedia('(any-hover: none)').matches || 'click' === state.tooltips.appearanceType) {
1171
+ setModal({
1172
+ geoName: key,
1173
+ keyedData: value
1174
+ })
1025
1175
 
1026
- if(newData) {
1027
- newState.data = newData
1028
- }
1029
- }
1176
+ return
1177
+ }
1030
1178
 
1031
- // This code goes through and adds the defaults for every property declaring in the initial state at the top.
1032
- // This allows you to easily add new properties to the config without having to worry about accounting for backwards compatibility.
1033
- // Right now this does not work recursively -- only on first and second level properties. So state -> prop1 -> childprop1
1034
- Object.keys(newState).forEach( (key) => {
1035
- if("object" === typeof newState[key] && false === Array.isArray(newState[key])) {
1036
- if(initialState[key] ) {
1037
- Object.keys(initialState[key]).forEach( (property) => {
1038
- if(undefined === newState[key][property]) {
1039
- newState[key][property] = initialState[key][property]
1040
- }
1041
- })
1042
- }
1043
- }
1044
- })
1179
+ // Otherwise if this item has a link specified for it, do regular navigation.
1180
+ if (state.columns.navigate && value[state.columns.navigate.name]) {
1181
+ navigationHandler(value[state.columns.navigate.name])
1182
+ }
1183
+ }
1045
1184
 
1046
- // If there's a name for the geo, add UIDs
1047
- if(newState.columns.geo.name || newState.columns.geo.fips) {
1048
- addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
1049
- }
1185
+ const validateFipsCodeLength = (newState) => {
1186
+ if (newState.general.geoType === 'us-county' || newState.general.geoType === 'single-state' || newState.general.geoType === 'us' && newState?.data) {
1050
1187
 
1051
- if(newState.dataTable.forceDisplay === undefined){
1052
- newState.dataTable.forceDisplay = !isDashboard;
1053
- }
1188
+ newState?.data.forEach(dataPiece => {
1189
+ if (dataPiece[newState.columns.geo.name]) {
1054
1190
 
1191
+ if (!isNaN(parseInt(dataPiece[newState.columns.geo.name])) && dataPiece[newState.columns.geo.name].length === 4) {
1192
+ dataPiece[newState.columns.geo.name] = 0 + dataPiece[newState.columns.geo.name]
1193
+ }
1194
+ dataPiece[newState.columns.geo.name] = dataPiece[newState.columns.geo.name].toString()
1195
+ }
1196
+ })
1197
+ }
1198
+ return newState
1199
+ }
1200
+
1201
+ const handleMapAriaLabels = (state = '', testing = false) => {
1202
+ if (testing) console.log(`handleMapAriaLabels Testing On: ${state}`)
1203
+ try {
1204
+ if (!state.general.geoType) throw Error('handleMapAriaLabels: no geoType found in state')
1205
+ let ariaLabel = ''
1206
+ switch (state.general.geoType) {
1207
+ case 'world':
1208
+ ariaLabel += 'World map'
1209
+ break
1210
+ case 'us':
1211
+ ariaLabel += 'United States map'
1212
+ break
1213
+ case 'us-county':
1214
+ ariaLabel += `United States county map`
1215
+ break
1216
+ case 'single-state':
1217
+ ariaLabel += `${state.general.statePicked.stateName} county map`
1218
+ break
1219
+ case 'us-region':
1220
+ ariaLabel += `United States HHS Region map`
1221
+ break
1222
+ default:
1223
+ ariaLabel = 'Data visualization container'
1224
+ break
1225
+ }
1226
+
1227
+ if (state.general.title) {
1228
+ ariaLabel += ` with the title: ${state.general.title}`
1229
+ }
1230
+
1231
+ return ariaLabel
1232
+ } catch (e) {
1233
+ console.error(e.message)
1234
+ }
1235
+ }
1055
1236
 
1056
- validateFipsCodeLength(newState);
1057
- setState(newState)
1237
+ const loadConfig = async (configObj) => {
1238
+ // Set loading flag
1239
+ if (!loading) setLoading(true)
1058
1240
 
1059
- // Done loading
1060
- setLoading(false)
1241
+ // Create new config object the same way each time no matter when this method is called.
1242
+ let newState = {
1243
+ ...initialState,
1244
+ ...configObj
1061
1245
  }
1062
1246
 
1063
- const init = async () => {
1064
- let configData = null
1065
1247
 
1066
- // Load the configuration data passed to this component if it exists
1067
- if(config) {
1068
- configData = config
1069
- }
1248
+ // If a dataUrl property exists, always pull from that.
1249
+ if (newState.dataUrl) {
1250
+ if(newState.dataUrl[0] === '/') {
1251
+ newState.dataUrl = 'http://' + hostname + newState.dataUrl
1252
+ }
1070
1253
 
1071
- // If the config passed is a string, try to load it as an ajax
1072
- if(configUrl) {
1073
- configData = await fetchRemoteData(configUrl)
1074
- }
1254
+ // handle urls with spaces in the name.
1255
+ if (newState.dataUrl) newState.dataUrl = encodeURI(`${newState.dataUrl}?v=${cacheBustingString()}`)
1256
+ let newData = await fetchRemoteData(newState.dataUrl )
1257
+
1258
+ if (newData && newState.dataDescription) {
1259
+ newData = transform.autoStandardize(newData)
1260
+ newData = transform.developerStandardize(newData, newState.dataDescription)
1261
+ }
1262
+
1263
+ if (newData) {
1264
+ newState.data = newData
1265
+ }
1266
+ }
1075
1267
 
1076
- // Once we have a config verify that it is an object and load it
1077
- if('object' === typeof configData) {
1078
- loadConfig(configData)
1268
+ // This code goes through and adds the defaults for every property declaring in the initial state at the top.
1269
+ // This allows you to easily add new properties to the config without having to worry about accounting for backwards compatibility.
1270
+ // Right now this does not work recursively -- only on first and second level properties. So state -> prop1 -> childprop1
1271
+ Object.keys(newState).forEach((key) => {
1272
+ if ('object' === typeof newState[key] && false === Array.isArray(newState[key])) {
1273
+ if (initialState[key]) {
1274
+ Object.keys(initialState[key]).forEach((property) => {
1275
+ if (undefined === newState[key][property]) {
1276
+ newState[key][property] = initialState[key][property]
1277
+ }
1278
+ })
1079
1279
  }
1280
+ }
1281
+ })
1282
+
1283
+ // If there's a name for the geo, add UIDs
1284
+ if (newState.columns.geo.name || newState.columns.geo.fips) {
1285
+ addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
1080
1286
  }
1081
1287
 
1082
- // Initial load
1083
- useEffect(() => {
1084
- init()
1085
- }, [])
1288
+ if (newState.dataTable.forceDisplay === undefined) {
1289
+ newState.dataTable.forceDisplay = !isDashboard
1290
+ }
1086
1291
 
1087
- useEffect(() => {
1088
- if (state.data) {
1089
- let newData = generateRuntimeData(state);
1090
- setRuntimeData(newData);
1091
- }
1092
- }, [state.general.statePicked]);
1292
+ validateFipsCodeLength(newState)
1293
+ setState(newState)
1294
+ setLoading(false)
1295
+ }
1093
1296
 
1297
+ const init = async () => {
1298
+ let configData = null
1094
1299
 
1300
+ // Load the configuration data passed to this component if it exists
1301
+ if (config) {
1302
+ configData = config
1303
+ }
1095
1304
 
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]);
1305
+ // If the config passed is a string, try to load it as an ajax
1306
+ if (configUrl) {
1307
+ configData = await fetchRemoteData(configUrl)
1308
+ }
1105
1309
 
1106
- useEffect(() => {
1310
+ // Once we have a config verify that it is an object and load it
1311
+ if ('object' === typeof configData) {
1312
+ loadConfig(configData)
1313
+ }
1314
+ }
1107
1315
 
1108
- // UID
1109
- if(state.data && state.columns.geo.name && state.columns.geo.name !== state.data.fromColumn) {
1110
- addUIDs(state, state.columns.geo.name)
1111
- }
1316
+ // Initial load
1317
+ useEffect(() => {
1318
+ init()
1319
+ }, [])
1112
1320
 
1113
- // Filters
1114
- const hashFilters = hashObj(state.filters)
1115
- let filters;
1321
+ useEffect(() => {
1322
+ if (state && !coveLoadedHasRan && container) {
1323
+ publish('cove_loaded', { config: state })
1324
+ setCoveLoadedHasRan(true)
1325
+ }
1326
+ }, [ state, container ])
1116
1327
 
1117
- if(state.filters && hashFilters !== runtimeFilters.fromHash) {
1118
- filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
1328
+ useEffect(() => {
1329
+ if (state.data) {
1330
+ let newData = generateRuntimeData(state)
1331
+ setRuntimeData(newData)
1332
+ }
1333
+ }, [ state.general.statePicked ])
1119
1334
 
1120
- if(filters) {
1121
- setRuntimeFilters(filters)
1122
- }
1123
- }
1124
-
1125
- const hashLegend = hashObj({
1126
- color: state.color,
1127
- customColors: state.customColors,
1128
- numberOfItems: state.legend.numberOfItems,
1129
- type: state.legend.type,
1130
- separateZero: state.legend.separateZero ?? false,
1131
- categoryValuesOrder: state.legend.categoryValuesOrder,
1132
- specialClasses: state.legend.specialClasses,
1133
- geoType: state.general.geoType,
1134
- data: state.data
1135
- })
1335
+ // When geotype changes
1336
+ useEffect(() => {
1337
+ // UID
1338
+ if (state.data && state.columns.geo.name) {
1339
+ addUIDs(state, state.columns.geo.name)
1340
+ }
1341
+ }, [ state ])
1136
1342
 
1137
- const hashData = hashObj({
1138
- columns: state.columns,
1139
- geoType: state.general.geoType,
1140
- type: state.general.type,
1141
- geo: state.columns.geo.name,
1142
- primary: state.columns.primary.name,
1143
- data: state.data,
1144
- ...runtimeFilters
1145
- })
1343
+ useEffect(() => {
1344
+ // UID
1345
+ if (state.data && state.columns.geo.name && state.columns.geo.name !== state.data.fromColumn) {
1346
+ addUIDs(state, state.columns.geo.name)
1347
+ }
1146
1348
 
1147
- // Data
1148
- let newRuntimeData;
1149
- if(hashData !== runtimeData.fromHash && state.data?.fromColumn) {
1150
- newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1151
- setRuntimeData(newRuntimeData)
1152
- }
1349
+ // Filters
1350
+ const hashFilters = hashObj(state.filters)
1351
+ let filters
1153
1352
 
1154
- // Legend
1155
- if (hashLegend !== runtimeLegend.fromHash && (undefined === runtimeData.init || newRuntimeData)) {
1156
- const legend = generateRuntimeLegend(state, newRuntimeData || runtimeData, hashLegend)
1157
- setRuntimeLegend(legend)
1158
- }
1159
- }, [state])
1353
+ if (state.filters && hashFilters !== runtimeFilters.fromHash) {
1354
+ filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
1160
1355
 
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
-
1174
- // Legend - Update when runtimeData does
1175
- if(hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
1176
- const legend = generateRuntimeLegend(state, runtimeData)
1177
- setRuntimeLegend(legend)
1178
- }
1179
- }, [runtimeData])
1356
+ if (filters) {
1357
+ setRuntimeFilters(filters)
1358
+ }
1359
+ }
1360
+
1361
+ const hashLegend = hashObj({
1362
+ color: state.color,
1363
+ customColors: state.customColors,
1364
+ numberOfItems: state.legend.numberOfItems,
1365
+ type: state.legend.type,
1366
+ separateZero: state.legend.separateZero ?? false,
1367
+ categoryValuesOrder: state.legend.categoryValuesOrder,
1368
+ specialClasses: state.legend.specialClasses,
1369
+ geoType: state.general.geoType,
1370
+ data: state.data,
1371
+ ...runtimeLegend
1372
+ })
1373
+
1374
+ const hashData = hashObj({
1375
+ columns: state.columns,
1376
+ geoType: state.general.geoType,
1377
+ type: state.general.type,
1378
+ geo: state.columns.geo.name,
1379
+ primary: state.columns.primary.name,
1380
+ data: state.data,
1381
+ ...runtimeFilters,
1382
+ mapPosition: state.mapPosition
1383
+ })
1180
1384
 
1181
- if(config) {
1182
- useEffect(() => {
1183
- loadConfig(config)
1184
- }, [config.data])
1385
+ // Data
1386
+ let newRuntimeData
1387
+ if (hashData !== runtimeData.fromHash && state.data?.fromColumn) {
1388
+ const newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1389
+ setRuntimeData(newRuntimeData)
1185
1390
  }
1186
1391
 
1187
- // Destructuring for more readable JSX
1188
- const { general, tooltips, dataTable } = state
1189
- const { title = '', subtext = ''} = general
1392
+ // Legend
1393
+ if (hashLegend !== runtimeLegend.fromHash && (undefined === runtimeData.init || newRuntimeData)) {
1394
+ const legend = generateRuntimeLegend(state, newRuntimeData || runtimeData, hashLegend)
1395
+ setRuntimeLegend(legend)
1396
+ }
1190
1397
 
1191
- // Outer container classes
1192
- let outerContainerClasses = [
1193
- 'cdc-open-viz-module',
1194
- 'cdc-map-outer-container',
1195
- currentViewport
1196
- ]
1398
+ }, [ state ])
1399
+
1400
+ useEffect(() => {
1401
+ const hashLegend = hashObj({
1402
+ color: state.color,
1403
+ customColors: state.customColors,
1404
+ numberOfItems: state.legend.numberOfItems,
1405
+ type: state.legend.type,
1406
+ separateZero: state.legend.separateZero ?? false,
1407
+ categoryValuesOrder: state.legend.categoryValuesOrder,
1408
+ specialClasses: state.legend.specialClasses,
1409
+ geoType: state.general.geoType,
1410
+ data: state.data
1411
+ })
1197
1412
 
1198
- if(className) {
1199
- outerContainerClasses.push(className)
1413
+ // Legend - Update when runtimeData does
1414
+ if (hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
1415
+ const legend = generateRuntimeLegend(state, runtimeData)
1416
+ setRuntimeLegend(legend)
1200
1417
  }
1201
1418
 
1202
- // Map container classes
1203
- let mapContainerClasses = [
1204
- 'map-container',
1205
- state.legend.position,
1206
- state.general.type,
1207
- state.general.geoType
1208
- ]
1419
+ }, [ runtimeData ])
1209
1420
 
1210
- if(modal) {
1211
- mapContainerClasses.push('modal-background')
1421
+ if (config) {
1422
+ useEffect(() => {
1423
+ loadConfig(config)
1424
+ }, [ config.data ])
1425
+ }
1426
+
1427
+ // Destructuring for more readable JSX
1428
+ const { general, tooltips, dataTable } = state
1429
+ const { title = '', subtext = '' } = general
1430
+
1431
+ // Outer container classes
1432
+ let outerContainerClasses = [
1433
+ 'cdc-open-viz-module',
1434
+ 'cdc-map-outer-container',
1435
+ currentViewport
1436
+ ]
1437
+
1438
+ if (className) {
1439
+ outerContainerClasses.push(className)
1440
+ }
1441
+
1442
+ // Map container classes
1443
+ let mapContainerClasses = [
1444
+ 'map-container',
1445
+ state.legend.position,
1446
+ state.general.type,
1447
+ state.general.geoType,
1448
+ 'outline-none'
1449
+ ]
1450
+
1451
+ if (modal) {
1452
+ mapContainerClasses.push('modal-background')
1453
+ }
1454
+
1455
+ if (general.type === 'navigation' && true === general.fullBorder) {
1456
+ mapContainerClasses.push('full-border')
1457
+ }
1458
+
1459
+ // Props passed to all map types
1460
+ const mapProps = {
1461
+ state,
1462
+ data: runtimeData,
1463
+ rebuildTooltips: ReactTooltip.rebuild,
1464
+ applyTooltipsToGeo,
1465
+ closeModal,
1466
+ navigationHandler,
1467
+ geoClickHandler,
1468
+ applyLegendToRow,
1469
+ displayGeoName,
1470
+ runtimeLegend,
1471
+ generateColorsArray,
1472
+ titleCase,
1473
+ setState,
1474
+ setRuntimeData,
1475
+ generateRuntimeData,
1476
+ setFilteredCountryCode,
1477
+ filteredCountryCode,
1478
+ position,
1479
+ setPosition,
1480
+ setSharedFilterValue,
1481
+ hasZoom: state.general.allowMapZoom,
1482
+ handleMapAriaLabels
1483
+ }
1484
+
1485
+ if (!mapProps.data || !state.data) return <Loading/>
1486
+
1487
+ const hasDataTable = state.runtime.editorErrorMessage.length === 0 && true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading
1488
+
1489
+ const handleMapTabbing = () => {
1490
+ let tabbingID
1491
+
1492
+ // 1) skip to legend
1493
+ if (general.showSidebar) {
1494
+ tabbingID = '#legend'
1212
1495
  }
1213
1496
 
1214
- if(general.type === 'navigation' && true === general.fullBorder) {
1215
- mapContainerClasses.push('full-border')
1497
+ // 2) skip to data table if it exists and not a navigation map
1498
+ if (hasDataTable && !general.showSidebar) {
1499
+ tabbingID = `#dataTableSection__${Date.now()}`
1216
1500
  }
1217
1501
 
1218
- // Props passed to all map types
1219
- const mapProps = {
1220
- state,
1221
- data: runtimeData,
1222
- rebuildTooltips : ReactTooltip.rebuild,
1223
- applyTooltipsToGeo,
1224
- closeModal,
1225
- navigationHandler,
1226
- geoClickHandler,
1227
- applyLegendToRow,
1228
- displayGeoName,
1229
- runtimeLegend,
1230
- generateColorsArray,
1231
- titleCase
1502
+ // 3) if its a navigation map skip to the dropdown.
1503
+ if (state.general.type === 'navigation') {
1504
+ tabbingID = `#dropdown-${Date.now()}`
1232
1505
  }
1233
1506
 
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
-
1239
- return (
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
- );
1507
+ // 4) handle other options
1508
+ return tabbingID || '#!'
1509
+ }
1510
+
1511
+ const tabId = handleMapTabbing()
1512
+
1513
+ return (
1514
+ <div className={outerContainerClasses.join(' ')} ref={outerContainerRef}>
1515
+ {isEditor && (
1516
+ <EditorPanel
1517
+ isDashboard={isDashboard}
1518
+ state={state}
1519
+ setState={setState}
1520
+ loadConfig={loadConfig}
1521
+ setParentConfig={setConfig}
1522
+ setRuntimeFilters={setRuntimeFilters}
1523
+ runtimeFilters={runtimeFilters}
1524
+ runtimeLegend={runtimeLegend}
1525
+ columnsInData={Object.keys(state.data[0])}
1526
+ />
1527
+ )}
1528
+ {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && <section className={`cdc-map-inner-container ${currentViewport}`} aria-label={'Map: ' + title}>
1529
+ {!window.matchMedia('(any-hover: none)').matches && 'hover' === tooltips.appearanceType && (
1530
+ <ReactTooltip
1531
+ id="tooltip"
1532
+ place="right"
1533
+ type="light"
1534
+ html={true}
1535
+ className={tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}
1536
+ />
1537
+ )}
1538
+ {state.general.title &&
1539
+ <header className={general.showTitle === true ? 'visible' : 'hidden'} {...(!general.showTitle || !state.general.title ? { 'aria-hidden': true } : { 'aria-hidden': false })}>
1540
+ <div role="heading" className={'map-title ' + general.headerColor} tabIndex="0" aria-level="2">
1541
+ <sup>{general.superTitle}</sup>
1542
+ <div>{parse(title)}</div>
1543
+ </div>
1544
+ </header>
1545
+ }
1546
+
1547
+ <div>
1548
+ {general.introText && <section className="introText">{parse(general.introText)}</section>}
1549
+ </div>
1550
+
1551
+ <section
1552
+ role="button"
1553
+ tabIndex="0"
1554
+ className={mapContainerClasses.join(' ')}
1555
+ onClick={(e) => closeModal(e)}
1556
+ onKeyDown={(e) => {
1557
+ if (e.keyCode === 13) {
1558
+ closeModal(e)
1559
+ }
1560
+ }}
1561
+ >
1562
+ {general.showDownloadMediaButton === true && (
1563
+ <div className="map-downloads" data-html2canvas-ignore>
1564
+ <div className="map-downloads__ui btn-group">
1565
+ <button
1566
+ className="btn"
1567
+ title="Download Map as Image"
1568
+ onClick={() => generateMedia(outerContainerRef.current, 'image')}
1569
+ >
1570
+ <DownloadImg className="btn__icon" title="Download Map as Image"/>
1571
+ </button>
1572
+ <button
1573
+ className="btn"
1574
+ title="Download Map as PDF"
1575
+ onClick={() => generateMedia(outerContainerRef.current, 'pdf')}
1576
+ >
1577
+ <DownloadPdf className="btn__icon" title="Download Map as PDF"/>
1578
+ </button>
1579
+ </div>
1580
+ </div>
1581
+ )}
1582
+
1583
+ <a id="skip-geo-container" className="cdcdataviz-sr-only-focusable" href={tabId}>
1584
+ Skip Over Map Container
1585
+ </a>
1586
+
1587
+ <section className="geography-container outline-none" ref={mapSvg} tabIndex="0">
1588
+ {currentViewport && (
1589
+ <section className="geography-container" ref={mapSvg}>
1590
+ {modal && (
1591
+ <Modal
1592
+ type={general.type}
1593
+ viewport={currentViewport}
1594
+ applyTooltipsToGeo={applyTooltipsToGeo}
1595
+ applyLegendToRow={applyLegendToRow}
1596
+ capitalize={state.tooltips.capitalizeLabels}
1597
+ content={modal}
1598
+ />
1599
+ )}
1600
+ {'single-state' === general.geoType && (
1601
+ <SingleStateMap supportedTerritories={supportedTerritories} {...mapProps} />
1602
+ )}
1603
+ {('us' === general.geoType && 'us-geocode' !== state.general.type) && (
1604
+ <UsaMap supportedTerritories={supportedTerritories} {...mapProps} />
1605
+ )}
1606
+ {'us-region' === general.geoType && (
1607
+ <UsaRegionMap supportedTerritories={supportedTerritories} {...mapProps} />
1608
+ )}
1609
+ {'world' === general.geoType && (
1610
+ <WorldMap supportedCountries={supportedCountries} {...mapProps} />
1611
+ )}
1612
+ {('us-county' === general.geoType) && (
1613
+ <CountyMap
1614
+ supportedCountries={supportedCountries}
1615
+ {...mapProps}
1616
+ />
1617
+ )}
1618
+ {'data' === general.type && logo && <img src={logo} alt="" className="map-logo"/>}
1619
+ </section>
1620
+
1621
+ )}
1622
+ </section>
1623
+
1624
+ {general.showSidebar && 'navigation' !== general.type && (
1625
+ <Sidebar
1626
+ viewport={currentViewport}
1627
+ legend={state.legend}
1628
+ runtimeLegend={runtimeLegend}
1629
+ setRuntimeLegend={setRuntimeLegend}
1630
+ runtimeFilters={runtimeFilters}
1631
+ columns={state.columns}
1632
+ sharing={state.sharing}
1633
+ prefix={state.columns.primary.prefix}
1634
+ suffix={state.columns.primary.suffix}
1635
+ setState={setState}
1636
+ resetLegendToggles={resetLegendToggles}
1637
+ changeFilterActive={changeFilterActive}
1638
+ setAccessibleStatus={setAccessibleStatus}
1639
+ displayDataAsText={displayDataAsText}
1640
+ />
1641
+ )}
1642
+ </section>
1643
+
1644
+ {'navigation' === general.type && (
1645
+ <NavigationMenu
1646
+ mapTabbingID={tabId}
1647
+ displayGeoName={displayGeoName}
1648
+ data={runtimeData}
1649
+ options={general}
1650
+ columns={state.columns}
1651
+ navigationHandler={(val) => navigationHandler(val)}
1652
+ />
1653
+ )}
1654
+
1655
+ {link && link}
1656
+
1657
+ {subtext.length > 0 && <p className="subtext">{parse(subtext)}</p>}
1658
+
1659
+ {state.runtime.editorErrorMessage.length === 0 && true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading && (
1660
+ <DataTable
1661
+ state={state}
1662
+ rawData={state.data}
1663
+ navigationHandler={navigationHandler}
1664
+ expandDataTable={general.expandDataTable}
1665
+ headerColor={general.headerColor}
1666
+ columns={state.columns}
1667
+ showDownloadButton={general.showDownloadButton}
1668
+ runtimeLegend={runtimeLegend}
1669
+ runtimeData={runtimeData}
1670
+ displayDataAsText={displayDataAsText}
1671
+ displayGeoName={displayGeoName}
1672
+ applyLegendToRow={applyLegendToRow}
1673
+ tableTitle={dataTable.title}
1674
+ indexTitle={dataTable.indexLabel}
1675
+ mapTitle={general.title}
1676
+ viewport={currentViewport}
1677
+ formatLegendLocation={formatLegendLocation}
1678
+ setFilteredCountryCode={setFilteredCountryCode}
1679
+ tabbingId={tabId}
1680
+ />
1681
+ )}
1682
+
1683
+ {general.footnotes && <section className="footnotes">{parse(general.footnotes)}</section>}
1684
+ </section>}
1685
+
1686
+ <div aria-live="assertive" className="cdcdataviz-sr-only">
1687
+ {accessibleStatus}
1688
+ </div>
1689
+ </div>
1690
+ )
1382
1691
  }
1383
1692
 
1384
1693
  export default memo(CdcMap)