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