@cdc/map 2.6.4 → 4.22.10

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