@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
@@ -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'
@@ -42,9 +44,9 @@ import {
42
44
  displayGeoName,
43
45
  SVG_HEIGHT,
44
46
  SVG_VIEWBOX,
45
- SVG_WIDTH,
46
- hashObj
47
+ SVG_WIDTH
47
48
  } from '../../../helpers'
49
+ import { hashObj } from '@cdc/core/helpers/hashObj'
48
50
  const { features: unitedStatesHex } = topoFeature(hexTopoJSON, hexTopoJSON.objects.states)
49
51
 
50
52
  const offsets = {
@@ -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,9 @@ const UsaMap = () => {
190
200
  strokeColor='#fff'
191
201
  territoryData={territoryData}
192
202
  backgroundColor={styles.fill}
203
+ mapId={mapId}
204
+ getSyncProps={getSyncProps}
205
+ syncHandlers={syncHandlers}
193
206
  />
194
207
  )
195
208
 
@@ -242,6 +255,9 @@ const UsaMap = () => {
242
255
  territoryData={territoryData}
243
256
  tabIndex={-1}
244
257
  backgroundColor={styles.fill}
258
+ mapId={mapId}
259
+ getSyncProps={getSyncProps}
260
+ syncHandlers={syncHandlers}
245
261
  />
246
262
  )
247
263
  }
@@ -256,6 +272,10 @@ const UsaMap = () => {
256
272
  return <></>
257
273
  }
258
274
 
275
+ if (config.smallMultiples?.mode) {
276
+ return <SmallMultiples />
277
+ }
278
+
259
279
  // Constructs and displays markup for all geos on the map (except territories right now)
260
280
  const constructGeoJsx = (geographies, projection) => {
261
281
  let showLabel = general.displayStateLabels
@@ -437,6 +457,7 @@ const UsaMap = () => {
437
457
  return (
438
458
  <g data-name={geoName} key={key} tabIndex={-1}>
439
459
  <g
460
+ {...getSyncProps(geoKey)}
440
461
  className='geo-group'
441
462
  style={styles}
442
463
  onClick={() => geoClickHandler(geoDisplayName, geoData)}
@@ -444,7 +465,7 @@ const UsaMap = () => {
444
465
  data-tooltip-id={`tooltip__${tooltipId}`}
445
466
  data-tooltip-html={tooltip}
446
467
  tabIndex={-1}
447
- onMouseEnter={() => {
468
+ onMouseEnter={e => {
448
469
  // Track hover analytics event if this is a new location
449
470
  const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
450
471
  publishAnalyticsEvent({
@@ -457,6 +478,10 @@ const UsaMap = () => {
457
478
  location: geoDisplayName,
458
479
  specifics: `location: ${locationName?.toLowerCase()}`
459
480
  })
481
+ syncHandlers.onMouseEnter(geoKey, e.clientY)
482
+ }}
483
+ onMouseLeave={() => {
484
+ syncHandlers.onMouseLeave()
460
485
  }}
461
486
  >
462
487
  {/* state path */}
@@ -499,7 +524,7 @@ const UsaMap = () => {
499
524
  height={patternSizes[size] ?? 6}
500
525
  width={patternSizes[size] ?? 6}
501
526
  stroke={patternColor}
502
- strokeWidth={0.75}
527
+ strokeWidth={patternLinesStrokeWidth}
503
528
  orientation={['diagonalRightToLeft']}
504
529
  />
505
530
  )}
@@ -590,7 +615,7 @@ const UsaMap = () => {
590
615
  <text
591
616
  x={x}
592
617
  y={y}
593
- fontSize={isMobileStateLabelViewport(currentViewport) ? 16 : 13}
618
+ fontSize={isMobileStateLabelViewport(vizViewport) ? 16 : 13}
594
619
  fontWeight={900}
595
620
  strokeWidth='1'
596
621
  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
@@ -11,6 +11,7 @@ import CityList from '../CityList'
11
11
  import BubbleList from '../BubbleList'
12
12
  import ZoomControls from '../ZoomControls'
13
13
  import { supportedCountries } from '../../data/supported-geos'
14
+ import { getCountriesPicked } from '../../helpers/getCountriesPicked'
14
15
  import {
15
16
  getGeoFillColor,
16
17
  getGeoStrokeColor,
@@ -24,6 +25,7 @@ import {
24
25
  } from '../../helpers'
25
26
  import useGeoClickHandler from '../../hooks/useGeoClickHandler'
26
27
  import useApplyTooltipsToGeo from '../../hooks/useApplyTooltipsToGeo'
28
+ import useCountryZoom from '../../hooks/useCountryZoom'
27
29
  import generateRuntimeData from '../../helpers/generateRuntimeData'
28
30
  import { applyLegendToRow } from '../../helpers/applyLegendToRow'
29
31
 
@@ -33,17 +35,24 @@ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
33
35
 
34
36
  let projection = geoMercator()
35
37
 
38
+ const GRAYED_OUT_COLOR = '#d3d3d3'
39
+
40
+ type MapPosition = { coordinates: number[]; zoom: number }
41
+
36
42
  const WorldMap = () => {
37
43
  // prettier-ignore
38
44
  const {
39
45
  runtimeData,
40
- position,
46
+ position: mapPosition,
41
47
  config,
42
48
  tooltipId,
43
49
  runtimeLegend,
44
50
  interactionLabel
45
51
  } = useContext(ConfigContext)
46
52
 
53
+ // Type assertion: position from context is actually the map viewport position, not legend position
54
+ const position = mapPosition as unknown as MapPosition
55
+
47
56
  const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
48
57
 
49
58
  const { type, allowMapZoom } = config.general
@@ -51,12 +60,16 @@ const WorldMap = () => {
51
60
  const [world, setWorld] = useState(null)
52
61
  const { geoClickHandler } = useGeoClickHandler()
53
62
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
63
+
64
+ const { centerOnCountries } = useCountryZoom(world)
65
+
54
66
  const dispatch = useContext(MapDispatchContext)
55
67
 
56
68
  useEffect(() => {
57
69
  const fetchData = async () => {
58
70
  import(/* webpackChunkName: "world-topo" */ './data/world-topo.json').then(topoJSON => {
59
- setWorld(feature(topoJSON, topoJSON.objects.countries).features)
71
+ const worldFeatures = feature(topoJSON, topoJSON.objects.countries).features
72
+ setWorld(worldFeatures)
60
73
  })
61
74
  }
62
75
  fetchData()
@@ -66,6 +79,19 @@ const WorldMap = () => {
66
79
  return <></>
67
80
  }
68
81
 
82
+ // Filter countries based on selection
83
+ const getFilteredWorld = () => {
84
+ if (!config.general.countriesPicked || config.general.countriesPicked.length === 0) {
85
+ return world // Show all countries if none selected
86
+ }
87
+
88
+ // Always show all countries when multi-country mode is active
89
+ // Individual country styling will handle hide/grayed-out states
90
+ return world
91
+ }
92
+
93
+ const filteredWorld = getFilteredWorld()
94
+
69
95
  const handleFiltersReset = () => {
70
96
  const newRuntimeData = generateRuntimeData(config)
71
97
  publishAnalyticsEvent({
@@ -89,7 +115,15 @@ const WorldMap = () => {
89
115
  eventLabel: interactionLabel,
90
116
  vizTitle: getVizTitle(config)
91
117
  })
92
- dispatch({ type: 'SET_POSITION', payload: { coordinates: [0, 30], zoom: 1 } })
118
+
119
+ // If countries are selected, center on them; otherwise, use default world position
120
+ const countriesPicked = getCountriesPicked(config)
121
+
122
+ if (countriesPicked && countriesPicked.length > 0) {
123
+ centerOnCountries('reset')
124
+ } else {
125
+ dispatch({ type: 'SET_POSITION', payload: { coordinates: [0, 30], zoom: 1 } })
126
+ }
93
127
  }
94
128
 
95
129
  const handleZoomIn = position => {
@@ -189,25 +223,57 @@ const WorldMap = () => {
189
223
  const geoStrokeColor = getGeoStrokeColor(config)
190
224
  const geoFillColor = getGeoFillColor(config)
191
225
 
226
+ // Check if this country should be greyed out for multi-country selection
227
+ const countriesPicked = getCountriesPicked(config)
228
+
229
+ const isGreyedOut = Boolean(
230
+ countriesPicked.length > 0 &&
231
+ config.general.hideUnselectedCountries !== true &&
232
+ !countriesPicked.some(country => country.iso === geo.properties.iso || country.name === geoDisplayName)
233
+ )
234
+
235
+ // Determine visual state for TDD tests
236
+ const isSelected = countriesPicked.some(
237
+ country => country.iso === geo.properties.iso || country.name === geoDisplayName
238
+ )
239
+ const isHidden = countriesPicked.length > 0 && config.general.hideUnselectedCountries === true && !isSelected
240
+
241
+ // Build CSS class names for TDD tests
242
+ let geoClassName = ''
243
+ if (countriesPicked.length > 0) {
244
+ if (isSelected) {
245
+ geoClassName = 'selected'
246
+ } else if (isGreyedOut) {
247
+ geoClassName = 'grayed-out'
248
+ } else if (isHidden) {
249
+ geoClassName = 'hidden'
250
+ }
251
+ }
252
+
192
253
  let styles: Record<string, string | Record<string, string>> = {
193
- fill: geoFillColor,
194
- cursor: 'default'
254
+ fill: isGreyedOut ? GRAYED_OUT_COLOR : geoFillColor,
255
+ cursor: 'default',
256
+ ...(isGreyedOut && { opacity: '0.3' })
195
257
  }
196
258
 
197
- const strokeWidth = 0.9
259
+ // Scale stroke width inversely with zoom level to maintain consistent visual thickness
260
+ // At zoom=1, use base width of 0.9; at zoom=4, use 0.225; etc.
261
+ const baseStrokeWidth = 0.9
262
+ const currentZoom = position?.zoom || 1
263
+ const strokeWidth = baseStrokeWidth / currentZoom
198
264
 
199
265
  // If a legend applies, return it with appropriate information.
200
266
  const toolTip = applyTooltipsToGeo(geoDisplayName, geoData)
201
267
  if (legendColors && legendColors[0] !== '#000000' && type !== 'bubble') {
202
268
  styles = {
203
269
  ...styles,
204
- fill: type !== 'world-geocode' ? legendColors[0] : geoFillColor,
270
+ fill: isGreyedOut ? GRAYED_OUT_COLOR : type !== 'world-geocode' ? legendColors[0] : geoFillColor,
205
271
  cursor: 'default',
206
272
  '&:hover': {
207
- fill: type !== 'world-geocode' ? legendColors[1] : geoFillColor
273
+ fill: isGreyedOut ? GRAYED_OUT_COLOR : type !== 'world-geocode' ? legendColors[1] : geoFillColor
208
274
  },
209
275
  '&:active': {
210
- fill: type !== 'world-geocode' ? legendColors[2] : geoFillColor
276
+ fill: isGreyedOut ? GRAYED_OUT_COLOR : type !== 'world-geocode' ? legendColors[2] : geoFillColor
211
277
  }
212
278
  }
213
279
 
@@ -228,6 +294,7 @@ const WorldMap = () => {
228
294
  path={path}
229
295
  stroke={geoStrokeColor}
230
296
  strokeWidth={strokeWidth}
297
+ className={geoClassName}
231
298
  onClick={() => geoClickHandler(geoDisplayName, geoData)}
232
299
  onMouseEnter={() => {
233
300
  // Track hover analytics event if this is a new location
@@ -245,6 +312,7 @@ const WorldMap = () => {
245
312
  }}
246
313
  data-tooltip-id={`tooltip__${tooltipId}`}
247
314
  data-tooltip-html={toolTip}
315
+ data-country-code={geo.properties.iso}
248
316
  tabIndex={-1}
249
317
  />
250
318
  )
@@ -260,6 +328,7 @@ const WorldMap = () => {
260
328
  strokeWidth={strokeWidth}
261
329
  styles={styles}
262
330
  path={path}
331
+ className={geoClassName}
263
332
  onMouseEnter={() => {
264
333
  // Track hover analytics event if this is a new location
265
334
  const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
@@ -276,6 +345,7 @@ const WorldMap = () => {
276
345
  }}
277
346
  data-tooltip-id={`tooltip__${tooltipId}`}
278
347
  data-tooltip-html={toolTip}
348
+ data-country-code={geo.properties.iso}
279
349
  />
280
350
  )
281
351
  })
@@ -305,7 +375,7 @@ const WorldMap = () => {
305
375
  width={SVG_WIDTH}
306
376
  height={SVG_HEIGHT}
307
377
  >
308
- <Mercator data={world}>{({ features }) => constructGeoJsx(features)}</Mercator>
378
+ <Mercator data={filteredWorld}>{({ features }) => constructGeoJsx(features)}</Mercator>
309
379
  </ZoomableGroup>
310
380
  </svg>
311
381
  ) : (
@@ -319,7 +389,7 @@ const WorldMap = () => {
319
389
  width={SVG_WIDTH}
320
390
  height={SVG_HEIGHT}
321
391
  >
322
- <Mercator data={world}>{({ features }) => constructGeoJsx(features)}</Mercator>
392
+ <Mercator data={filteredWorld}>{({ features }) => constructGeoJsx(features)}</Mercator>
323
393
  </ZoomableGroup>
324
394
  </svg>
325
395
  )}
@@ -24,6 +24,7 @@ const createInitialState = () => {
24
24
  headerColor: 'theme-blue',
25
25
  title: '',
26
26
  showTitle: true,
27
+ titleStyle: 'small',
27
28
  showSidebar: true,
28
29
  showDownloadMediaButton: false,
29
30
  displayAsHex: false,
@@ -140,6 +141,16 @@ const createInitialState = () => {
140
141
  },
141
142
  filterBehavior: 'Filter Change',
142
143
  filterIntro: '',
144
+ smallMultiples: {
145
+ mode: '',
146
+ tileColumn: '',
147
+ tilesPerRowDesktop: 2,
148
+ tilesPerRowMobile: 1,
149
+ tileOrderType: 'asc',
150
+ tileOrder: [],
151
+ tileTitles: {},
152
+ synchronizedTooltips: true
153
+ },
143
154
  markupVariables: [],
144
155
  enableMarkupVariables: false
145
156
  }
@@ -98,74 +98,6 @@ export const supportedRegions = {
98
98
  'region 10': ['REGION 10', 'R10']
99
99
  }
100
100
 
101
- /**
102
- * State Name to ISO Code Mapping
103
- *
104
- * Maps proper case state names to their corresponding ISO 3166-2 codes.
105
- * Provides reverse lookup capability for the supportedStates table.
106
- *
107
- * Structure: { 'State Name': 'US-XX' }
108
- * - Key: Proper case state name (e.g., 'California')
109
- * - Value: ISO 3166-2 state code (e.g., 'US-CA')
110
- *
111
- * Used in:
112
- * - Data processing when state names need to be converted to ISO codes
113
- * - Validation and normalization of state data
114
- */
115
- export const stateToIso = {
116
- // States
117
- Alabama: 'US-AL',
118
- Alaska: 'US-AK',
119
- Arizona: 'US-AZ',
120
- Arkansas: 'US-AR',
121
- California: 'US-CA',
122
- Colorado: 'US-CO',
123
- Connecticut: 'US-CT',
124
- Delaware: 'US-DE',
125
- Florida: 'US-FL',
126
- Georgia: 'US-GA',
127
- Hawaii: 'US-HI',
128
- Idaho: 'US-ID',
129
- Illinois: 'US-IL',
130
- Indiana: 'US-IN',
131
- Iowa: 'US-IA',
132
- Kansas: 'US-KS',
133
- Kentucky: 'US-KY',
134
- Louisiana: 'US-LA',
135
- Maine: 'US-ME',
136
- Maryland: 'US-MD',
137
- Massachusetts: 'US-MA',
138
- Michigan: 'US-MI',
139
- Minnesota: 'US-MN',
140
- Mississippi: 'US-MS',
141
- Missouri: 'US-MO',
142
- Montana: 'US-MT',
143
- Nebraska: 'US-NE',
144
- Nevada: 'US-NV',
145
- 'New Hampshire': 'US-NH',
146
- 'New Jersey': 'US-NJ',
147
- 'New Mexico': 'US-NM',
148
- 'New York': 'US-NY',
149
- 'North Carolina': 'US-NC',
150
- 'North Dakota': 'US-ND',
151
- Ohio: 'US-OH',
152
- Oklahoma: 'US-OK',
153
- Oregon: 'US-OR',
154
- Pennsylvania: 'US-PA',
155
- 'Rhode Island': 'US-RI',
156
- 'South Carolina': 'US-SC',
157
- 'South Dakota': 'US-SD',
158
- Tennessee: 'US-TN',
159
- Texas: 'US-TX',
160
- Utah: 'US-UT',
161
- Vermont: 'US-VT',
162
- Virginia: 'US-VA',
163
- Washington: 'US-WA',
164
- 'West Virginia': 'US-WV',
165
- Wisconsin: 'US-WI',
166
- Wyoming: 'US-WY'
167
- }
168
-
169
101
  /**
170
102
  * State FIPS Code to Two-Letter Abbreviation Mapping
171
103
  *
@@ -750,7 +682,7 @@ export const supportedCities = {
750
682
  'GREAT PLAINS TRIBAL LEADERS HEALTH BOARD': [-103.22444, 44.083054],
751
683
  'GREENSBORO': [-79.791977, 36.072636],
752
684
  'HENDERSON': [-114.981720, 36.039524],
753
- 'HERSHEY': [-76.6779444, 40.2849997 ],
685
+ 'HERSHEY': [-76.6779444, 40.2849997],
754
686
  'HIALEAH': [-80.278107, 25.857595],
755
687
  'HONOLULU': [-157.858337, 21.306944],
756
688
  'HOPI TRIBE': [-110.5035, 35.7833],
@@ -783,7 +715,7 @@ export const supportedCities = {
783
715
  'LUBBOCK': [-101.855164, 33.577862],
784
716
  'MADISON': [-89.401230, 43.073051],
785
717
  'MARION COUNTY, INDIANA': [-86.136543, 39.781029],
786
- 'MARION':[-88.9330556,37.7305556],
718
+ 'MARION': [-88.9330556, 37.7305556],
787
719
  'MEMPHIS': [-90.048981, 35.149532],
788
720
  'MESA': [-111.831474, 33.415184],
789
721
  'MIAMI': [-80.191788, 25.761681],
@@ -808,7 +740,7 @@ export const supportedCities = {
808
740
  'OLYMPIA': [-122.9382403, 47.0394791],
809
741
  'OMAHA': [-95.934502, 41.256538],
810
742
  'ORLANDO': [-81.379234, 28.538336],
811
- 'PASADENA':[-95.209099,29.691063],
743
+ 'PASADENA': [-95.209099, 29.691063],
812
744
  'PHILADELPHIA': [-75.165222, 39.952583],
813
745
  'PHOENIX': [-112.074036, 33.448376],
814
746
  'PITTSBURGH': [-79.995888, 40.440624],
@@ -828,9 +760,9 @@ export const supportedCities = {
828
760
  'SACRAMENTO': [-121.494400, 38.581573],
829
761
  'SAINT PAUL': [-93.089958, 44.953705],
830
762
  'SALEM, ALABAMA': [-85.2386, 32.5968],
831
- 'SALEM, CONNECTICUT': [-72.2754,41.4904],
763
+ 'SALEM, CONNECTICUT': [-72.2754, 41.4904],
832
764
  'SALEM, FLORIDA': [-83.4129, 29.8869],
833
- 'SALEM, ILLINOIS':[-88.945618,38.626991],
765
+ 'SALEM, ILLINOIS': [-88.945618, 38.626991],
834
766
  'SALEM, MASSACHUSETTS': [-70.8955, 42.5197],
835
767
  'SALEM, OR': [-123.0351, 44.9429],
836
768
  'SALEM, OREGON': [-123.0351, 44.9429],
@@ -839,19 +771,19 @@ export const supportedCities = {
839
771
  'SALUDA, VIRGINIA': [-76.5950, 37.6064],
840
772
  'SAN ANTONIO': [-98.493629, 29.424122],
841
773
  'SAN BENITO': [-97.6311, 26.1326],
842
- 'SAN BERNARDINO':[-117.302399,34.115784],
774
+ 'SAN BERNARDINO': [-117.302399, 34.115784],
843
775
  'SAN DIEGO': [-117.161087, 32.715736],
844
776
  'SAN FRANCISCO': [-122.419418, 37.774929],
845
777
  'SAN JOSE': [-121.886330, 37.338207],
846
778
  'SANTA ANA': [-117.867653, 33.745472],
847
- 'SANTA CLARA':[-121.955238,37.354107],
779
+ 'SANTA CLARA': [-121.955238, 37.354107],
848
780
  'SCOTTSDALE': [-111.926048, 33.494171],
849
781
  'SEATTLE': [-122.332069, 47.606209],
850
782
  'SOUTH PUGET INTERTRIBAL PLANNING AGENCY': [-123.0832, 47.1241],
851
783
  'SOUTHCENTRAL FOUNDATION': [-149.7971, 61.1821],
852
784
  'SOUTHEAST ALASKA REGIONAL HEALTH CONSORTIUM': [-135.3369, 57.05479],
853
785
  'SPOKANE': [-117.426048, 47.658779],
854
- 'ST PAUL': [ -93.089958, 44.953705],
786
+ 'ST PAUL': [-93.089958, 44.953705],
855
787
  'ST. LOUIS': [-90.199402, 38.627003],
856
788
  'ST. PETERSBURG': [-82.640289, 27.767601],
857
789
  'STOCKTON': [-121.290779, 37.957703],
@@ -25,8 +25,16 @@ const geoLookups: Record<string, GeoLookup> = {
25
25
  country: { keys: countryKeys, data: supportedCountries }
26
26
  }
27
27
 
28
- const memoizedFindUID = (geoName: string, type: keyof typeof geoLookups): string | undefined => {
28
+ const memoizedFindUID = (
29
+ geoName: string,
30
+ type: keyof typeof geoLookups,
31
+ caseInsensitive = false
32
+ ): string | undefined => {
29
33
  const lookup = geoLookups[type]
34
+ if (caseInsensitive) {
35
+ const lowerGeoName = geoName.toLowerCase()
36
+ return lookup.keys.find(key => lookup.data[key].some(name => name.toLowerCase() === lowerGeoName))
37
+ }
30
38
  return lookup.keys.find(key => lookup.data[key].includes(geoName))
31
39
  }
32
40
 
@@ -72,7 +80,10 @@ const handleUSLocation = (row: DataRow, geoColumn: string, displayAsHex: boolean
72
80
 
73
81
  const handleWorldLocation = (row: DataRow, geoColumn: string, isWorldGeocodeType: boolean): string | null => {
74
82
  const geoName = row[geoColumn]
75
- let uid = memoizedFindUID(geoName, 'country')
83
+ if (!geoName) return null
84
+
85
+ // Use case-insensitive matching for world countries to handle various input formats
86
+ let uid = memoizedFindUID(geoName, 'country', true)
76
87
  if (!uid && (isWorldGeocodeType || geoName)) {
77
88
  uid = findCityUID(geoName)
78
89
  }
@@ -65,6 +65,30 @@ export const applyColorToLegend = (legendIdx: number, config: MapConfig, result:
65
65
  color = mapPaletteNameMigrations[color]
66
66
  }
67
67
 
68
+ // Check for customColorsOrdered first (direct 1-to-1 mapping, no distribution)
69
+ if (general?.palette?.customColorsOrdered && Array.isArray(general.palette.customColorsOrdered)) {
70
+ const customColorsOrdered = general.palette.customColorsOrdered
71
+
72
+ // Count actual special classes in the result array
73
+ const actualSpecialClassCount = result.filter(item => item.special).length
74
+ const colorIdx = legendIdx - actualSpecialClassCount
75
+
76
+ // Handle special classes coloring
77
+ if (result[legendIdx]?.special) {
78
+ const specialClassColors = chroma.scale(['#D4D4D4', '#939393']).colors(actualSpecialClassCount)
79
+ const specialClassIdx = result.slice(0, legendIdx + 1).filter(item => item.special).length - 1
80
+ return specialClassColors[specialClassIdx]
81
+ }
82
+
83
+ // Direct 1-to-1 mapping with customColorsOrdered (no distribution array)
84
+ if (colorIdx >= 0 && colorIdx < customColorsOrdered.length) {
85
+ return customColorsOrdered[colorIdx]
86
+ }
87
+
88
+ // Fallback to last color if index out of bounds
89
+ return customColorsOrdered[customColorsOrdered.length - 1] || '#d3d3d3'
90
+ }
91
+
68
92
  // Try multiple approaches to find the palette
69
93
  let mapColorPalette = general?.palette?.customColors
70
94
 
@@ -118,7 +142,7 @@ export const applyColorToLegend = (legendIdx: number, config: MapConfig, result:
118
142
  // For category legends, use the actual result length
119
143
  const isNumericLegend = legend && ['equalnumber', 'equalinterval'].includes(legend.type)
120
144
  const nonSpecialItemCount = isNumericLegend
121
- ? (legend.numberOfItems || result.length)
145
+ ? legend.numberOfItems || result.length
122
146
  : result.length - actualSpecialClassCount
123
147
 
124
148
  const amt =
@@ -1,5 +1,6 @@
1
1
  import { generateColorsArray } from '@cdc/core/helpers/generateColorsArray'
2
- import { hashObj, DEFAULT_MAP_BACKGROUND } from '../helpers'
2
+ import { hashObj } from '@cdc/core/helpers/hashObj'
3
+ import { DEFAULT_MAP_BACKGROUND, DISABLED_MAP_COLOR } from '../helpers'
3
4
  import { mapColorPalettes as colorPalettes } from '@cdc/core/data/colorPalettes'
4
5
  import { MapConfig } from '../types/MapConfig'
5
6
  import { type RuntimeLegend } from '../types/runtimeLegend'
@@ -42,8 +43,9 @@ export const applyLegendToRow = (
42
43
  const idx = legendMemo.current.get(hash)!
43
44
  const disabledIdx = showSpecialClassesLast ? legendSpecialClassLastMemo.current.get(hash) ?? idx : idx
44
45
 
45
- if (runtimeLegend.items?.[disabledIdx]?.disabled) {
46
- return generateColorsArray(DEFAULT_MAP_BACKGROUND)
46
+ // Note: DISABLED_MAP_COLOR is used in UsaMap.County.tsx to check for hidden bubbles. Should be refactored to use the hidden value when that is implemented.
47
+ if (runtimeLegend.items?.[disabledIdx]?.disabled || runtimeLegend.items?.[disabledIdx]?.hidden) {
48
+ return generateColorsArray(DISABLED_MAP_COLOR)
47
49
  }
48
50
 
49
51
  const legendBinColor = runtimeLegend.items.find(o => o.bin === idx)?.color