@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
@@ -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
  )}
@@ -140,6 +140,16 @@ const createInitialState = () => {
140
140
  },
141
141
  filterBehavior: 'Filter Change',
142
142
  filterIntro: '',
143
+ smallMultiples: {
144
+ mode: '',
145
+ tileColumn: '',
146
+ tilesPerRowDesktop: 2,
147
+ tilesPerRowMobile: 1,
148
+ tileOrderType: 'asc',
149
+ tileOrder: [],
150
+ tileTitles: {},
151
+ synchronizedTooltips: true
152
+ },
143
153
  markupVariables: [],
144
154
  enableMarkupVariables: false
145
155
  }
@@ -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,20 +1,7 @@
1
1
  export const SVG_WIDTH = 880
2
2
  export const SVG_HEIGHT = 500
3
- export const SVG_PADDING = 50
3
+ export const SVG_PADDING = 25
4
4
  export const SVG_VIEWBOX = `0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`
5
- export const HEADER_COLORS = [
6
- 'theme-blue',
7
- 'theme-purple',
8
- 'theme-brown',
9
- 'theme-teal',
10
- 'theme-pink',
11
- 'theme-orange',
12
- 'theme-slate',
13
- 'theme-indigo',
14
- 'theme-cyan',
15
- 'theme-green',
16
- 'theme-amber'
17
- ]
18
5
  export const MAX_ZOOM_LEVEL = 4
19
6
 
20
7
  export const SUPPORTED_DC_NAMES = [
@@ -45,7 +32,6 @@ export const DEFAULT_MAP_BACKGROUND = '#DFE1E2'
45
32
 
46
33
  // Component constants
47
34
  export const LOGO_MAX_WIDTH = '50px'
48
- export const STORYBOOK_PORT = 6006
49
35
 
50
36
  // CSV Parsing Configuration
51
37
  export const CSV_PARSE_CONFIG = {
@@ -4,10 +4,12 @@ import {
4
4
  supportedTerritories,
5
5
  supportedCountries,
6
6
  supportedCounties,
7
+ supportedCities,
7
8
  stateKeys,
8
9
  territoryKeys,
9
10
  countryKeys,
10
- countyKeys
11
+ countyKeys,
12
+ cityKeys
11
13
  } from '../data/supported-geos'
12
14
 
13
15
  /**
@@ -20,14 +22,17 @@ import {
20
22
  export const displayGeoName = (key: string, convertFipsCodes = true): string => {
21
23
  if (!convertFipsCodes) return key
22
24
  let value = key
25
+ let wasLookedUp = false
23
26
 
24
27
  // Map to first item in values array which is the preferred label
25
28
  if (stateKeys.includes(value)) {
26
29
  value = titleCase(supportedStates[key][0])
30
+ wasLookedUp = true
27
31
  }
28
32
 
29
33
  if (territoryKeys.includes(value)) {
30
34
  value = titleCase(supportedTerritories[key][0])
35
+ wasLookedUp = true
31
36
  if (value === 'U.s. Virgin Islands') {
32
37
  value = 'U.S. Virgin Islands'
33
38
  }
@@ -35,10 +40,17 @@ export const displayGeoName = (key: string, convertFipsCodes = true): string =>
35
40
 
36
41
  if (countryKeys.includes(value)) {
37
42
  value = titleCase(supportedCountries[key][0])
43
+ wasLookedUp = true
38
44
  }
39
45
 
40
46
  if (countyKeys.includes(value)) {
41
47
  value = titleCase(supportedCounties[key])
48
+ wasLookedUp = true
49
+ }
50
+
51
+ if (cityKeys.includes(value)) {
52
+ value = titleCase(String(value) || '')
53
+ wasLookedUp = true
42
54
  }
43
55
 
44
56
  const dict = {
@@ -51,11 +63,14 @@ export const displayGeoName = (key: string, convertFipsCodes = true): string =>
51
63
 
52
64
  if (Object.keys(dict).includes(value)) {
53
65
  value = dict[value]
66
+ wasLookedUp = true
54
67
  }
55
- // if you get here and it's 2 letters then DONT titleCase state abbreviations like "AL"
56
- if (value?.length === 2 || value === 'U.S. Virgin Islands') {
68
+
69
+ // If value was looked up from our dictionaries and needs formatting, or if it's a 2-letter abbreviation, return as-is
70
+ if (value?.length === 2 || value === 'U.S. Virgin Islands' || wasLookedUp) {
57
71
  return value
58
72
  } else {
59
- return value
73
+ // Apply titleCase to unrecognized values (e.g., "DISTRICT OF COLUMBIA" -> "District of Columbia")
74
+ return titleCase(value)
60
75
  }
61
76
  }
@@ -598,5 +598,3 @@ export const generateRuntimeLegend = (
598
598
  }
599
599
  }
600
600
  }
601
-
602
- export default generateRuntimeLegend
@@ -0,0 +1,103 @@
1
+ import { supportedCountries } from '../data/supported-geos'
2
+ import type { MapConfig } from '../types/MapConfig'
3
+
4
+ export interface CountryPickedInfo {
5
+ iso: string
6
+ name: string
7
+ }
8
+
9
+ export const getCountriesPicked = (config: MapConfig): CountryPickedInfo[] => {
10
+ if (!config.general.countriesPicked || config.general.countriesPicked.length === 0) {
11
+ return []
12
+ }
13
+
14
+ return config.general.countriesPicked.map(country => {
15
+ // Validate that the ISO code exists in our supported countries
16
+ if (!supportedCountries[country.iso]) {
17
+ console.error(`Country ISO code "${country.iso}" not found in supported countries.`)
18
+ }
19
+
20
+ return {
21
+ iso: country.iso,
22
+ name: country.name
23
+ }
24
+ })
25
+ }
26
+
27
+ /**
28
+ * ISO codes that are in supported-geos.js but don't have geometries in world-topo.json
29
+ * These are filtered out to prevent users from selecting countries that won't render
30
+ */
31
+ const EXCLUDED_ISOS = new Set([
32
+ // US Territories (not in topology - grouped with USA or missing)
33
+ 'ASM',
34
+ 'GUM',
35
+ 'MNP',
36
+ 'VIR',
37
+ // Small territories/islands without separate geometries
38
+ 'ALA',
39
+ 'AIA',
40
+ 'AND',
41
+ 'ABW',
42
+ 'BES',
43
+ 'BMU',
44
+ 'BVT',
45
+ 'CXR',
46
+ 'CCK',
47
+ 'COK',
48
+ 'CUW',
49
+ 'FRO',
50
+ 'GGY',
51
+ 'HMD',
52
+ 'IMN',
53
+ 'JEY',
54
+ 'LIE',
55
+ 'MCO',
56
+ 'MSR',
57
+ 'NRU',
58
+ 'NIU',
59
+ 'NFK',
60
+ 'PCN',
61
+ 'SGS',
62
+ 'SJM',
63
+ 'TKL',
64
+ 'TCA',
65
+ 'TUV',
66
+ 'VAT',
67
+ 'WLF'
68
+ ])
69
+
70
+ /**
71
+ * Helper to get all supported countries formatted for dropdown options
72
+ * Filters to only valid ISO 3166-1 alpha-3 codes and removes countries without topology
73
+ */
74
+ export const getSupportedCountryOptions = () => {
75
+ return Object.keys(supportedCountries)
76
+ .filter(iso => /^[A-Z]{3}$/.test(iso)) // Only proper 3-letter ISO codes
77
+ .filter(iso => !EXCLUDED_ISOS.has(iso)) // Exclude countries without topology
78
+ .map(iso => ({
79
+ value: iso,
80
+ label: supportedCountries[iso][0] // Use the first (primary) name
81
+ }))
82
+ .sort((a, b) => a.label.localeCompare(b.label)) // Sort alphabetically by name
83
+ }
84
+
85
+ /**
86
+ * Helper to determine if the map should show only selected countries
87
+ * Returns true if countries are selected, false if showing all countries
88
+ */
89
+ export const isMultiCountryActive = (config: MapConfig): boolean => {
90
+ return Boolean(config.general.countriesPicked && config.general.countriesPicked.length > 0)
91
+ }
92
+
93
+ /**
94
+ * Helper to determine display mode for unselected countries
95
+ * Returns 'hidden' if hideUnselectedCountries is true, 'grayed' if false (default)
96
+ */
97
+ export const getUnselectedCountryDisplayMode = (config: MapConfig): 'hidden' | 'grayed' | 'normal' => {
98
+ if (!isMultiCountryActive(config)) {
99
+ return 'normal' // Show all countries normally when none are specifically selected
100
+ }
101
+
102
+ return config.general.hideUnselectedCountries ? 'hidden' : 'grayed'
103
+ }
@@ -1,4 +1,5 @@
1
1
  import { type MapConfig } from './../types/MapConfig'
2
+ import { isMultiCountryActive } from './getCountriesPicked'
2
3
 
3
4
  export const getMapContainerClasses = (state: MapConfig, modal) => {
4
5
  const { general } = state
@@ -19,5 +20,11 @@ export const getMapContainerClasses = (state: MapConfig, modal) => {
19
20
  if (general.type === 'navigation' && true === general.fullBorder) {
20
21
  mapContainerClasses.push('full-border')
21
22
  }
23
+
24
+ // Add multi-country class when multi-country mode is active
25
+ if (isMultiCountryActive(state)) {
26
+ mapContainerClasses.push('multi-country-selected')
27
+ }
28
+
22
29
  return mapContainerClasses
23
30
  }
@@ -1,6 +1,6 @@
1
1
  import { MapConfig } from '../types/MapConfig'
2
2
 
3
- export interface PatternInfo {
3
+ interface PatternInfo {
4
4
  pattern?: string
5
5
  dataKey: string
6
6
  size?: string
@@ -8,10 +8,7 @@ export interface PatternInfo {
8
8
  color?: string
9
9
  }
10
10
 
11
- export const getPatternForRow = (
12
- rowObj: Record<string, any>,
13
- config: MapConfig
14
- ): PatternInfo | null => {
11
+ export const getPatternForRow = (rowObj: Record<string, any>, config: MapConfig): PatternInfo | null => {
15
12
  if (!config.map?.patterns || !rowObj) {
16
13
  return null
17
14
  }