@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.
Files changed (119) hide show
  1. package/.idea/map.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/dist/cdcmap.js +31254 -32242
  5. package/examples/hex-colors.json +3 -3
  6. package/examples/m2.json +32904 -0
  7. package/examples/private/test.json +470 -1457
  8. package/examples/private/{mmr.json → wastewatermap.json} +86 -115
  9. package/index.html +36 -63
  10. package/package.json +7 -19
  11. package/src/CdcMap.tsx +56 -1552
  12. package/src/CdcMapComponent.tsx +608 -0
  13. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
  14. package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
  15. package/src/_stories/CdcMap.Table.stories.tsx +19 -0
  16. package/src/_stories/CdcMap.stories.tsx +12 -1
  17. package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
  18. package/src/_stories/_mock/default-patterns.json +8 -5
  19. package/src/_stories/_mock/legend-bins.json +428 -0
  20. package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
  21. package/src/cdcMapComponent.styles.css +9 -0
  22. package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
  23. package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
  24. package/src/components/BubbleList.tsx +135 -49
  25. package/src/components/CityList.tsx +89 -87
  26. package/src/components/DataTable.tsx +8 -8
  27. package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
  28. package/src/components/EditorPanel/components/Error.tsx +9 -2
  29. package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
  30. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
  32. package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
  33. package/src/components/Geo.tsx +9 -1
  34. package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
  35. package/src/components/Legend/components/Legend.tsx +92 -87
  36. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
  37. package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
  38. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
  39. package/src/components/Legend/components/index.scss +74 -17
  40. package/src/components/Modal.tsx +17 -7
  41. package/src/components/NavigationMenu.tsx +11 -9
  42. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
  43. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
  44. package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
  45. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
  46. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
  47. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
  48. package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
  49. package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
  50. package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
  51. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
  52. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
  53. package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
  54. package/src/components/UsaMap/helpers/map.ts +1 -1
  55. package/src/components/UsaMap/helpers/shapes.ts +20 -7
  56. package/src/components/WorldMap/WorldMap.tsx +64 -118
  57. package/src/components/WorldMap/worldMap.styles.css +28 -0
  58. package/src/components/ZoomControls.tsx +15 -13
  59. package/src/components/zoomControls.styles.css +53 -0
  60. package/src/context.ts +17 -9
  61. package/src/data/initial-state.js +5 -2
  62. package/src/helpers/addUIDs.ts +150 -0
  63. package/src/helpers/applyColorToLegend.ts +39 -64
  64. package/src/helpers/applyLegendToRow.ts +51 -0
  65. package/src/helpers/colorDistributions.ts +12 -0
  66. package/src/helpers/constants.ts +44 -0
  67. package/src/helpers/displayGeoName.ts +9 -2
  68. package/src/helpers/formatLegendLocation.ts +3 -2
  69. package/src/helpers/generateColorsArray.ts +2 -1
  70. package/src/helpers/generateRuntimeData.ts +78 -0
  71. package/src/helpers/generateRuntimeFilters.ts +63 -0
  72. package/src/helpers/generateRuntimeLegend.ts +566 -0
  73. package/src/helpers/generateRuntimeLegendHash.ts +16 -15
  74. package/src/helpers/getColumnNames.ts +19 -0
  75. package/src/helpers/getMapContainerClasses.ts +23 -0
  76. package/src/helpers/getStatePicked.ts +8 -0
  77. package/src/helpers/handleMapTabbing.ts +31 -0
  78. package/src/helpers/hashObj.ts +1 -1
  79. package/src/helpers/index.ts +22 -0
  80. package/src/helpers/navigationHandler.ts +3 -3
  81. package/src/helpers/resetLegendToggles.ts +13 -0
  82. package/src/helpers/setBinNumbers.ts +5 -0
  83. package/src/helpers/sortSpecialClassesLast.ts +7 -0
  84. package/src/helpers/tests/getColumnNames.test.ts +52 -0
  85. package/src/helpers/titleCase.ts +1 -1
  86. package/src/helpers/toggleLegendActive.ts +25 -0
  87. package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
  88. package/src/hooks/useColumnsRequiredChecker.ts +51 -0
  89. package/src/hooks/useGeoClickHandler.ts +45 -0
  90. package/src/hooks/useLegendSeparators.ts +26 -0
  91. package/src/hooks/useMapLayers.tsx +34 -60
  92. package/src/hooks/useModal.ts +22 -0
  93. package/src/hooks/useResizeObserver.ts +4 -5
  94. package/src/hooks/useStateZoom.tsx +52 -75
  95. package/src/hooks/useTooltip.ts +2 -3
  96. package/src/index.jsx +3 -9
  97. package/src/scss/editor-panel.scss +3 -99
  98. package/src/scss/main.scss +1 -19
  99. package/src/scss/map.scss +15 -220
  100. package/src/store/map.actions.ts +46 -0
  101. package/src/store/map.reducer.ts +96 -0
  102. package/src/types/Annotations.ts +24 -0
  103. package/src/types/MapConfig.ts +23 -3
  104. package/src/types/MapContext.ts +36 -35
  105. package/src/types/Modal.ts +1 -0
  106. package/src/types/RuntimeData.ts +3 -0
  107. package/examples/private/DEV-9644.json +0 -184
  108. package/examples/private/DEV-9989.json +0 -229
  109. package/examples/private/ardi.json +0 -180
  110. package/examples/private/colors 2.json +0 -416
  111. package/examples/private/colors.json +0 -416
  112. package/examples/private/colors.json.zip +0 -0
  113. package/examples/private/customColors.json +0 -45348
  114. package/examples/test.json +0 -183
  115. package/src/helpers/closeModal.ts +0 -9
  116. package/src/scss/btn.scss +0 -69
  117. package/src/scss/filters.scss +0 -27
  118. package/src/scss/variables.scss +0 -1
  119. /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
+ }