@cdc/map 4.25.10 → 4.26.1

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 (107) 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 +58397 -55987
  7. package/examples/example-city-state.json +9 -1
  8. package/examples/multi-country-centering.json +45 -0
  9. package/examples/private/city_styles_variable.json +877 -0
  10. package/examples/private/colors-2.json +221 -0
  11. package/examples/private/colors.json +221 -0
  12. package/examples/private/map-filter-issue.json +2260 -0
  13. package/examples/private/map-legend.json +5303 -0
  14. package/index.html +27 -36
  15. package/package.json +6 -5
  16. package/src/CdcMapComponent.tsx +86 -26
  17. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  18. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  19. package/src/_stories/CdcMap.Editor.stories.tsx +3426 -0
  20. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  21. package/src/_stories/CdcMap.stories.tsx +116 -4
  22. package/src/_stories/_mock/column-wrap-test.json +265 -0
  23. package/src/_stories/_mock/multi-country-hide.json +78 -0
  24. package/src/_stories/_mock/multi-country.json +95 -0
  25. package/src/_stories/_mock/multi-state.json +887 -20403
  26. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  27. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  28. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  29. package/src/_stories/_mock/usa-state-gradient.json +3 -4
  30. package/src/components/BubbleList.tsx +1 -1
  31. package/src/components/CityList.tsx +24 -18
  32. package/src/components/EditorPanel/components/EditorPanel.tsx +2380 -2206
  33. package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
  34. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -19
  35. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
  36. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +351 -0
  37. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  38. package/src/components/Geo.tsx +20 -3
  39. package/src/components/Legend/components/Legend.tsx +58 -75
  40. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +1 -1
  41. package/src/components/Legend/components/index.scss +23 -6
  42. package/src/components/NavigationMenu.tsx +16 -13
  43. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  44. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  45. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  46. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  47. package/src/components/SmallMultiples/index.tsx +3 -0
  48. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +18 -3
  49. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  50. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  51. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +29 -9
  52. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +7 -0
  53. package/src/components/UsaMap/components/UsaMap.County.tsx +16 -4
  54. package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
  55. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +29 -12
  56. package/src/components/UsaMap/components/UsaMap.State.tsx +30 -5
  57. package/src/components/UsaMap/helpers/map.ts +2 -2
  58. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  59. package/src/components/WorldMap/WorldMap.tsx +81 -11
  60. package/src/data/initial-state.js +11 -0
  61. package/src/data/supported-geos.js +8 -76
  62. package/src/helpers/addUIDs.ts +13 -2
  63. package/src/helpers/applyColorToLegend.ts +25 -1
  64. package/src/helpers/applyLegendToRow.ts +5 -3
  65. package/src/helpers/constants.ts +3 -15
  66. package/src/helpers/displayGeoName.ts +22 -4
  67. package/src/helpers/generateRuntimeFilters.ts +1 -1
  68. package/src/helpers/generateRuntimeLegend.ts +1 -3
  69. package/src/helpers/generateRuntimeLegendHash.ts +1 -1
  70. package/src/helpers/getCountriesPicked.ts +103 -0
  71. package/src/helpers/getMapContainerClasses.ts +7 -0
  72. package/src/helpers/getPatternForRow.ts +2 -5
  73. package/src/helpers/index.ts +2 -4
  74. package/src/helpers/isLegendItemDisabled.ts +2 -2
  75. package/src/helpers/resetLegendToggles.ts +1 -0
  76. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  77. package/src/helpers/tests/hashObj.test.ts +1 -1
  78. package/src/helpers/tests/titleCase.test.ts +76 -0
  79. package/src/helpers/titleCase.ts +13 -13
  80. package/src/helpers/toggleLegendActive.ts +76 -8
  81. package/src/helpers/urlDataHelpers.ts +1 -1
  82. package/src/hooks/useCountryZoom.tsx +241 -0
  83. package/src/hooks/useGeoClickHandler.ts +1 -1
  84. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  85. package/src/hooks/useResizeObserver.ts +8 -2
  86. package/src/hooks/useStateZoom.tsx +7 -4
  87. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  88. package/src/index.jsx +1 -0
  89. package/src/scss/editor-panel.scss +4 -440
  90. package/src/scss/main.scss +1 -1
  91. package/src/scss/map.scss +12 -15
  92. package/src/store/map.actions.ts +7 -7
  93. package/src/test/CdcMap.test.jsx +1 -1
  94. package/src/types/MapConfig.ts +32 -11
  95. package/src/types/MapContext.ts +6 -0
  96. package/src/types/runtimeLegend.ts +2 -1
  97. package/LICENSE +0 -201
  98. package/src/components/DataTable.tsx +0 -413
  99. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  100. package/src/components/MapControls.tsx +0 -44
  101. package/src/helpers/getUniqueValues.ts +0 -19
  102. package/src/helpers/hashObj.ts +0 -25
  103. package/src/hooks/useActiveElement.ts +0 -19
  104. package/src/hooks/useLegendSeparators.ts +0 -26
  105. package/src/scss/mixins.scss +0 -47
  106. package/src/types/Annotations.ts +0 -24
  107. /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
@@ -0,0 +1,105 @@
1
+ import React, { useState, useEffect, useRef, RefObject } from 'react'
2
+
3
+ interface SynchronizedTooltipProps {
4
+ tileTooltipId: string
5
+ opacity: number
6
+ containerRef: RefObject<HTMLElement>
7
+ }
8
+
9
+ /**
10
+ * Custom tooltip component for small multiples that responds to synthetic events
11
+ * This bypasses react-tooltip's lazy initialization issues
12
+ */
13
+ const SynchronizedTooltip: React.FC<SynchronizedTooltipProps> = ({ tileTooltipId, opacity, containerRef }) => {
14
+ const [tooltipState, setTooltipState] = useState<{
15
+ visible: boolean
16
+ html: string
17
+ x: number
18
+ y: number
19
+ }>({
20
+ visible: false,
21
+ html: '',
22
+ x: 0,
23
+ y: 0
24
+ })
25
+
26
+ const tooltipRef = useRef<HTMLDivElement>(null)
27
+ const currentGeoRef = useRef<string | null>(null)
28
+
29
+ useEffect(() => {
30
+ const handleMouseEnter = (e: MouseEvent) => {
31
+ const target = e.target as HTMLElement
32
+ const tooltipId = target.getAttribute('data-tooltip-id')
33
+
34
+ if (tooltipId === `tooltip__${tileTooltipId}`) {
35
+ const tooltipHtml = target.getAttribute('data-tooltip-html')
36
+ const geoId = target.getAttribute('data-geo-id') || ''
37
+
38
+ // Don't show tooltip if there's no content
39
+ if (!tooltipHtml || tooltipHtml === 'undefined' || tooltipHtml.trim() === '') {
40
+ setTooltipState(prev => ({ ...prev, visible: false }))
41
+ currentGeoRef.current = null
42
+ return
43
+ }
44
+
45
+ // Only update if we're entering a different geography or showing for first time
46
+ if (geoId !== currentGeoRef.current) {
47
+ currentGeoRef.current = geoId
48
+
49
+ // Position tooltip relative to the geography element's bounding box
50
+ const rect = target.getBoundingClientRect()
51
+ const x = rect.left + rect.width / 2
52
+ const y = rect.top + rect.height / 2
53
+
54
+ setTooltipState({
55
+ visible: true,
56
+ html: tooltipHtml,
57
+ x,
58
+ y
59
+ })
60
+ }
61
+ }
62
+ }
63
+
64
+ const handleMouseLeave = (e: MouseEvent) => {
65
+ const target = e.target as HTMLElement
66
+ const tooltipId = target.getAttribute('data-tooltip-id')
67
+
68
+ if (tooltipId === `tooltip__${tileTooltipId}`) {
69
+ setTooltipState(prev => ({ ...prev, visible: false }))
70
+ currentGeoRef.current = null
71
+ }
72
+ }
73
+
74
+ const container = containerRef.current
75
+ if (!container) return
76
+
77
+ container.addEventListener('mouseenter', handleMouseEnter, true)
78
+ container.addEventListener('mouseleave', handleMouseLeave, true)
79
+
80
+ return () => {
81
+ container.removeEventListener('mouseenter', handleMouseEnter, true)
82
+ container.removeEventListener('mouseleave', handleMouseLeave, true)
83
+ }
84
+ }, [tileTooltipId, containerRef])
85
+
86
+ if (!tooltipState.visible) return null
87
+
88
+ return (
89
+ <div
90
+ ref={tooltipRef}
91
+ className='tooltip tooltip-test'
92
+ style={{
93
+ position: 'fixed',
94
+ left: `${tooltipState.x + 10}px`,
95
+ top: `${tooltipState.y + 10}px`,
96
+ background: `rgba(255,255,255, ${opacity / 100})`,
97
+ pointerEvents: 'none',
98
+ zIndex: 9999
99
+ }}
100
+ dangerouslySetInnerHTML={{ __html: tooltipState.html }}
101
+ />
102
+ )
103
+ }
104
+
105
+ export default SynchronizedTooltip
@@ -0,0 +1,3 @@
1
+ export { default } from './SmallMultiples'
2
+ export { default as SmallMultiples } from './SmallMultiples'
3
+ export { default as SmallMultipleTile } from './SmallMultipleTile'
@@ -8,6 +8,7 @@ import { applyLegendToRow } from '../../../../helpers/applyLegendToRow'
8
8
  import useGeoClickHandler, { geoClickHandler } from '././../../../../hooks/useGeoClickHandler'
9
9
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
10
10
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
11
+ import { useSynchronizedGeographies } from '../../../../hooks/useSynchronizedGeographies'
11
12
 
12
13
  interface CountyOutputProps {
13
14
  counties: any[]
@@ -23,6 +24,7 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
23
24
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
24
25
  const geoFillColor = getGeoFillColor(config)
25
26
  const { geoClickHandler } = useGeoClickHandler()
27
+ const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
26
28
 
27
29
  return (
28
30
  <>
@@ -71,14 +73,16 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
71
73
 
72
74
  return (
73
75
  <g
76
+ {...getSyncProps(geoKey)}
74
77
  key={`key--${county.id}`}
75
- className={`county county--${geoDisplayName.split(' ').join('')} county--${geoData[config.columns.geo.name]
76
- }`}
78
+ className={`county county--${geoDisplayName.split(' ').join('')} county--${
79
+ geoData[config.columns.geo.name]
80
+ }`}
77
81
  style={styles}
78
82
  onClick={() => geoClickHandler(geoDisplayName, geoData)}
79
83
  data-tooltip-id={`tooltip__${tooltipId}`}
80
84
  data-tooltip-html={toolTip}
81
- onMouseEnter={() => {
85
+ onMouseEnter={e => {
82
86
  // Track hover analytics event if this is a new location
83
87
  const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
84
88
  publishAnalyticsEvent({
@@ -91,6 +95,10 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
91
95
  location: geoDisplayName,
92
96
  specifics: `location: ${locationName?.toLowerCase()}`
93
97
  })
98
+ syncHandlers.onMouseEnter(geoKey, e.clientY)
99
+ }}
100
+ onMouseLeave={() => {
101
+ syncHandlers.onMouseLeave()
94
102
  }}
95
103
  >
96
104
  <path
@@ -105,11 +113,18 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
105
113
  } else {
106
114
  return (
107
115
  <g
116
+ {...getSyncProps(geoKey)}
108
117
  key={`key--${county.id}`}
109
118
  className={`county county--${geoDisplayName.split(' ').join('')}`}
110
119
  style={{ fill: geoFillColor }}
111
120
  data-tooltip-id={`tooltip__${tooltipId}`}
112
121
  data-tooltip-html={toolTip}
122
+ onMouseEnter={e => {
123
+ syncHandlers.onMouseEnter(geoKey, e.clientY)
124
+ }}
125
+ onMouseLeave={() => {
126
+ syncHandlers.onMouseLeave()
127
+ }}
113
128
  >
114
129
  <path
115
130
  tabIndex={-1}
@@ -12,7 +12,7 @@ type TerritoriesSectionProps = {
12
12
  }
13
13
 
14
14
  const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, logo, config, territoriesData }) => {
15
- const { currentViewport } = useContext<MapContext>(ConfigContext)
15
+ const { currentViewport, vizViewport } = useContext<MapContext>(ConfigContext)
16
16
 
17
17
  // filter territioriesData into the two groups below
18
18
  const freelyAssociatedKeys = territoriesData.filter(territory => {
@@ -38,7 +38,7 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
38
38
  return a.props.label.localeCompare(b.props.label)
39
39
  })
40
40
 
41
- const isMobileViewport = isMobileTerritoryViewport(currentViewport)
41
+ const isMobileViewport = isMobileTerritoryViewport(vizViewport)
42
42
  const SVG_GAP = 9
43
43
  const SVG_WIDTH = isMobileViewport ? 30 : 45
44
44
 
@@ -55,10 +55,18 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
55
55
  <div className='d-flex flex-wrap' style={{ columnGap: '1.5rem' }}>
56
56
  {(usTerritories.length > 0 || config.general.territoriesAlwaysShow) && (
57
57
  <div>
58
- <span className='territories-label'>U.S. territories</span>
58
+ <span className='territories-label' style={{ fontSize: isMobileViewport ? '0.8rem' : '1rem' }}>
59
+ U.S. territories
60
+ </span>
59
61
  <span
60
- className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
61
- style={{ minWidth: `${usTerritories.length * SVG_WIDTH + (usTerritories.length - 1) * SVG_GAP}px` }}
62
+ className={`${isMobileViewport ? 'mt-1 mb-3' : 'mt-2 mb-4'} d-flex territories`}
63
+ style={
64
+ {
65
+ minWidth: `${usTerritories.length * SVG_WIDTH + (usTerritories.length - 1) * SVG_GAP}px`,
66
+ '--territory-svg-max-width': `${SVG_WIDTH}px`,
67
+ '--territory-svg-min-width': `${SVG_WIDTH}px`
68
+ } as React.CSSProperties
69
+ }
62
70
  >
63
71
  {usTerritories}
64
72
  </span>
@@ -66,14 +74,20 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
66
74
  )}
67
75
  {(freelyAssociatedStates.length > 0 || config.general.territoriesAlwaysShow) && (
68
76
  <div>
69
- <span className='territories-label'>Freely associated states</span>
77
+ <span className='territories-label' style={{ fontSize: isMobileViewport ? '0.8rem' : '1rem' }}>
78
+ Freely associated states
79
+ </span>
70
80
  <span
71
- className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
72
- style={{
73
- minWidth: `${
74
- freelyAssociatedStates.length * SVG_WIDTH + (freelyAssociatedStates.length - 1) * SVG_GAP
75
- }px`
76
- }}
81
+ className={`${isMobileViewport ? 'mt-1 mb-3' : 'mt-2 mb-4'} d-flex territories`}
82
+ style={
83
+ {
84
+ minWidth: `${
85
+ freelyAssociatedStates.length * SVG_WIDTH + (freelyAssociatedStates.length - 1) * SVG_GAP
86
+ }px`,
87
+ '--territory-svg-max-width': `${SVG_WIDTH}px`,
88
+ '--territory-svg-min-width': `${SVG_WIDTH}px`
89
+ } as React.CSSProperties
90
+ }
77
91
  >
78
92
  {freelyAssociatedStates}
79
93
  </span>
@@ -43,12 +43,17 @@ const TerritoryHexagon = ({
43
43
  territory,
44
44
  territoryData,
45
45
  textColor,
46
+ getSyncProps,
47
+ syncHandlers,
46
48
  ...props
47
49
  }) => {
48
50
  const { config } = useContext<MapContext>(ConfigContext)
49
51
 
50
52
  const isHex = config.general.displayAsHex
51
53
 
54
+ // Construct geography key: use territory prop if available, otherwise construct from label
55
+ const geoKey = territory || `US-${label}`
56
+
52
57
  // Labels
53
58
  const hexagonLabel = (geo, bgColor = '#FFFFFF', projection) => {
54
59
  let centroid = projection ? projection(geoCentroid(geo)) : [22, 17.5]
@@ -133,7 +138,14 @@ const TerritoryHexagon = ({
133
138
  fontSize={14}
134
139
  x={'50%'}
135
140
  y={y}
136
- style={{ fill: 'currentColor', stroke: strokeColor, fontWeight: 900, opacity: 1, fillOpacity: 1 }}
141
+ style={{
142
+ fill: 'currentColor',
143
+ stroke: strokeColor,
144
+ fontWeight: 900,
145
+ opacity: 1,
146
+ fillOpacity: 1,
147
+ pointerEvents: 'none'
148
+ }}
137
149
  paintOrder='stroke'
138
150
  textAnchor='middle'
139
151
  verticalAnchor='middle'
@@ -143,7 +155,9 @@ const TerritoryHexagon = ({
143
155
  >
144
156
  {abbr.substring(3)}
145
157
  </Text>
146
- {config.general.displayAsHex && config.hexMap.type === 'shapes' && getArrowDirection(territoryData, geo, true)}
158
+ {config.general.displayAsHex &&
159
+ config.hexMap.type === 'shapes' &&
160
+ getArrowDirection(territoryData, geo, true)}
147
161
  </>
148
162
  )
149
163
  }
@@ -151,7 +165,7 @@ const TerritoryHexagon = ({
151
165
  let [dx, dy] = offsets[abbr]
152
166
 
153
167
  return (
154
- <g>
168
+ <g style={{ pointerEvents: 'none' }}>
155
169
  <line
156
170
  x1={centroid[0]}
157
171
  y1={centroid[1]}
@@ -179,11 +193,23 @@ const TerritoryHexagon = ({
179
193
 
180
194
  return (
181
195
  <svg viewBox='-1 -1 46 53' className='territory-wrapper--hex'>
182
- <g {...props} data-tooltip-html={dataTooltipHtml} data-tooltip-id={dataTooltipId} onClick={handleShapeClick}>
196
+ <g
197
+ {...(getSyncProps ? getSyncProps(geoKey) : {})}
198
+ {...props}
199
+ data-tooltip-html={dataTooltipHtml}
200
+ data-tooltip-id={dataTooltipId}
201
+ onClick={handleShapeClick}
202
+ >
183
203
  <polygon
184
204
  stroke={stroke}
185
205
  strokeWidth={strokeWidth}
186
206
  points='22 0 44 12.702 44 38.105 22 50.807 0 38.105 0 12.702'
207
+ onMouseEnter={e => {
208
+ syncHandlers?.onMouseEnter(geoKey, e.clientY)
209
+ }}
210
+ onMouseLeave={() => {
211
+ syncHandlers?.onMouseLeave()
212
+ }}
187
213
  />
188
214
  {config.general.displayAsHex && hexagonLabel(territoryData, stroke, false)}
189
215
  </g>
@@ -18,6 +18,10 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
18
18
  territory,
19
19
  textColor,
20
20
  backgroundColor,
21
+ mapId,
22
+ svgStyle,
23
+ getSyncProps,
24
+ syncHandlers,
21
25
  ...props
22
26
  }) => {
23
27
  const { config } = useContext<MapContext>(ConfigContext)
@@ -25,17 +29,32 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
25
29
  const rectanglePath =
26
30
  'M42,0.5 C42.8284271,0.5 43.5,1.17157288 43.5,2 L43.5,2 L43.5,26 C43.5,26.8284271 42.8284271,27.5 42,27.5 L42,27.5 L3,27.5 C2.17157288,27.5 1.5,26.8284271 1.5,26 L1.5,26 L1.5,2 C1.5,1.17157288 2.17157288,0.5 3,0.5 L3,0.5 Z'
27
31
 
32
+ const geoKey = territory || `US-${label}`
33
+
28
34
  return (
29
- <svg viewBox='0 0 45 29' key={territory} className={territory}>
35
+ <svg viewBox='0 0 45 29' key={geoKey} className={geoKey} style={svgStyle}>
30
36
  <g
37
+ {...(getSyncProps ? getSyncProps(geoKey) : {})}
31
38
  {...otherProps}
32
39
  strokeLinejoin='round'
33
40
  tabIndex={-1}
34
41
  onClick={handleShapeClick}
35
42
  data-tooltip-id={dataTooltipId}
36
43
  data-tooltip-html={dataTooltipHtml}
44
+ onMouseEnter={e => {
45
+ syncHandlers?.onMouseEnter(geoKey, e.clientY)
46
+ }}
47
+ onMouseLeave={() => {
48
+ syncHandlers?.onMouseLeave()
49
+ }}
37
50
  >
38
- <path stroke={stroke} strokeWidth={strokeWidth} d={rectanglePath} {...otherProps} />
51
+ <path
52
+ stroke={stroke}
53
+ strokeWidth={strokeWidth}
54
+ d={rectanglePath}
55
+ style={{ pointerEvents: 'none' }}
56
+ {...otherProps}
57
+ />
39
58
  <text
40
59
  textAnchor='middle'
41
60
  dominantBaseline='middle'
@@ -45,14 +64,14 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
45
64
  stroke={strokeColor}
46
65
  className='territory-text'
47
66
  paintOrder='stroke'
48
- onClick={handleShapeClick}
67
+ style={{ pointerEvents: 'none' }}
49
68
  data-tooltip-id={dataTooltipId}
50
69
  data-tooltip-html={dataTooltipHtml}
51
70
  >
52
71
  {label}
53
72
  </text>
54
73
 
55
- {config.map.patterns.map((patternData, patternIndex) => {
74
+ {config.map?.patterns?.map((patternData, patternIndex) => {
56
75
  const patternColor = patternData.color || getContrastColor('#FFF', backgroundColor)
57
76
  const hasMatchingValues = patternData.dataValue === territoryData?.[patternData.dataKey]
58
77
 
@@ -63,7 +82,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
63
82
  <>
64
83
  {patternData?.pattern === 'waves' && (
65
84
  <PatternWaves
66
- id={`territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
85
+ id={`${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
67
86
  height={patternSizes[patternData?.size] ?? 10}
68
87
  width={patternSizes[patternData?.size] ?? 10}
69
88
  fill={patternColor}
@@ -73,7 +92,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
73
92
  )}
74
93
  {patternData?.pattern === 'circles' && (
75
94
  <PatternCircles
76
- id={`territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
95
+ id={`${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
77
96
  height={patternSizes[patternData?.size] ?? 10}
78
97
  width={patternSizes[patternData?.size] ?? 10}
79
98
  fill={patternColor}
@@ -84,7 +103,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
84
103
  )}
85
104
  {patternData?.pattern === 'lines' && (
86
105
  <PatternLines
87
- id={`territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
106
+ id={`${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
88
107
  height={patternSizes[patternData?.size] ?? 6}
89
108
  width={patternSizes[patternData?.size] ?? 6}
90
109
  stroke={patternColor}
@@ -96,7 +115,8 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
96
115
  stroke={stroke}
97
116
  strokeWidth={strokeWidth}
98
117
  d={rectanglePath}
99
- fill={`url(#territory-${territory}-${patternData?.dataKey}--${patternIndex})`}
118
+ fill={`url(#${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex})`}
119
+ style={{ pointerEvents: 'none' }}
100
120
  className={[
101
121
  `territory-pattern-${patternData?.dataKey}`,
102
122
  `territory-pattern-${patternData?.dataKey}--${patternData.dataValue}`
@@ -111,7 +131,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
111
131
  stroke={strokeColor}
112
132
  className='territory-text'
113
133
  paintOrder='stroke'
114
- onClick={handleShapeClick}
134
+ style={{ pointerEvents: 'none' }}
115
135
  data-tooltip-id={dataTooltipId}
116
136
  data-tooltip-html={dataTooltipHtml}
117
137
  >
@@ -11,4 +11,11 @@ export type TerritoryShape = {
11
11
  territory: string
12
12
  territoryData: object
13
13
  textColor: string
14
+ mapId?: string
15
+ svgStyle?: React.CSSProperties
16
+ getSyncProps?: (geoKey: string) => any
17
+ syncHandlers?: {
18
+ onMouseEnter: (geoKey: string, clientY: number) => void
19
+ onMouseLeave: () => void
20
+ }
14
21
  }
@@ -15,7 +15,7 @@ import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
15
15
  import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
16
16
  import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo'
17
17
  import { MapConfig } from '../../../types/MapConfig'
18
- import { DEFAULT_MAP_BACKGROUND } from '../../../helpers/constants'
18
+ import { DEFAULT_MAP_BACKGROUND, DISABLED_MAP_COLOR } from '../../../helpers/constants'
19
19
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
20
20
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
21
21
 
@@ -501,8 +501,20 @@ const CountyMap = () => {
501
501
  const distance = Math.hypot(pixelCoords[0] - x, pixelCoords[1] - y)
502
502
  if (
503
503
  distance < 15 &&
504
- applyLegendToRow(runtimeData[runtimeKeys[i]], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo) &&
505
- !isLegendItemDisabled(runtimeData[runtimeKeys[i]], runtimeLegend, legendMemo, legendSpecialClassLastMemo, config)
504
+ applyLegendToRow(
505
+ runtimeData[runtimeKeys[i]],
506
+ config,
507
+ runtimeLegend,
508
+ legendMemo,
509
+ legendSpecialClassLastMemo
510
+ ) &&
511
+ !isLegendItemDisabled(
512
+ runtimeData[runtimeKeys[i]],
513
+ runtimeLegend,
514
+ legendMemo,
515
+ legendSpecialClassLastMemo,
516
+ config
517
+ )
506
518
  ) {
507
519
  hoveredGeo = runtimeData[runtimeKeys[i]]
508
520
  hoveredGeoIndex = i
@@ -696,7 +708,7 @@ const CountyMap = () => {
696
708
  ? applyLegendToRow(runtimeData[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
697
709
  : false
698
710
  if (legendValues) {
699
- if (legendValues?.[0] === '#000000' || legendValues?.[0] === DEFAULT_MAP_BACKGROUND) return
711
+ if (legendValues?.[0] === '#000000' || legendValues?.[0] === DISABLED_MAP_COLOR) return
700
712
  const shapeType = config.visual.cityStyle.toLowerCase()
701
713
  const shapeProperties = createShapeProperties(shapeType, pixelCoords, legendValues, config, geoRadius)
702
714
  if (shapeProperties) {
@@ -12,6 +12,7 @@ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
12
12
  import ConfigContext from '../../../context'
13
13
  import { useLegendMemoContext } from '../../../context/LegendMemoContext'
14
14
  import Annotation from '../../Annotation'
15
+ import SmallMultiples from '../../SmallMultiples/SmallMultiples'
15
16
 
16
17
  // Data
17
18
  import { supportedTerritories } from '../../../data/supported-geos'
@@ -23,6 +24,7 @@ import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
23
24
  import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo'
24
25
  import './UsaMap.Region.styles.css'
25
26
  import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
27
+ import { useSynchronizedGeographies } from '../../../hooks/useSynchronizedGeographies'
26
28
 
27
29
  type TerritoryRectProps = {
28
30
  posX?: number
@@ -59,6 +61,7 @@ const UsaRegionMap = () => {
59
61
  const [focusedStates, setFocusedStates] = useState(null)
60
62
  const { geoClickHandler } = useGeoClickHandler()
61
63
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
64
+ const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
62
65
  const { general } = config
63
66
  const { displayStateLabels, territoriesLabel, displayAsHex, type } = general
64
67
  const tooltipInteractionType = config.tooltips.appearanceType
@@ -88,6 +91,11 @@ const UsaRegionMap = () => {
88
91
  return <></>
89
92
  }
90
93
 
94
+ // Early return for small multiples rendering
95
+ if (config.smallMultiples?.mode) {
96
+ return <SmallMultiples />
97
+ }
98
+
91
99
  const geoStrokeColor = getGeoStrokeColor(config)
92
100
  const geoFillColor = getGeoFillColor(config)
93
101
 
@@ -212,6 +220,7 @@ const UsaRegionMap = () => {
212
220
 
213
221
  return (
214
222
  <g
223
+ {...getSyncProps(geoKey)}
215
224
  key={key}
216
225
  className='geo-group'
217
226
  style={styles}
@@ -219,7 +228,7 @@ const UsaRegionMap = () => {
219
228
  data-tooltip-id={`tooltip__${tooltipId}`}
220
229
  data-tooltip-html={toolTip}
221
230
  tabIndex={-1}
222
- onMouseEnter={() => {
231
+ onMouseEnter={e => {
223
232
  // Track hover analytics event if this is a new location
224
233
  const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
225
234
  publishAnalyticsEvent({
@@ -232,6 +241,10 @@ const UsaRegionMap = () => {
232
241
  location: geoDisplayName,
233
242
  specifics: `location: ${locationName?.toLowerCase()}`
234
243
  })
244
+ syncHandlers.onMouseEnter(geoKey, e.clientY)
245
+ }}
246
+ onMouseLeave={() => {
247
+ syncHandlers.onMouseLeave()
235
248
  }}
236
249
  >
237
250
  <path tabIndex={-1} className='single-geo' stroke={geoStrokeColor} strokeWidth={1} d={path} />
@@ -13,6 +13,7 @@ import ZoomControls from '../../ZoomControls'
13
13
  import { MapContext } from '../../../types/MapContext'
14
14
  import useStateZoom from '../../../hooks/useStateZoom'
15
15
  import { Text } from '@visx/text'
16
+ import SmallMultiples from '../../SmallMultiples/SmallMultiples'
16
17
 
17
18
  import './UsaMap.SingleState.styles.css'
18
19
 
@@ -37,11 +38,12 @@ const SingleStateMap: React.FC = () => {
37
38
  position,
38
39
  topoData,
39
40
  scale,
40
- translate
41
+ translate,
42
+ useDynamicViewbox
41
43
  } = useContext<MapContext>(ConfigContext)
42
44
 
43
45
  const dispatch = useContext(MapDispatchContext)
44
- const { handleMoveEnd, handleZoomIn, handleZoomOut, handleZoomReset, projection } = useStateZoom(topoData)
46
+ const { handleMoveEnd, handleZoomIn, handleZoomOut, handleZoomReset, projection, bounds } = useStateZoom(topoData)
45
47
 
46
48
  // Memoize statesPicked to prevent creating new arrays on every render
47
49
  const statesPicked = useMemo(() => {
@@ -56,12 +58,22 @@ const SingleStateMap: React.FC = () => {
56
58
 
57
59
  const { geoClickHandler } = useGeoClickHandler()
58
60
 
59
- const cityListProjection = geoAlbersUsaTerritories()
60
- .translate([SVG_WIDTH / 2, SVG_HEIGHT / 2])
61
- .scale(1)
62
61
  const geoStrokeColor = getGeoStrokeColor(config)
63
62
  const path = geoPath().projection(projection)
64
63
 
64
+ const dynamicViewBox = useMemo(() => {
65
+ if (!useDynamicViewbox || !bounds) {
66
+ return SVG_VIEWBOX
67
+ }
68
+
69
+ const x = Math.floor(bounds[0][0] - SVG_PADDING)
70
+ const y = Math.floor(bounds[0][1] - SVG_PADDING)
71
+ const width = Math.ceil(bounds[1][0] - bounds[0][0] + SVG_PADDING * 2)
72
+ const height = Math.ceil(bounds[1][1] - bounds[0][1] + SVG_PADDING * 2)
73
+
74
+ return `${x} ${y} ${width} ${height}`
75
+ }, [useDynamicViewbox, bounds])
76
+
65
77
  useEffect(() => {
66
78
  let currentYear = getCurrentTopoYear(config, runtimeFilters)
67
79
 
@@ -80,6 +92,11 @@ const SingleStateMap: React.FC = () => {
80
92
  )
81
93
  }
82
94
 
95
+ // Early return for small multiples rendering
96
+ if (config.smallMultiples?.mode) {
97
+ return <SmallMultiples />
98
+ }
99
+
83
100
  const checkForNoData = () => {
84
101
  // If no statesPicked, return true
85
102
  if (statesPicked?.every(sp => !sp.fipsCode)) return true
@@ -112,10 +129,10 @@ const SingleStateMap: React.FC = () => {
112
129
  />
113
130
  )
114
131
 
115
- // Push city list
132
+ // Push city list - use projection from useStateZoom which is fitted to ALL selected states
116
133
  geosJsx.push(
117
134
  <CityList
118
- projection={cityListProjection}
135
+ projection={projection}
119
136
  key='cities'
120
137
  geoClickHandler={geoClickHandler}
121
138
  titleCase={titleCase}
@@ -131,7 +148,7 @@ const SingleStateMap: React.FC = () => {
131
148
  <ErrorBoundary component='SingleStateMap'>
132
149
  {!!statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
133
150
  <svg
134
- viewBox={SVG_VIEWBOX}
151
+ viewBox={dynamicViewBox}
135
152
  preserveAspectRatio='xMinYMin'
136
153
  className='svg-container'
137
154
  role='img'
@@ -172,7 +189,7 @@ const SingleStateMap: React.FC = () => {
172
189
  statesToShow
173
190
  ]}
174
191
  >
175
- {({ features, projection }) => {
192
+ {({ features }) => {
176
193
  return (
177
194
  <g
178
195
  id='mapGroup'
@@ -183,7 +200,7 @@ const SingleStateMap: React.FC = () => {
183
200
  data-scale=''
184
201
  key='countyMapGroup'
185
202
  >
186
- {constructGeoJsx(features, projection)}
203
+ {constructGeoJsx(features)}
187
204
  </g>
188
205
  )
189
206
  }}
@@ -194,7 +211,7 @@ const SingleStateMap: React.FC = () => {
194
211
  )}
195
212
  {!!statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
196
213
  <svg
197
- viewBox={SVG_VIEWBOX}
214
+ viewBox={dynamicViewBox}
198
215
  preserveAspectRatio='xMinYMin'
199
216
  className='svg-container'
200
217
  role='img'
@@ -247,7 +264,7 @@ const SingleStateMap: React.FC = () => {
247
264
 
248
265
  {checkForNoData() && (
249
266
  <svg
250
- viewBox={SVG_VIEWBOX}
267
+ viewBox={dynamicViewBox}
251
268
  preserveAspectRatio='xMinYMin'
252
269
  className='svg-container'
253
270
  role='img'