@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
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
// Vendor
|
|
2
|
+
import React, { useEffect, useRef, useId, useReducer, useContext, useMemo } from 'react'
|
|
3
|
+
import 'whatwg-fetch'
|
|
4
|
+
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
5
|
+
import Papa from 'papaparse'
|
|
6
|
+
import parse from 'html-react-parser'
|
|
7
|
+
import 'react-tooltip/dist/react-tooltip.css'
|
|
8
|
+
|
|
9
|
+
// Core Components
|
|
10
|
+
import DataTable from '@cdc/core/components/DataTable'
|
|
11
|
+
import Filters from '@cdc/core/components/Filters'
|
|
12
|
+
import Layout from '@cdc/core/components/Layout'
|
|
13
|
+
import MediaControls from '@cdc/core/components/MediaControls'
|
|
14
|
+
import SkipTo from '@cdc/core/components/elements/SkipTo'
|
|
15
|
+
import Title from '@cdc/core/components/ui/Title'
|
|
16
|
+
import Waiting from '@cdc/core/components/Waiting'
|
|
17
|
+
import FootnotesStandAlone from '@cdc/core/components/Footnotes/FootnotesStandAlone'
|
|
18
|
+
|
|
19
|
+
// types
|
|
20
|
+
import { type MapConfig } from './types/MapConfig'
|
|
21
|
+
import { Datasets } from '@cdc/core/types/DataSet'
|
|
22
|
+
|
|
23
|
+
// Sass
|
|
24
|
+
import './scss/main.scss'
|
|
25
|
+
import './cdcMapComponent.styles.css'
|
|
26
|
+
|
|
27
|
+
// Core Helpers
|
|
28
|
+
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
29
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
30
|
+
import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
|
|
31
|
+
import { publish } from '@cdc/core/helpers/events'
|
|
32
|
+
import { generateRuntimeFilters } from './helpers/generateRuntimeFilters'
|
|
33
|
+
import { type MapReducerType, MapState } from './store/map.reducer'
|
|
34
|
+
import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
|
|
35
|
+
|
|
36
|
+
// Map Helpers
|
|
37
|
+
import {
|
|
38
|
+
addUIDs,
|
|
39
|
+
displayGeoName,
|
|
40
|
+
formatLegendLocation,
|
|
41
|
+
getMapContainerClasses,
|
|
42
|
+
generateRuntimeLegendHash,
|
|
43
|
+
handleMapTabbing,
|
|
44
|
+
hashObj,
|
|
45
|
+
navigationHandler
|
|
46
|
+
} from './helpers'
|
|
47
|
+
import generateRuntimeLegend from './helpers/generateRuntimeLegend'
|
|
48
|
+
import generateRuntimeData from './helpers/generateRuntimeData'
|
|
49
|
+
|
|
50
|
+
// Child Components
|
|
51
|
+
import Annotation from './components/Annotation'
|
|
52
|
+
import ConfigContext, { MapDispatchContext } from './context'
|
|
53
|
+
import EditorPanel from './components/EditorPanel'
|
|
54
|
+
import Error from './components/EditorPanel/components/Error'
|
|
55
|
+
import Legend from './components/Legend'
|
|
56
|
+
import Modal from './components/Modal'
|
|
57
|
+
import NavigationMenu from './components/NavigationMenu'
|
|
58
|
+
import UsaMap from './components/UsaMap'
|
|
59
|
+
import WorldMap from './components/WorldMap'
|
|
60
|
+
import GoogleMap from './components/GoogleMap'
|
|
61
|
+
|
|
62
|
+
// hooks
|
|
63
|
+
import useResizeObserver from './hooks/useResizeObserver'
|
|
64
|
+
import { VizFilter } from '@cdc/core/types/VizFilter'
|
|
65
|
+
import { getInitialState, mapReducer } from './store/map.reducer'
|
|
66
|
+
import { RuntimeData } from './types/RuntimeData'
|
|
67
|
+
import EditorContext from '@cdc/editor/src/ConfigContext'
|
|
68
|
+
import MapActions from './store/map.actions'
|
|
69
|
+
import _ from 'lodash'
|
|
70
|
+
import useModal from './hooks/useModal'
|
|
71
|
+
|
|
72
|
+
type CdcMapComponent = {
|
|
73
|
+
config: MapConfig
|
|
74
|
+
isEditor?: boolean
|
|
75
|
+
isDashboard?: boolean
|
|
76
|
+
link?: string
|
|
77
|
+
logo?: string
|
|
78
|
+
navigationHandler: Function
|
|
79
|
+
setSharedFilter: Function
|
|
80
|
+
setSharedFilterValue: Function
|
|
81
|
+
datasets?: Datasets
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const CdcMapComponent: React.FC<CdcMapComponent> = ({
|
|
85
|
+
config: configObj,
|
|
86
|
+
navigationHandler: customNavigationHandler,
|
|
87
|
+
isDashboard = false,
|
|
88
|
+
isEditor = false,
|
|
89
|
+
logo = '',
|
|
90
|
+
setSharedFilter,
|
|
91
|
+
setSharedFilterValue,
|
|
92
|
+
link,
|
|
93
|
+
setConfig: setParentConfig,
|
|
94
|
+
loadConfig,
|
|
95
|
+
datasets
|
|
96
|
+
}) => {
|
|
97
|
+
const initialState = getInitialState(configObj)
|
|
98
|
+
|
|
99
|
+
const [mapState, dispatch] = useReducer<MapReducerType<MapState, MapActions>>(mapReducer, initialState as MapState)
|
|
100
|
+
|
|
101
|
+
const {
|
|
102
|
+
loading,
|
|
103
|
+
displayPanel,
|
|
104
|
+
runtimeData,
|
|
105
|
+
runtimeFilters,
|
|
106
|
+
runtimeLegend,
|
|
107
|
+
config,
|
|
108
|
+
modal,
|
|
109
|
+
accessibleStatus,
|
|
110
|
+
filteredCountryCode,
|
|
111
|
+
position,
|
|
112
|
+
scale,
|
|
113
|
+
translate,
|
|
114
|
+
projection,
|
|
115
|
+
stateToShow,
|
|
116
|
+
requiredColumns,
|
|
117
|
+
topoData,
|
|
118
|
+
coveLoadedHasRan,
|
|
119
|
+
isDraggingAnnotation
|
|
120
|
+
} = mapState
|
|
121
|
+
|
|
122
|
+
const editorContext = useContext(EditorContext)
|
|
123
|
+
|
|
124
|
+
const setConfig = (newMapConfig: MapConfig): void => {
|
|
125
|
+
dispatch({ type: 'SET_CONFIG', payload: newMapConfig })
|
|
126
|
+
if (isEditor && !isDashboard) {
|
|
127
|
+
editorContext.setTempConfig(newMapConfig)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const _newConfig = getInitialState(_.cloneDeep(configObj)).config
|
|
133
|
+
if (configObj.data) {
|
|
134
|
+
_newConfig.data = configObj.data
|
|
135
|
+
}
|
|
136
|
+
setConfig(_newConfig)
|
|
137
|
+
}, [configObj.data]) // eslint-disable-line
|
|
138
|
+
|
|
139
|
+
const setRuntimeData = (data: RuntimeData) => {
|
|
140
|
+
dispatch({ type: 'SET_RUNTIME_DATA', payload: data })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const setRuntimeFilters = (filters: VizFilter[]) => {
|
|
144
|
+
dispatch({ type: 'SET_RUNTIME_FILTERS', payload: filters })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const setRuntimeLegend = legend => {
|
|
148
|
+
dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const _setRuntimeData = (data: any) => {
|
|
152
|
+
const _newFilters = addValuesToFilters(data, [])
|
|
153
|
+
setConfig({ ...config, filters: _newFilters })
|
|
154
|
+
if (config) {
|
|
155
|
+
setRuntimeData(data)
|
|
156
|
+
} else {
|
|
157
|
+
setRuntimeFilters(data)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const transform = new DataTransform()
|
|
161
|
+
|
|
162
|
+
// Refs
|
|
163
|
+
const innerContainerRef = useRef()
|
|
164
|
+
const legendMemo = useRef(new Map())
|
|
165
|
+
const legendRef = useRef(null)
|
|
166
|
+
const legendSpecialClassLastMemo = useRef(new Map())
|
|
167
|
+
const mapSvg = useRef(null)
|
|
168
|
+
const tooltipRef = useRef(null)
|
|
169
|
+
|
|
170
|
+
// IDs
|
|
171
|
+
const imageId = useMemo(() => `download-id-${Math.random().toString(36).substr(2, 9)}`, [])
|
|
172
|
+
const legendId = useId()
|
|
173
|
+
const mapId = useId()
|
|
174
|
+
const tooltipId = 'test'
|
|
175
|
+
|
|
176
|
+
// hooks
|
|
177
|
+
const { currentViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
|
|
178
|
+
|
|
179
|
+
const reloadURLData = async () => {
|
|
180
|
+
if (config.dataUrl) {
|
|
181
|
+
const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl, window.location.origin)
|
|
182
|
+
let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
|
|
183
|
+
|
|
184
|
+
let isUpdateNeeded = false
|
|
185
|
+
config.filters.forEach(filter => {
|
|
186
|
+
if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
|
|
187
|
+
qsParams[filter.queryParameter] = filter.active
|
|
188
|
+
isUpdateNeeded = true
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
if (!isUpdateNeeded) return
|
|
193
|
+
|
|
194
|
+
let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${Object.keys(qsParams)
|
|
195
|
+
.map((param, i) => {
|
|
196
|
+
let qs = i === 0 ? '?' : '&'
|
|
197
|
+
qs += param + '='
|
|
198
|
+
qs += qsParams[param]
|
|
199
|
+
return qs
|
|
200
|
+
})
|
|
201
|
+
.join('')}`
|
|
202
|
+
|
|
203
|
+
let data
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const regex = /(?:\.([^.]+))?$/
|
|
207
|
+
|
|
208
|
+
const ext = regex.exec(dataUrl.pathname)[1]
|
|
209
|
+
if ('csv' === ext || isSolrCsv(dataUrlFinal)) {
|
|
210
|
+
data = await fetch(dataUrlFinal)
|
|
211
|
+
.then(response => response.text())
|
|
212
|
+
.then(responseText => {
|
|
213
|
+
const parsedCsv = Papa.parse(responseText, {
|
|
214
|
+
header: true,
|
|
215
|
+
dynamicTyping: true,
|
|
216
|
+
skipEmptyLines: true,
|
|
217
|
+
encoding: 'utf-8'
|
|
218
|
+
})
|
|
219
|
+
return parsedCsv.data
|
|
220
|
+
})
|
|
221
|
+
} else if ('json' === ext || isSolrJson(dataUrlFinal)) {
|
|
222
|
+
data = await fetch(dataUrlFinal).then(response => response.json())
|
|
223
|
+
} else {
|
|
224
|
+
data = []
|
|
225
|
+
}
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.error(`Cannot parse URL: ${dataUrlFinal}`) // eslint-disable-line
|
|
228
|
+
console.log(e) // eslint-disable-line
|
|
229
|
+
data = []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (config.dataDescription) {
|
|
233
|
+
data = transform.autoStandardize(data)
|
|
234
|
+
data = transform.developerStandardize(data, config.dataDescription)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const newConfig = _.cloneDeep(config)
|
|
238
|
+
newConfig.data = data
|
|
239
|
+
newConfig.runtimeDataUrl = dataUrlFinal
|
|
240
|
+
|
|
241
|
+
setConfig(newConfig)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (config && !runtimeData.init && !coveLoadedHasRan && container) {
|
|
247
|
+
publish('cove_loaded', { config: config })
|
|
248
|
+
dispatch({ type: 'SET_COVE_LOADED_HAS_RAN', payload: true })
|
|
249
|
+
}
|
|
250
|
+
}, [config, container, runtimeData.init])
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
// UID
|
|
254
|
+
if (config.data && config.columns.geo.name && config.columns.geo.name !== config.data.fromColumn) {
|
|
255
|
+
addUIDs(config, config.columns.geo.name)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Filters
|
|
259
|
+
const hashFilters = hashObj(config.filters)
|
|
260
|
+
let filters: VizFilter[]
|
|
261
|
+
|
|
262
|
+
if (config.filters && (config || hashFilters !== runtimeFilters.fromHash)) {
|
|
263
|
+
filters = generateRuntimeFilters({ ...config, data: configObj.data }, hashFilters, runtimeFilters)
|
|
264
|
+
|
|
265
|
+
if (filters) {
|
|
266
|
+
filters.forEach((filter: VizFilter, index: number) => {
|
|
267
|
+
const queryStringFilterValue = getQueryStringFilterValue(filter)
|
|
268
|
+
if (queryStringFilterValue) {
|
|
269
|
+
filters[index].active = queryStringFilterValue
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
setRuntimeFilters(filters)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const hashLegend = generateRuntimeLegendHash(config, runtimeFilters)
|
|
277
|
+
|
|
278
|
+
const hashData = hashObj({
|
|
279
|
+
data: config.data,
|
|
280
|
+
columns: config.columns,
|
|
281
|
+
geoType: config.general.geoType,
|
|
282
|
+
type: config.general.type,
|
|
283
|
+
geo: config.columns.geo.name,
|
|
284
|
+
primary: config.columns.primary.name,
|
|
285
|
+
mapPosition: config.mapPosition,
|
|
286
|
+
map: config.map,
|
|
287
|
+
table: config.table,
|
|
288
|
+
...runtimeFilters
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// Data
|
|
292
|
+
if (hashData !== runtimeData?.fromHash && config.data?.fromColumn) {
|
|
293
|
+
const isCategoryLegend = config?.legend?.type === 'category'
|
|
294
|
+
const newRuntimeData = generateRuntimeData(
|
|
295
|
+
{ ...config, data: configObj.data },
|
|
296
|
+
filters || runtimeFilters,
|
|
297
|
+
hashData,
|
|
298
|
+
isCategoryLegend,
|
|
299
|
+
config.table.showNonGeoData
|
|
300
|
+
)
|
|
301
|
+
setRuntimeData(newRuntimeData)
|
|
302
|
+
} else {
|
|
303
|
+
if (hashLegend !== runtimeLegend?.fromHash && undefined === runtimeData?.init) {
|
|
304
|
+
const legend = generateRuntimeLegend(
|
|
305
|
+
config,
|
|
306
|
+
runtimeData,
|
|
307
|
+
hashLegend,
|
|
308
|
+
setConfig,
|
|
309
|
+
runtimeFilters,
|
|
310
|
+
legendMemo,
|
|
311
|
+
legendSpecialClassLastMemo
|
|
312
|
+
)
|
|
313
|
+
setRuntimeLegend(legend)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}, [config, configObj.data])
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
const hashLegend = generateRuntimeLegendHash(config, runtimeFilters)
|
|
320
|
+
const legend = generateRuntimeLegend(
|
|
321
|
+
{ ...config, data: configObj.data },
|
|
322
|
+
runtimeData,
|
|
323
|
+
hashLegend,
|
|
324
|
+
setConfig,
|
|
325
|
+
runtimeFilters,
|
|
326
|
+
legendMemo,
|
|
327
|
+
legendSpecialClassLastMemo
|
|
328
|
+
)
|
|
329
|
+
setRuntimeLegend(legend)
|
|
330
|
+
}, [runtimeData, config, runtimeFilters])
|
|
331
|
+
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (!isDashboard) {
|
|
334
|
+
reloadURLData()
|
|
335
|
+
}
|
|
336
|
+
}, [JSON.stringify(config.filters)])
|
|
337
|
+
|
|
338
|
+
const { general, tooltips, table, columns } = config
|
|
339
|
+
const { subtext = '', geoType } = general
|
|
340
|
+
const { showDownloadImgButton, showDownloadPdfButton, headerColor, introText } = general
|
|
341
|
+
const { closeModal } = useModal()
|
|
342
|
+
|
|
343
|
+
let title = config.general.title
|
|
344
|
+
|
|
345
|
+
if (isEditor) {
|
|
346
|
+
if (!title || title === '') title = 'Map Title'
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!table.label || table.label === '') table.label = 'Data Table'
|
|
350
|
+
|
|
351
|
+
const mapProps = {
|
|
352
|
+
setParentConfig,
|
|
353
|
+
container,
|
|
354
|
+
content: modal,
|
|
355
|
+
currentViewport,
|
|
356
|
+
customNavigationHandler,
|
|
357
|
+
data: runtimeData,
|
|
358
|
+
dimensions,
|
|
359
|
+
filteredCountryCode,
|
|
360
|
+
innerContainerRef,
|
|
361
|
+
isDashboard,
|
|
362
|
+
isEditor,
|
|
363
|
+
legendMemo,
|
|
364
|
+
legendSpecialClassLastMemo,
|
|
365
|
+
logo,
|
|
366
|
+
mapId,
|
|
367
|
+
outerContainerRef,
|
|
368
|
+
position,
|
|
369
|
+
projection,
|
|
370
|
+
runtimeData,
|
|
371
|
+
runtimeFilters,
|
|
372
|
+
runtimeLegend,
|
|
373
|
+
scale,
|
|
374
|
+
setConfig,
|
|
375
|
+
setRuntimeData,
|
|
376
|
+
setRuntimeFilters,
|
|
377
|
+
setRuntimeLegend,
|
|
378
|
+
setSharedFilter,
|
|
379
|
+
setSharedFilterValue,
|
|
380
|
+
config,
|
|
381
|
+
stateToShow,
|
|
382
|
+
tooltipId,
|
|
383
|
+
tooltipRef,
|
|
384
|
+
topoData,
|
|
385
|
+
translate,
|
|
386
|
+
isDraggingAnnotation,
|
|
387
|
+
loadConfig
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!config.data) return <></>
|
|
391
|
+
|
|
392
|
+
const tabId = handleMapTabbing(config, loading, legendId)
|
|
393
|
+
|
|
394
|
+
// this only shows in Dashboard config mode and only if Show Table is also set
|
|
395
|
+
const tableLink = (
|
|
396
|
+
<a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
|
|
397
|
+
{config.dataKey} (Go to Table)
|
|
398
|
+
</a>
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
const sectionClassNames = () => {
|
|
402
|
+
const classes = ['cove-component__content', 'cdc-map-inner-container', `${currentViewport}`, `${headerColor}`]
|
|
403
|
+
if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
|
|
404
|
+
return classes.join(' ')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<ConfigContext.Provider value={mapProps}>
|
|
409
|
+
<MapDispatchContext.Provider value={dispatch}>
|
|
410
|
+
<Layout.VisualizationWrapper
|
|
411
|
+
config={config}
|
|
412
|
+
isEditor={isEditor}
|
|
413
|
+
ref={outerContainerRef}
|
|
414
|
+
currentViewport={currentViewport}
|
|
415
|
+
imageId={imageId}
|
|
416
|
+
showEditorPanel={config.showEditorPanel}
|
|
417
|
+
>
|
|
418
|
+
{isEditor && <EditorPanel datasets={datasets} />}
|
|
419
|
+
<Layout.Responsive isEditor={isEditor}>
|
|
420
|
+
{requiredColumns?.length > 0 && (
|
|
421
|
+
<Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
|
|
422
|
+
)}
|
|
423
|
+
{!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
|
|
424
|
+
<section className={sectionClassNames()} aria-label={'Map: ' + title} ref={innerContainerRef}>
|
|
425
|
+
{config?.runtime?.editorErrorMessage.length > 0 && <Error />}
|
|
426
|
+
<Title
|
|
427
|
+
title={title}
|
|
428
|
+
superTitle={general.superTitle}
|
|
429
|
+
config={config}
|
|
430
|
+
classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${headerColor}`]}
|
|
431
|
+
/>
|
|
432
|
+
<SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
|
|
433
|
+
{config?.annotations?.length > 0 && (
|
|
434
|
+
<SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
{introText && <section className='introText mb-4'>{parse(introText)}</section>}
|
|
438
|
+
|
|
439
|
+
{config?.filters?.length > 0 && (
|
|
440
|
+
<Filters
|
|
441
|
+
config={config}
|
|
442
|
+
setConfig={setConfig}
|
|
443
|
+
filteredData={runtimeFilters}
|
|
444
|
+
setFilters={_setRuntimeData}
|
|
445
|
+
dimensions={dimensions}
|
|
446
|
+
standaloneMap={!config}
|
|
447
|
+
/>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
<div
|
|
451
|
+
role='region'
|
|
452
|
+
tabIndex={0}
|
|
453
|
+
className={getMapContainerClasses(config, modal).join(' ')}
|
|
454
|
+
onClick={e => closeModal(e, modal)}
|
|
455
|
+
onKeyDown={e => {
|
|
456
|
+
if (e.key === 'Enter') {
|
|
457
|
+
closeModal(e, modal)
|
|
458
|
+
}
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
<section
|
|
462
|
+
className='outline-none geography-container w-100 position-relative'
|
|
463
|
+
ref={mapSvg}
|
|
464
|
+
tabIndex='0'
|
|
465
|
+
>
|
|
466
|
+
{currentViewport && (
|
|
467
|
+
<>
|
|
468
|
+
{modal && <Modal />}
|
|
469
|
+
{'single-state' === geoType && <UsaMap.SingleState />}
|
|
470
|
+
{'us' === geoType && 'us-geocode' !== config.general.type && <UsaMap.State />}
|
|
471
|
+
{'us-region' === geoType && <UsaMap.Region />}
|
|
472
|
+
{'us-county' === geoType && <UsaMap.County />}
|
|
473
|
+
{'world' === geoType && <WorldMap />}
|
|
474
|
+
{'google-map' === geoType && <GoogleMap />}
|
|
475
|
+
{
|
|
476
|
+
/* logo is handled in UsaMap.State when applicable */
|
|
477
|
+
// prettier-ignore
|
|
478
|
+
'data' === general.type && logo && ('us' !== geoType || 'us-geocode' === general.type) && (
|
|
479
|
+
<img src={logo} alt='' className='map-logo' style={{ maxWidth: '50px' }} />
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
</>
|
|
483
|
+
)}
|
|
484
|
+
</section>
|
|
485
|
+
|
|
486
|
+
{general.showSidebar && 'navigation' !== general.type && (
|
|
487
|
+
<Legend
|
|
488
|
+
dimensions={dimensions}
|
|
489
|
+
ref={legendRef}
|
|
490
|
+
skipId={tabId}
|
|
491
|
+
containerWidthPadding={0}
|
|
492
|
+
currentViewport={currentViewport}
|
|
493
|
+
/>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
{'navigation' === general.type && (
|
|
498
|
+
<NavigationMenu
|
|
499
|
+
mapTabbingID={tabId}
|
|
500
|
+
displayGeoName={displayGeoName}
|
|
501
|
+
data={runtimeData}
|
|
502
|
+
options={general}
|
|
503
|
+
columns={config.columns}
|
|
504
|
+
navigationHandler={val => navigationHandler('_blank', val, customNavigationHandler)}
|
|
505
|
+
/>
|
|
506
|
+
)}
|
|
507
|
+
|
|
508
|
+
{/* Link (to data table?) */}
|
|
509
|
+
{isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
|
|
510
|
+
|
|
511
|
+
{subtext.length > 0 && <p className='subtext mt-4'>{parse(subtext)}</p>}
|
|
512
|
+
|
|
513
|
+
<MediaControls.Section classes={['download-buttons']}>
|
|
514
|
+
{showDownloadImgButton && (
|
|
515
|
+
<MediaControls.Button
|
|
516
|
+
text='Download Image'
|
|
517
|
+
title='Download Chart as Image'
|
|
518
|
+
type='image'
|
|
519
|
+
state={config}
|
|
520
|
+
elementToCapture={imageId}
|
|
521
|
+
/>
|
|
522
|
+
)}
|
|
523
|
+
{showDownloadPdfButton && (
|
|
524
|
+
<MediaControls.Button
|
|
525
|
+
text='Download PDF'
|
|
526
|
+
title='Download Chart as PDF'
|
|
527
|
+
type='pdf'
|
|
528
|
+
state={config}
|
|
529
|
+
elementToCapture={imageId}
|
|
530
|
+
/>
|
|
531
|
+
)}
|
|
532
|
+
</MediaControls.Section>
|
|
533
|
+
|
|
534
|
+
{config?.runtime?.editorErrorMessage.length === 0 &&
|
|
535
|
+
true === table.forceDisplay &&
|
|
536
|
+
general.type !== 'navigation' &&
|
|
537
|
+
false === loading && (
|
|
538
|
+
<DataTable
|
|
539
|
+
columns={columns}
|
|
540
|
+
config={config}
|
|
541
|
+
currentViewport={currentViewport}
|
|
542
|
+
displayGeoName={displayGeoName}
|
|
543
|
+
expandDataTable={table.expanded}
|
|
544
|
+
formatLegendLocation={key =>
|
|
545
|
+
formatLegendLocation(key, runtimeData?.[key]?.[config.columns.geo.name])
|
|
546
|
+
}
|
|
547
|
+
headerColor={general.headerColor}
|
|
548
|
+
imageRef={imageId}
|
|
549
|
+
indexTitle={table.indexLabel}
|
|
550
|
+
innerContainerRef={innerContainerRef}
|
|
551
|
+
legendMemo={legendMemo}
|
|
552
|
+
legendSpecialClassLastMemo={legendSpecialClassLastMemo}
|
|
553
|
+
navigationHandler={navigationHandler}
|
|
554
|
+
outerContainerRef={outerContainerRef}
|
|
555
|
+
rawData={config.data}
|
|
556
|
+
runtimeData={runtimeData}
|
|
557
|
+
runtimeLegend={runtimeLegend}
|
|
558
|
+
showDownloadImgButton={showDownloadImgButton}
|
|
559
|
+
showDownloadPdfButton={showDownloadPdfButton}
|
|
560
|
+
tabbingId={tabId}
|
|
561
|
+
tableTitle={table.label}
|
|
562
|
+
vizTitle={general.title}
|
|
563
|
+
wrapColumns={table.wrapColumns}
|
|
564
|
+
/>
|
|
565
|
+
)}
|
|
566
|
+
|
|
567
|
+
{config.annotations?.length > 0 && <Annotation.Dropdown />}
|
|
568
|
+
|
|
569
|
+
{general.footnotes && <section className='footnotes pt-2 mt-4'>{parse(general.footnotes)}</section>}
|
|
570
|
+
</section>
|
|
571
|
+
)}
|
|
572
|
+
|
|
573
|
+
<div aria-live='assertive' className='cdcdataviz-sr-only'>
|
|
574
|
+
{accessibleStatus}
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
{!isDraggingAnnotation &&
|
|
578
|
+
!window.matchMedia('(any-hover: none)').matches &&
|
|
579
|
+
'hover' === tooltips.appearanceType && (
|
|
580
|
+
<ReactTooltip
|
|
581
|
+
id={`tooltip__${tooltipId}`}
|
|
582
|
+
float={true}
|
|
583
|
+
className={`${
|
|
584
|
+
tooltips.capitalizeLabels ? 'capitalize tooltip tooltip-test' : 'tooltip tooltip-test'
|
|
585
|
+
}`}
|
|
586
|
+
style={{ background: `rgba(255,255,255, ${config.tooltips.opacity / 100})`, color: 'black' }}
|
|
587
|
+
/>
|
|
588
|
+
)}
|
|
589
|
+
<div
|
|
590
|
+
ref={tooltipRef}
|
|
591
|
+
id={`tooltip__${tooltipId}-canvas`}
|
|
592
|
+
className='tooltip'
|
|
593
|
+
style={{
|
|
594
|
+
background: `rgba(255,255,255,${config.tooltips.opacity / 100})`,
|
|
595
|
+
position: 'absolute',
|
|
596
|
+
whiteSpace: 'nowrap',
|
|
597
|
+
display: 'none' // can't use d-none here
|
|
598
|
+
}}
|
|
599
|
+
></div>
|
|
600
|
+
<FootnotesStandAlone config={config.footnotes} filters={config.filters?.filter(f => f.filterFootnotes)} />
|
|
601
|
+
</Layout.Responsive>
|
|
602
|
+
</Layout.VisualizationWrapper>
|
|
603
|
+
</MapDispatchContext.Provider>
|
|
604
|
+
</ConfigContext.Provider>
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export default CdcMapComponent
|
|
@@ -33,6 +33,16 @@ export const Gradient_With_Box: Story = {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export const Gradient_With_Space: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
config: editConfigKeys(UsGradient, [
|
|
39
|
+
{ path: ['legend', 'subStyle'], value: 'linear blocks' },
|
|
40
|
+
{ path: ['legend', 'spaces'], value: '2' },
|
|
41
|
+
{ path: ['legend', 'hideBorder'], value: false }
|
|
42
|
+
])
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export const Gradient_With_Text: Story = {
|
|
37
47
|
args: {
|
|
38
48
|
config: editConfigKeys(UsGradient, [
|
|
@@ -3,7 +3,10 @@ import CdcMap from '../CdcMap'
|
|
|
3
3
|
import SingleStateWithFilters from './_mock/DEV-8942.json'
|
|
4
4
|
import CustomLayerMap from './_mock/custom-layer-map.json'
|
|
5
5
|
import WastewaterMap from './_mock/wastewater-map.json'
|
|
6
|
+
import legendTests from './_mock/legends/legend-tests.json'
|
|
6
7
|
import { editConfigKeys } from '@cdc/chart/src/helpers/configHelpers'
|
|
8
|
+
import { userEvent, within } from '@storybook/testing-library'
|
|
9
|
+
import { expect } from '@storybook/jest'
|
|
7
10
|
|
|
8
11
|
const meta: Meta<typeof CdcMap> = {
|
|
9
12
|
title: 'Components/Templates/Map/Legend',
|
|
@@ -38,3 +41,67 @@ export const Legend_Bottom_Single_Row: Story = {
|
|
|
38
41
|
])
|
|
39
42
|
}
|
|
40
43
|
}
|
|
44
|
+
|
|
45
|
+
export const Legend_Tests: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
config: legendTests,
|
|
48
|
+
isEditor: true
|
|
49
|
+
},
|
|
50
|
+
play: async ({ canvasElement }) => {
|
|
51
|
+
const canvas = within(canvasElement)
|
|
52
|
+
const user = userEvent.setup()
|
|
53
|
+
const coolGray90 = '#dfe1e2'
|
|
54
|
+
const legendSelection = 'section > ul > li:nth-child(3) > span.legend-item.me-2'
|
|
55
|
+
const legendTextSelection = 'section > ul > li:nth-child(3) > span:nth-child(2)'
|
|
56
|
+
|
|
57
|
+
// Wait for the component to render fully
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
59
|
+
|
|
60
|
+
// Query the Florida element directly
|
|
61
|
+
const florida = canvasElement.querySelector('#Florida')
|
|
62
|
+
|
|
63
|
+
// Check the fill color
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
65
|
+
await expect(florida).toHaveStyle('fill: rgb(255, 237, 160)')
|
|
66
|
+
|
|
67
|
+
// Click the legend element for Florida and recheck
|
|
68
|
+
await user.click(canvasElement.querySelector(legendSelection))
|
|
69
|
+
await expect(florida).toHaveStyle(`fill: ${coolGray90}`)
|
|
70
|
+
|
|
71
|
+
// Sidebar Accordion Click
|
|
72
|
+
const filtersAccordionButton = await canvas.findByRole('button', { name: 'Filters' })
|
|
73
|
+
await user.click(filtersAccordionButton)
|
|
74
|
+
|
|
75
|
+
// Add Filter Button Click
|
|
76
|
+
const addFilterButton = await canvas.findByRole('button', { name: 'Add Filter' })
|
|
77
|
+
await user.click(addFilterButton)
|
|
78
|
+
|
|
79
|
+
// Open Filter Click
|
|
80
|
+
const caretDownButton = await canvas.getByText('New Filter').previousElementSibling
|
|
81
|
+
await user.click(caretDownButton)
|
|
82
|
+
|
|
83
|
+
// Select Filter
|
|
84
|
+
const select = await canvasElement.querySelector('select[name="columnName"]')
|
|
85
|
+
await user.selectOptions(select, 'Location')
|
|
86
|
+
|
|
87
|
+
// expect legend selection text to include 6 - 10
|
|
88
|
+
await expect(canvasElement.querySelector(legendTextSelection)).toHaveTextContent('6 - 10')
|
|
89
|
+
|
|
90
|
+
// Change the filter value from Home > Vehicle
|
|
91
|
+
const filterBySelect = await canvas.getByLabelText(/Filter by undefined/i)
|
|
92
|
+
await userEvent.selectOptions(filterBySelect, 'Vehicle')
|
|
93
|
+
await expect(canvasElement.querySelector(legendTextSelection)).toHaveTextContent('5 - 9')
|
|
94
|
+
|
|
95
|
+
// click on unified legend
|
|
96
|
+
await canvas.getByRole('button', { name: 'Legend' }).click()
|
|
97
|
+
|
|
98
|
+
// Click on unified legend button using Storybook Testing Library
|
|
99
|
+
const checkbox = await canvas.getByRole('checkbox', { name: /Unified Legend/i })
|
|
100
|
+
await user.click(checkbox)
|
|
101
|
+
|
|
102
|
+
// verify legendTextSelection is 5 - 10
|
|
103
|
+
await expect(canvasElement.querySelector(legendTextSelection)).toHaveTextContent('5 - 11')
|
|
104
|
+
await userEvent.selectOptions(filterBySelect, 'Home')
|
|
105
|
+
await expect(canvasElement.querySelector(legendTextSelection)).toHaveTextContent('5 - 11')
|
|
106
|
+
}
|
|
107
|
+
}
|