@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
@@ -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,9 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
18
18
  territory,
19
19
  textColor,
20
20
  backgroundColor,
21
+ svgStyle,
22
+ getSyncProps,
23
+ syncHandlers,
21
24
  ...props
22
25
  }) => {
23
26
  const { config } = useContext<MapContext>(ConfigContext)
@@ -25,17 +28,32 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
25
28
  const rectanglePath =
26
29
  '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
30
 
31
+ const geoKey = territory || `US-${label}`
32
+
28
33
  return (
29
- <svg viewBox='0 0 45 29' key={territory} className={territory}>
34
+ <svg viewBox='0 0 45 29' key={geoKey} className={geoKey} style={svgStyle}>
30
35
  <g
36
+ {...(getSyncProps ? getSyncProps(geoKey) : {})}
31
37
  {...otherProps}
32
38
  strokeLinejoin='round'
33
39
  tabIndex={-1}
34
40
  onClick={handleShapeClick}
35
41
  data-tooltip-id={dataTooltipId}
36
42
  data-tooltip-html={dataTooltipHtml}
43
+ onMouseEnter={e => {
44
+ syncHandlers?.onMouseEnter(geoKey, e.clientY)
45
+ }}
46
+ onMouseLeave={() => {
47
+ syncHandlers?.onMouseLeave()
48
+ }}
37
49
  >
38
- <path stroke={stroke} strokeWidth={strokeWidth} d={rectanglePath} {...otherProps} />
50
+ <path
51
+ stroke={stroke}
52
+ strokeWidth={strokeWidth}
53
+ d={rectanglePath}
54
+ style={{ pointerEvents: 'none' }}
55
+ {...otherProps}
56
+ />
39
57
  <text
40
58
  textAnchor='middle'
41
59
  dominantBaseline='middle'
@@ -45,7 +63,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
45
63
  stroke={strokeColor}
46
64
  className='territory-text'
47
65
  paintOrder='stroke'
48
- onClick={handleShapeClick}
66
+ style={{ pointerEvents: 'none' }}
49
67
  data-tooltip-id={dataTooltipId}
50
68
  data-tooltip-html={dataTooltipHtml}
51
69
  >
@@ -97,6 +115,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
97
115
  strokeWidth={strokeWidth}
98
116
  d={rectanglePath}
99
117
  fill={`url(#territory-${territory}-${patternData?.dataKey}--${patternIndex})`}
118
+ style={{ pointerEvents: 'none' }}
100
119
  className={[
101
120
  `territory-pattern-${patternData?.dataKey}`,
102
121
  `territory-pattern-${patternData?.dataKey}--${patternData.dataValue}`
@@ -111,7 +130,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
111
130
  stroke={strokeColor}
112
131
  className='territory-text'
113
132
  paintOrder='stroke'
114
- onClick={handleShapeClick}
133
+ style={{ pointerEvents: 'none' }}
115
134
  data-tooltip-id={dataTooltipId}
116
135
  data-tooltip-html={dataTooltipHtml}
117
136
  >
@@ -11,4 +11,10 @@ export type TerritoryShape = {
11
11
  territory: string
12
12
  territoryData: object
13
13
  textColor: string
14
+ svgStyle?: React.CSSProperties
15
+ getSyncProps?: (geoKey: string) => any
16
+ syncHandlers?: {
17
+ onMouseEnter: (geoKey: string, clientY: number) => void
18
+ onMouseLeave: () => void
19
+ }
14
20
  }
@@ -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
@@ -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(() => {
@@ -62,6 +64,19 @@ const SingleStateMap: React.FC = () => {
62
64
  const geoStrokeColor = getGeoStrokeColor(config)
63
65
  const path = geoPath().projection(projection)
64
66
 
67
+ const dynamicViewBox = useMemo(() => {
68
+ if (!useDynamicViewbox || !bounds) {
69
+ return SVG_VIEWBOX
70
+ }
71
+
72
+ const x = Math.floor(bounds[0][0] - SVG_PADDING)
73
+ const y = Math.floor(bounds[0][1] - SVG_PADDING)
74
+ const width = Math.ceil(bounds[1][0] - bounds[0][0] + SVG_PADDING * 2)
75
+ const height = Math.ceil(bounds[1][1] - bounds[0][1] + SVG_PADDING * 2)
76
+
77
+ return `${x} ${y} ${width} ${height}`
78
+ }, [useDynamicViewbox, bounds])
79
+
65
80
  useEffect(() => {
66
81
  let currentYear = getCurrentTopoYear(config, runtimeFilters)
67
82
 
@@ -80,6 +95,11 @@ const SingleStateMap: React.FC = () => {
80
95
  )
81
96
  }
82
97
 
98
+ // Early return for small multiples rendering
99
+ if (config.smallMultiples?.mode) {
100
+ return <SmallMultiples />
101
+ }
102
+
83
103
  const checkForNoData = () => {
84
104
  // If no statesPicked, return true
85
105
  if (statesPicked?.every(sp => !sp.fipsCode)) return true
@@ -131,7 +151,7 @@ const SingleStateMap: React.FC = () => {
131
151
  <ErrorBoundary component='SingleStateMap'>
132
152
  {!!statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
133
153
  <svg
134
- viewBox={SVG_VIEWBOX}
154
+ viewBox={dynamicViewBox}
135
155
  preserveAspectRatio='xMinYMin'
136
156
  className='svg-container'
137
157
  role='img'
@@ -194,7 +214,7 @@ const SingleStateMap: React.FC = () => {
194
214
  )}
195
215
  {!!statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
196
216
  <svg
197
- viewBox={SVG_VIEWBOX}
217
+ viewBox={dynamicViewBox}
198
218
  preserveAspectRatio='xMinYMin'
199
219
  className='svg-container'
200
220
  role='img'
@@ -247,7 +267,7 @@ const SingleStateMap: React.FC = () => {
247
267
 
248
268
  {checkForNoData() && (
249
269
  <svg
250
- viewBox={SVG_VIEWBOX}
270
+ viewBox={dynamicViewBox}
251
271
  preserveAspectRatio='xMinYMin'
252
272
  className='svg-container'
253
273
  role='img'
@@ -26,6 +26,8 @@ import { useLegendMemoContext } from '../../../context/LegendMemoContext'
26
26
  import { MapContext } from '../../../types/MapContext'
27
27
  import { checkColorContrast, getContrastColor, outlinedTextColor } from '@cdc/core/helpers/cove/accessibility'
28
28
  import TerritoriesSection from './TerritoriesSection'
29
+ import SmallMultiples from '../../SmallMultiples'
30
+ import { useSynchronizedGeographies } from '../../../hooks/useSynchronizedGeographies'
29
31
 
30
32
  import { isMobileStateLabelViewport } from '@cdc/core/helpers/viewports'
31
33
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
@@ -80,6 +82,8 @@ const UsaMap = () => {
80
82
  mapId,
81
83
  logo,
82
84
  currentViewport,
85
+ vizViewport,
86
+ dimensions,
83
87
  translate,
84
88
  runtimeLegend,
85
89
  interactionLabel
@@ -87,6 +91,8 @@ const UsaMap = () => {
87
91
 
88
92
  const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
89
93
 
94
+ const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
95
+
90
96
  let isFilterValueSupported = false
91
97
  const { general, columns, tooltips, hexMap, map, annotations } = config
92
98
  const { displayAsHex } = general
@@ -165,6 +171,10 @@ const UsaMap = () => {
165
171
  const geoStrokeColor = getGeoStrokeColor(config)
166
172
  const geoFillColor = getGeoFillColor(config)
167
173
 
174
+ // Chrome needs wider stroke for small maps or it doesn't render the pattern
175
+ const mapWidth = dimensions?.[0] || 880
176
+ const patternLinesStrokeWidth = mapWidth < 200 ? 1.75 : mapWidth < 375 ? 1.25 : 0.75
177
+
168
178
  const territories = territoriesData.map((territory, territoryIndex) => {
169
179
  const Shape = displayAsHex ? Territory.Hexagon : Territory.Rectangle
170
180
 
@@ -190,6 +200,8 @@ const UsaMap = () => {
190
200
  strokeColor='#fff'
191
201
  territoryData={territoryData}
192
202
  backgroundColor={styles.fill}
203
+ getSyncProps={getSyncProps}
204
+ syncHandlers={syncHandlers}
193
205
  />
194
206
  )
195
207
 
@@ -242,6 +254,8 @@ const UsaMap = () => {
242
254
  territoryData={territoryData}
243
255
  tabIndex={-1}
244
256
  backgroundColor={styles.fill}
257
+ getSyncProps={getSyncProps}
258
+ syncHandlers={syncHandlers}
245
259
  />
246
260
  )
247
261
  }
@@ -256,6 +270,10 @@ const UsaMap = () => {
256
270
  return <></>
257
271
  }
258
272
 
273
+ if (config.smallMultiples?.mode) {
274
+ return <SmallMultiples />
275
+ }
276
+
259
277
  // Constructs and displays markup for all geos on the map (except territories right now)
260
278
  const constructGeoJsx = (geographies, projection) => {
261
279
  let showLabel = general.displayStateLabels
@@ -437,6 +455,7 @@ const UsaMap = () => {
437
455
  return (
438
456
  <g data-name={geoName} key={key} tabIndex={-1}>
439
457
  <g
458
+ {...getSyncProps(geoKey)}
440
459
  className='geo-group'
441
460
  style={styles}
442
461
  onClick={() => geoClickHandler(geoDisplayName, geoData)}
@@ -444,7 +463,7 @@ const UsaMap = () => {
444
463
  data-tooltip-id={`tooltip__${tooltipId}`}
445
464
  data-tooltip-html={tooltip}
446
465
  tabIndex={-1}
447
- onMouseEnter={() => {
466
+ onMouseEnter={e => {
448
467
  // Track hover analytics event if this is a new location
449
468
  const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
450
469
  publishAnalyticsEvent({
@@ -457,6 +476,10 @@ const UsaMap = () => {
457
476
  location: geoDisplayName,
458
477
  specifics: `location: ${locationName?.toLowerCase()}`
459
478
  })
479
+ syncHandlers.onMouseEnter(geoKey, e.clientY)
480
+ }}
481
+ onMouseLeave={() => {
482
+ syncHandlers.onMouseLeave()
460
483
  }}
461
484
  >
462
485
  {/* state path */}
@@ -499,7 +522,7 @@ const UsaMap = () => {
499
522
  height={patternSizes[size] ?? 6}
500
523
  width={patternSizes[size] ?? 6}
501
524
  stroke={patternColor}
502
- strokeWidth={0.75}
525
+ strokeWidth={patternLinesStrokeWidth}
503
526
  orientation={['diagonalRightToLeft']}
504
527
  />
505
528
  )}
@@ -590,7 +613,7 @@ const UsaMap = () => {
590
613
  <text
591
614
  x={x}
592
615
  y={y}
593
- fontSize={isMobileStateLabelViewport(currentViewport) ? 16 : 13}
616
+ fontSize={isMobileStateLabelViewport(vizViewport) ? 16 : 13}
594
617
  fontWeight={900}
595
618
  strokeWidth='1'
596
619
  paintOrder='stroke'
@@ -1,7 +1,7 @@
1
1
  import { feature } from 'topojson-client'
2
2
  import usExtendedGeography from './../data/us-extended-geography.json'
3
3
 
4
- export const getCountyTopoURL = year => {
4
+ const getCountyTopoURL = year => {
5
5
  return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
6
6
  }
7
7
 
@@ -85,7 +85,7 @@ export const isTopoReady = (topoData, state, runtimeFilters) => {
85
85
  return topoData?.year && (!currentYear || currentYear === topoData?.year)
86
86
  }
87
87
 
88
- export const hasMoreThanFromHash = (data: { [key: string]: any }): boolean => {
88
+ const hasMoreThanFromHash = (data: { [key: string]: any }): boolean => {
89
89
  // Get all keys of the data object
90
90
  const keys = Object.keys(data)
91
91
 
@@ -1,4 +1,4 @@
1
- export const drawCircle = (circle, context, state) => {
1
+ const drawCircle = (circle, context, state) => {
2
2
  const percentOfOriginalSize = 0.75
3
3
  const scaleVal = 1
4
4
  const adjustedGeoRadius =
@@ -12,7 +12,8 @@ export const drawCircle = (circle, context, state) => {
12
12
  context.fill()
13
13
  context.stroke()
14
14
  }
15
- export const drawSquare = (square, context, state) => {
15
+
16
+ const drawSquare = (square, context, state) => {
16
17
  const percentOfOriginalSize = 0.75
17
18
  const scaleVal = 1.75
18
19
  const sideLength = square.size * scaleVal
@@ -32,7 +33,7 @@ export const drawSquare = (square, context, state) => {
32
33
  context.stroke()
33
34
  }
34
35
 
35
- export const drawDiamond = (diamond, context, state) => {
36
+ const drawDiamond = (diamond, context, state) => {
36
37
  const percentOfOriginalSize = 0.75
37
38
  const scaleVal = 2.2
38
39
  const fullSize = diamond.size * scaleVal
@@ -69,7 +70,8 @@ export const drawDiamond = (diamond, context, state) => {
69
70
  context.fill()
70
71
  context.stroke()
71
72
  }
72
- export const drawTriangle = (triangle, context, state) => {
73
+
74
+ const drawTriangle = (triangle, context, state) => {
73
75
  const percentOfOriginalSize = 0.75
74
76
  const scaleVal = 2.2
75
77
  const baseLength = triangle.size * scaleVal
@@ -102,7 +104,8 @@ export const drawTriangle = (triangle, context, state) => {
102
104
  context.fill()
103
105
  context.stroke()
104
106
  }
105
- export const drawStar = (star, context, state) => {
107
+
108
+ const drawStar = (star, context, state) => {
106
109
  const percentOfOriginalSize = 0.75
107
110
  const scaleVal = 2.2
108
111
  const spikes = star.spikes
@@ -155,7 +158,7 @@ export const drawStar = (star, context, state) => {
155
158
  context.stroke()
156
159
  }
157
160
 
158
- export const drawPin = (pin, ctx, state) => {
161
+ const drawPin = (pin, ctx, state) => {
159
162
  const scaleVal = 10
160
163
  const percentOfOriginalSize = 0.75
161
164
  const baseSize = pin.size * scaleVal