@cdc/map 4.25.8 → 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 (84) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/dist/cdcmap.js +54263 -52600
  3. package/examples/private/c.json +290 -0
  4. package/examples/private/canvas-city-hover.json +787 -0
  5. package/examples/private/d.json +345 -0
  6. package/examples/private/g.json +1 -0
  7. package/examples/private/h.json +105911 -0
  8. package/examples/private/measles-data.json +378 -0
  9. package/examples/private/measles.json +211 -0
  10. package/examples/private/north-dakota.json +1132 -0
  11. package/examples/private/state-with-pattern.json +883 -0
  12. package/index.html +35 -34
  13. package/package.json +26 -5
  14. package/src/CdcMap.tsx +23 -8
  15. package/src/CdcMapComponent.tsx +215 -309
  16. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  17. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  18. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  19. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  20. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.stories.tsx +15 -5
  22. package/src/_stories/GoogleMap.stories.tsx +2 -2
  23. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  24. package/src/_stories/_mock/equal-number.json +1109 -0
  25. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  26. package/src/components/BubbleList.tsx +16 -12
  27. package/src/components/CityList.tsx +85 -107
  28. package/src/components/DataTable.tsx +37 -9
  29. package/src/components/EditorPanel/components/EditorPanel.tsx +177 -165
  30. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  32. package/src/components/Geo.tsx +2 -0
  33. package/src/components/Legend/components/Legend.tsx +109 -73
  34. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  35. package/src/components/MapContainer.tsx +52 -0
  36. package/src/components/MapControls.tsx +44 -0
  37. package/src/components/NavigationMenu.tsx +11 -2
  38. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  39. package/src/components/UsaMap/components/UsaMap.County.tsx +111 -37
  40. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  41. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +6 -6
  42. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  43. package/src/components/UsaMap/helpers/map.ts +2 -2
  44. package/src/components/WorldMap/WorldMap.tsx +113 -25
  45. package/src/components/ZoomControls.tsx +6 -9
  46. package/src/context/LegendMemoContext.tsx +30 -0
  47. package/src/context.ts +1 -40
  48. package/src/data/initial-state.js +143 -130
  49. package/src/data/supported-geos.js +17 -2
  50. package/src/helpers/applyColorToLegend.ts +116 -20
  51. package/src/helpers/applyLegendToRow.ts +10 -6
  52. package/src/helpers/componentHelpers.ts +8 -0
  53. package/src/helpers/constants.ts +12 -0
  54. package/src/helpers/dataTableHelpers.ts +6 -0
  55. package/src/helpers/displayGeoName.ts +1 -1
  56. package/src/helpers/generateRuntimeLegend.ts +44 -8
  57. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  58. package/src/helpers/getColumnNames.ts +1 -1
  59. package/src/helpers/getPatternForRow.ts +36 -0
  60. package/src/helpers/getStatesPicked.ts +8 -5
  61. package/src/helpers/index.ts +11 -3
  62. package/src/helpers/isLegendItemDisabled.ts +16 -0
  63. package/src/helpers/mapObserverHelpers.ts +40 -0
  64. package/src/helpers/resetLegendToggles.ts +3 -2
  65. package/src/helpers/toggleLegendActive.ts +6 -11
  66. package/src/helpers/urlDataHelpers.ts +70 -0
  67. package/src/hooks/useGeoClickHandler.ts +35 -1
  68. package/src/hooks/useLegendMemo.ts +17 -0
  69. package/src/hooks/useMapLayers.tsx +5 -4
  70. package/src/hooks/useStateZoom.tsx +25 -6
  71. package/src/hooks/useTooltip.ts +1 -2
  72. package/src/index.jsx +0 -2
  73. package/src/store/map.reducer.ts +17 -6
  74. package/src/test/CdcMap.test.jsx +11 -0
  75. package/src/types/MapConfig.ts +23 -14
  76. package/src/types/MapContext.ts +0 -7
  77. package/src/types/runtimeLegend.ts +17 -1
  78. package/vite.config.js +2 -7
  79. package/vitest.config.ts +16 -0
  80. package/src/coreStyles_map.scss +0 -3
  81. package/src/helpers/colorDistributions.ts +0 -12
  82. package/src/helpers/generateColorsArray.ts +0 -14
  83. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  84. 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,22 +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'
71
72
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
73
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
72
74
 
73
75
  type CdcMapComponent = {
74
76
  config: MapConfig
@@ -79,6 +81,8 @@ type CdcMapComponent = {
79
81
  navigationHandler: Function
80
82
  setSharedFilter: Function
81
83
  setSharedFilterValue: Function
84
+ setConfig?: Function
85
+ loadConfig?: Function
82
86
  datasets?: Datasets
83
87
  interactionLabel: string
84
88
  }
@@ -95,7 +99,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
95
99
  setConfig: setParentConfig,
96
100
  loadConfig,
97
101
  datasets,
98
- interactionLabel = ''
102
+ interactionLabel = 'no link provided'
99
103
  }) => {
100
104
  const initialState = getInitialState(configObj)
101
105
 
@@ -132,151 +136,44 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
132
136
  }
133
137
 
134
138
  useEffect(() => {
135
- const _newConfig = getInitialState(_.cloneDeep(configObj)).config
139
+ const _newConfig = getInitialState(cloneConfig(configObj)).config
136
140
  if (configObj.data) {
137
141
  _newConfig.data = configObj.data
138
142
  }
139
143
  setConfig(_newConfig)
140
144
  }, [configObj.data]) // eslint-disable-line
141
145
 
142
- const setRuntimeData = (data: RuntimeData) => {
143
- dispatch({ type: 'SET_RUNTIME_DATA', payload: data })
144
- }
145
-
146
- const setRuntimeFilters = (filters: VizFilter[]) => {
147
- dispatch({ type: 'SET_RUNTIME_FILTERS', payload: filters })
148
- }
149
-
150
- const setRuntimeLegend = legend => {
151
- dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
152
- }
153
-
154
146
  const _setRuntimeData = (data: any) => {
155
147
  const _newFilters = addValuesToFilters(data, [])
156
148
  setConfig({ ...config, filters: _newFilters })
157
149
  if (config) {
158
- setRuntimeData(data)
150
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: data })
159
151
  } else {
160
- setRuntimeFilters(data)
152
+ dispatch({ type: 'SET_RUNTIME_FILTERS', payload: data })
161
153
  }
162
154
  }
163
- const transform = new DataTransform()
164
155
 
165
156
  // Refs
166
157
  const innerContainerRef = useRef()
167
- const legendMemo = useRef(new Map())
168
158
  const legendRef = useRef(null)
169
- const legendSpecialClassLastMemo = useRef(new Map())
170
159
  const mapSvg = useRef(null)
171
160
  const tooltipRef = useRef(null)
172
161
 
162
+ // Legend memo hook
163
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemo()
164
+
173
165
  // IDs
174
- 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)}`, [])
175
167
  const legendId = useId()
176
168
  const mapId = useId()
177
- const tooltipId = 'test'
169
+ const tooltipId = 'tooltipId'
178
170
 
179
171
  // hooks
180
172
  const { currentViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
181
173
 
182
- const reloadURLData = async () => {
183
- if (config.dataUrl) {
184
- const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl, window.location.origin)
185
- let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
186
-
187
- let isUpdateNeeded = false
188
- config.filters.forEach(filter => {
189
- if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
190
- qsParams[filter.queryParameter] = filter.active
191
- isUpdateNeeded = true
192
- }
193
- })
194
-
195
- if (!isUpdateNeeded) return
196
-
197
- let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${Object.keys(qsParams)
198
- .map((param, i) => {
199
- let qs = i === 0 ? '?' : '&'
200
- qs += param + '='
201
- qs += qsParams[param]
202
- return qs
203
- })
204
- .join('')}`
205
-
206
- let data
207
-
208
- try {
209
- const regex = /(?:\.([^.]+))?$/
210
-
211
- const ext = regex.exec(dataUrl.pathname)[1]
212
- if ('csv' === ext || isSolrCsv(dataUrlFinal)) {
213
- data = await fetch(dataUrlFinal)
214
- .then(response => response.text())
215
- .then(responseText => {
216
- const parsedCsv = Papa.parse(responseText, {
217
- header: true,
218
- dynamicTyping: true,
219
- skipEmptyLines: true,
220
- encoding: 'utf-8'
221
- })
222
- return parsedCsv.data
223
- })
224
- } else if ('json' === ext || isSolrJson(dataUrlFinal)) {
225
- data = await fetch(dataUrlFinal).then(response => response.json())
226
- } else {
227
- data = []
228
- }
229
- } catch (e) {
230
- console.error(`Cannot parse URL: ${dataUrlFinal}`) // eslint-disable-line
231
- console.log(e) // eslint-disable-line
232
- data = []
233
- }
234
-
235
- if (config.dataDescription) {
236
- data = transform.autoStandardize(data)
237
- data = transform.developerStandardize(data, config.dataDescription)
238
- }
239
-
240
- const newConfig = _.cloneDeep(config)
241
- newConfig.data = data
242
- newConfig.runtimeDataUrl = dataUrlFinal
243
-
244
- setConfig(newConfig)
245
- }
246
- }
247
-
248
- /**
249
- * Publishes 'cove_loaded' only after the map SVG is rendered in the DOM.
250
- * Checks immediately, then uses a MutationObserver as a fallback for async rendering.
251
- * Update the mapSvg ref if the map container changes.
252
- */
253
- const observeMapSvgLoaded = (mapSvgRef, config, coveLoadedHasRan, publish, dispatch) => {
254
- // Immediate check in case SVG is already present
255
- const svgEl = mapSvgRef.current?.querySelector('svg')
256
- if (svgEl && svgEl.childNodes.length > 0) {
257
- publish('cove_loaded', { config })
258
- dispatch({ type: 'SET_COVE_LOADED_HAS_RAN', payload: true })
259
- return () => {}
260
- }
261
-
262
- // Fallback to observer for async SVG rendering
263
- const observer = new MutationObserver(() => {
264
- const svgEl = mapSvgRef.current?.querySelector('svg')
265
- if (svgEl && svgEl.childNodes.length > 0) {
266
- publish('cove_loaded', { config })
267
- dispatch({ type: 'SET_COVE_LOADED_HAS_RAN', payload: true })
268
- observer.disconnect()
269
- }
270
- })
271
-
272
- observer.observe(mapSvgRef.current, { childList: true, subtree: true })
273
-
274
- return () => observer.disconnect()
275
- }
276
-
277
174
  useEffect(() => {
278
175
  if (!mapSvg.current || coveLoadedHasRan) return
279
- return observeMapSvgLoaded(mapSvg, config, coveLoadedHasRan, publish, dispatch)
176
+ return observeMapSvgLoaded(mapSvg, config, coveLoadedHasRan, dispatch)
280
177
  }, [config, loading, runtimeData, coveLoadedHasRan])
281
178
 
282
179
  useEffect(() => {
@@ -299,7 +196,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
299
196
  filters[index].active = queryStringFilterValue
300
197
  }
301
198
  })
302
- setRuntimeFilters(filters)
199
+ dispatch({ type: 'SET_RUNTIME_FILTERS', payload: filters })
303
200
  }
304
201
  }
305
202
 
@@ -328,7 +225,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
328
225
  isCategoryLegend,
329
226
  config.table.showNonGeoData
330
227
  )
331
- setRuntimeData(newRuntimeData)
228
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: newRuntimeData })
332
229
  } else {
333
230
  if (hashLegend !== runtimeLegend?.fromHash && undefined === runtimeData?.init) {
334
231
  const legend = generateRuntimeLegend(
@@ -340,7 +237,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
340
237
  legendMemo,
341
238
  legendSpecialClassLastMemo
342
239
  )
343
- setRuntimeLegend(legend)
240
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
344
241
  }
345
242
  }
346
243
  }, [config, configObj.data])
@@ -356,21 +253,68 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
356
253
  legendMemo,
357
254
  legendSpecialClassLastMemo
358
255
  )
359
- setRuntimeLegend(legend)
256
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
360
257
  }, [runtimeData, config, runtimeFilters])
361
258
 
362
259
  useEffect(() => {
363
260
  if (!isDashboard) {
364
- reloadURLData()
261
+ reloadURLData(config, setConfig)
365
262
  }
366
263
  }, [JSON.stringify(config.filters)])
367
264
 
368
265
  const { general, tooltips, table, columns } = config
369
- const { subtext = '', geoType } = general
370
- const { showDownloadImgButton, showDownloadPdfButton, headerColor, introText } = general
266
+ const { subtext = '', geoType, showDownloadImgButton, showDownloadPdfButton, headerColor, introText } = general
371
267
  const { closeModal } = useModal()
372
268
 
373
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
+ }
374
318
 
375
319
  if (isEditor) {
376
320
  if (!title || title === '') title = 'Map Title'
@@ -384,17 +328,12 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
384
328
  content: modal,
385
329
  currentViewport,
386
330
  customNavigationHandler,
387
- data: runtimeData,
388
331
  dimensions,
389
332
  filteredCountryCode,
390
- innerContainerRef,
391
333
  isDashboard,
392
334
  isEditor,
393
- legendMemo,
394
- legendSpecialClassLastMemo,
395
335
  logo,
396
336
  mapId,
397
- outerContainerRef,
398
337
  position,
399
338
  projection,
400
339
  runtimeData,
@@ -402,9 +341,6 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
402
341
  runtimeLegend,
403
342
  scale,
404
343
  setConfig,
405
- setRuntimeData,
406
- setRuntimeFilters,
407
- setRuntimeLegend,
408
344
  setSharedFilter,
409
345
  setSharedFilterValue,
410
346
  config,
@@ -428,159 +364,128 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
428
364
  href={`#data-table-${config.dataKey}`}
429
365
  className='margin-left-href'
430
366
  onClick={() => {
431
- publishAnalyticsEvent(
432
- 'link_to_data_table_click',
433
- 'click',
434
- `${interactionLabel}|#data-table-${config.dataKey}`,
435
- 'map'
436
- )
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
+ })
437
376
  }}
438
377
  >
439
378
  {config.dataKey} (Go to Table)
440
379
  </a>
441
380
  )
442
381
 
443
- const sectionClassNames = () => {
444
- const classes = ['cove-component__content', 'cdc-map-inner-container', `${currentViewport}`, `${headerColor}`]
445
- if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
446
- return classes.join(' ')
447
- }
448
-
449
382
  return (
450
- <ConfigContext.Provider value={mapProps}>
451
- <MapDispatchContext.Provider value={dispatch}>
452
- <Layout.VisualizationWrapper
453
- config={config}
454
- isEditor={isEditor}
455
- ref={outerContainerRef}
456
- currentViewport={currentViewport}
457
- imageId={imageId}
458
- showEditorPanel={config.showEditorPanel}
459
- >
460
- {isEditor && <EditorPanel datasets={datasets} />}
461
- <Layout.Responsive isEditor={isEditor}>
462
- {requiredColumns?.length > 0 && (
463
- <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
464
- )}
465
- {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
466
- <section className={sectionClassNames()} aria-label={'Map: ' + title} ref={innerContainerRef}>
467
- {config?.runtime?.editorErrorMessage.length > 0 && <Error />}
468
- <Title
469
- title={title}
470
- superTitle={general.superTitle}
471
- config={config}
472
- classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${headerColor}`]}
473
- />
474
- <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
475
- {config?.annotations?.length > 0 && (
476
- <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
477
- )}
478
-
479
- {introText && <section className='introText mb-4'>{parse(introText)}</section>}
480
-
481
- {config?.filters?.length > 0 && (
482
- <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}
483
413
  config={config}
484
- setConfig={setConfig}
485
- filteredData={runtimeFilters}
486
- setFilters={_setRuntimeData}
487
- dimensions={dimensions}
488
- standaloneMap={!config}
489
- interactionLabel={interactionLabel}
414
+ classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${headerColor}`]}
490
415
  />
491
- )}
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
+ )}
492
420
 
493
- <div
494
- role='region'
495
- tabIndex={0}
496
- className={getMapContainerClasses(config, modal).join(' ')}
497
- onClick={e => closeModal(e, modal)}
498
- onKeyDown={e => {
499
- if (e.key === 'Enter') {
500
- closeModal(e, modal)
501
- }
502
- }}
503
- >
504
- <section
505
- className='outline-none geography-container w-100 position-relative'
506
- ref={mapSvg}
507
- tabIndex='0'
508
- >
509
- {currentViewport && (
510
- <>
511
- {modal && <Modal />}
512
- {'single-state' === geoType && <UsaMap.SingleState />}
513
- {'us' === geoType && 'us-geocode' !== config.general.type && <UsaMap.State />}
514
- {'us-region' === geoType && <UsaMap.Region />}
515
- {'us-county' === geoType && <UsaMap.County />}
516
- {'world' === geoType && <WorldMap />}
517
- {'google-map' === geoType && <GoogleMap />}
518
- {
519
- /* logo is handled in UsaMap.State when applicable */
520
- // prettier-ignore
521
- 'data' === general.type && logo && ('us' !== geoType || 'us-geocode' === general.type) && (
522
- <img src={logo} alt='' className='map-logo' style={{ maxWidth: '50px' }} />
523
- )
524
- }
525
- </>
526
- )}
527
- </section>
421
+ {processedIntroText && <section className='introText mb-4'>{parse(processedIntroText)}</section>}
528
422
 
529
- {general.showSidebar && 'navigation' !== general.type && (
530
- <Legend
423
+ {config?.filters?.length > 0 && (
424
+ <Filters
425
+ config={config}
426
+ setConfig={setConfig}
427
+ filteredData={runtimeFilters}
428
+ setFilters={_setRuntimeData}
531
429
  dimensions={dimensions}
532
- ref={legendRef}
533
- skipId={tabId}
534
- containerWidthPadding={0}
535
- currentViewport={currentViewport}
430
+ standaloneMap={!config}
536
431
  interactionLabel={interactionLabel}
537
432
  />
538
433
  )}
539
- </div>
540
-
541
- {'navigation' === general.type && (
542
- <NavigationMenu
543
- mapTabbingID={tabId}
544
- displayGeoName={displayGeoName}
545
- data={runtimeData}
546
- options={general}
547
- columns={config.columns}
548
- navigationHandler={val => navigationHandler('_blank', val, customNavigationHandler)}
549
- />
550
- )}
551
434
 
552
- {/* Link (to data table?) */}
553
- {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
+ />
554
455
 
555
- {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>
556
467
 
557
- <MediaControls.Section classes={['download-buttons']}>
558
- {showDownloadImgButton && (
559
- <MediaControls.Button
560
- text='Download Image'
561
- title='Download Chart as Image'
562
- type='image'
563
- state={config}
564
- elementToCapture={imageId}
565
- interactionLabel={interactionLabel}
566
- />
567
- )}
568
- {showDownloadPdfButton && (
569
- <MediaControls.Button
570
- text='Download PDF'
571
- title='Download Chart as PDF'
572
- type='pdf'
573
- state={config}
574
- interactionLabel={interactionLabel}
575
- 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)}
576
476
  />
577
477
  )}
578
- </MediaControls.Section>
579
478
 
580
- {config?.runtime?.editorErrorMessage.length === 0 &&
581
- true === table.forceDisplay &&
582
- general.type !== 'navigation' &&
583
- 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) && (
584
489
  <DataTable
585
490
  columns={columns}
586
491
  config={config}
@@ -611,42 +516,43 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
611
516
  />
612
517
  )}
613
518
 
614
- {config.annotations?.length > 0 && <Annotation.Dropdown />}
615
-
616
- {general.footnotes && <section className='footnotes pt-2 mt-4'>{parse(general.footnotes)}</section>}
617
- </section>
618
- )}
619
-
620
- <div aria-live='assertive' className='cdcdataviz-sr-only'>
621
- {accessibleStatus}
622
- </div>
623
-
624
- {!isDraggingAnnotation &&
625
- !window.matchMedia('(any-hover: none)').matches &&
626
- 'hover' === tooltips.appearanceType && (
627
- <ReactTooltip
628
- id={`tooltip__${tooltipId}`}
629
- float={true}
630
- className={`tooltip tooltip-test`}
631
- style={{ background: `rgba(255,255,255, ${config.tooltips.opacity / 100})`, color: 'black' }}
632
- />
519
+ {config.annotations?.length > 0 && <Annotation.Dropdown />}
520
+
521
+ {processedFootnotes && <section className='footnotes pt-2 mt-4'>{parse(processedFootnotes)}</section>}
522
+ </section>
633
523
  )}
634
- <div
635
- ref={tooltipRef}
636
- id={`tooltip__${tooltipId}-canvas`}
637
- className='tooltip'
638
- style={{
639
- background: `rgba(255,255,255,${config.tooltips.opacity / 100})`,
640
- position: 'absolute',
641
- whiteSpace: 'nowrap',
642
- display: 'none' // can't use d-none here
643
- }}
644
- ></div>
645
- <FootnotesStandAlone config={config.footnotes} filters={config.filters?.filter(f => f.filterFootnotes)} />
646
- </Layout.Responsive>
647
- </Layout.VisualizationWrapper>
648
- </MapDispatchContext.Provider>
649
- </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>
650
556
  )
651
557
  }
652
558