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