@cdc/map 4.25.10 → 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 (88) hide show
  1. package/.claude/agents/typescript-organizer.md +118 -0
  2. package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
  3. package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
  4. package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
  5. package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
  6. package/dist/cdcmap.js +27405 -25783
  7. package/examples/example-city-state.json +9 -1
  8. package/examples/multi-country-centering.json +45 -0
  9. package/examples/private/colors-2.json +221 -0
  10. package/examples/private/colors.json +221 -0
  11. package/index.html +2 -1
  12. package/package.json +4 -4
  13. package/src/CdcMapComponent.tsx +44 -20
  14. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  15. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  16. package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
  17. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  18. package/src/_stories/CdcMap.stories.tsx +22 -4
  19. package/src/_stories/_mock/column-wrap-test.json +265 -0
  20. package/src/_stories/_mock/multi-country-hide.json +78 -0
  21. package/src/_stories/_mock/multi-country.json +95 -0
  22. package/src/_stories/_mock/multi-state.json +887 -20403
  23. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  24. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  25. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  26. package/src/_stories/_mock/usa-state-gradient.json +2 -4
  27. package/src/components/BubbleList.tsx +1 -1
  28. package/src/components/EditorPanel/components/EditorPanel.tsx +630 -564
  29. package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
  30. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
  31. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
  32. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  33. package/src/components/Geo.tsx +20 -3
  34. package/src/components/Legend/components/Legend.tsx +34 -34
  35. package/src/components/Legend/components/index.scss +1 -1
  36. package/src/components/NavigationMenu.tsx +16 -13
  37. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  38. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  39. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  40. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  41. package/src/components/SmallMultiples/index.tsx +3 -0
  42. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +18 -3
  43. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  44. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  45. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
  46. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
  47. package/src/components/UsaMap/components/UsaMap.County.tsx +14 -2
  48. package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
  49. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +25 -5
  50. package/src/components/UsaMap/components/UsaMap.State.tsx +26 -3
  51. package/src/components/UsaMap/helpers/map.ts +2 -2
  52. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  53. package/src/components/WorldMap/WorldMap.tsx +81 -11
  54. package/src/data/initial-state.js +10 -0
  55. package/src/data/supported-geos.js +8 -76
  56. package/src/helpers/addUIDs.ts +13 -2
  57. package/src/helpers/applyColorToLegend.ts +25 -1
  58. package/src/helpers/constants.ts +1 -15
  59. package/src/helpers/displayGeoName.ts +19 -4
  60. package/src/helpers/generateRuntimeLegend.ts +0 -2
  61. package/src/helpers/getCountriesPicked.ts +103 -0
  62. package/src/helpers/getMapContainerClasses.ts +7 -0
  63. package/src/helpers/getPatternForRow.ts +2 -5
  64. package/src/helpers/index.ts +1 -9
  65. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  66. package/src/helpers/tests/titleCase.test.ts +76 -0
  67. package/src/helpers/titleCase.ts +13 -13
  68. package/src/helpers/urlDataHelpers.ts +1 -1
  69. package/src/hooks/useCountryZoom.tsx +241 -0
  70. package/src/hooks/useGeoClickHandler.ts +1 -1
  71. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  72. package/src/hooks/useResizeObserver.ts +5 -2
  73. package/src/hooks/useStateZoom.tsx +5 -2
  74. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  75. package/src/index.jsx +1 -0
  76. package/src/scss/editor-panel.scss +4 -440
  77. package/src/scss/main.scss +1 -1
  78. package/src/scss/map.scss +12 -15
  79. package/src/store/map.actions.ts +7 -7
  80. package/src/types/MapConfig.ts +30 -11
  81. package/src/types/MapContext.ts +6 -0
  82. package/src/types/runtimeLegend.ts +1 -1
  83. package/src/components/DataTable.tsx +0 -413
  84. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  85. package/src/hooks/useActiveElement.ts +0 -19
  86. package/src/scss/mixins.scss +0 -47
  87. package/src/types/Annotations.ts +0 -24
  88. /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
@@ -0,0 +1,241 @@
1
+ import { useContext, useEffect, useMemo, useCallback, useRef } from 'react'
2
+ import ConfigContext, { MapDispatchContext } from '../context'
3
+ import { geoMercator } from 'd3-geo'
4
+ import { MapContext } from '../types/MapContext'
5
+ import { geoPath, GeoPath } from 'd3-geo'
6
+ import { getCountriesPicked } from '../helpers/getCountriesPicked'
7
+ import { SVG_HEIGHT, SVG_WIDTH, SVG_PADDING } from '../helpers'
8
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
9
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
10
+
11
+ interface CountryData {
12
+ geometry: { type: 'Polygon' | 'MultiPolygon'; coordinates: number[][][][] | number[][][] }
13
+ // ISO code of country
14
+ id: string
15
+ // Country properties
16
+ properties: { name: string; iso: string }
17
+ type: 'Feature'
18
+ }
19
+
20
+ const useCountryZoom = (worldData: CountryData[]) => {
21
+ const { config, interactionLabel } = useContext<MapContext>(ConfigContext)
22
+ const dispatch = useContext(MapDispatchContext)
23
+
24
+ // Ref to track last centered countries to prevent unnecessary re-centering
25
+ const lastCenteredCountries = useRef<string>('')
26
+
27
+ // Get countriesPicked with memoization
28
+ const countriesPicked = useMemo(() => {
29
+ const result = getCountriesPicked(config)
30
+
31
+ if (!result) return []
32
+ if (!Array.isArray(result)) return [result]
33
+ const isoList = result.map(country => country.iso)
34
+ return isoList
35
+ }, [config.general.countriesPicked])
36
+
37
+ // Memoize expensive computations for country centering
38
+ const projectionData = useMemo(() => {
39
+ if (!worldData || !countriesPicked.length) {
40
+ return {
41
+ projection: geoMercator(),
42
+ countryCenter: [0, 30] as [number, number], // Default world center
43
+ countryZoom: 1
44
+ }
45
+ }
46
+
47
+ const baseProjection = geoMercator()
48
+ .translate([SVG_WIDTH / 2, SVG_HEIGHT / 2])
49
+ .scale(1)
50
+
51
+ // List of ISO codes for small/uninhabited territories that should be excluded from zoom calculations
52
+ // These tiny islands can distort the bounding box and cause excessive zoom out
53
+ const excludedTerritories = [
54
+ 'ATF', // French Southern and Antarctic Lands (Kerguelen, Crozet, etc.)
55
+ 'HMD', // Heard Island and McDonald Islands
56
+ 'SGS', // South Georgia and South Sandwich Islands
57
+ 'BVT' // Bouvet Island
58
+ ]
59
+
60
+ // List of specific territory names that should be excluded even if they share an ISO with a main country
61
+ const excludedTerritoryNames = [
62
+ 'Saint Paul and Amsterdam Islands', // French territory in Indian Ocean
63
+ 'Kerguelen Islands',
64
+ 'Crozet Islands'
65
+ ]
66
+
67
+ // Filter world data to selected countries, excluding small territories from zoom calculation
68
+ const selectedCountriesData = worldData.filter(country => {
69
+ // Match by ISO code OR by name (to handle cases where config uses name instead of ISO)
70
+ const matchesSelectedCountry = countriesPicked.some(
71
+ iso => iso === country.properties.iso || iso === country.properties.name
72
+ )
73
+ const isExcludedByISO = excludedTerritories.includes(country.properties.iso)
74
+ const isExcludedByName = excludedTerritoryNames.some(
75
+ name => country.properties.name && country.properties.name.includes(name)
76
+ )
77
+
78
+ return matchesSelectedCountry && !isExcludedByISO && !isExcludedByName
79
+ })
80
+
81
+ if (!selectedCountriesData.length) {
82
+ return {
83
+ projection: baseProjection,
84
+ countryCenter: [0, 30] as [number, number], // Default world center
85
+ countryZoom: 1
86
+ }
87
+ }
88
+
89
+ // Create a feature collection for selected countries
90
+ const combinedData = {
91
+ type: 'FeatureCollection',
92
+ features: selectedCountriesData
93
+ }
94
+
95
+ // Fit projection to selected countries - this calculates the optimal scale and translate
96
+ const fittedProjection = baseProjection.fitExtent(
97
+ [
98
+ [SVG_PADDING, SVG_PADDING],
99
+ [SVG_WIDTH - SVG_PADDING, SVG_HEIGHT - SVG_PADDING]
100
+ ],
101
+ combinedData as any
102
+ )
103
+
104
+ // Calculate zoom level from the fitted projection
105
+ // The fitExtent method automatically calculates the scale needed to fit the features
106
+ const fittedScale = fittedProjection.scale()
107
+
108
+ // Use the default geoMercator scale (around 153) as base reference
109
+ const defaultMercatorScale = 153 // Standard geoMercator scale for world view
110
+ let calculatedZoom = fittedScale / defaultMercatorScale
111
+
112
+ // Clamp zoom to reasonable bounds
113
+ // Allow zoom out to 0.5x for large multi-country spans (e.g., Argentina + Canada)
114
+ // Max zoom of 10x for small countries
115
+ const clampedZoom = Math.max(0.5, Math.min(10, calculatedZoom))
116
+
117
+ // Calculate the center point using BOUNDING BOX CENTER (not area-weighted centroid)
118
+ // This ensures equal geographic treatment regardless of country size
119
+ const path: GeoPath = geoPath().projection(fittedProjection)
120
+ const bounds = path.bounds(combinedData as any)
121
+
122
+ // Calculate geographic bounds
123
+ const geoBounds = {
124
+ topLeft: fittedProjection.invert([bounds[0][0], bounds[0][1]]),
125
+ bottomRight: fittedProjection.invert([bounds[1][0], bounds[1][1]])
126
+ }
127
+
128
+ // Use bounding box midpoint as center (NOT area-weighted centroid which favors larger countries)
129
+ const countryCenter: [number, number] = [
130
+ (geoBounds.topLeft[0] + geoBounds.bottomRight[0]) / 2,
131
+ (geoBounds.topLeft[1] + geoBounds.bottomRight[1]) / 2
132
+ ]
133
+
134
+ const result = {
135
+ projection: fittedProjection,
136
+ countryCenter,
137
+ countryZoom: clampedZoom
138
+ }
139
+
140
+ return result
141
+ }, [worldData, countriesPicked.join(',')]) // Use string key for more stable memoization
142
+
143
+ const centerOnCountries = useCallback(
144
+ (zoomFunction: string = '') => {
145
+ if (!countriesPicked.length) {
146
+ return
147
+ }
148
+
149
+ const _prevPosition = config.mapPosition || { coordinates: [0, 30], zoom: 1 }
150
+ let newZoom = projectionData.countryZoom
151
+ let newCoordinates = projectionData.countryCenter
152
+
153
+ if (zoomFunction === 'zoomIn' && projectionData.countryZoom < 4) {
154
+ newZoom = Math.min(4, projectionData.countryZoom * 1.5)
155
+ newCoordinates = projectionData.countryCenter
156
+ publishAnalyticsEvent({
157
+ vizType: 'map',
158
+ vizSubType: getVizSubType(config),
159
+ eventType: 'zoom_in',
160
+ eventAction: 'click',
161
+ eventLabel: interactionLabel,
162
+ vizTitle: getVizTitle(config),
163
+ specifics: `zoom_level: ${Math.floor(newZoom)}`
164
+ })
165
+ } else if (zoomFunction === 'zoomOut' && projectionData.countryZoom > 1) {
166
+ newZoom = Math.max(1, projectionData.countryZoom / 1.5)
167
+ newCoordinates = projectionData.countryCenter
168
+ } else if (zoomFunction === 'reset') {
169
+ newZoom = projectionData.countryZoom
170
+ newCoordinates = projectionData.countryCenter
171
+ } else if (zoomFunction === 'center' || zoomFunction === '') {
172
+ // Auto-center with calculated zoom - this is the main centering logic
173
+ newZoom = projectionData.countryZoom
174
+ newCoordinates = projectionData.countryCenter
175
+ }
176
+
177
+ const payload = { coordinates: newCoordinates, zoom: newZoom }
178
+ dispatch({ type: 'SET_POSITION', payload })
179
+
180
+ if (zoomFunction === 'reset') {
181
+ publishAnalyticsEvent({
182
+ vizType: 'map',
183
+ vizSubType: getVizSubType(config),
184
+ eventType: 'map_reset_zoom_level',
185
+ eventAction: 'click',
186
+ eventLabel: interactionLabel,
187
+ vizTitle: getVizTitle(config)
188
+ })
189
+ }
190
+ },
191
+ [
192
+ config.mapPosition,
193
+ projectionData.countryCenter,
194
+ projectionData.countryZoom,
195
+ interactionLabel,
196
+ dispatch,
197
+ countriesPicked
198
+ ]
199
+ )
200
+
201
+ // Auto-center when countries are selected/changed
202
+ useEffect(() => {
203
+ const countriesKey = countriesPicked.sort().join(',')
204
+
205
+ if (!worldData || !countriesPicked.length) {
206
+ lastCenteredCountries.current = ''
207
+ return
208
+ }
209
+
210
+ // Only re-center if countries have actually changed
211
+ if (lastCenteredCountries.current === countriesKey) {
212
+ return
213
+ }
214
+
215
+ lastCenteredCountries.current = countriesKey
216
+
217
+ // Immediately center on the selected countries with calculated zoom
218
+ const payload = {
219
+ coordinates: projectionData.countryCenter,
220
+ zoom: projectionData.countryZoom
221
+ }
222
+
223
+ // Additional validation before dispatch
224
+ const [lng, lat] = payload.coordinates
225
+ const coordsValid = lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90
226
+ const zoomValid = payload.zoom >= 0.1 && payload.zoom <= 10
227
+
228
+ if (coordsValid && zoomValid) {
229
+ dispatch({ type: 'SET_POSITION', payload })
230
+ }
231
+ }, [worldData, countriesPicked, projectionData.countryCenter, projectionData.countryZoom, dispatch])
232
+
233
+ return {
234
+ centerOnCountries,
235
+ countriesPicked,
236
+ countryCenter: projectionData.countryCenter,
237
+ countryZoom: projectionData.countryZoom
238
+ }
239
+ }
240
+
241
+ export default useCountryZoom
@@ -31,7 +31,7 @@ const useGeoClickHandler = () => {
31
31
  }
32
32
 
33
33
  // If modals are set, or we are on a mobile viewport, display modal
34
- if (window.matchMedia('(any-hover: none)').matches || 'click' === state.tooltips.appearanceType) {
34
+ if ('click' === state.tooltips.appearanceType) {
35
35
  const modalData = {
36
36
  geoName: geoDisplayName,
37
37
  keyedData: geoData
@@ -0,0 +1,110 @@
1
+ import { useRef, useImperativeHandle, ForwardedRef } from 'react'
2
+
3
+ /**
4
+ * Interface for geography element refs exposed to parent components
5
+ * Follows chart package pattern for consistency
6
+ */
7
+ export interface MapRefInterface {
8
+ /**
9
+ * Programmatically trigger tooltip at a specific geography (data-centric approach)
10
+ * @param geoId - Geography identifier (FIPS code, name, ISO code, etc.)
11
+ * @param yCoordinate - Y coordinate for tooltip positioning (from source event)
12
+ */
13
+ triggerTooltipAtGeo: (geoId: string, yCoordinate: number) => void
14
+
15
+ /**
16
+ * Hide all tooltips for this map tile
17
+ */
18
+ hideTooltip: () => void
19
+ }
20
+
21
+ interface UseProgrammaticMapTooltipProps {
22
+ mapRef: ForwardedRef<MapRefInterface>
23
+ tooltipId: string
24
+ }
25
+
26
+ /**
27
+ * Custom hook to provide programmatic tooltip control for small multiples synchronization
28
+ *
29
+ * This hook enables tooltips to be triggered programmatically on geography elements
30
+ * using synthetic mouse events. It's designed to work with react-tooltip v5.
31
+ *
32
+ */
33
+ export const useProgrammaticMapTooltip = ({ mapRef, tooltipId }: UseProgrammaticMapTooltipProps) => {
34
+ // Store refs to all geography elements in this map
35
+ const geoElementRefs = useRef<Record<string, SVGElement>>({})
36
+
37
+ /**
38
+ * Register a geography element so it can be programmatically controlled
39
+ * Call this in the ref callback of each geography element
40
+ */
41
+ const registerGeoElement = (geoId: string, element: SVGElement | null) => {
42
+ geoElementRefs.current[geoId] = element
43
+ }
44
+
45
+ const unregisterGeoElement = (geoId: string) => {
46
+ delete geoElementRefs.current[geoId]
47
+ }
48
+
49
+ // Expose programmatic tooltip methods via ref
50
+ useImperativeHandle(
51
+ mapRef,
52
+ () => ({
53
+ /**
54
+ * Trigger tooltip at specific geography
55
+ */
56
+ triggerTooltipAtGeo: (geoId: string, yCoordinate: number) => {
57
+ const geoElement = geoElementRefs.current[geoId]
58
+ if (!geoElement) {
59
+ return
60
+ }
61
+
62
+ // Get the horizontal center of the geography element
63
+ const rect = geoElement.getBoundingClientRect()
64
+ const centerX = rect.left + rect.width / 2
65
+
66
+ // Create synthetic mouseenter event with coordinates
67
+ const syntheticEvent = new MouseEvent('mouseenter', {
68
+ bubbles: true,
69
+ cancelable: true,
70
+ clientX: centerX,
71
+ clientY: yCoordinate,
72
+ screenX: window.screenX + centerX,
73
+ screenY: window.screenY + yCoordinate,
74
+ view: window
75
+ })
76
+
77
+ Object.defineProperty(syntheticEvent, 'currentTarget', {
78
+ value: geoElement,
79
+ writable: false
80
+ })
81
+
82
+ Object.defineProperty(syntheticEvent, 'target', {
83
+ value: geoElement,
84
+ writable: false
85
+ })
86
+
87
+ geoElement.dispatchEvent(syntheticEvent)
88
+ },
89
+
90
+ hideTooltip: () => {
91
+ Object.values(geoElementRefs.current).forEach(element => {
92
+ const syntheticEvent = new MouseEvent('mouseleave', {
93
+ bubbles: true,
94
+ cancelable: true,
95
+ view: window
96
+ })
97
+
98
+ element.dispatchEvent(syntheticEvent)
99
+ })
100
+ }
101
+ }),
102
+ [tooltipId]
103
+ )
104
+
105
+ return {
106
+ registerGeoElement,
107
+ unregisterGeoElement,
108
+ geoElementRefs
109
+ }
110
+ }
@@ -8,17 +8,20 @@ import ResizeObserver from 'resize-observer-polyfill'
8
8
  export const useResizeObserver = (isEditor: boolean) => {
9
9
  const [dimensions, setDimensions] = useState<DimensionsType>([0, 0])
10
10
  const [currentViewport, setCurrentViewport] = useState<ViewPort>(null)
11
+ const [vizViewport, setVizViewport] = useState<ViewPort>(null)
11
12
  const [container, setContainer] = useState<HTMLElement | null>(null)
12
13
 
13
14
  const resizeObserver = new ResizeObserver(entries => {
14
15
  for (let entry of entries) {
15
16
  let { width, height } = entry.contentRect
16
17
 
17
- width = isEditor ? width - EDITOR_WIDTH : width
18
+ const editorIsOpen = isEditor && !!document.querySelector('.editor-panel:not(.hidden)')
19
+ width = editorIsOpen ? width - EDITOR_WIDTH : width
18
20
 
19
21
  const newViewport = getViewport(width)
20
22
 
21
23
  setCurrentViewport(newViewport)
24
+ setVizViewport(newViewport)
22
25
 
23
26
  setDimensions([width, height])
24
27
  }
@@ -35,7 +38,7 @@ export const useResizeObserver = (isEditor: boolean) => {
35
38
  }
36
39
  }, [])
37
40
 
38
- return { resizeObserver, dimensions, currentViewport, outerContainerRef, container }
41
+ return { resizeObserver, dimensions, currentViewport, vizViewport, outerContainerRef, container }
39
42
  }
40
43
 
41
44
  export default useResizeObserver
@@ -67,7 +67,9 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
67
67
  const featureCenter = combinedData ? path.centroid(combinedData as any) : [0, 0]
68
68
  const stateCenter = newProjection.invert(featureCenter)
69
69
 
70
- return { projection, newProjection, stateCenter }
70
+ const bounds = combinedData ? path.bounds(combinedData as any) : null
71
+
72
+ return { projection, newProjection, stateCenter, bounds }
71
73
  }, [topoData, statesPicked])
72
74
 
73
75
  const setScaleAndTranslate = useCallback(
@@ -176,7 +178,8 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
176
178
  handleZoomOut,
177
179
  handleMoveEnd,
178
180
  handleZoomReset,
179
- projection: projectionData.projection
181
+ projection: projectionData.projection,
182
+ bounds: projectionData.bounds
180
183
  }
181
184
  }
182
185
 
@@ -0,0 +1,56 @@
1
+ import { useContext } from 'react'
2
+ import ConfigContext from '../context'
3
+ import { useProgrammaticMapTooltip } from './useProgrammaticMapTooltip'
4
+
5
+ /**
6
+ * Hook to enable synchronized tooltips for map geographies in small multiples
7
+ *
8
+ * This abstracts all the small multiples synchronization logic so it can be
9
+ * easily added to any map type (US, Region, SingleState, World, etc.)
10
+ *
11
+ */
12
+ export const useSynchronizedGeographies = () => {
13
+ const { handleSmallMultipleHover, mapRefForSync, tooltipId } = useContext(ConfigContext)
14
+
15
+ // Set up programmatic tooltip control for this map
16
+ const { registerGeoElement, unregisterGeoElement } = useProgrammaticMapTooltip({
17
+ mapRef: mapRefForSync || { current: null },
18
+ tooltipId
19
+ })
20
+
21
+ /**
22
+ * Returns props to spread onto geography elements for synchronized tooltip support
23
+ * Includes ref for programmatic control and data-geo-id for tooltip tracking
24
+ *
25
+ * @param geoKey - Geography identifier (FIPS code, state abbreviation, etc.)
26
+ */
27
+ const getSyncProps = (geoKey: string) => ({
28
+ ref: (el: SVGElement | null) => {
29
+ if (el) {
30
+ registerGeoElement(geoKey, el)
31
+ } else {
32
+ unregisterGeoElement(geoKey)
33
+ }
34
+ },
35
+ 'data-geo-id': geoKey
36
+ })
37
+
38
+ /**
39
+ * Event handler helpers for synchronized tooltips
40
+ * Call these inside your existing event handlers
41
+ */
42
+ const syncHandlers = {
43
+ onMouseEnter: (geoKey: string, clientY: number) => {
44
+ handleSmallMultipleHover?.(geoKey, clientY)
45
+ },
46
+
47
+ onMouseLeave: () => {
48
+ handleSmallMultipleHover?.(null)
49
+ }
50
+ }
51
+
52
+ return {
53
+ getSyncProps,
54
+ syncHandlers
55
+ }
56
+ }
package/src/index.jsx CHANGED
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import ReactDOM from 'react-dom/client'
3
3
 
4
4
  import '@cdc/core/styles/cove-main.scss'
5
+ import '@cdc/core/components/EditorPanel/EditorPanel.styles.css'
5
6
 
6
7
  import CdcMap from './CdcMap'
7
8