@cdc/map 4.25.8 → 4.25.11

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 (137) hide show
  1. package/.claude/agents/typescript-organizer.md +118 -0
  2. package/.claude/settings.local.json +30 -0
  3. package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
  4. package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
  5. package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
  6. package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
  7. package/dist/cdcmap.js +56991 -53706
  8. package/examples/example-city-state.json +9 -1
  9. package/examples/multi-country-centering.json +45 -0
  10. package/examples/private/c.json +290 -0
  11. package/examples/private/canvas-city-hover.json +787 -0
  12. package/examples/private/colors-2.json +221 -0
  13. package/examples/private/colors.json +221 -0
  14. package/examples/private/d.json +345 -0
  15. package/examples/private/g.json +1 -0
  16. package/examples/private/h.json +105911 -0
  17. package/examples/private/measles-data.json +378 -0
  18. package/examples/private/measles.json +211 -0
  19. package/examples/private/north-dakota.json +1132 -0
  20. package/examples/private/state-with-pattern.json +883 -0
  21. package/index.html +36 -34
  22. package/package.json +26 -5
  23. package/src/CdcMap.tsx +23 -8
  24. package/src/CdcMapComponent.tsx +238 -308
  25. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  26. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  27. package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
  28. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  29. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  30. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  31. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  32. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  33. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  34. package/src/_stories/CdcMap.stories.tsx +37 -9
  35. package/src/_stories/GoogleMap.stories.tsx +2 -2
  36. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  37. package/src/_stories/_mock/column-wrap-test.json +265 -0
  38. package/src/_stories/_mock/equal-number.json +1109 -0
  39. package/src/_stories/_mock/multi-country-hide.json +78 -0
  40. package/src/_stories/_mock/multi-country.json +95 -0
  41. package/src/_stories/_mock/multi-state.json +887 -20403
  42. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  43. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  44. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  45. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  46. package/src/_stories/_mock/usa-state-gradient.json +2 -4
  47. package/src/components/BubbleList.tsx +17 -13
  48. package/src/components/CityList.tsx +85 -107
  49. package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
  50. package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
  51. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
  53. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  54. package/src/components/Geo.tsx +22 -3
  55. package/src/components/Legend/components/Legend.tsx +76 -40
  56. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  57. package/src/components/Legend/components/index.scss +1 -1
  58. package/src/components/MapContainer.tsx +52 -0
  59. package/src/components/MapControls.tsx +44 -0
  60. package/src/components/NavigationMenu.tsx +27 -15
  61. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  62. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  63. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  64. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  65. package/src/components/SmallMultiples/index.tsx +3 -0
  66. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
  67. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  68. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  69. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
  70. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
  71. package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
  72. package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
  73. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
  74. package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
  75. package/src/components/UsaMap/helpers/map.ts +4 -4
  76. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  77. package/src/components/WorldMap/WorldMap.tsx +193 -35
  78. package/src/components/ZoomControls.tsx +6 -9
  79. package/src/context/LegendMemoContext.tsx +30 -0
  80. package/src/context.ts +1 -40
  81. package/src/data/initial-state.js +153 -130
  82. package/src/data/supported-geos.js +25 -78
  83. package/src/helpers/addUIDs.ts +13 -2
  84. package/src/helpers/applyColorToLegend.ts +140 -20
  85. package/src/helpers/applyLegendToRow.ts +10 -6
  86. package/src/helpers/componentHelpers.ts +8 -0
  87. package/src/helpers/constants.ts +12 -14
  88. package/src/helpers/dataTableHelpers.ts +6 -0
  89. package/src/helpers/displayGeoName.ts +18 -3
  90. package/src/helpers/generateRuntimeLegend.ts +44 -10
  91. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  92. package/src/helpers/getColumnNames.ts +1 -1
  93. package/src/helpers/getCountriesPicked.ts +103 -0
  94. package/src/helpers/getMapContainerClasses.ts +7 -0
  95. package/src/helpers/getPatternForRow.ts +33 -0
  96. package/src/helpers/getStatesPicked.ts +8 -5
  97. package/src/helpers/index.ts +3 -3
  98. package/src/helpers/isLegendItemDisabled.ts +16 -0
  99. package/src/helpers/mapObserverHelpers.ts +40 -0
  100. package/src/helpers/resetLegendToggles.ts +3 -2
  101. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  102. package/src/helpers/tests/titleCase.test.ts +76 -0
  103. package/src/helpers/titleCase.ts +13 -13
  104. package/src/helpers/toggleLegendActive.ts +6 -11
  105. package/src/helpers/urlDataHelpers.ts +70 -0
  106. package/src/hooks/useCountryZoom.tsx +241 -0
  107. package/src/hooks/useGeoClickHandler.ts +36 -2
  108. package/src/hooks/useLegendMemo.ts +17 -0
  109. package/src/hooks/useMapLayers.tsx +5 -4
  110. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  111. package/src/hooks/useResizeObserver.ts +5 -2
  112. package/src/hooks/useStateZoom.tsx +30 -8
  113. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  114. package/src/hooks/useTooltip.ts +1 -2
  115. package/src/index.jsx +1 -2
  116. package/src/scss/editor-panel.scss +4 -440
  117. package/src/scss/main.scss +1 -1
  118. package/src/scss/map.scss +12 -15
  119. package/src/store/map.actions.ts +7 -7
  120. package/src/store/map.reducer.ts +17 -6
  121. package/src/test/CdcMap.test.jsx +11 -0
  122. package/src/types/MapConfig.ts +46 -18
  123. package/src/types/MapContext.ts +6 -7
  124. package/src/types/runtimeLegend.ts +17 -1
  125. package/vite.config.js +2 -7
  126. package/vitest.config.ts +16 -0
  127. package/src/components/DataTable.tsx +0 -385
  128. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  129. package/src/coreStyles_map.scss +0 -3
  130. package/src/helpers/colorDistributions.ts +0 -12
  131. package/src/helpers/generateColorsArray.ts +0 -14
  132. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  133. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
  134. package/src/hooks/useActiveElement.ts +0 -19
  135. package/src/scss/mixins.scss +0 -47
  136. package/src/types/Annotations.ts +0 -24
  137. /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
@@ -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 {
@@ -44,8 +40,13 @@ import {
44
40
  hashObj,
45
41
  navigationHandler
46
42
  } from './helpers'
47
- import generateRuntimeLegend from './helpers/generateRuntimeLegend'
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
+ import { prepareSmallMultiplesDataTable } from './helpers/smallMultiplesHelpers'
49
50
 
50
51
  // Child Components
51
52
  import Annotation from './components/Annotation'
@@ -53,22 +54,24 @@ import ConfigContext, { MapDispatchContext } from './context'
53
54
  import EditorPanel from './components/EditorPanel'
54
55
  import Error from './components/EditorPanel/components/Error'
55
56
  import Legend from './components/Legend'
56
- import Modal from './components/Modal'
57
+ import MapContainer from './components/MapContainer'
58
+ import MapControls from './components/MapControls'
57
59
  import NavigationMenu from './components/NavigationMenu'
58
- import UsaMap from './components/UsaMap'
59
- import WorldMap from './components/WorldMap'
60
- import GoogleMap from './components/GoogleMap'
61
60
 
62
61
  // hooks
63
62
  import useResizeObserver from './hooks/useResizeObserver'
63
+ import useLegendMemo from './hooks/useLegendMemo'
64
+ import { LegendMemoProvider } from './context/LegendMemoContext'
64
65
  import { VizFilter } from '@cdc/core/types/VizFilter'
65
66
  import { getInitialState, mapReducer } from './store/map.reducer'
66
67
  import { RuntimeData } from './types/RuntimeData'
67
- import EditorContext from '@cdc/editor/src/ConfigContext'
68
+ import EditorContext from '@cdc/core/contexts/EditorContext'
68
69
  import MapActions from './store/map.actions'
69
70
  import _ from 'lodash'
71
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
70
72
  import useModal from './hooks/useModal'
71
73
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
74
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
72
75
 
73
76
  type CdcMapComponent = {
74
77
  config: MapConfig
@@ -79,6 +82,8 @@ type CdcMapComponent = {
79
82
  navigationHandler: Function
80
83
  setSharedFilter: Function
81
84
  setSharedFilterValue: Function
85
+ setConfig?: Function
86
+ loadConfig?: Function
82
87
  datasets?: Datasets
83
88
  interactionLabel: string
84
89
  }
@@ -95,7 +100,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
95
100
  setConfig: setParentConfig,
96
101
  loadConfig,
97
102
  datasets,
98
- interactionLabel = ''
103
+ interactionLabel = 'no link provided'
99
104
  }) => {
100
105
  const initialState = getInitialState(configObj)
101
106
 
@@ -132,151 +137,44 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
132
137
  }
133
138
 
134
139
  useEffect(() => {
135
- const _newConfig = getInitialState(_.cloneDeep(configObj)).config
140
+ const _newConfig = getInitialState(cloneConfig(configObj)).config
136
141
  if (configObj.data) {
137
142
  _newConfig.data = configObj.data
138
143
  }
139
144
  setConfig(_newConfig)
140
145
  }, [configObj.data]) // eslint-disable-line
141
146
 
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
147
  const _setRuntimeData = (data: any) => {
155
148
  const _newFilters = addValuesToFilters(data, [])
156
149
  setConfig({ ...config, filters: _newFilters })
157
150
  if (config) {
158
- setRuntimeData(data)
151
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: data })
159
152
  } else {
160
- setRuntimeFilters(data)
153
+ dispatch({ type: 'SET_RUNTIME_FILTERS', payload: data })
161
154
  }
162
155
  }
163
- const transform = new DataTransform()
164
156
 
165
157
  // Refs
166
158
  const innerContainerRef = useRef()
167
- const legendMemo = useRef(new Map())
168
159
  const legendRef = useRef(null)
169
- const legendSpecialClassLastMemo = useRef(new Map())
170
160
  const mapSvg = useRef(null)
171
161
  const tooltipRef = useRef(null)
172
162
 
163
+ // Legend memo hook
164
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemo()
165
+
173
166
  // IDs
174
- const imageId = useMemo(() => `download-id-${Math.random().toString(36).substr(2, 9)}`, [])
167
+ const imageId = useMemo(() => `download-id-${Math.random().toString(36).substring(2, 11)}`, [])
175
168
  const legendId = useId()
176
169
  const mapId = useId()
177
- const tooltipId = 'test'
170
+ const tooltipId = 'tooltipId'
178
171
 
179
172
  // hooks
180
- const { currentViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
181
-
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
- }
173
+ const { currentViewport, vizViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
276
174
 
277
175
  useEffect(() => {
278
176
  if (!mapSvg.current || coveLoadedHasRan) return
279
- return observeMapSvgLoaded(mapSvg, config, coveLoadedHasRan, publish, dispatch)
177
+ return observeMapSvgLoaded(mapSvg, config, coveLoadedHasRan, dispatch)
280
178
  }, [config, loading, runtimeData, coveLoadedHasRan])
281
179
 
282
180
  useEffect(() => {
@@ -299,7 +197,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
299
197
  filters[index].active = queryStringFilterValue
300
198
  }
301
199
  })
302
- setRuntimeFilters(filters)
200
+ dispatch({ type: 'SET_RUNTIME_FILTERS', payload: filters })
303
201
  }
304
202
  }
305
203
 
@@ -328,11 +226,14 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
328
226
  isCategoryLegend,
329
227
  config.table.showNonGeoData
330
228
  )
331
- setRuntimeData(newRuntimeData)
229
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: newRuntimeData })
332
230
  } else {
333
231
  if (hashLegend !== runtimeLegend?.fromHash && undefined === runtimeData?.init) {
334
232
  const legend = generateRuntimeLegend(
335
- config,
233
+ {
234
+ ...config,
235
+ legend: { ...config.legend, unified: config.smallMultiples?.mode ? true : config.legend?.unified }
236
+ },
336
237
  runtimeData,
337
238
  hashLegend,
338
239
  setConfig,
@@ -340,7 +241,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
340
241
  legendMemo,
341
242
  legendSpecialClassLastMemo
342
243
  )
343
- setRuntimeLegend(legend)
244
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
344
245
  }
345
246
  }
346
247
  }, [config, configObj.data])
@@ -348,7 +249,11 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
348
249
  useEffect(() => {
349
250
  const hashLegend = generateRuntimeLegendHash(config, runtimeFilters)
350
251
  const legend = generateRuntimeLegend(
351
- { ...config, data: configObj.data },
252
+ {
253
+ ...config,
254
+ data: configObj.data,
255
+ legend: { ...config.legend, unified: config.smallMultiples?.mode ? true : config.legend?.unified }
256
+ },
352
257
  runtimeData,
353
258
  hashLegend,
354
259
  setConfig,
@@ -356,21 +261,68 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
356
261
  legendMemo,
357
262
  legendSpecialClassLastMemo
358
263
  )
359
- setRuntimeLegend(legend)
264
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legend })
360
265
  }, [runtimeData, config, runtimeFilters])
361
266
 
362
267
  useEffect(() => {
363
268
  if (!isDashboard) {
364
- reloadURLData()
269
+ reloadURLData(config, setConfig)
365
270
  }
366
271
  }, [JSON.stringify(config.filters)])
367
272
 
368
273
  const { general, tooltips, table, columns } = config
369
- const { subtext = '', geoType } = general
370
- const { showDownloadImgButton, showDownloadPdfButton, headerColor, introText } = general
274
+ const { subtext = '', geoType, showDownloadImgButton, showDownloadPdfButton, headerColor, introText } = general
371
275
  const { closeModal } = useModal()
372
276
 
373
277
  let title = config.general.title
278
+ let processedSuperTitle = general.superTitle
279
+ let processedSubtext = subtext
280
+ let processedIntroText = introText
281
+ let processedFootnotes = general.footnotes
282
+
283
+ // Process markup variables if enabled
284
+ if (config.enableMarkupVariables && config.markupVariables?.length > 0) {
285
+ // Combine viz filters with dashboard filters for markup processing
286
+ const combinedFilters = [...(config.filters || []), ...(config.dashboardFilters || [])]
287
+
288
+ const markupOptions = { isEditor, filters: combinedFilters }
289
+
290
+ if (title) {
291
+ title = processMarkupVariables(title, config.data || [], config.markupVariables, markupOptions).processedContent
292
+ }
293
+ if (general.superTitle) {
294
+ processedSuperTitle = processMarkupVariables(
295
+ general.superTitle,
296
+ config.data || [],
297
+ config.markupVariables,
298
+ markupOptions
299
+ ).processedContent
300
+ }
301
+ if (subtext) {
302
+ processedSubtext = processMarkupVariables(
303
+ subtext,
304
+ config.data || [],
305
+ config.markupVariables,
306
+ markupOptions
307
+ ).processedContent
308
+ }
309
+ if (introText) {
310
+ processedIntroText = processMarkupVariables(
311
+ introText,
312
+ config.data || [],
313
+ config.markupVariables,
314
+ markupOptions
315
+ ).processedContent
316
+ }
317
+ if (general.footnotes) {
318
+ processedFootnotes = processMarkupVariables(
319
+ general.footnotes,
320
+ config.data || [],
321
+ config.markupVariables,
322
+ markupOptions
323
+ ).processedContent
324
+ }
325
+ }
374
326
 
375
327
  if (isEditor) {
376
328
  if (!title || title === '') title = 'Map Title'
@@ -383,18 +335,14 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
383
335
  container,
384
336
  content: modal,
385
337
  currentViewport,
338
+ vizViewport,
386
339
  customNavigationHandler,
387
- data: runtimeData,
388
340
  dimensions,
389
341
  filteredCountryCode,
390
- innerContainerRef,
391
342
  isDashboard,
392
343
  isEditor,
393
- legendMemo,
394
- legendSpecialClassLastMemo,
395
344
  logo,
396
345
  mapId,
397
- outerContainerRef,
398
346
  position,
399
347
  projection,
400
348
  runtimeData,
@@ -402,9 +350,6 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
402
350
  runtimeLegend,
403
351
  scale,
404
352
  setConfig,
405
- setRuntimeData,
406
- setRuntimeFilters,
407
- setRuntimeLegend,
408
353
  setSharedFilter,
409
354
  setSharedFilterValue,
410
355
  config,
@@ -428,167 +373,147 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
428
373
  href={`#data-table-${config.dataKey}`}
429
374
  className='margin-left-href'
430
375
  onClick={() => {
431
- publishAnalyticsEvent(
432
- 'link_to_data_table_click',
433
- 'click',
434
- `${interactionLabel}|#data-table-${config.dataKey}`,
435
- 'map'
436
- )
376
+ publishAnalyticsEvent({
377
+ vizType: config.type,
378
+ vizSubType: getVizSubType(config),
379
+ eventType: `link_to_data_table_click`,
380
+ eventAction: 'click',
381
+ eventLabel: `${interactionLabel}`,
382
+ vizTitle: getVizTitle(config),
383
+ specifics: `table: #data-table-${config.dataKey}`
384
+ })
437
385
  }}
438
386
  >
439
387
  {config.dataKey} (Go to Table)
440
388
  </a>
441
389
  )
442
390
 
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(' ')
391
+ // Prepare data table props (pivot if small multiples mode is enabled)
392
+ let dataTableConfig = config
393
+ let dataTableColumns = columns
394
+ let dataTableRuntimeData = runtimeData
395
+ if (config.smallMultiples?.mode) {
396
+ const prepared = prepareSmallMultiplesDataTable(config, columns, runtimeData)
397
+ dataTableConfig = prepared.config
398
+ dataTableColumns = prepared.columns
399
+ dataTableRuntimeData = prepared.runtimeData
447
400
  }
448
401
 
449
402
  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
403
+ <LegendMemoProvider legendMemo={legendMemo} legendSpecialClassLastMemo={legendSpecialClassLastMemo}>
404
+ <ConfigContext.Provider value={mapProps}>
405
+ <MapDispatchContext.Provider value={dispatch}>
406
+ <Layout.VisualizationWrapper
407
+ config={config}
408
+ isEditor={isEditor}
409
+ ref={outerContainerRef}
410
+ currentViewport={currentViewport}
411
+ imageId={imageId}
412
+ showEditorPanel={config.showEditorPanel}
413
+ >
414
+ {isEditor && <EditorPanel datasets={datasets} />}
415
+ <Layout.Responsive isEditor={isEditor}>
416
+ {requiredColumns?.length > 0 && (
417
+ <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
418
+ )}
419
+ {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
420
+ <section
421
+ className={buildSectionClassNames(
422
+ currentViewport,
423
+ headerColor,
424
+ config?.runtime?.editorErrorMessage.length > 0
425
+ )}
426
+ aria-label={'Map: ' + title}
427
+ ref={innerContainerRef}
428
+ >
429
+ {config?.runtime?.editorErrorMessage.length > 0 && <Error />}
430
+ <Title
431
+ title={title}
432
+ superTitle={processedSuperTitle}
483
433
  config={config}
484
- setConfig={setConfig}
485
- filteredData={runtimeFilters}
486
- setFilters={_setRuntimeData}
487
- dimensions={dimensions}
488
- standaloneMap={!config}
489
- interactionLabel={interactionLabel}
434
+ classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${headerColor}`]}
490
435
  />
491
- )}
492
-
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>
436
+ <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
437
+ {config?.annotations?.length > 0 && (
438
+ <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
439
+ )}
528
440
 
529
- {general.showSidebar && 'navigation' !== general.type && (
530
- <Legend
441
+ {processedIntroText && <section className='introText mb-4'>{parse(processedIntroText)}</section>}
442
+
443
+ {config?.filters?.length > 0 && (
444
+ <Filters
445
+ config={config}
446
+ setConfig={setConfig}
447
+ filteredData={runtimeFilters}
448
+ setFilters={_setRuntimeData}
531
449
  dimensions={dimensions}
532
- ref={legendRef}
533
- skipId={tabId}
534
- containerWidthPadding={0}
535
- currentViewport={currentViewport}
450
+ standaloneMap={!config}
536
451
  interactionLabel={interactionLabel}
537
452
  />
538
453
  )}
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
454
 
552
- {/* Link (to data table?) */}
553
- {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
455
+ <div
456
+ role='region'
457
+ tabIndex={0}
458
+ className={getMapContainerClasses(config, modal).join(' ')}
459
+ onClick={e => closeModal(e, modal)}
460
+ onKeyDown={e => {
461
+ if (e.key === 'Enter') {
462
+ closeModal(e, modal)
463
+ }
464
+ }}
465
+ >
466
+ <MapContainer
467
+ config={config}
468
+ modal={modal}
469
+ currentViewport={currentViewport}
470
+ geoType={geoType}
471
+ general={general}
472
+ logo={logo}
473
+ mapSvgRef={mapSvg}
474
+ />
554
475
 
555
- {subtext.length > 0 && <p className='subtext mt-4'>{parse(subtext)}</p>}
476
+ {general.showSidebar && 'navigation' !== general.type && (
477
+ <Legend
478
+ dimensions={dimensions}
479
+ ref={legendRef}
480
+ skipId={tabId}
481
+ containerWidthPadding={0}
482
+ currentViewport={currentViewport}
483
+ interactionLabel={interactionLabel}
484
+ />
485
+ )}
486
+ </div>
556
487
 
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}
488
+ {'navigation' === general.type && (
489
+ <NavigationMenu
490
+ mapTabbingID={tabId}
491
+ displayGeoName={displayGeoName}
492
+ data={runtimeData}
493
+ options={general}
494
+ columns={config.columns}
495
+ navigationHandler={val => navigationHandler('_blank', val, customNavigationHandler)}
576
496
  />
577
497
  )}
578
- </MediaControls.Section>
579
498
 
580
- {config?.runtime?.editorErrorMessage.length === 0 &&
581
- true === table.forceDisplay &&
582
- general.type !== 'navigation' &&
583
- false === loading && (
499
+ {/* Link (to data table?) */}
500
+ {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink
501
+ ? tableLink
502
+ : link && link}
503
+
504
+ {processedSubtext.length > 0 && <p className='subtext mt-4'>{parse(processedSubtext)}</p>}
505
+
506
+ <MapControls config={config} imageId={imageId} interactionLabel={interactionLabel} />
507
+
508
+ {shouldShowDataTable(config, table, general, loading) && (
584
509
  <DataTable
585
- columns={columns}
586
- config={config}
510
+ columns={dataTableColumns}
511
+ config={dataTableConfig}
587
512
  currentViewport={currentViewport}
588
513
  displayGeoName={displayGeoName}
589
514
  expandDataTable={table.expanded}
590
515
  formatLegendLocation={key =>
591
- formatLegendLocation(key, runtimeData?.[key]?.[config.columns.geo.name])
516
+ formatLegendLocation(key, dataTableRuntimeData?.[key]?.[config.columns.geo.name])
592
517
  }
593
518
  headerColor={general.headerColor}
594
519
  imageRef={imageId}
@@ -598,8 +523,8 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
598
523
  legendSpecialClassLastMemo={legendSpecialClassLastMemo}
599
524
  navigationHandler={navigationHandler}
600
525
  outerContainerRef={outerContainerRef}
601
- rawData={config.data}
602
- runtimeData={runtimeData}
526
+ rawData={dataTableConfig.data}
527
+ runtimeData={dataTableRuntimeData}
603
528
  runtimeLegend={runtimeLegend}
604
529
  showDownloadImgButton={showDownloadImgButton}
605
530
  showDownloadPdfButton={showDownloadPdfButton}
@@ -611,19 +536,17 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
611
536
  />
612
537
  )}
613
538
 
614
- {config.annotations?.length > 0 && <Annotation.Dropdown />}
539
+ {config.annotations?.length > 0 && <Annotation.Dropdown />}
615
540
 
616
- {general.footnotes && <section className='footnotes pt-2 mt-4'>{parse(general.footnotes)}</section>}
617
- </section>
618
- )}
541
+ {processedFootnotes && <section className='footnotes pt-2 mt-4'>{parse(processedFootnotes)}</section>}
542
+ </section>
543
+ )}
619
544
 
620
- <div aria-live='assertive' className='cdcdataviz-sr-only'>
621
- {accessibleStatus}
622
- </div>
545
+ <div aria-live='assertive' className='cdcdataviz-sr-only'>
546
+ {accessibleStatus}
547
+ </div>
623
548
 
624
- {!isDraggingAnnotation &&
625
- !window.matchMedia('(any-hover: none)').matches &&
626
- 'hover' === tooltips.appearanceType && (
549
+ {!isDraggingAnnotation && 'hover' === tooltips.appearanceType && (
627
550
  <ReactTooltip
628
551
  id={`tooltip__${tooltipId}`}
629
552
  float={true}
@@ -631,22 +554,29 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
631
554
  style={{ background: `rgba(255,255,255, ${config.tooltips.opacity / 100})`, color: 'black' }}
632
555
  />
633
556
  )}
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>
557
+ <div
558
+ ref={tooltipRef}
559
+ id={`tooltip__${tooltipId}-canvas`}
560
+ className='tooltip'
561
+ style={{
562
+ background: `rgba(255,255,255,${config.tooltips.opacity / 100})`,
563
+ position: 'absolute',
564
+ whiteSpace: 'nowrap',
565
+ display: 'none' // can't use d-none here
566
+ }}
567
+ ></div>
568
+ <FootnotesStandAlone
569
+ config={config.footnotes}
570
+ filters={config.filters?.filter(f => f.filterFootnotes)}
571
+ markupVariables={config.markupVariables}
572
+ enableMarkupVariables={config.enableMarkupVariables}
573
+ data={config.data}
574
+ />
575
+ </Layout.Responsive>
576
+ </Layout.VisualizationWrapper>
577
+ </MapDispatchContext.Provider>
578
+ </ConfigContext.Provider>
579
+ </LegendMemoProvider>
650
580
  )
651
581
  }
652
582