@cdc/map 4.25.3 → 4.25.6
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/.idea/map.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/dist/cdcmap.js +31254 -32242
- package/examples/hex-colors.json +3 -3
- package/examples/m2.json +32904 -0
- package/examples/private/test.json +470 -1457
- package/examples/private/{mmr.json → wastewatermap.json} +86 -115
- package/index.html +36 -63
- package/package.json +7 -19
- package/src/CdcMap.tsx +56 -1552
- package/src/CdcMapComponent.tsx +608 -0
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
- package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
- package/src/_stories/CdcMap.Table.stories.tsx +19 -0
- package/src/_stories/CdcMap.stories.tsx +12 -1
- package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
- package/src/_stories/_mock/default-patterns.json +8 -5
- package/src/_stories/_mock/legend-bins.json +428 -0
- package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
- package/src/cdcMapComponent.styles.css +9 -0
- package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
- package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
- package/src/components/BubbleList.tsx +135 -49
- package/src/components/CityList.tsx +89 -87
- package/src/components/DataTable.tsx +8 -8
- package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
- package/src/components/EditorPanel/components/Error.tsx +9 -2
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
- package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
- package/src/components/Geo.tsx +9 -1
- package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
- package/src/components/Legend/components/Legend.tsx +92 -87
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
- package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
- package/src/components/Legend/components/index.scss +74 -17
- package/src/components/Modal.tsx +17 -7
- package/src/components/NavigationMenu.tsx +11 -9
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
- package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
- package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
- package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
- package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
- package/src/components/UsaMap/helpers/map.ts +1 -1
- package/src/components/UsaMap/helpers/shapes.ts +20 -7
- package/src/components/WorldMap/WorldMap.tsx +64 -118
- package/src/components/WorldMap/worldMap.styles.css +28 -0
- package/src/components/ZoomControls.tsx +15 -13
- package/src/components/zoomControls.styles.css +53 -0
- package/src/context.ts +17 -9
- package/src/data/initial-state.js +5 -2
- package/src/helpers/addUIDs.ts +150 -0
- package/src/helpers/applyColorToLegend.ts +39 -64
- package/src/helpers/applyLegendToRow.ts +51 -0
- package/src/helpers/colorDistributions.ts +12 -0
- package/src/helpers/constants.ts +44 -0
- package/src/helpers/displayGeoName.ts +9 -2
- package/src/helpers/formatLegendLocation.ts +3 -2
- package/src/helpers/generateColorsArray.ts +2 -1
- package/src/helpers/generateRuntimeData.ts +78 -0
- package/src/helpers/generateRuntimeFilters.ts +63 -0
- package/src/helpers/generateRuntimeLegend.ts +566 -0
- package/src/helpers/generateRuntimeLegendHash.ts +16 -15
- package/src/helpers/getColumnNames.ts +19 -0
- package/src/helpers/getMapContainerClasses.ts +23 -0
- package/src/helpers/getStatePicked.ts +8 -0
- package/src/helpers/handleMapTabbing.ts +31 -0
- package/src/helpers/hashObj.ts +1 -1
- package/src/helpers/index.ts +22 -0
- package/src/helpers/navigationHandler.ts +3 -3
- package/src/helpers/resetLegendToggles.ts +13 -0
- package/src/helpers/setBinNumbers.ts +5 -0
- package/src/helpers/sortSpecialClassesLast.ts +7 -0
- package/src/helpers/tests/getColumnNames.test.ts +52 -0
- package/src/helpers/titleCase.ts +1 -1
- package/src/helpers/toggleLegendActive.ts +25 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
- package/src/hooks/useColumnsRequiredChecker.ts +51 -0
- package/src/hooks/useGeoClickHandler.ts +45 -0
- package/src/hooks/useLegendSeparators.ts +26 -0
- package/src/hooks/useMapLayers.tsx +34 -60
- package/src/hooks/useModal.ts +22 -0
- package/src/hooks/useResizeObserver.ts +4 -5
- package/src/hooks/useStateZoom.tsx +52 -75
- package/src/hooks/useTooltip.ts +2 -3
- package/src/index.jsx +3 -9
- package/src/scss/editor-panel.scss +3 -99
- package/src/scss/main.scss +1 -19
- package/src/scss/map.scss +15 -220
- package/src/store/map.actions.ts +46 -0
- package/src/store/map.reducer.ts +96 -0
- package/src/types/Annotations.ts +24 -0
- package/src/types/MapConfig.ts +23 -3
- package/src/types/MapContext.ts +36 -35
- package/src/types/Modal.ts +1 -0
- package/src/types/RuntimeData.ts +3 -0
- package/examples/private/DEV-9644.json +0 -184
- package/examples/private/DEV-9989.json +0 -229
- package/examples/private/ardi.json +0 -180
- package/examples/private/colors 2.json +0 -416
- package/examples/private/colors.json +0 -416
- package/examples/private/colors.json.zip +0 -0
- package/examples/private/customColors.json +0 -45348
- package/examples/test.json +0 -183
- package/src/helpers/closeModal.ts +0 -9
- package/src/scss/btn.scss +0 -69
- package/src/scss/filters.scss +0 -27
- package/src/scss/variables.scss +0 -1
- /package/src/hooks/{useActiveElement.js → useActiveElement.ts} +0 -0
package/src/CdcMap.tsx
CHANGED
|
@@ -1,1143 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import * as d3 from 'd3'
|
|
4
|
-
import _ from 'lodash'
|
|
5
|
-
import 'whatwg-fetch'
|
|
6
|
-
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
7
|
-
import Papa from 'papaparse'
|
|
8
|
-
import parse from 'html-react-parser'
|
|
9
|
-
import 'react-tooltip/dist/react-tooltip.css'
|
|
10
|
-
|
|
11
|
-
// Core Components
|
|
12
|
-
import DataTable from '@cdc/core/components/DataTable'
|
|
13
|
-
import Filters, { useFilters } from '@cdc/core/components/Filters'
|
|
14
|
-
import Layout from '@cdc/core/components/Layout'
|
|
15
|
-
import MediaControls from '@cdc/core/components/MediaControls'
|
|
16
|
-
import SkipTo from '@cdc/core/components/elements/SkipTo'
|
|
17
|
-
import Title from '@cdc/core/components/ui/Title'
|
|
18
|
-
import Waiting from '@cdc/core/components/Waiting'
|
|
19
|
-
|
|
20
|
-
// types
|
|
21
|
-
import { type Coordinate, type MapConfig } from './types/MapConfig'
|
|
22
|
-
import { displayGeoName } from './helpers/displayGeoName'
|
|
23
|
-
|
|
24
|
-
// Data
|
|
25
|
-
import { countryCoordinates } from './data/country-coordinates'
|
|
26
|
-
import {
|
|
27
|
-
supportedCities,
|
|
28
|
-
supportedCounties,
|
|
29
|
-
supportedCountries,
|
|
30
|
-
supportedRegions,
|
|
31
|
-
supportedStates,
|
|
32
|
-
supportedStatesFipsCodes,
|
|
33
|
-
supportedTerritories
|
|
34
|
-
} from './data/supported-geos'
|
|
35
|
-
import colorPalettes from '@cdc/core/data/colorPalettes'
|
|
36
|
-
import initialState from './data/initial-state'
|
|
37
|
-
|
|
38
|
-
// Assets
|
|
39
|
-
import ExternalIcon from './images/external-link.svg'
|
|
40
|
-
|
|
41
|
-
// Sass
|
|
42
|
-
import './scss/main.scss'
|
|
43
|
-
import './scss/btn.scss'
|
|
44
|
-
|
|
45
|
-
// Core Helpers
|
|
46
|
-
import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
|
|
1
|
+
import React, { useState, useEffect, useContext } from 'react'
|
|
2
|
+
import CdcMapComponent from './CdcMapComponent'
|
|
47
3
|
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
48
|
-
import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
|
|
49
|
-
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
50
4
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
51
|
-
import
|
|
52
|
-
import
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
import Annotation from './components/Annotation'
|
|
69
|
-
import ConfigContext from './context'
|
|
70
|
-
import EditorPanel from './components/EditorPanel'
|
|
71
|
-
import Error from './components/EditorPanel/components/Error'
|
|
72
|
-
import Legend from './components/Legend'
|
|
73
|
-
import Modal from './components/Modal'
|
|
74
|
-
import NavigationMenu from './components/NavigationMenu'
|
|
75
|
-
import UsaMap from './components/UsaMap'
|
|
76
|
-
import WorldMap from './components/WorldMap'
|
|
77
|
-
import GoogleMap from './components/GoogleMap'
|
|
78
|
-
|
|
79
|
-
// hooks
|
|
80
|
-
import useTooltip from './hooks/useTooltip'
|
|
81
|
-
import useResizeObserver from './hooks/useResizeObserver'
|
|
82
|
-
import { formatLegendLocation } from './helpers/formatLegendLocation'
|
|
83
|
-
|
|
84
|
-
// Data props
|
|
85
|
-
const stateKeys = Object.keys(supportedStates)
|
|
86
|
-
const territoryKeys = Object.keys(supportedTerritories)
|
|
87
|
-
const regionKeys = Object.keys(supportedRegions)
|
|
88
|
-
const countryKeys = Object.keys(supportedCountries)
|
|
89
|
-
const countyKeys = Object.keys(supportedCounties)
|
|
90
|
-
const cityKeys = Object.keys(supportedCities)
|
|
5
|
+
import initialState from './data/initial-state'
|
|
6
|
+
import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
|
|
7
|
+
import { addUIDs, validateFipsCodeLength } from './helpers'
|
|
8
|
+
import EditorContext from '@cdc/editor/src/ConfigContext'
|
|
9
|
+
import { MapConfig } from './types/MapConfig'
|
|
10
|
+
import _, { set } from 'lodash'
|
|
11
|
+
|
|
12
|
+
type CdcMapProps = {
|
|
13
|
+
config: MapConfig
|
|
14
|
+
configUrl?: string
|
|
15
|
+
isEditor?: boolean
|
|
16
|
+
isDashboard?: boolean
|
|
17
|
+
link?: string
|
|
18
|
+
logo?: string
|
|
19
|
+
navigationHandler: Function
|
|
20
|
+
setConfig: Function
|
|
21
|
+
}
|
|
91
22
|
|
|
92
|
-
const CdcMap = ({
|
|
93
|
-
config,
|
|
23
|
+
const CdcMap: React.FC<CdcMapProps> = ({
|
|
94
24
|
navigationHandler: customNavigationHandler,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
isDebug = false,
|
|
25
|
+
isEditor,
|
|
26
|
+
isDashboard,
|
|
98
27
|
configUrl,
|
|
99
28
|
logo = '',
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
setSharedFilterValue,
|
|
103
|
-
link
|
|
29
|
+
link,
|
|
30
|
+
config: editorsConfig
|
|
104
31
|
}) => {
|
|
105
|
-
const
|
|
106
|
-
const [
|
|
107
|
-
const [scale, setScale] = useState(1)
|
|
108
|
-
const [state, setState] = useState<MapConfig>({ ...initialState })
|
|
109
|
-
const [isDraggingAnnotation, setIsDraggingAnnotation] = useState(false)
|
|
110
|
-
const [loading, setLoading] = useState(true)
|
|
111
|
-
const [displayPanel, setDisplayPanel] = useState(true)
|
|
112
|
-
const [topoData, setTopoData] = useState<{}>({})
|
|
113
|
-
const [runtimeFilters, setRuntimeFilters] = useState([])
|
|
114
|
-
const [runtimeData, setRuntimeData] = useState({ init: true })
|
|
115
|
-
const _setRuntimeData = (data: any) => {
|
|
116
|
-
if (config) {
|
|
117
|
-
setRuntimeData(data)
|
|
118
|
-
} else {
|
|
119
|
-
setRuntimeFilters(data)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const [runtimeLegend, setRuntimeLegend] = useState([])
|
|
123
|
-
const [stateToShow, setStateToShow] = useState(null)
|
|
124
|
-
const [modal, setModal] = useState(null)
|
|
125
|
-
const [accessibleStatus, setAccessibleStatus] = useState('')
|
|
126
|
-
const [filteredCountryCode, setFilteredCountryCode] = useState()
|
|
127
|
-
const [position, setPosition] = useState(state.mapPosition)
|
|
128
|
-
const [coveLoadedHasRan, setCoveLoadedHasRan] = useState(false)
|
|
129
|
-
const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`) // eslint-disable-line
|
|
130
|
-
const [requiredColumns, setRequiredColumns] = useState(null) // Simple state so we know if we need more information before parsing the map
|
|
131
|
-
const [projection, setProjection] = useState(null)
|
|
132
|
-
const { currentViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
|
|
133
|
-
|
|
134
|
-
const legendRef = useRef(null)
|
|
135
|
-
const tooltipRef = useRef(null)
|
|
136
|
-
const legendId = useId()
|
|
137
|
-
// create random tooltipId
|
|
138
|
-
const tooltipId = `${Math.random().toString(16).slice(-4)}`
|
|
139
|
-
const mapId = useId()
|
|
140
|
-
|
|
141
|
-
const { handleSorting } = useFilters({ config: state, setConfig: setState })
|
|
142
|
-
let legendMemo = useRef(new Map())
|
|
143
|
-
let legendSpecialClassLastMemo = useRef(new Map())
|
|
144
|
-
let innerContainerRef = useRef()
|
|
145
|
-
|
|
146
|
-
if (isDebug) console.log('CdcMap state=', state) // <eslint-disable-line></eslint-disable-line>
|
|
147
|
-
|
|
148
|
-
const handleDragStateChange = isDragging => {
|
|
149
|
-
setIsDraggingAnnotation(isDragging)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const columnsRequiredChecker = useCallback(() => {
|
|
153
|
-
let columnList = []
|
|
154
|
-
|
|
155
|
-
// Geo is always required
|
|
156
|
-
if ('' === state.columns.geo.name) {
|
|
157
|
-
columnList.push('Geography')
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Primary is required if we're on a data map or a point map
|
|
161
|
-
if ('navigation' !== state.general.type && '' === state.columns.primary.name) {
|
|
162
|
-
columnList.push('Primary')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Navigate is required for navigation maps
|
|
166
|
-
if ('navigation' === state.general.type && '' === state.columns.navigate.name) {
|
|
167
|
-
columnList.push('Navigation')
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
|
|
172
|
-
'' === state.columns.latitude.name
|
|
173
|
-
) {
|
|
174
|
-
columnList.push('Latitude')
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
|
|
179
|
-
'' === state.columns.longitude.name
|
|
180
|
-
) {
|
|
181
|
-
columnList.push('Longitude')
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (columnList.length === 0) columnList = null
|
|
185
|
-
|
|
186
|
-
setRequiredColumns(columnList)
|
|
187
|
-
}, [state.columns, state.general.type])
|
|
188
|
-
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
try {
|
|
191
|
-
if (filteredCountryCode) {
|
|
192
|
-
const coordinates = countryCoordinates[filteredCountryCode]
|
|
193
|
-
const long = coordinates[1]
|
|
194
|
-
const lat = coordinates[0]
|
|
195
|
-
const reversedCoordinates: Coordinate = [long, lat]
|
|
196
|
-
|
|
197
|
-
setState({
|
|
198
|
-
...state,
|
|
199
|
-
mapPosition: { coordinates: reversedCoordinates, zoom: 3 }
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
} catch (e) {
|
|
203
|
-
console.error('COVE: Failed to set world map zoom.') // eslint-disable-line
|
|
204
|
-
}
|
|
205
|
-
}, [filteredCountryCode]) // eslint-disable-line
|
|
206
|
-
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
setTimeout(() => {
|
|
209
|
-
if (filteredCountryCode) {
|
|
210
|
-
const filteredCountryObj = runtimeData[filteredCountryCode]
|
|
211
|
-
const tmpData = {
|
|
212
|
-
[filteredCountryCode]: filteredCountryObj
|
|
213
|
-
}
|
|
214
|
-
setRuntimeData(tmpData)
|
|
215
|
-
}
|
|
216
|
-
}, 100)
|
|
217
|
-
}, [filteredCountryCode]) // eslint-disable-line
|
|
218
|
-
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
if (state.mapPosition) {
|
|
221
|
-
setPosition(state.mapPosition)
|
|
222
|
-
}
|
|
223
|
-
}, [state.mapPosition, setPosition])
|
|
224
|
-
|
|
225
|
-
// 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
|
|
226
|
-
// We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
|
|
227
|
-
// eslint-disable-next-line
|
|
228
|
-
const addUIDs = useCallback((configObj, fromColumn) => {
|
|
229
|
-
configObj.data.forEach(row => {
|
|
230
|
-
let uid = null
|
|
231
|
-
|
|
232
|
-
if (row.uid) row.uid = null // Wipe existing UIDs
|
|
233
|
-
|
|
234
|
-
// United States check
|
|
235
|
-
if ('us' === configObj.general.geoType && configObj.columns.geo.name) {
|
|
236
|
-
// const geoName = row[configObj.columns.geo.name] && typeof row[configObj.columns.geo.name] === "string" ? row[configObj.columns.geo.name].toUpperCase() : '';
|
|
237
|
-
let geoName = ''
|
|
238
|
-
|
|
239
|
-
if (row[configObj.columns.geo.name] !== undefined && row[configObj.columns.geo.name] !== null) {
|
|
240
|
-
geoName = String(row[configObj.columns.geo.name])
|
|
241
|
-
geoName = geoName.toUpperCase()
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// States
|
|
245
|
-
uid = stateKeys.find(key => supportedStates[key].includes(geoName))
|
|
246
|
-
|
|
247
|
-
// Territories
|
|
248
|
-
if (!uid) {
|
|
249
|
-
uid = territoryKeys.find(key => supportedTerritories[key].includes(geoName))
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Cities
|
|
253
|
-
if (!uid && geoName) {
|
|
254
|
-
uid = cityKeys.find(key => key === geoName.toUpperCase())
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (state.general.displayAsHex) {
|
|
258
|
-
const upperCaseKey = geoName.toUpperCase()
|
|
259
|
-
const supportedDc = [
|
|
260
|
-
'WASHINGTON D.C.',
|
|
261
|
-
'DISTRICT OF COLUMBIA',
|
|
262
|
-
'WASHINGTON DC',
|
|
263
|
-
'DC',
|
|
264
|
-
'WASHINGTON DC.',
|
|
265
|
-
'D.C.',
|
|
266
|
-
'D.C'
|
|
267
|
-
]
|
|
268
|
-
if (supportedDc.includes(upperCaseKey)) {
|
|
269
|
-
uid = 'US-DC'
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if ('us-region' === configObj.general.geoType && configObj.columns.geo.name) {
|
|
275
|
-
// const geoName = row[configObj.columns.geo.name] && typeof row[configObj.columns.geo.name] === "string" ? row[configObj.columns.geo.name].toUpperCase() : '';
|
|
276
|
-
let geoName = ''
|
|
277
|
-
|
|
278
|
-
if (row[configObj.columns.geo.name] !== undefined && row[configObj.columns.geo.name] !== null) {
|
|
279
|
-
geoName = String(row[configObj.columns.geo.name])
|
|
280
|
-
geoName = geoName.toUpperCase()
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Regions
|
|
284
|
-
uid = regionKeys.find(key => supportedRegions[key].includes(geoName))
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// World Check
|
|
288
|
-
if ('world' === configObj.general.geoType) {
|
|
289
|
-
const geoName = row[configObj.columns.geo.name]
|
|
290
|
-
|
|
291
|
-
uid = countryKeys.find(key => supportedCountries[key].includes(geoName))
|
|
292
|
-
|
|
293
|
-
// Cities
|
|
294
|
-
if (!uid && 'world-geocode' === state.general.type) {
|
|
295
|
-
uid = cityKeys.find(key => key === geoName?.toUpperCase())
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Cities
|
|
299
|
-
if (!uid && geoName) {
|
|
300
|
-
uid = cityKeys.find(key => key === geoName.toUpperCase())
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// County Check
|
|
305
|
-
if (
|
|
306
|
-
('us-county' === configObj.general.geoType || 'single-state' === configObj.general.geoType) &&
|
|
307
|
-
'us-geocode' !== configObj.general.type
|
|
308
|
-
) {
|
|
309
|
-
const fips = row[configObj.columns.geo.name]
|
|
310
|
-
uid = countyKeys.find(key => key === fips)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if ('us-geocode' === state.general.type) {
|
|
314
|
-
uid = row[state.columns.geo.name]
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
!uid &&
|
|
319
|
-
state.columns.latitude?.name &&
|
|
320
|
-
state.columns.longitude?.name &&
|
|
321
|
-
row[state.columns.latitude?.name] &&
|
|
322
|
-
row[state.columns.longitude?.name]
|
|
323
|
-
) {
|
|
324
|
-
uid = `${row[state.columns.geo.name]}`
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (uid) {
|
|
328
|
-
Object.defineProperty(row, 'uid', {
|
|
329
|
-
value: uid,
|
|
330
|
-
writable: true
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
configObj.data.fromColumn = fromColumn
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
// eslint-disable-next-line
|
|
339
|
-
const generateRuntimeLegend = useCallback((configObj, runtimeFilters, hash) => {
|
|
340
|
-
const newLegendMemo = new Map() // Reset memoization
|
|
341
|
-
const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
|
|
342
|
-
let primaryCol = configObj.columns.primary.name,
|
|
343
|
-
isBubble = configObj.general.type === 'bubble',
|
|
344
|
-
categoricalCol = configObj.columns.categorical ? configObj.columns.categorical.name : undefined,
|
|
345
|
-
type = configObj.legend.type,
|
|
346
|
-
number = configObj.legend.numberOfItems,
|
|
347
|
-
result = []
|
|
348
|
-
|
|
349
|
-
// Add a hash for what we're working from if passed
|
|
350
|
-
if (hash) {
|
|
351
|
-
result.fromHash = hash
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
result.runtimeDataHash = runtimeFilters?.fromHash
|
|
355
|
-
|
|
356
|
-
// Unified will base the legend off ALL of the data maps received. Otherwise, it will use
|
|
357
|
-
let dataSet = configObj.legend.unified ? configObj.data : Object.values(runtimeData)
|
|
358
|
-
let specialClasses = 0
|
|
359
|
-
let specialClassesHash = {}
|
|
360
|
-
|
|
361
|
-
// Special classes
|
|
362
|
-
if (configObj.legend.specialClasses.length) {
|
|
363
|
-
if (typeof configObj.legend.specialClasses[0] === 'object') {
|
|
364
|
-
configObj.legend.specialClasses.forEach(specialClass => {
|
|
365
|
-
dataSet = dataSet.filter(row => {
|
|
366
|
-
const val = String(row[specialClass.key])
|
|
367
|
-
|
|
368
|
-
if (specialClass.value === val) {
|
|
369
|
-
if (undefined === specialClassesHash[val]) {
|
|
370
|
-
specialClassesHash[val] = true
|
|
371
|
-
|
|
372
|
-
result.push({
|
|
373
|
-
special: true,
|
|
374
|
-
value: val,
|
|
375
|
-
label: specialClass.label
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
|
|
379
|
-
|
|
380
|
-
specialClasses += 1
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
let specialColor: number
|
|
32
|
+
const editorContext = useContext(EditorContext)
|
|
33
|
+
const [config, _setConfig] = useState(editorsConfig ?? null)
|
|
384
34
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return false
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return true
|
|
394
|
-
})
|
|
395
|
-
})
|
|
396
|
-
} else {
|
|
397
|
-
dataSet = dataSet.filter(row => {
|
|
398
|
-
const val = row[primaryCol]
|
|
399
|
-
|
|
400
|
-
if (configObj.legend.specialClasses.includes(val)) {
|
|
401
|
-
// apply the special color to the legend
|
|
402
|
-
if (undefined === specialClassesHash[val]) {
|
|
403
|
-
specialClassesHash[val] = true
|
|
404
|
-
|
|
405
|
-
result.push({
|
|
406
|
-
special: true,
|
|
407
|
-
value: val
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
|
|
411
|
-
|
|
412
|
-
specialClasses += 1
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
let specialColor = ''
|
|
416
|
-
|
|
417
|
-
// color the state if val is in row
|
|
418
|
-
if (Object.values(row).includes(val)) {
|
|
419
|
-
specialColor = result.findIndex(p => p.value === val)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
newLegendMemo.set(hashObj(row), specialColor)
|
|
423
|
-
|
|
424
|
-
return false
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return true
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Category
|
|
433
|
-
if ('category' === type) {
|
|
434
|
-
let uniqueValues = new Map()
|
|
435
|
-
let count = 0
|
|
436
|
-
|
|
437
|
-
for (let i = 0; i < dataSet.length; i++) {
|
|
438
|
-
let row = dataSet[i]
|
|
439
|
-
let value = isBubble && categoricalCol && row[categoricalCol] ? row[categoricalCol] : row[primaryCol]
|
|
440
|
-
if (undefined === value) continue
|
|
441
|
-
|
|
442
|
-
if (false === uniqueValues.has(value)) {
|
|
443
|
-
uniqueValues.set(value, [hashObj(row)])
|
|
444
|
-
count++
|
|
445
|
-
} else {
|
|
446
|
-
uniqueValues.get(value).push(hashObj(row))
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
let sorted = [...uniqueValues.keys()]
|
|
451
|
-
|
|
452
|
-
if (configObj.legend.additionalCategories) {
|
|
453
|
-
configObj.legend.additionalCategories.forEach(additionalCategory => {
|
|
454
|
-
if (additionalCategory && indexOfIgnoreType(sorted, additionalCategory) === -1) {
|
|
455
|
-
sorted.push(additionalCategory)
|
|
456
|
-
}
|
|
457
|
-
})
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Apply custom sorting or regular sorting
|
|
461
|
-
let configuredOrder = configObj.legend.categoryValuesOrder ?? []
|
|
462
|
-
|
|
463
|
-
if (configuredOrder.length) {
|
|
464
|
-
sorted.sort((a, b) => {
|
|
465
|
-
let aVal = configuredOrder.indexOf(a)
|
|
466
|
-
let bVal = configuredOrder.indexOf(b)
|
|
467
|
-
if (aVal === bVal) return 0
|
|
468
|
-
if (aVal === -1) return 1
|
|
469
|
-
if (bVal === -1) return -1
|
|
470
|
-
return aVal - bVal
|
|
471
|
-
})
|
|
472
|
-
} else {
|
|
473
|
-
sorted.sort((a, b) => a - b)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Add legend item for each
|
|
477
|
-
sorted.forEach(val => {
|
|
478
|
-
result.push({
|
|
479
|
-
value: val
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
let lastIdx = result.length - 1
|
|
483
|
-
let arr = uniqueValues.get(val)
|
|
484
|
-
|
|
485
|
-
if (arr) {
|
|
486
|
-
arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
|
|
487
|
-
}
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
// Add color to new legend item
|
|
491
|
-
for (let i = 0; i < result.length; i++) {
|
|
492
|
-
result[i].color = applyColorToLegend(i, state, result)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
legendMemo.current = newLegendMemo
|
|
496
|
-
|
|
497
|
-
// before returning the legend result
|
|
498
|
-
// add property for bin number and set to index location
|
|
499
|
-
result.forEach((row, i) => {
|
|
500
|
-
row.bin = i // set bin number to index
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
// Move all special legend items from "Special Classes" to the end of the legend
|
|
504
|
-
if (state.legend.showSpecialClassesLast) {
|
|
505
|
-
let specialRows = result.filter(d => d.special === true)
|
|
506
|
-
let otherRows = result.filter(d => !d.special)
|
|
507
|
-
result = [...otherRows, ...specialRows]
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const assignSpecialClassLastIndex = (value, key) => {
|
|
511
|
-
const newIndex = result.findIndex(d => d.bin === value)
|
|
512
|
-
newLegendSpecialClassLastMemo.set(key, newIndex)
|
|
513
|
-
}
|
|
514
|
-
newLegendMemo.forEach(assignSpecialClassLastIndex)
|
|
515
|
-
legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
|
|
516
|
-
|
|
517
|
-
// filter special classes from results
|
|
518
|
-
const specialValues = result.filter(d => d.special).map(d => d.value)
|
|
519
|
-
return result.filter(d => d.special || !specialValues.includes(d.value))
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
let uniqueValues = {}
|
|
523
|
-
dataSet.forEach(datum => {
|
|
524
|
-
uniqueValues[datum[primaryCol]] = true
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
let legendNumber = Math.min(number, Object.keys(uniqueValues).length)
|
|
528
|
-
|
|
529
|
-
// Separate zero
|
|
530
|
-
if (true === configObj.legend.separateZero && !state.general.equalNumberOptIn) {
|
|
531
|
-
let addLegendItem = false
|
|
532
|
-
|
|
533
|
-
for (let i = 0; i < dataSet.length; i++) {
|
|
534
|
-
if (dataSet[i][primaryCol] === 0) {
|
|
535
|
-
addLegendItem = true
|
|
536
|
-
|
|
537
|
-
let row = dataSet.splice(i, 1)[0]
|
|
538
|
-
|
|
539
|
-
newLegendMemo.set(hashObj(row), result.length)
|
|
540
|
-
i--
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (addLegendItem) {
|
|
545
|
-
legendNumber -= 1 // This zero takes up one legend item
|
|
546
|
-
|
|
547
|
-
// Add new legend item
|
|
548
|
-
result.push({
|
|
549
|
-
min: 0,
|
|
550
|
-
max: 0
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
let lastIdx = result.length - 1
|
|
554
|
-
|
|
555
|
-
// Add color to new legend item
|
|
556
|
-
result[lastIdx].color = applyColorToLegend(lastIdx, state, result)
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Sort data for use in equalnumber or equalinterval
|
|
561
|
-
if (state.general.type !== 'us-geocode') {
|
|
562
|
-
dataSet = dataSet
|
|
563
|
-
.filter(row => typeof row[primaryCol] === 'number')
|
|
564
|
-
.sort((a, b) => {
|
|
565
|
-
let aNum = a[primaryCol]
|
|
566
|
-
let bNum = b[primaryCol]
|
|
567
|
-
|
|
568
|
-
return aNum - bNum
|
|
569
|
-
})
|
|
35
|
+
const setConfig = newConfig => {
|
|
36
|
+
_setConfig(newConfig)
|
|
37
|
+
if (isEditor && !isDashboard) {
|
|
38
|
+
editorContext.setTempConfig(newConfig)
|
|
570
39
|
}
|
|
571
|
-
|
|
572
|
-
// Equal Number
|
|
573
|
-
if (type === 'equalnumber') {
|
|
574
|
-
// start work on changing legend functionality
|
|
575
|
-
// FALSE === ignore old version for now.
|
|
576
|
-
if (!state.general.equalNumberOptIn) {
|
|
577
|
-
let numberOfRows = dataSet.length
|
|
578
|
-
|
|
579
|
-
let remainder
|
|
580
|
-
let changingNumber = legendNumber
|
|
581
|
-
|
|
582
|
-
let chunkAmt
|
|
583
|
-
|
|
584
|
-
// Loop through the array until it has been split into equal subarrays
|
|
585
|
-
while (numberOfRows > 0) {
|
|
586
|
-
remainder = numberOfRows % changingNumber
|
|
587
|
-
chunkAmt = Math.floor(numberOfRows / changingNumber)
|
|
588
|
-
|
|
589
|
-
if (remainder > 0) {
|
|
590
|
-
chunkAmt += 1
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
let removedRows = dataSet.splice(0, chunkAmt)
|
|
594
|
-
|
|
595
|
-
let min = removedRows[0][primaryCol],
|
|
596
|
-
max = removedRows[removedRows.length - 1][primaryCol]
|
|
597
|
-
|
|
598
|
-
// eslint-disable-next-line
|
|
599
|
-
removedRows.forEach(row => {
|
|
600
|
-
newLegendMemo.set(hashObj(row), result.length)
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
result.push({
|
|
604
|
-
min,
|
|
605
|
-
max
|
|
606
|
-
})
|
|
607
|
-
|
|
608
|
-
result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
|
|
609
|
-
|
|
610
|
-
changingNumber -= 1
|
|
611
|
-
numberOfRows -= chunkAmt
|
|
612
|
-
}
|
|
613
|
-
} else {
|
|
614
|
-
// get nums
|
|
615
|
-
let domainNums = new Set(dataSet.map(item => item[state.columns.primary.name]))
|
|
616
|
-
|
|
617
|
-
domainNums = d3.extent(domainNums)
|
|
618
|
-
|
|
619
|
-
let colors = colorPalettes[state.color]
|
|
620
|
-
let colorRange = colors.slice(0, state.legend.numberOfItems)
|
|
621
|
-
|
|
622
|
-
const getDomain = () => {
|
|
623
|
-
// backwards compatibility
|
|
624
|
-
if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
|
|
625
|
-
return _.uniq(
|
|
626
|
-
dataSet.map(item =>
|
|
627
|
-
Number(item[state.columns.primary.name]).toFixed(Number(state?.columns?.primary?.roundToPlace))
|
|
628
|
-
)
|
|
629
|
-
)
|
|
630
|
-
}
|
|
631
|
-
return _.uniq(dataSet.map(item => Math.round(Number(item[state.columns.primary.name]))))
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const getBreaks = scale => {
|
|
635
|
-
// backwards compatibility
|
|
636
|
-
if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
|
|
637
|
-
return scale.quantiles().map(b => Number(b)?.toFixed(Number(state?.columns?.primary?.roundToPlace)))
|
|
638
|
-
}
|
|
639
|
-
return scale.quantiles().map(item => Number(Math.round(item)))
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
let scale = d3
|
|
643
|
-
.scaleQuantile()
|
|
644
|
-
.domain(getDomain()) // min/max values
|
|
645
|
-
.range(colorRange) // set range to our colors array
|
|
646
|
-
|
|
647
|
-
const breaks = getBreaks(scale)
|
|
648
|
-
|
|
649
|
-
// if separating zero force it into breaks
|
|
650
|
-
if (breaks[0] !== 0) {
|
|
651
|
-
breaks.unshift(0)
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// eslint-disable-next-line array-callback-return
|
|
655
|
-
breaks.map((item, index) => {
|
|
656
|
-
const setMin = index => {
|
|
657
|
-
let min = breaks[index]
|
|
658
|
-
|
|
659
|
-
// if first break is a seperated zero, min is zero
|
|
660
|
-
if (index === 0 && state.legend.separateZero) {
|
|
661
|
-
min = 0
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// if we're on the second break, and separating out zero, increment min to 1.
|
|
665
|
-
if (index === 1 && state.legend.separateZero) {
|
|
666
|
-
min = 1
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// // in starting position and zero in the data
|
|
670
|
-
// if((index === state.legend.specialClasses?.length ) && (state.legend.specialClasses.length !== 0)) {
|
|
671
|
-
// min = breaks[index]
|
|
672
|
-
// }
|
|
673
|
-
return min
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const getDecimalPlace = n => {
|
|
677
|
-
return Math.pow(10, -n)
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const setMax = index => {
|
|
681
|
-
let max = Number(breaks[index + 1]) - getDecimalPlace(Number(state?.columns?.primary?.roundToPlace))
|
|
682
|
-
|
|
683
|
-
if (index === 0 && state.legend.separateZero) {
|
|
684
|
-
max = 0
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (index + 1 === breaks.length) {
|
|
688
|
-
max = domainNums[1]
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return max
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
let min = setMin(index)
|
|
695
|
-
let max = setMax(index, min)
|
|
696
|
-
|
|
697
|
-
result.push({
|
|
698
|
-
min,
|
|
699
|
-
max,
|
|
700
|
-
color: scale(item)
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
dataSet.forEach(row => {
|
|
704
|
-
let number = row[state.columns.primary.name]
|
|
705
|
-
let updated = result.length - 1
|
|
706
|
-
|
|
707
|
-
if (result[updated]?.min === (null || undefined) || result[updated]?.max === (null || undefined)) return
|
|
708
|
-
|
|
709
|
-
if (number >= result[updated].min && number <= result[updated].max) {
|
|
710
|
-
newLegendMemo.set(hashObj(row), updated)
|
|
711
|
-
}
|
|
712
|
-
})
|
|
713
|
-
})
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Equal Interval
|
|
718
|
-
if (type === 'equalinterval' && dataSet?.length !== 0) {
|
|
719
|
-
if (!dataSet || dataSet.length === 0) {
|
|
720
|
-
setState({
|
|
721
|
-
...state,
|
|
722
|
-
runtime: {
|
|
723
|
-
...state.runtime,
|
|
724
|
-
editorErrorMessage: 'Error setting equal interval legend type'
|
|
725
|
-
}
|
|
726
|
-
})
|
|
727
|
-
return
|
|
728
|
-
}
|
|
729
|
-
dataSet = dataSet.filter(row => row[primaryCol] !== undefined)
|
|
730
|
-
let dataMin = dataSet[0][primaryCol]
|
|
731
|
-
let dataMax = dataSet[dataSet.length - 1][primaryCol]
|
|
732
|
-
|
|
733
|
-
let pointer = 0 // Start at beginning of dataSet
|
|
734
|
-
|
|
735
|
-
for (let i = 0; i < legendNumber; i++) {
|
|
736
|
-
let interval = Math.abs(dataMax - dataMin) / legendNumber
|
|
737
|
-
|
|
738
|
-
let min = dataMin + interval * i
|
|
739
|
-
let max = min + interval
|
|
740
|
-
|
|
741
|
-
// If this is the last loop, assign actual max of data as the end point
|
|
742
|
-
if (i === legendNumber - 1) max = dataMax
|
|
743
|
-
|
|
744
|
-
// Add rows in dataSet that belong to this new legend item since we've got the data sorted
|
|
745
|
-
while (pointer < dataSet.length && dataSet[pointer][primaryCol] <= max) {
|
|
746
|
-
newLegendMemo.set(hashObj(dataSet[pointer]), result.length)
|
|
747
|
-
pointer += 1
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
let range = {
|
|
751
|
-
min: Math.round(min * 100) / 100,
|
|
752
|
-
max: Math.round(max * 100) / 100
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
result.push(range)
|
|
756
|
-
|
|
757
|
-
result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
result.forEach((legendItem, idx) => {
|
|
762
|
-
legendItem.color = applyColorToLegend(idx, state, result)
|
|
763
|
-
})
|
|
764
|
-
|
|
765
|
-
legendMemo.current = newLegendMemo
|
|
766
|
-
|
|
767
|
-
if (state.general.geoType === 'world') {
|
|
768
|
-
const runtimeDataKeys = Object.keys(runtimeFilters)
|
|
769
|
-
const isCountriesWithNoDataState =
|
|
770
|
-
configObj.data === undefined ? false : !countryKeys.every(countryKey => runtimeDataKeys.includes(countryKey))
|
|
771
|
-
|
|
772
|
-
if (result.length > 0 && isCountriesWithNoDataState) {
|
|
773
|
-
result.push({
|
|
774
|
-
min: null,
|
|
775
|
-
max: null,
|
|
776
|
-
color: getGeoFillColor(state)
|
|
777
|
-
})
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
//----------
|
|
782
|
-
// DEV-784
|
|
783
|
-
// before returning the legend result
|
|
784
|
-
// add property for bin number and set to index location
|
|
785
|
-
result.forEach((row, i) => {
|
|
786
|
-
row.bin = i // set bin number to index
|
|
787
|
-
})
|
|
788
|
-
|
|
789
|
-
// Move all special legend items from "Special Classes" to the end of the legend
|
|
790
|
-
if (state.legend.showSpecialClassesLast) {
|
|
791
|
-
let specialRows = result.filter(d => d.special === true)
|
|
792
|
-
let otherRows = result.filter(d => !d.special)
|
|
793
|
-
result = [...otherRows, ...specialRows]
|
|
794
|
-
}
|
|
795
|
-
//-----------
|
|
796
|
-
|
|
797
|
-
const assignSpecialClassLastIndex = (value, key) => {
|
|
798
|
-
const newIndex = result.findIndex(d => d.bin === value)
|
|
799
|
-
newLegendSpecialClassLastMemo.set(key, newIndex)
|
|
800
|
-
}
|
|
801
|
-
newLegendMemo.forEach(assignSpecialClassLastIndex)
|
|
802
|
-
legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
|
|
803
|
-
|
|
804
|
-
return result
|
|
805
|
-
})
|
|
806
|
-
|
|
807
|
-
// eslint-disable-next-line
|
|
808
|
-
const generateRuntimeFilters = useCallback((configObj, hash, runtimeFilters) => {
|
|
809
|
-
if (typeof configObj === 'undefined' || undefined === configObj.filters || configObj.filters.length === 0) return []
|
|
810
|
-
|
|
811
|
-
let filters = []
|
|
812
|
-
|
|
813
|
-
if (hash) filters.fromHash = hash
|
|
814
|
-
|
|
815
|
-
configObj?.filters.forEach(
|
|
816
|
-
(
|
|
817
|
-
{
|
|
818
|
-
columnName,
|
|
819
|
-
label,
|
|
820
|
-
labels,
|
|
821
|
-
queryParameter,
|
|
822
|
-
orderedValues,
|
|
823
|
-
active,
|
|
824
|
-
values,
|
|
825
|
-
type,
|
|
826
|
-
showDropdown,
|
|
827
|
-
setByQueryParameter
|
|
828
|
-
},
|
|
829
|
-
idx
|
|
830
|
-
) => {
|
|
831
|
-
let newFilter = runtimeFilters[idx]
|
|
832
|
-
|
|
833
|
-
const sort = (a, b) => {
|
|
834
|
-
const asc = configObj.filters[idx].order !== 'desc'
|
|
835
|
-
return String(asc ? a : b).localeCompare(String(asc ? b : a), 'en', { numeric: true })
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (type !== 'url') {
|
|
839
|
-
values = getUniqueValues(state.data, columnName)
|
|
840
|
-
|
|
841
|
-
if (configObj.filters[idx].order === 'cust') {
|
|
842
|
-
if (configObj.filters[idx]?.values.length > 0) {
|
|
843
|
-
values = configObj.filters[idx].values
|
|
844
|
-
}
|
|
845
|
-
} else {
|
|
846
|
-
values = values.sort(sort)
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if (undefined === newFilter) {
|
|
851
|
-
newFilter = {}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
newFilter.order = configObj.filters[idx].order ? configObj.filters[idx].order : 'asc'
|
|
855
|
-
newFilter.type = type
|
|
856
|
-
newFilter.label = label ?? ''
|
|
857
|
-
newFilter.columnName = columnName
|
|
858
|
-
newFilter.orderedValues = orderedValues
|
|
859
|
-
newFilter.queryParameter = queryParameter
|
|
860
|
-
newFilter.labels = labels
|
|
861
|
-
newFilter.values = values
|
|
862
|
-
newFilter.setByQueryParameter = setByQueryParameter
|
|
863
|
-
handleSorting(newFilter)
|
|
864
|
-
newFilter.active = active || configObj.filters[idx].defaultValue || values[0] // Default to first found value
|
|
865
|
-
newFilter.defaultValue = configObj.filters[idx].defaultValue || ''
|
|
866
|
-
newFilter.filterStyle = configObj.filters[idx].filterStyle ? configObj.filters[idx].filterStyle : 'dropdown'
|
|
867
|
-
newFilter.showDropdown = showDropdown
|
|
868
|
-
newFilter.subGrouping = configObj.filters[idx].subGrouping
|
|
869
|
-
|
|
870
|
-
filters.push(newFilter)
|
|
871
|
-
}
|
|
872
|
-
)
|
|
873
|
-
|
|
874
|
-
return filters
|
|
875
|
-
})
|
|
876
|
-
|
|
877
|
-
// Calculates what's going to be displayed on the map and data table at render.
|
|
878
|
-
// eslint-disable-next-line
|
|
879
|
-
const generateRuntimeData = useCallback((configObj, filters, hash, test) => {
|
|
880
|
-
try {
|
|
881
|
-
const result = {}
|
|
882
|
-
|
|
883
|
-
// Adding property this way prevents it from being enumerated
|
|
884
|
-
Object.defineProperty(result, 'fromHash', {
|
|
885
|
-
value: hash
|
|
886
|
-
})
|
|
887
|
-
|
|
888
|
-
addUIDs(configObj, configObj.columns.geo.name)
|
|
889
|
-
configObj.data.forEach(row => {
|
|
890
|
-
if (test) {
|
|
891
|
-
console.log('object', configObj) // eslint-disable-line
|
|
892
|
-
console.log('row', row) // eslint-disable-line
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if (undefined === row.uid) return false // No UID for this row, we can't use for mapping
|
|
896
|
-
const configPrimaryName = configObj.columns.primary.name
|
|
897
|
-
if (row[configPrimaryName]) {
|
|
898
|
-
row[configPrimaryName] = numberFromString(row[configPrimaryName], state)
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// If this is a navigation only map, skip if it doesn't have a URL
|
|
902
|
-
|
|
903
|
-
if ('navigation' === configObj.general.type) {
|
|
904
|
-
let navigateUrl = row[configObj.columns.navigate.name] || ''
|
|
905
|
-
|
|
906
|
-
if (undefined !== navigateUrl && typeof navigateUrl === 'string') {
|
|
907
|
-
// Strip hidden characters before we check length
|
|
908
|
-
navigateUrl = navigateUrl.replace(/(\r\n|\n|\r)/gm, '')
|
|
909
|
-
}
|
|
910
|
-
if (0 === navigateUrl?.length) {
|
|
911
|
-
return false
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Filters
|
|
916
|
-
if (filters?.length) {
|
|
917
|
-
for (let i = 0; i < filters.length; i++) {
|
|
918
|
-
const { columnName, active, type, filterStyle, subGrouping } = filters[i]
|
|
919
|
-
const isDataFilter = type !== 'url'
|
|
920
|
-
const matchingValue = String(active) === String(row[columnName]) // Group
|
|
921
|
-
if (isDataFilter && !matchingValue) return false // Bail out, data doesn't match the filter selection
|
|
922
|
-
if (filterStyle == 'nested-dropdown') {
|
|
923
|
-
const matchingSubValue = String(row[subGrouping?.columnName]) === String(subGrouping?.active)
|
|
924
|
-
if (subGrouping?.active && !matchingSubValue) {
|
|
925
|
-
return false // Bail out, data doesn't match the subgroup selection
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
// Don't add additional rows with same UID
|
|
931
|
-
if (result[row.uid] === undefined) {
|
|
932
|
-
result[row.uid] = row
|
|
933
|
-
}
|
|
934
|
-
})
|
|
935
|
-
return result
|
|
936
|
-
} catch (e) {
|
|
937
|
-
console.error('COVE: ', e) // eslint-disable-line
|
|
938
|
-
}
|
|
939
|
-
})
|
|
940
|
-
|
|
941
|
-
const mapSvg = useRef(null)
|
|
942
|
-
|
|
943
|
-
// this is passed DOWN into the various components
|
|
944
|
-
// then they do a lookup based on the bin number as index into here
|
|
945
|
-
const applyLegendToRow = rowObj => {
|
|
946
|
-
try {
|
|
947
|
-
if (!rowObj) throw new Error('COVE: No rowObj in applyLegendToRow')
|
|
948
|
-
// Navigation mapchanged
|
|
949
|
-
if ('navigation' === state.general.type) {
|
|
950
|
-
let mapColorPalette = colorPalettes[state.color] || colorPalettes['bluegreenreverse']
|
|
951
|
-
return generateColorsArray(mapColorPalette[3])
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
let hash = hashObj(rowObj)
|
|
955
|
-
|
|
956
|
-
if (legendMemo.current.has(hash)) {
|
|
957
|
-
let idx = legendMemo.current.get(hash)
|
|
958
|
-
let disabledIdx = idx
|
|
959
|
-
|
|
960
|
-
if (state.legend.showSpecialClassesLast) {
|
|
961
|
-
disabledIdx = legendSpecialClassLastMemo.current.get(hash)
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
if (runtimeLegend[disabledIdx]?.disabled) return false
|
|
965
|
-
|
|
966
|
-
// changed to use bin prop to get color instead of idx
|
|
967
|
-
// bc we re-order legend when showSpecialClassesLast is checked
|
|
968
|
-
let legendBinColor = runtimeLegend.find(o => o.bin === idx)?.color
|
|
969
|
-
return generateColorsArray(legendBinColor, runtimeLegend[idx]?.special)
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Fail state
|
|
973
|
-
return generateColorsArray()
|
|
974
|
-
} catch (e) {
|
|
975
|
-
console.error('COVE: ', e) // eslint-disable-line
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// This resets all active legend toggles.
|
|
980
|
-
const resetLegendToggles = async () => {
|
|
981
|
-
let newLegend = [...runtimeLegend]
|
|
982
|
-
|
|
983
|
-
newLegend.forEach(legendItem => {
|
|
984
|
-
delete legendItem.disabled
|
|
985
|
-
})
|
|
986
|
-
|
|
987
|
-
newLegend.runtimeDataHash = runtimeLegend.runtimeDataHash
|
|
988
|
-
|
|
989
|
-
setRuntimeLegend(newLegend)
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// todo: convert to store or context eventually.
|
|
993
|
-
const { buildTooltip } = useTooltip({ state, displayGeoName, supportedStatesFipsCodes })
|
|
994
|
-
|
|
995
|
-
const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
|
|
996
|
-
let toolTipText = buildTooltip(row, geoName, '')
|
|
997
|
-
|
|
998
|
-
// We convert the markup into JSX and add a navigation link if it's going into a modal.
|
|
999
|
-
if ('jsx' === returnType) {
|
|
1000
|
-
toolTipText = [<div key='modal-content'>{parse(toolTipText)}</div>]
|
|
1001
|
-
|
|
1002
|
-
if (state.columns.hasOwnProperty('navigate') && row[state.columns.navigate.name]) {
|
|
1003
|
-
toolTipText.push(
|
|
1004
|
-
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions,jsx-a11y/anchor-is-valid
|
|
1005
|
-
<a
|
|
1006
|
-
href='#'
|
|
1007
|
-
className='navigation-link'
|
|
1008
|
-
key='modal-navigation-link'
|
|
1009
|
-
onClick={e => {
|
|
1010
|
-
e.preventDefault()
|
|
1011
|
-
navigationHandler(
|
|
1012
|
-
state.general.navigationTarget,
|
|
1013
|
-
row[state.columns.navigate.name],
|
|
1014
|
-
customNavigationHandler
|
|
1015
|
-
)
|
|
1016
|
-
}}
|
|
1017
|
-
>
|
|
1018
|
-
{state.tooltips.linkLabel}
|
|
1019
|
-
{isDomainExternal(row[state.columns.navigate.name]) && <ExternalIcon className='inline-icon ms-1' />}
|
|
1020
|
-
</a>
|
|
1021
|
-
)
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
return toolTipText
|
|
1026
40
|
}
|
|
1027
41
|
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
setSharedFilter(state.uid, value)
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// If world-geocode map zoom to geo point
|
|
1034
|
-
if (['world-geocode'].includes(state.general.type)) {
|
|
1035
|
-
const lat = value[state.columns.latitude.name]
|
|
1036
|
-
const long = value[state.columns.longitude.name]
|
|
1037
|
-
|
|
1038
|
-
setState({
|
|
1039
|
-
...state,
|
|
1040
|
-
mapPosition: { coordinates: [long, lat], zoom: 3 }
|
|
1041
|
-
})
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// If modals are set, or we are on a mobile viewport, display modal
|
|
1045
|
-
if (window.matchMedia('(any-hover: none)').matches || 'click' === state.tooltips.appearanceType) {
|
|
1046
|
-
setModal({
|
|
1047
|
-
geoName: key,
|
|
1048
|
-
keyedData: value
|
|
1049
|
-
})
|
|
1050
|
-
|
|
1051
|
-
return
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// Otherwise if this item has a link specified for it, do regular navigation.
|
|
1055
|
-
if (state.columns.navigate && value[state.columns.navigate.name]) {
|
|
1056
|
-
navigationHandler(state.general.navigationTarget, value[state.columns.navigate.name], customNavigationHandler)
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
const reloadURLData = async () => {
|
|
1061
|
-
if (state.dataUrl) {
|
|
1062
|
-
const dataUrl = new URL(state.runtimeDataUrl || state.dataUrl, window.location.origin)
|
|
1063
|
-
let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
1064
|
-
|
|
1065
|
-
let isUpdateNeeded = false
|
|
1066
|
-
state.filters.forEach(filter => {
|
|
1067
|
-
if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
|
|
1068
|
-
qsParams[filter.queryParameter] = filter.active
|
|
1069
|
-
isUpdateNeeded = true
|
|
1070
|
-
}
|
|
1071
|
-
})
|
|
1072
|
-
|
|
1073
|
-
if (!isUpdateNeeded) return
|
|
1074
|
-
|
|
1075
|
-
let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${Object.keys(qsParams)
|
|
1076
|
-
.map((param, i) => {
|
|
1077
|
-
let qs = i === 0 ? '?' : '&'
|
|
1078
|
-
qs += param + '='
|
|
1079
|
-
qs += qsParams[param]
|
|
1080
|
-
return qs
|
|
1081
|
-
})
|
|
1082
|
-
.join('')}`
|
|
1083
|
-
|
|
1084
|
-
let data
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
const regex = /(?:\.([^.]+))?$/
|
|
1088
|
-
|
|
1089
|
-
const ext = regex.exec(dataUrl.pathname)[1]
|
|
1090
|
-
if ('csv' === ext || isSolrCsv(dataUrlFinal)) {
|
|
1091
|
-
data = await fetch(dataUrlFinal)
|
|
1092
|
-
.then(response => response.text())
|
|
1093
|
-
.then(responseText => {
|
|
1094
|
-
const parsedCsv = Papa.parse(responseText, {
|
|
1095
|
-
header: true,
|
|
1096
|
-
dynamicTyping: true,
|
|
1097
|
-
skipEmptyLines: true,
|
|
1098
|
-
encoding: 'utf-8'
|
|
1099
|
-
})
|
|
1100
|
-
return parsedCsv.data
|
|
1101
|
-
})
|
|
1102
|
-
} else if ('json' === ext || isSolrJson(dataUrlFinal)) {
|
|
1103
|
-
data = await fetch(dataUrlFinal).then(response => response.json())
|
|
1104
|
-
} else {
|
|
1105
|
-
data = []
|
|
1106
|
-
}
|
|
1107
|
-
} catch (e) {
|
|
1108
|
-
console.error(`Cannot parse URL: ${dataUrlFinal}`) // eslint-disable-line
|
|
1109
|
-
console.log(e) // eslint-disable-line
|
|
1110
|
-
data = []
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (state.dataDescription) {
|
|
1114
|
-
data = transform.autoStandardize(data)
|
|
1115
|
-
data = transform.developerStandardize(data, state.dataDescription)
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
setState({ ...state, runtimeDataUrl: dataUrlFinal, data })
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
42
|
+
const [loading, setLoading] = useState(true)
|
|
43
|
+
const transform = new DataTransform()
|
|
1121
44
|
|
|
1122
45
|
const loadConfig = async configObj => {
|
|
1123
|
-
// Set loading flag
|
|
1124
46
|
if (!loading) setLoading(true)
|
|
47
|
+
const configToLoad = editorsConfig ?? configObj
|
|
1125
48
|
|
|
1126
|
-
// Create new config object the same way each time no matter when this method is called.
|
|
1127
49
|
let newState = {
|
|
1128
50
|
...initialState,
|
|
1129
|
-
...
|
|
51
|
+
...configToLoad
|
|
1130
52
|
}
|
|
1131
|
-
|
|
1132
|
-
const urlFilters = newState.filters
|
|
1133
|
-
? newState.filters.filter(filter => filter.type === 'url').length > 0
|
|
1134
|
-
? true
|
|
1135
|
-
: false
|
|
1136
|
-
: false
|
|
1137
|
-
|
|
1138
|
-
if (newState.dataUrl && !urlFilters) {
|
|
1139
|
-
// handle urls with spaces in the name.
|
|
1140
|
-
if (newState.dataUrl) newState.dataUrl = `${newState.dataUrl}`
|
|
53
|
+
if (newState.dataUrl) {
|
|
1141
54
|
let newData = await fetchRemoteData(newState.dataUrl, 'map')
|
|
1142
55
|
|
|
1143
56
|
if (newData && newState.dataDescription) {
|
|
@@ -1155,9 +68,6 @@ const CdcMap = ({
|
|
|
1155
68
|
newState.data = transform.developerStandardize(newState.data, newState.dataDescription)
|
|
1156
69
|
}
|
|
1157
70
|
|
|
1158
|
-
// This code goes through and adds the defaults for every property declaring in the initial state at the top.
|
|
1159
|
-
// This allows you to easily add new properties to the config without having to worry about accounting for backwards compatibility.
|
|
1160
|
-
// Right now this does not work recursively -- only on first and second level properties. So state -> prop1 -> childPropOne
|
|
1161
71
|
Object.keys(newState).forEach(key => {
|
|
1162
72
|
if ('object' === typeof newState[key] && false === Array.isArray(newState[key])) {
|
|
1163
73
|
if (initialState[key]) {
|
|
@@ -1170,465 +80,59 @@ const CdcMap = ({
|
|
|
1170
80
|
}
|
|
1171
81
|
})
|
|
1172
82
|
|
|
1173
|
-
// If there's a name for the geo, add UIDs
|
|
1174
83
|
if (newState.columns.geo.name || newState.columns.geo.fips) {
|
|
1175
84
|
addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
|
|
1176
85
|
}
|
|
1177
86
|
|
|
1178
87
|
if (newState.table.forceDisplay === undefined) {
|
|
1179
|
-
newState.table.forceDisplay =
|
|
88
|
+
newState.table.forceDisplay = true
|
|
1180
89
|
}
|
|
1181
90
|
|
|
1182
91
|
validateFipsCodeLength(newState)
|
|
1183
92
|
|
|
1184
|
-
// add ability to rename state properties over time.
|
|
1185
93
|
const processedConfig = { ...coveUpdateWorker(newState) }
|
|
1186
94
|
|
|
1187
95
|
setTimeout(() => {
|
|
1188
|
-
|
|
96
|
+
setConfig(processedConfig)
|
|
1189
97
|
setLoading(false)
|
|
1190
98
|
}, 10)
|
|
1191
99
|
}
|
|
1192
100
|
|
|
1193
101
|
const init = async () => {
|
|
1194
|
-
let
|
|
1195
|
-
|
|
1196
|
-
// Load the configuration data passed to this component if it exists
|
|
1197
|
-
if (config) {
|
|
1198
|
-
configData = config
|
|
1199
|
-
}
|
|
102
|
+
let _newConfig = _.cloneDeep(config ?? initialState)
|
|
1200
103
|
|
|
1201
|
-
// If the config passed is a string, try to load it as an ajax
|
|
1202
104
|
if (configUrl) {
|
|
1203
|
-
|
|
105
|
+
_newConfig = await fetchRemoteData(configUrl)
|
|
1204
106
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
if ('object' === typeof configData) {
|
|
1208
|
-
loadConfig(configData)
|
|
107
|
+
if ('object' === typeof _newConfig) {
|
|
108
|
+
loadConfig(_newConfig)
|
|
1209
109
|
}
|
|
1210
110
|
}
|
|
1211
111
|
|
|
1212
|
-
// Initial load
|
|
1213
112
|
useEffect(() => {
|
|
1214
113
|
init()
|
|
1215
|
-
}, [])
|
|
1216
|
-
|
|
1217
|
-
useEffect(() => {
|
|
1218
|
-
if (state && !runtimeData.init && !coveLoadedHasRan && container) {
|
|
1219
|
-
publish('cove_loaded', { config: state })
|
|
1220
|
-
setCoveLoadedHasRan(true)
|
|
1221
|
-
}
|
|
1222
|
-
}, [state, container, runtimeData.init]) // eslint-disable-line
|
|
1223
|
-
|
|
1224
|
-
useEffect(() => {
|
|
1225
|
-
// When geotype changes - add UID
|
|
1226
|
-
if (state.data && state.columns.geo.name) {
|
|
1227
|
-
addUIDs(state, state.columns.geo.name)
|
|
1228
|
-
}
|
|
1229
|
-
}, [state]) // eslint-disable-line
|
|
114
|
+
}, [])
|
|
1230
115
|
|
|
1231
116
|
useEffect(() => {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
addUIDs(state, state.columns.geo.name)
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// Filters
|
|
1238
|
-
const hashFilters = hashObj(state.filters)
|
|
1239
|
-
let filters
|
|
1240
|
-
|
|
1241
|
-
if (state.filters && (config || hashFilters !== runtimeFilters.fromHash)) {
|
|
1242
|
-
filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
|
|
1243
|
-
|
|
1244
|
-
if (filters) {
|
|
1245
|
-
filters.forEach((filter, index) => {
|
|
1246
|
-
const queryStringFilterValue = getQueryStringFilterValue(filter)
|
|
1247
|
-
if (queryStringFilterValue) {
|
|
1248
|
-
filters[index].active = queryStringFilterValue
|
|
1249
|
-
}
|
|
1250
|
-
})
|
|
1251
|
-
setRuntimeFilters(filters)
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
|
|
1256
|
-
|
|
1257
|
-
const hashData = hashObj({
|
|
1258
|
-
data: state.data,
|
|
1259
|
-
columns: state.columns,
|
|
1260
|
-
geoType: state.general.geoType,
|
|
1261
|
-
type: state.general.type,
|
|
1262
|
-
geo: state.columns.geo.name,
|
|
1263
|
-
primary: state.columns.primary.name,
|
|
1264
|
-
mapPosition: state.mapPosition,
|
|
1265
|
-
map: state.map,
|
|
1266
|
-
...runtimeFilters
|
|
1267
|
-
})
|
|
1268
|
-
|
|
1269
|
-
// Data
|
|
1270
|
-
if (hashData !== runtimeData?.fromHash && state.data?.fromColumn) {
|
|
1271
|
-
const newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
|
|
1272
|
-
|
|
1273
|
-
setRuntimeData(newRuntimeData)
|
|
1274
|
-
} else {
|
|
1275
|
-
if (hashLegend !== runtimeLegend?.fromHash && undefined === runtimeData.init) {
|
|
1276
|
-
const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
|
|
1277
|
-
setRuntimeLegend(legend)
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}, [state]) // eslint-disable-line
|
|
1281
|
-
|
|
1282
|
-
useEffect(() => {
|
|
1283
|
-
const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
|
|
1284
|
-
|
|
1285
|
-
// Legend - Update when runtimeData does
|
|
1286
|
-
const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
|
|
1287
|
-
setRuntimeLegend(legend)
|
|
1288
|
-
}, [
|
|
1289
|
-
runtimeData,
|
|
1290
|
-
state.legend.unified,
|
|
1291
|
-
state.legend.showSpecialClassesLast,
|
|
1292
|
-
state.legend.separateZero,
|
|
1293
|
-
state.general.equalNumberOptIn,
|
|
1294
|
-
state.legend.numberOfItems,
|
|
1295
|
-
state.legend.specialClasses,
|
|
1296
|
-
state.legend.additionalCategories
|
|
1297
|
-
]) // eslint-disable-line
|
|
117
|
+
init()
|
|
118
|
+
}, [configUrl])
|
|
1298
119
|
|
|
1299
120
|
useEffect(() => {
|
|
1300
|
-
|
|
1301
|
-
}, [
|
|
1302
|
-
|
|
1303
|
-
if (config) {
|
|
1304
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
1305
|
-
useEffect(() => {
|
|
1306
|
-
loadConfig(config)
|
|
1307
|
-
}, [config.data]) // eslint-disable-line
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// Destructuring for more readable JSX
|
|
1311
|
-
const { general, tooltips, table } = state
|
|
1312
|
-
let { title, subtext = '', geoType } = general
|
|
1313
|
-
|
|
1314
|
-
// if no title AND in editor then set a default
|
|
1315
|
-
if (isEditor) {
|
|
1316
|
-
if (!title || title === '') title = 'Map Title'
|
|
1317
|
-
}
|
|
1318
|
-
if (!table.label || table.label === '') table.label = 'Data Table'
|
|
121
|
+
setConfig(editorsConfig)
|
|
122
|
+
}, [editorsConfig])
|
|
1319
123
|
|
|
1320
|
-
|
|
1321
|
-
let mapContainerClasses = [
|
|
1322
|
-
'map-container',
|
|
1323
|
-
state.legend?.position,
|
|
1324
|
-
state.general.type,
|
|
1325
|
-
state.general.geoType,
|
|
1326
|
-
'outline-none'
|
|
1327
|
-
]
|
|
1328
|
-
|
|
1329
|
-
if (modal) {
|
|
1330
|
-
mapContainerClasses.push('modal-background')
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
if (general.type === 'navigation' && true === general.fullBorder) {
|
|
1334
|
-
mapContainerClasses.push('full-border')
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// Props passed to all map types
|
|
1338
|
-
const mapProps = {
|
|
1339
|
-
projection,
|
|
1340
|
-
setProjection,
|
|
1341
|
-
stateToShow,
|
|
1342
|
-
setStateToShow,
|
|
1343
|
-
setScale,
|
|
1344
|
-
setTranslate,
|
|
1345
|
-
scale,
|
|
1346
|
-
translate,
|
|
1347
|
-
isDraggingAnnotation,
|
|
1348
|
-
handleDragStateChange,
|
|
1349
|
-
applyLegendToRow,
|
|
1350
|
-
applyTooltipsToGeo,
|
|
1351
|
-
container,
|
|
1352
|
-
content: modal,
|
|
1353
|
-
data: runtimeData,
|
|
1354
|
-
displayGeoName,
|
|
1355
|
-
filteredCountryCode,
|
|
1356
|
-
generateColorsArray,
|
|
1357
|
-
generateRuntimeData,
|
|
1358
|
-
geoClickHandler,
|
|
1359
|
-
hasZoom: state.general.allowMapZoom,
|
|
1360
|
-
innerContainerRef,
|
|
1361
|
-
isDashboard,
|
|
1362
|
-
isDebug,
|
|
1363
|
-
isEditor,
|
|
1364
|
-
loadConfig,
|
|
1365
|
-
logo,
|
|
1366
|
-
position,
|
|
1367
|
-
resetLegendToggles,
|
|
1368
|
-
runtimeFilters,
|
|
1369
|
-
runtimeLegend,
|
|
1370
|
-
runtimeData,
|
|
1371
|
-
setAccessibleStatus,
|
|
1372
|
-
setFilteredCountryCode,
|
|
1373
|
-
setParentConfig: setConfig,
|
|
1374
|
-
setPosition,
|
|
1375
|
-
setRuntimeData,
|
|
1376
|
-
setRuntimeFilters,
|
|
1377
|
-
setRuntimeLegend,
|
|
1378
|
-
setSharedFilterValue,
|
|
1379
|
-
setState,
|
|
1380
|
-
state,
|
|
1381
|
-
tooltipId,
|
|
1382
|
-
tooltipRef,
|
|
1383
|
-
topoData,
|
|
1384
|
-
setTopoData,
|
|
1385
|
-
mapId,
|
|
1386
|
-
outerContainerRef,
|
|
1387
|
-
dimensions,
|
|
1388
|
-
currentViewport
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
if (!mapProps.data || !state.data) return <></>
|
|
1392
|
-
|
|
1393
|
-
const hasDataTable =
|
|
1394
|
-
state.runtime.editorErrorMessage.length === 0 &&
|
|
1395
|
-
true === table.forceDisplay &&
|
|
1396
|
-
general.type !== 'navigation' &&
|
|
1397
|
-
false === loading
|
|
1398
|
-
|
|
1399
|
-
const handleMapTabbing = () => {
|
|
1400
|
-
let tabbingID
|
|
1401
|
-
|
|
1402
|
-
// 1) skip to legend
|
|
1403
|
-
if (general.showSidebar) {
|
|
1404
|
-
tabbingID = legendId
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// 2) skip to data table if it exists and not a navigation map
|
|
1408
|
-
if (hasDataTable && !general.showSidebar) {
|
|
1409
|
-
tabbingID = `dataTableSection__${Date.now()}`
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// 3) if it's a navigation map skip to the dropdown.
|
|
1413
|
-
if (state.general.type === 'navigation') {
|
|
1414
|
-
tabbingID = `dropdown-${Date.now()}`
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// 4) handle other options
|
|
1418
|
-
return tabbingID || '!'
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const tabId = handleMapTabbing()
|
|
1422
|
-
|
|
1423
|
-
// this only shows in Dashboard config mode and only if Show Table is also set
|
|
1424
|
-
const tableLink = (
|
|
1425
|
-
<a href={`#data-table-${state.dataKey}`} className='margin-left-href'>
|
|
1426
|
-
{state.dataKey} (Go to Table)
|
|
1427
|
-
</a>
|
|
1428
|
-
)
|
|
1429
|
-
|
|
1430
|
-
const sectionClassNames = () => {
|
|
1431
|
-
const classes = [
|
|
1432
|
-
'cove-component__content',
|
|
1433
|
-
'cdc-map-inner-container',
|
|
1434
|
-
`${currentViewport}`,
|
|
1435
|
-
`${state?.general?.headerColor}`
|
|
1436
|
-
]
|
|
1437
|
-
if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
|
|
1438
|
-
return classes.join(' ')
|
|
1439
|
-
}
|
|
124
|
+
if (loading) return null
|
|
1440
125
|
|
|
1441
126
|
return (
|
|
1442
|
-
<
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
{isEditor && <EditorPanel columnsRequiredChecker={columnsRequiredChecker} />}
|
|
1452
|
-
<Layout.Responsive isEditor={isEditor}>
|
|
1453
|
-
{requiredColumns && (
|
|
1454
|
-
<Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
|
|
1455
|
-
)}
|
|
1456
|
-
{!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
|
|
1457
|
-
<section className={sectionClassNames()} aria-label={'Map: ' + title} ref={innerContainerRef}>
|
|
1458
|
-
{state?.runtime?.editorErrorMessage.length > 0 && <Error state={state} />}
|
|
1459
|
-
{/* prettier-ignore */}
|
|
1460
|
-
<Title
|
|
1461
|
-
title={title}
|
|
1462
|
-
superTitle={general.superTitle}
|
|
1463
|
-
config={config}
|
|
1464
|
-
classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${general.headerColor}`]}
|
|
1465
|
-
/>
|
|
1466
|
-
<SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
|
|
1467
|
-
{state?.annotations?.length > 0 && (
|
|
1468
|
-
<SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
|
|
1469
|
-
)}
|
|
1470
|
-
|
|
1471
|
-
{general.introText && <section className='introText mb-4'>{parse(general.introText)}</section>}
|
|
1472
|
-
|
|
1473
|
-
{state?.filters?.length > 0 && (
|
|
1474
|
-
<Filters
|
|
1475
|
-
config={state}
|
|
1476
|
-
setConfig={setState}
|
|
1477
|
-
filteredData={runtimeFilters}
|
|
1478
|
-
setFilteredData={_setRuntimeData}
|
|
1479
|
-
dimensions={dimensions}
|
|
1480
|
-
standaloneMap={!config}
|
|
1481
|
-
/>
|
|
1482
|
-
)}
|
|
1483
|
-
|
|
1484
|
-
<div
|
|
1485
|
-
role='region'
|
|
1486
|
-
tabIndex='0'
|
|
1487
|
-
className={mapContainerClasses.join(' ')}
|
|
1488
|
-
onClick={e => closeModal(e, modal, setModal)}
|
|
1489
|
-
onKeyDown={e => {
|
|
1490
|
-
if (e.key === 'Enter') {
|
|
1491
|
-
closeModal(e, modal, setModal)
|
|
1492
|
-
}
|
|
1493
|
-
}}
|
|
1494
|
-
style={{ padding: '15px 0px', margin: '0px' }}
|
|
1495
|
-
>
|
|
1496
|
-
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
|
1497
|
-
<section className='outline-none geography-container w-100' ref={mapSvg} tabIndex='0'>
|
|
1498
|
-
{currentViewport && (
|
|
1499
|
-
<>
|
|
1500
|
-
{modal && <Modal />}
|
|
1501
|
-
{'single-state' === geoType && <UsaMap.SingleState />}
|
|
1502
|
-
{'us' === geoType && 'us-geocode' !== state.general.type && <UsaMap.State />}
|
|
1503
|
-
{'us-region' === geoType && <UsaMap.Region />}
|
|
1504
|
-
{'us-county' === geoType && <UsaMap.County />}
|
|
1505
|
-
{'world' === geoType && <WorldMap />}
|
|
1506
|
-
{/* logo is handled in UsaMap.State when applicable */}
|
|
1507
|
-
{'google-map' === geoType && <GoogleMap />}
|
|
1508
|
-
{'data' === general.type && logo && ('us' !== geoType || 'us-geocode' === state.general.type) && (
|
|
1509
|
-
<img src={logo} alt='' className='map-logo' style={{ maxWidth: '50px' }} />
|
|
1510
|
-
)}
|
|
1511
|
-
</>
|
|
1512
|
-
)}
|
|
1513
|
-
</section>
|
|
1514
|
-
|
|
1515
|
-
{general.showSidebar && 'navigation' !== general.type && (
|
|
1516
|
-
<Legend
|
|
1517
|
-
dimensions={dimensions}
|
|
1518
|
-
ref={legendRef}
|
|
1519
|
-
skipId={tabId}
|
|
1520
|
-
containerWidthPadding={0}
|
|
1521
|
-
currentViewport={currentViewport}
|
|
1522
|
-
/>
|
|
1523
|
-
)}
|
|
1524
|
-
</div>
|
|
1525
|
-
|
|
1526
|
-
{'navigation' === general.type && (
|
|
1527
|
-
<NavigationMenu
|
|
1528
|
-
mapTabbingID={tabId}
|
|
1529
|
-
displayGeoName={displayGeoName}
|
|
1530
|
-
data={runtimeData}
|
|
1531
|
-
options={general}
|
|
1532
|
-
columns={state.columns}
|
|
1533
|
-
navigationHandler={val => navigationHandler('_blank', val, customNavigationHandler)}
|
|
1534
|
-
/>
|
|
1535
|
-
)}
|
|
1536
|
-
|
|
1537
|
-
{/* Link */}
|
|
1538
|
-
{isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
|
|
1539
|
-
|
|
1540
|
-
{subtext.length > 0 && <p className='subtext mt-4'>{parse(subtext)}</p>}
|
|
1541
|
-
|
|
1542
|
-
<MediaControls.Section classes={['download-buttons']}>
|
|
1543
|
-
{state.general.showDownloadImgButton && (
|
|
1544
|
-
<MediaControls.Button
|
|
1545
|
-
text='Download Image'
|
|
1546
|
-
title='Download Chart as Image'
|
|
1547
|
-
type='image'
|
|
1548
|
-
state={state}
|
|
1549
|
-
elementToCapture={imageId}
|
|
1550
|
-
/>
|
|
1551
|
-
)}
|
|
1552
|
-
{state.general.showDownloadPdfButton && (
|
|
1553
|
-
<MediaControls.Button
|
|
1554
|
-
text='Download PDF'
|
|
1555
|
-
title='Download Chart as PDF'
|
|
1556
|
-
type='pdf'
|
|
1557
|
-
state={state}
|
|
1558
|
-
elementToCapture={imageId}
|
|
1559
|
-
/>
|
|
1560
|
-
)}
|
|
1561
|
-
</MediaControls.Section>
|
|
1562
|
-
|
|
1563
|
-
{state.runtime.editorErrorMessage.length === 0 &&
|
|
1564
|
-
true === table.forceDisplay &&
|
|
1565
|
-
general.type !== 'navigation' &&
|
|
1566
|
-
false === loading && (
|
|
1567
|
-
<DataTable
|
|
1568
|
-
config={state}
|
|
1569
|
-
rawData={state.data}
|
|
1570
|
-
navigationHandler={navigationHandler}
|
|
1571
|
-
expandDataTable={table.expanded}
|
|
1572
|
-
headerColor={general.headerColor}
|
|
1573
|
-
columns={state.columns}
|
|
1574
|
-
showFullGeoNameInCSV={table.showFullGeoNameInCSV}
|
|
1575
|
-
runtimeLegend={runtimeLegend}
|
|
1576
|
-
runtimeData={runtimeData}
|
|
1577
|
-
displayGeoName={displayGeoName}
|
|
1578
|
-
applyLegendToRow={applyLegendToRow}
|
|
1579
|
-
tableTitle={table.label}
|
|
1580
|
-
indexTitle={table.indexLabel}
|
|
1581
|
-
vizTitle={general.title}
|
|
1582
|
-
viewport={currentViewport}
|
|
1583
|
-
formatLegendLocation={key =>
|
|
1584
|
-
formatLegendLocation(key, runtimeData?.[key]?.[state.columns.geo.name])
|
|
1585
|
-
}
|
|
1586
|
-
setFilteredCountryCode={setFilteredCountryCode}
|
|
1587
|
-
tabbingId={tabId}
|
|
1588
|
-
showDownloadImgButton={state.general.showDownloadImgButton}
|
|
1589
|
-
showDownloadPdfButton={state.general.showDownloadPdfButton}
|
|
1590
|
-
innerContainerRef={innerContainerRef}
|
|
1591
|
-
outerContainerRef={outerContainerRef}
|
|
1592
|
-
imageRef={imageId}
|
|
1593
|
-
isDebug={isDebug}
|
|
1594
|
-
wrapColumns={table.wrapColumns}
|
|
1595
|
-
/>
|
|
1596
|
-
)}
|
|
1597
|
-
|
|
1598
|
-
{state.annotations.length > 0 && <Annotation.Dropdown />}
|
|
1599
|
-
|
|
1600
|
-
{general.footnotes && <section className='footnotes pt-2 mt-4'>{parse(general.footnotes)}</section>}
|
|
1601
|
-
</section>
|
|
1602
|
-
)}
|
|
1603
|
-
|
|
1604
|
-
<div aria-live='assertive' className='cdcdataviz-sr-only'>
|
|
1605
|
-
{accessibleStatus}
|
|
1606
|
-
</div>
|
|
1607
|
-
|
|
1608
|
-
{!isDraggingAnnotation &&
|
|
1609
|
-
!window.matchMedia('(any-hover: none)').matches &&
|
|
1610
|
-
'hover' === tooltips.appearanceType && (
|
|
1611
|
-
<ReactTooltip
|
|
1612
|
-
id={`tooltip__${tooltipId}`}
|
|
1613
|
-
float={true}
|
|
1614
|
-
className={`${tooltips.capitalizeLabels ? 'capitalize tooltip tooltip-test' : 'tooltip tooltip-test'}`}
|
|
1615
|
-
style={{ background: `rgba(255,255,255, ${state.tooltips.opacity / 100})`, color: 'black' }}
|
|
1616
|
-
/>
|
|
1617
|
-
)}
|
|
1618
|
-
<div
|
|
1619
|
-
ref={tooltipRef}
|
|
1620
|
-
id={`tooltip__${tooltipId}-canvas`}
|
|
1621
|
-
className='tooltip'
|
|
1622
|
-
style={{
|
|
1623
|
-
background: `rgba(255,255,255,${state.tooltips.opacity / 100})`,
|
|
1624
|
-
position: 'absolute',
|
|
1625
|
-
whiteSpace: 'nowrap',
|
|
1626
|
-
display: 'none' // can't use d-none here
|
|
1627
|
-
}}
|
|
1628
|
-
></div>
|
|
1629
|
-
</Layout.Responsive>
|
|
1630
|
-
</Layout.VisualizationWrapper>
|
|
1631
|
-
</ConfigContext.Provider>
|
|
127
|
+
<CdcMapComponent
|
|
128
|
+
config={config}
|
|
129
|
+
navigationHandler={customNavigationHandler}
|
|
130
|
+
isEditor={isEditor}
|
|
131
|
+
isDashboard={isDashboard}
|
|
132
|
+
logo={logo}
|
|
133
|
+
link={link}
|
|
134
|
+
loadConfig={loadConfig}
|
|
135
|
+
/>
|
|
1632
136
|
)
|
|
1633
137
|
}
|
|
1634
138
|
|