@cdc/map 4.25.7 → 4.25.10

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 (99) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/CLAUDE.local.md +0 -0
  3. package/dist/cdcmap.js +54785 -53159
  4. package/examples/private/c.json +290 -0
  5. package/examples/private/canvas-city-hover.json +787 -0
  6. package/examples/private/d.json +345 -0
  7. package/examples/private/filter-map.json +909 -0
  8. package/examples/private/g.json +1 -0
  9. package/examples/private/h.json +105911 -0
  10. package/examples/private/measles-data.json +378 -0
  11. package/examples/private/measles.json +211 -0
  12. package/examples/private/north-dakota.json +1132 -0
  13. package/examples/private/rsv-data.json +532 -0
  14. package/examples/private/state-with-pattern.json +883 -0
  15. package/examples/private/test.json +222 -640
  16. package/index.html +1 -1
  17. package/package.json +26 -5
  18. package/src/CdcMap.tsx +28 -8
  19. package/src/CdcMapComponent.tsx +230 -306
  20. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  22. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  23. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  24. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  25. package/src/_stories/CdcMap.stories.tsx +18 -11
  26. package/src/_stories/GoogleMap.stories.tsx +2 -2
  27. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  28. package/src/_stories/_mock/equal-number.json +1109 -0
  29. package/src/_stories/_mock/multi-state.json +21389 -0
  30. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  31. package/src/components/BubbleList.tsx +16 -12
  32. package/src/components/CityList.tsx +88 -110
  33. package/src/components/DataTable.tsx +44 -12
  34. package/src/components/EditorPanel/components/EditorPanel.tsx +201 -203
  35. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  37. package/src/components/Geo.tsx +2 -0
  38. package/src/components/Legend/components/Legend.tsx +117 -93
  39. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  40. package/src/components/MapContainer.tsx +52 -0
  41. package/src/components/MapControls.tsx +44 -0
  42. package/src/components/Modal.tsx +2 -8
  43. package/src/components/NavigationMenu.tsx +13 -1
  44. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  45. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
  46. package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
  47. package/src/components/UsaMap/components/UsaMap.County.tsx +112 -33
  48. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  49. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +38 -26
  50. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  51. package/src/components/UsaMap/helpers/map.ts +16 -8
  52. package/src/components/WorldMap/WorldMap.tsx +116 -11
  53. package/src/components/ZoomControls.tsx +6 -9
  54. package/src/context/LegendMemoContext.tsx +30 -0
  55. package/src/context.ts +1 -39
  56. package/src/data/initial-state.js +143 -128
  57. package/src/data/supported-geos.js +202 -4
  58. package/src/helpers/addUIDs.ts +8 -8
  59. package/src/helpers/applyColorToLegend.ts +122 -45
  60. package/src/helpers/applyLegendToRow.ts +15 -13
  61. package/src/helpers/componentHelpers.ts +8 -0
  62. package/src/helpers/constants.ts +12 -0
  63. package/src/helpers/dataTableHelpers.ts +6 -0
  64. package/src/helpers/displayGeoName.ts +12 -7
  65. package/src/helpers/formatLegendLocation.ts +1 -3
  66. package/src/helpers/generateRuntimeLegend.ts +192 -340
  67. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  68. package/src/helpers/getColumnNames.ts +1 -1
  69. package/src/helpers/getPatternForRow.ts +36 -0
  70. package/src/helpers/getStatesPicked.ts +14 -0
  71. package/src/helpers/handleMapAriaLabels.ts +2 -2
  72. package/src/helpers/index.ts +11 -3
  73. package/src/helpers/isLegendItemDisabled.ts +16 -0
  74. package/src/helpers/mapObserverHelpers.ts +40 -0
  75. package/src/helpers/resetLegendToggles.ts +3 -2
  76. package/src/helpers/toggleLegendActive.ts +6 -11
  77. package/src/helpers/urlDataHelpers.ts +70 -0
  78. package/src/hooks/useGeoClickHandler.ts +35 -1
  79. package/src/hooks/useLegendMemo.ts +17 -0
  80. package/src/hooks/useMapLayers.tsx +5 -4
  81. package/src/hooks/useStateZoom.tsx +137 -88
  82. package/src/hooks/useTooltip.ts +1 -2
  83. package/src/index.jsx +6 -3
  84. package/src/scss/main.scss +23 -12
  85. package/src/store/map.actions.ts +2 -2
  86. package/src/store/map.reducer.ts +21 -10
  87. package/src/test/CdcMap.test.jsx +11 -0
  88. package/src/types/MapConfig.ts +25 -17
  89. package/src/types/MapContext.ts +2 -8
  90. package/src/types/runtimeLegend.ts +12 -10
  91. package/vite.config.js +2 -7
  92. package/vitest.config.ts +16 -0
  93. package/src/_stories/_mock/floating-point.json +0 -427
  94. package/src/coreStyles_map.scss +0 -3
  95. package/src/helpers/colorDistributions.ts +0 -12
  96. package/src/helpers/generateColorsArray.ts +0 -14
  97. package/src/helpers/getStatePicked.ts +0 -8
  98. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  99. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
@@ -2,7 +2,6 @@
2
2
  import React, { useEffect, useRef, useId, useReducer, useContext, useMemo } from 'react'
3
3
  import 'whatwg-fetch'
4
4
  import { Tooltip as ReactTooltip } from 'react-tooltip'
5
- import Papa from 'papaparse'
6
5
  import parse from 'html-react-parser'
7
6
  import 'react-tooltip/dist/react-tooltip.css'
8
7
 
@@ -10,7 +9,6 @@ import 'react-tooltip/dist/react-tooltip.css'
10
9
  import DataTable from '@cdc/core/components/DataTable'
11
10
  import Filters from '@cdc/core/components/Filters'
12
11
  import Layout from '@cdc/core/components/Layout'
13
- import MediaControls from '@cdc/core/components/MediaControls'
14
12
  import SkipTo from '@cdc/core/components/elements/SkipTo'
15
13
  import Title from '@cdc/core/components/ui/Title'
16
14
  import Waiting from '@cdc/core/components/Waiting'
@@ -25,13 +23,11 @@ import './scss/main.scss'
25
23
  import './cdcMapComponent.styles.css'
26
24
 
27
25
  // Core Helpers
28
- import { DataTransform } from '@cdc/core/helpers/DataTransform'
29
26
  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
27
  import { generateRuntimeFilters } from './helpers/generateRuntimeFilters'
33
28
  import { type MapReducerType, MapState } from './store/map.reducer'
34
29
  import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
30
+ import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
35
31
 
36
32
  // Map Helpers
37
33
  import {
@@ -46,6 +42,10 @@ import {
46
42
  } from './helpers'
47
43
  import generateRuntimeLegend from './helpers/generateRuntimeLegend'
48
44
  import generateRuntimeData from './helpers/generateRuntimeData'
45
+ import { reloadURLData } from './helpers/urlDataHelpers'
46
+ import { observeMapSvgLoaded } from './helpers/mapObserverHelpers'
47
+ import { buildSectionClassNames } from './helpers/componentHelpers'
48
+ import { shouldShowDataTable } from './helpers/dataTableHelpers'
49
49
 
50
50
  // Child Components
51
51
  import Annotation from './components/Annotation'
@@ -53,21 +53,24 @@ import ConfigContext, { MapDispatchContext } from './context'
53
53
  import EditorPanel from './components/EditorPanel'
54
54
  import Error from './components/EditorPanel/components/Error'
55
55
  import Legend from './components/Legend'
56
- import Modal from './components/Modal'
56
+ import MapContainer from './components/MapContainer'
57
+ import MapControls from './components/MapControls'
57
58
  import NavigationMenu from './components/NavigationMenu'
58
- import UsaMap from './components/UsaMap'
59
- import WorldMap from './components/WorldMap'
60
- import GoogleMap from './components/GoogleMap'
61
59
 
62
60
  // hooks
63
61
  import useResizeObserver from './hooks/useResizeObserver'
62
+ import useLegendMemo from './hooks/useLegendMemo'
63
+ import { LegendMemoProvider } from './context/LegendMemoContext'
64
64
  import { VizFilter } from '@cdc/core/types/VizFilter'
65
65
  import { getInitialState, mapReducer } from './store/map.reducer'
66
66
  import { RuntimeData } from './types/RuntimeData'
67
- import EditorContext from '@cdc/editor/src/ConfigContext'
67
+ import EditorContext from '@cdc/core/contexts/EditorContext'
68
68
  import MapActions from './store/map.actions'
69
69
  import _ from 'lodash'
70
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
70
71
  import useModal from './hooks/useModal'
72
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
73
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
71
74
 
72
75
  type CdcMapComponent = {
73
76
  config: MapConfig
@@ -78,7 +81,10 @@ type CdcMapComponent = {
78
81
  navigationHandler: Function
79
82
  setSharedFilter: Function
80
83
  setSharedFilterValue: Function
84
+ setConfig?: Function
85
+ loadConfig?: Function
81
86
  datasets?: Datasets
87
+ interactionLabel: string
82
88
  }
83
89
 
84
90
  const CdcMapComponent: React.FC<CdcMapComponent> = ({
@@ -92,7 +98,8 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
92
98
  link,
93
99
  setConfig: setParentConfig,
94
100
  loadConfig,
95
- datasets
101
+ datasets,
102
+ interactionLabel = 'no link provided'
96
103
  }) => {
97
104
  const initialState = getInitialState(configObj)
98
105
 
@@ -112,7 +119,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
112
119
  scale,
113
120
  translate,
114
121
  projection,
115
- stateToShow,
122
+ statesToShow,
116
123
  requiredColumns,
117
124
  topoData,
118
125
  coveLoadedHasRan,
@@ -129,151 +136,44 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
129
136
  }
130
137
 
131
138
  useEffect(() => {
132
- const _newConfig = getInitialState(_.cloneDeep(configObj)).config
139
+ const _newConfig = getInitialState(cloneConfig(configObj)).config
133
140
  if (configObj.data) {
134
141
  _newConfig.data = configObj.data
135
142
  }
136
143
  setConfig(_newConfig)
137
144
  }, [configObj.data]) // eslint-disable-line
138
145
 
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
146
  const _setRuntimeData = (data: any) => {
152
147
  const _newFilters = addValuesToFilters(data, [])
153
148
  setConfig({ ...config, filters: _newFilters })
154
149
  if (config) {
155
- setRuntimeData(data)
150
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: data })
156
151
  } else {
157
- setRuntimeFilters(data)
152
+ dispatch({ type: 'SET_RUNTIME_FILTERS', payload: data })
158
153
  }
159
154
  }
160
- const transform = new DataTransform()
161
155
 
162
156
  // Refs
163
157
  const innerContainerRef = useRef()
164
- const legendMemo = useRef(new Map())
165
158
  const legendRef = useRef(null)
166
- const legendSpecialClassLastMemo = useRef(new Map())
167
159
  const mapSvg = useRef(null)
168
160
  const tooltipRef = useRef(null)
169
161
 
162
+ // Legend memo hook
163
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemo()
164
+
170
165
  // IDs
171
- const imageId = useMemo(() => `download-id-${Math.random().toString(36).substr(2, 9)}`, [])
166
+ const imageId = useMemo(() => `download-id-${Math.random().toString(36).substring(2, 11)}`, [])
172
167
  const legendId = useId()
173
168
  const mapId = useId()
174
- const tooltipId = 'test'
169
+ const tooltipId = 'tooltipId'
175
170
 
176
171
  // hooks
177
172
  const { currentViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
178
173
 
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
- /**
246
- * Publishes 'cove_loaded' only after the map SVG is rendered in the DOM.
247
- * Checks immediately, then uses a MutationObserver as a fallback for async rendering.
248
- * Update the mapSvg ref if the map container changes.
249
- */
250
- const observeMapSvgLoaded = (mapSvgRef, config, coveLoadedHasRan, publish, dispatch) => {
251
- // Immediate check in case SVG is already present
252
- const svgEl = mapSvgRef.current?.querySelector('svg')
253
- if (svgEl && svgEl.childNodes.length > 0) {
254
- publish('cove_loaded', { config })
255
- dispatch({ type: 'SET_COVE_LOADED_HAS_RAN', payload: true })
256
- return () => {}
257
- }
258
-
259
- // Fallback to observer for async SVG rendering
260
- const observer = new MutationObserver(() => {
261
- const svgEl = mapSvgRef.current?.querySelector('svg')
262
- if (svgEl && svgEl.childNodes.length > 0) {
263
- publish('cove_loaded', { config })
264
- dispatch({ type: 'SET_COVE_LOADED_HAS_RAN', payload: true })
265
- observer.disconnect()
266
- }
267
- })
268
-
269
- observer.observe(mapSvgRef.current, { childList: true, subtree: true })
270
-
271
- return () => observer.disconnect()
272
- }
273
-
274
174
  useEffect(() => {
275
175
  if (!mapSvg.current || coveLoadedHasRan) return
276
- return observeMapSvgLoaded(mapSvg, config, coveLoadedHasRan, publish, dispatch)
176
+ return observeMapSvgLoaded(mapSvg, config, coveLoadedHasRan, dispatch)
277
177
  }, [config, loading, runtimeData, coveLoadedHasRan])
278
178
 
279
179
  useEffect(() => {
@@ -296,7 +196,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
296
196
  filters[index].active = queryStringFilterValue
297
197
  }
298
198
  })
299
- setRuntimeFilters(filters)
199
+ dispatch({ type: 'SET_RUNTIME_FILTERS', payload: filters })
300
200
  }
301
201
  }
302
202
 
@@ -325,7 +225,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
325
225
  isCategoryLegend,
326
226
  config.table.showNonGeoData
327
227
  )
328
- setRuntimeData(newRuntimeData)
228
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: newRuntimeData })
329
229
  } else {
330
230
  if (hashLegend !== runtimeLegend?.fromHash && undefined === runtimeData?.init) {
331
231
  const legend = generateRuntimeLegend(
@@ -337,7 +237,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
337
237
  legendMemo,
338
238
  legendSpecialClassLastMemo
339
239
  )
340
- setRuntimeLegend(legend)
240
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
341
241
  }
342
242
  }
343
243
  }, [config, configObj.data])
@@ -353,21 +253,68 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
353
253
  legendMemo,
354
254
  legendSpecialClassLastMemo
355
255
  )
356
- setRuntimeLegend(legend)
256
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
357
257
  }, [runtimeData, config, runtimeFilters])
358
258
 
359
259
  useEffect(() => {
360
260
  if (!isDashboard) {
361
- reloadURLData()
261
+ reloadURLData(config, setConfig)
362
262
  }
363
263
  }, [JSON.stringify(config.filters)])
364
264
 
365
265
  const { general, tooltips, table, columns } = config
366
- const { subtext = '', geoType } = general
367
- const { showDownloadImgButton, showDownloadPdfButton, headerColor, introText } = general
266
+ const { subtext = '', geoType, showDownloadImgButton, showDownloadPdfButton, headerColor, introText } = general
368
267
  const { closeModal } = useModal()
369
268
 
370
269
  let title = config.general.title
270
+ let processedSuperTitle = general.superTitle
271
+ let processedSubtext = subtext
272
+ let processedIntroText = introText
273
+ let processedFootnotes = general.footnotes
274
+
275
+ // Process markup variables if enabled
276
+ if (config.enableMarkupVariables && config.markupVariables?.length > 0) {
277
+ // Combine viz filters with dashboard filters for markup processing
278
+ const combinedFilters = [...(config.filters || []), ...(config.dashboardFilters || [])]
279
+
280
+ const markupOptions = { isEditor, filters: combinedFilters }
281
+
282
+ if (title) {
283
+ title = processMarkupVariables(title, config.data || [], config.markupVariables, markupOptions).processedContent
284
+ }
285
+ if (general.superTitle) {
286
+ processedSuperTitle = processMarkupVariables(
287
+ general.superTitle,
288
+ config.data || [],
289
+ config.markupVariables,
290
+ markupOptions
291
+ ).processedContent
292
+ }
293
+ if (subtext) {
294
+ processedSubtext = processMarkupVariables(
295
+ subtext,
296
+ config.data || [],
297
+ config.markupVariables,
298
+ markupOptions
299
+ ).processedContent
300
+ }
301
+ if (introText) {
302
+ processedIntroText = processMarkupVariables(
303
+ introText,
304
+ config.data || [],
305
+ config.markupVariables,
306
+ markupOptions
307
+ ).processedContent
308
+ }
309
+ if (general.footnotes) {
310
+ processedFootnotes = processMarkupVariables(
311
+ general.footnotes,
312
+ config.data || [],
313
+ config.markupVariables,
314
+ markupOptions
315
+ ).processedContent
316
+ }
317
+ }
371
318
 
372
319
  if (isEditor) {
373
320
  if (!title || title === '') title = 'Map Title'
@@ -381,17 +328,12 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
381
328
  content: modal,
382
329
  currentViewport,
383
330
  customNavigationHandler,
384
- data: runtimeData,
385
331
  dimensions,
386
332
  filteredCountryCode,
387
- innerContainerRef,
388
333
  isDashboard,
389
334
  isEditor,
390
- legendMemo,
391
- legendSpecialClassLastMemo,
392
335
  logo,
393
336
  mapId,
394
- outerContainerRef,
395
337
  position,
396
338
  projection,
397
339
  runtimeData,
@@ -399,19 +341,17 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
399
341
  runtimeLegend,
400
342
  scale,
401
343
  setConfig,
402
- setRuntimeData,
403
- setRuntimeFilters,
404
- setRuntimeLegend,
405
344
  setSharedFilter,
406
345
  setSharedFilterValue,
407
346
  config,
408
- stateToShow,
347
+ statesToShow,
409
348
  tooltipId,
410
349
  tooltipRef,
411
350
  topoData,
412
351
  translate,
413
352
  isDraggingAnnotation,
414
- loadConfig
353
+ loadConfig,
354
+ interactionLabel
415
355
  }
416
356
 
417
357
  if (!config.data) return <></>
@@ -420,148 +360,132 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
420
360
 
421
361
  // this only shows in Dashboard config mode and only if Show Table is also set
422
362
  const tableLink = (
423
- <a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
363
+ <a
364
+ href={`#data-table-${config.dataKey}`}
365
+ className='margin-left-href'
366
+ onClick={() => {
367
+ publishAnalyticsEvent({
368
+ vizType: config.type,
369
+ vizSubType: getVizSubType(config),
370
+ eventType: `link_to_data_table_click`,
371
+ eventAction: 'click',
372
+ eventLabel: `${interactionLabel}`,
373
+ vizTitle: getVizTitle(config),
374
+ specifics: `table: #data-table-${config.dataKey}`
375
+ })
376
+ }}
377
+ >
424
378
  {config.dataKey} (Go to Table)
425
379
  </a>
426
380
  )
427
381
 
428
- const sectionClassNames = () => {
429
- const classes = ['cove-component__content', 'cdc-map-inner-container', `${currentViewport}`, `${headerColor}`]
430
- if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
431
- return classes.join(' ')
432
- }
433
-
434
382
  return (
435
- <ConfigContext.Provider value={mapProps}>
436
- <MapDispatchContext.Provider value={dispatch}>
437
- <Layout.VisualizationWrapper
438
- config={config}
439
- isEditor={isEditor}
440
- ref={outerContainerRef}
441
- currentViewport={currentViewport}
442
- imageId={imageId}
443
- showEditorPanel={config.showEditorPanel}
444
- >
445
- {isEditor && <EditorPanel datasets={datasets} />}
446
- <Layout.Responsive isEditor={isEditor}>
447
- {requiredColumns?.length > 0 && (
448
- <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
449
- )}
450
- {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
451
- <section className={sectionClassNames()} aria-label={'Map: ' + title} ref={innerContainerRef}>
452
- {config?.runtime?.editorErrorMessage.length > 0 && <Error />}
453
- <Title
454
- title={title}
455
- superTitle={general.superTitle}
456
- config={config}
457
- classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${headerColor}`]}
458
- />
459
- <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
460
- {config?.annotations?.length > 0 && (
461
- <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
462
- )}
463
-
464
- {introText && <section className='introText mb-4'>{parse(introText)}</section>}
465
-
466
- {config?.filters?.length > 0 && (
467
- <Filters
383
+ <LegendMemoProvider legendMemo={legendMemo} legendSpecialClassLastMemo={legendSpecialClassLastMemo}>
384
+ <ConfigContext.Provider value={mapProps}>
385
+ <MapDispatchContext.Provider value={dispatch}>
386
+ <Layout.VisualizationWrapper
387
+ config={config}
388
+ isEditor={isEditor}
389
+ ref={outerContainerRef}
390
+ currentViewport={currentViewport}
391
+ imageId={imageId}
392
+ showEditorPanel={config.showEditorPanel}
393
+ >
394
+ {isEditor && <EditorPanel datasets={datasets} />}
395
+ <Layout.Responsive isEditor={isEditor}>
396
+ {requiredColumns?.length > 0 && (
397
+ <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
398
+ )}
399
+ {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
400
+ <section
401
+ className={buildSectionClassNames(
402
+ currentViewport,
403
+ headerColor,
404
+ config?.runtime?.editorErrorMessage.length > 0
405
+ )}
406
+ aria-label={'Map: ' + title}
407
+ ref={innerContainerRef}
408
+ >
409
+ {config?.runtime?.editorErrorMessage.length > 0 && <Error />}
410
+ <Title
411
+ title={title}
412
+ superTitle={processedSuperTitle}
468
413
  config={config}
469
- setConfig={setConfig}
470
- filteredData={runtimeFilters}
471
- setFilters={_setRuntimeData}
472
- dimensions={dimensions}
473
- standaloneMap={!config}
414
+ classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${headerColor}`]}
474
415
  />
475
- )}
416
+ <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
417
+ {config?.annotations?.length > 0 && (
418
+ <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
419
+ )}
476
420
 
477
- <div
478
- role='region'
479
- tabIndex={0}
480
- className={getMapContainerClasses(config, modal).join(' ')}
481
- onClick={e => closeModal(e, modal)}
482
- onKeyDown={e => {
483
- if (e.key === 'Enter') {
484
- closeModal(e, modal)
485
- }
486
- }}
487
- >
488
- <section
489
- className='outline-none geography-container w-100 position-relative'
490
- ref={mapSvg}
491
- tabIndex='0'
492
- >
493
- {currentViewport && (
494
- <>
495
- {modal && <Modal />}
496
- {'single-state' === geoType && <UsaMap.SingleState />}
497
- {'us' === geoType && 'us-geocode' !== config.general.type && <UsaMap.State />}
498
- {'us-region' === geoType && <UsaMap.Region />}
499
- {'us-county' === geoType && <UsaMap.County />}
500
- {'world' === geoType && <WorldMap />}
501
- {'google-map' === geoType && <GoogleMap />}
502
- {
503
- /* logo is handled in UsaMap.State when applicable */
504
- // prettier-ignore
505
- 'data' === general.type && logo && ('us' !== geoType || 'us-geocode' === general.type) && (
506
- <img src={logo} alt='' className='map-logo' style={{ maxWidth: '50px' }} />
507
- )
508
- }
509
- </>
510
- )}
511
- </section>
421
+ {processedIntroText && <section className='introText mb-4'>{parse(processedIntroText)}</section>}
512
422
 
513
- {general.showSidebar && 'navigation' !== general.type && (
514
- <Legend
423
+ {config?.filters?.length > 0 && (
424
+ <Filters
425
+ config={config}
426
+ setConfig={setConfig}
427
+ filteredData={runtimeFilters}
428
+ setFilters={_setRuntimeData}
515
429
  dimensions={dimensions}
516
- ref={legendRef}
517
- skipId={tabId}
518
- containerWidthPadding={0}
519
- currentViewport={currentViewport}
430
+ standaloneMap={!config}
431
+ interactionLabel={interactionLabel}
520
432
  />
521
433
  )}
522
- </div>
523
-
524
- {'navigation' === general.type && (
525
- <NavigationMenu
526
- mapTabbingID={tabId}
527
- displayGeoName={displayGeoName}
528
- data={runtimeData}
529
- options={general}
530
- columns={config.columns}
531
- navigationHandler={val => navigationHandler('_blank', val, customNavigationHandler)}
532
- />
533
- )}
534
434
 
535
- {/* Link (to data table?) */}
536
- {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
435
+ <div
436
+ role='region'
437
+ tabIndex={0}
438
+ className={getMapContainerClasses(config, modal).join(' ')}
439
+ onClick={e => closeModal(e, modal)}
440
+ onKeyDown={e => {
441
+ if (e.key === 'Enter') {
442
+ closeModal(e, modal)
443
+ }
444
+ }}
445
+ >
446
+ <MapContainer
447
+ config={config}
448
+ modal={modal}
449
+ currentViewport={currentViewport}
450
+ geoType={geoType}
451
+ general={general}
452
+ logo={logo}
453
+ mapSvgRef={mapSvg}
454
+ />
537
455
 
538
- {subtext.length > 0 && <p className='subtext mt-4'>{parse(subtext)}</p>}
456
+ {general.showSidebar && 'navigation' !== general.type && (
457
+ <Legend
458
+ dimensions={dimensions}
459
+ ref={legendRef}
460
+ skipId={tabId}
461
+ containerWidthPadding={0}
462
+ currentViewport={currentViewport}
463
+ interactionLabel={interactionLabel}
464
+ />
465
+ )}
466
+ </div>
539
467
 
540
- <MediaControls.Section classes={['download-buttons']}>
541
- {showDownloadImgButton && (
542
- <MediaControls.Button
543
- text='Download Image'
544
- title='Download Chart as Image'
545
- type='image'
546
- state={config}
547
- elementToCapture={imageId}
548
- />
549
- )}
550
- {showDownloadPdfButton && (
551
- <MediaControls.Button
552
- text='Download PDF'
553
- title='Download Chart as PDF'
554
- type='pdf'
555
- state={config}
556
- elementToCapture={imageId}
468
+ {'navigation' === general.type && (
469
+ <NavigationMenu
470
+ mapTabbingID={tabId}
471
+ displayGeoName={displayGeoName}
472
+ data={runtimeData}
473
+ options={general}
474
+ columns={config.columns}
475
+ navigationHandler={val => navigationHandler('_blank', val, customNavigationHandler)}
557
476
  />
558
477
  )}
559
- </MediaControls.Section>
560
478
 
561
- {config?.runtime?.editorErrorMessage.length === 0 &&
562
- true === table.forceDisplay &&
563
- general.type !== 'navigation' &&
564
- false === loading && (
479
+ {/* Link (to data table?) */}
480
+ {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink
481
+ ? tableLink
482
+ : link && link}
483
+
484
+ {processedSubtext.length > 0 && <p className='subtext mt-4'>{parse(processedSubtext)}</p>}
485
+
486
+ <MapControls config={config} imageId={imageId} interactionLabel={interactionLabel} />
487
+
488
+ {shouldShowDataTable(config, table, general, loading) && (
565
489
  <DataTable
566
490
  columns={columns}
567
491
  config={config}
@@ -588,47 +512,47 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
588
512
  tableTitle={table.label}
589
513
  vizTitle={general.title}
590
514
  wrapColumns={table.wrapColumns}
515
+ interactionLabel={interactionLabel}
591
516
  />
592
517
  )}
593
518
 
594
- {config.annotations?.length > 0 && <Annotation.Dropdown />}
595
-
596
- {general.footnotes && <section className='footnotes pt-2 mt-4'>{parse(general.footnotes)}</section>}
597
- </section>
598
- )}
599
-
600
- <div aria-live='assertive' className='cdcdataviz-sr-only'>
601
- {accessibleStatus}
602
- </div>
603
-
604
- {!isDraggingAnnotation &&
605
- !window.matchMedia('(any-hover: none)').matches &&
606
- 'hover' === tooltips.appearanceType && (
607
- <ReactTooltip
608
- id={`tooltip__${tooltipId}`}
609
- float={true}
610
- className={`${
611
- tooltips.capitalizeLabels ? 'capitalize tooltip tooltip-test' : 'tooltip tooltip-test'
612
- }`}
613
- style={{ background: `rgba(255,255,255, ${config.tooltips.opacity / 100})`, color: 'black' }}
614
- />
519
+ {config.annotations?.length > 0 && <Annotation.Dropdown />}
520
+
521
+ {processedFootnotes && <section className='footnotes pt-2 mt-4'>{parse(processedFootnotes)}</section>}
522
+ </section>
615
523
  )}
616
- <div
617
- ref={tooltipRef}
618
- id={`tooltip__${tooltipId}-canvas`}
619
- className='tooltip'
620
- style={{
621
- background: `rgba(255,255,255,${config.tooltips.opacity / 100})`,
622
- position: 'absolute',
623
- whiteSpace: 'nowrap',
624
- display: 'none' // can't use d-none here
625
- }}
626
- ></div>
627
- <FootnotesStandAlone config={config.footnotes} filters={config.filters?.filter(f => f.filterFootnotes)} />
628
- </Layout.Responsive>
629
- </Layout.VisualizationWrapper>
630
- </MapDispatchContext.Provider>
631
- </ConfigContext.Provider>
524
+
525
+ <div aria-live='assertive' className='cdcdataviz-sr-only'>
526
+ {accessibleStatus}
527
+ </div>
528
+
529
+ {!isDraggingAnnotation &&
530
+ !window.matchMedia('(any-hover: none)').matches &&
531
+ 'hover' === tooltips.appearanceType && (
532
+ <ReactTooltip
533
+ id={`tooltip__${tooltipId}`}
534
+ float={true}
535
+ className={`tooltip tooltip-test`}
536
+ style={{ background: `rgba(255,255,255, ${config.tooltips.opacity / 100})`, color: 'black' }}
537
+ />
538
+ )}
539
+ <div
540
+ ref={tooltipRef}
541
+ id={`tooltip__${tooltipId}-canvas`}
542
+ className='tooltip'
543
+ style={{
544
+ background: `rgba(255,255,255,${config.tooltips.opacity / 100})`,
545
+ position: 'absolute',
546
+ whiteSpace: 'nowrap',
547
+ display: 'none' // can't use d-none here
548
+ }}
549
+ ></div>
550
+ <FootnotesStandAlone config={config.footnotes} filters={config.filters?.filter(f => f.filterFootnotes)} />
551
+ </Layout.Responsive>
552
+ </Layout.VisualizationWrapper>
553
+ </MapDispatchContext.Provider>
554
+ </ConfigContext.Provider>
555
+ </LegendMemoProvider>
632
556
  )
633
557
  }
634
558