@cdc/map 4.25.3 → 4.25.5-1

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