@cdc/map 4.25.8 → 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 (137) hide show
  1. package/.claude/agents/typescript-organizer.md +118 -0
  2. package/.claude/settings.local.json +30 -0
  3. package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
  4. package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
  5. package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
  6. package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
  7. package/dist/cdcmap.js +56991 -53706
  8. package/examples/example-city-state.json +9 -1
  9. package/examples/multi-country-centering.json +45 -0
  10. package/examples/private/c.json +290 -0
  11. package/examples/private/canvas-city-hover.json +787 -0
  12. package/examples/private/colors-2.json +221 -0
  13. package/examples/private/colors.json +221 -0
  14. package/examples/private/d.json +345 -0
  15. package/examples/private/g.json +1 -0
  16. package/examples/private/h.json +105911 -0
  17. package/examples/private/measles-data.json +378 -0
  18. package/examples/private/measles.json +211 -0
  19. package/examples/private/north-dakota.json +1132 -0
  20. package/examples/private/state-with-pattern.json +883 -0
  21. package/index.html +36 -34
  22. package/package.json +26 -5
  23. package/src/CdcMap.tsx +23 -8
  24. package/src/CdcMapComponent.tsx +238 -308
  25. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  26. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  27. package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
  28. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  29. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  30. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  31. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  32. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  33. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  34. package/src/_stories/CdcMap.stories.tsx +37 -9
  35. package/src/_stories/GoogleMap.stories.tsx +2 -2
  36. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  37. package/src/_stories/_mock/column-wrap-test.json +265 -0
  38. package/src/_stories/_mock/equal-number.json +1109 -0
  39. package/src/_stories/_mock/multi-country-hide.json +78 -0
  40. package/src/_stories/_mock/multi-country.json +95 -0
  41. package/src/_stories/_mock/multi-state.json +887 -20403
  42. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  43. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  44. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  45. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  46. package/src/_stories/_mock/usa-state-gradient.json +2 -4
  47. package/src/components/BubbleList.tsx +17 -13
  48. package/src/components/CityList.tsx +85 -107
  49. package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
  50. package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
  51. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
  53. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  54. package/src/components/Geo.tsx +22 -3
  55. package/src/components/Legend/components/Legend.tsx +76 -40
  56. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  57. package/src/components/Legend/components/index.scss +1 -1
  58. package/src/components/MapContainer.tsx +52 -0
  59. package/src/components/MapControls.tsx +44 -0
  60. package/src/components/NavigationMenu.tsx +27 -15
  61. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  62. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  63. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  64. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  65. package/src/components/SmallMultiples/index.tsx +3 -0
  66. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
  67. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  68. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  69. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
  70. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
  71. package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
  72. package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
  73. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
  74. package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
  75. package/src/components/UsaMap/helpers/map.ts +4 -4
  76. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  77. package/src/components/WorldMap/WorldMap.tsx +193 -35
  78. package/src/components/ZoomControls.tsx +6 -9
  79. package/src/context/LegendMemoContext.tsx +30 -0
  80. package/src/context.ts +1 -40
  81. package/src/data/initial-state.js +153 -130
  82. package/src/data/supported-geos.js +25 -78
  83. package/src/helpers/addUIDs.ts +13 -2
  84. package/src/helpers/applyColorToLegend.ts +140 -20
  85. package/src/helpers/applyLegendToRow.ts +10 -6
  86. package/src/helpers/componentHelpers.ts +8 -0
  87. package/src/helpers/constants.ts +12 -14
  88. package/src/helpers/dataTableHelpers.ts +6 -0
  89. package/src/helpers/displayGeoName.ts +18 -3
  90. package/src/helpers/generateRuntimeLegend.ts +44 -10
  91. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  92. package/src/helpers/getColumnNames.ts +1 -1
  93. package/src/helpers/getCountriesPicked.ts +103 -0
  94. package/src/helpers/getMapContainerClasses.ts +7 -0
  95. package/src/helpers/getPatternForRow.ts +33 -0
  96. package/src/helpers/getStatesPicked.ts +8 -5
  97. package/src/helpers/index.ts +3 -3
  98. package/src/helpers/isLegendItemDisabled.ts +16 -0
  99. package/src/helpers/mapObserverHelpers.ts +40 -0
  100. package/src/helpers/resetLegendToggles.ts +3 -2
  101. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  102. package/src/helpers/tests/titleCase.test.ts +76 -0
  103. package/src/helpers/titleCase.ts +13 -13
  104. package/src/helpers/toggleLegendActive.ts +6 -11
  105. package/src/helpers/urlDataHelpers.ts +70 -0
  106. package/src/hooks/useCountryZoom.tsx +241 -0
  107. package/src/hooks/useGeoClickHandler.ts +36 -2
  108. package/src/hooks/useLegendMemo.ts +17 -0
  109. package/src/hooks/useMapLayers.tsx +5 -4
  110. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  111. package/src/hooks/useResizeObserver.ts +5 -2
  112. package/src/hooks/useStateZoom.tsx +30 -8
  113. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  114. package/src/hooks/useTooltip.ts +1 -2
  115. package/src/index.jsx +1 -2
  116. package/src/scss/editor-panel.scss +4 -440
  117. package/src/scss/main.scss +1 -1
  118. package/src/scss/map.scss +12 -15
  119. package/src/store/map.actions.ts +7 -7
  120. package/src/store/map.reducer.ts +17 -6
  121. package/src/test/CdcMap.test.jsx +11 -0
  122. package/src/types/MapConfig.ts +46 -18
  123. package/src/types/MapContext.ts +6 -7
  124. package/src/types/runtimeLegend.ts +17 -1
  125. package/vite.config.js +2 -7
  126. package/vitest.config.ts +16 -0
  127. package/src/components/DataTable.tsx +0 -385
  128. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  129. package/src/coreStyles_map.scss +0 -3
  130. package/src/helpers/colorDistributions.ts +0 -12
  131. package/src/helpers/generateColorsArray.ts +0 -14
  132. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  133. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
  134. package/src/hooks/useActiveElement.ts +0 -19
  135. package/src/scss/mixins.scss +0 -47
  136. package/src/types/Annotations.ts +0 -24
  137. /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
@@ -1,7 +1,9 @@
1
- import { generateColorsArray, hashObj } from '../helpers'
2
- import colorPalettes from '@cdc/core/data/colorPalettes'
1
+ import { generateColorsArray } from '@cdc/core/helpers/generateColorsArray'
2
+ import { hashObj, DEFAULT_MAP_BACKGROUND } from '../helpers'
3
+ import { mapColorPalettes as colorPalettes } from '@cdc/core/data/colorPalettes'
3
4
  import { MapConfig } from '../types/MapConfig'
4
5
  import { type RuntimeLegend } from '../types/runtimeLegend'
6
+ import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
5
7
 
6
8
  type Memo<T> = { current: Map<string, T> }
7
9
 
@@ -14,8 +16,9 @@ export const applyLegendToRow = (
14
16
  ): string[] => {
15
17
  if (!config) return null
16
18
 
17
- const { general, color, legend } = config
19
+ const { general, legend } = config
18
20
  const { type } = general
21
+ const color = general.palette?.name ?? 'bluegreenreverse'
19
22
  const { showSpecialClassesLast } = legend
20
23
 
21
24
  try {
@@ -25,21 +28,22 @@ export const applyLegendToRow = (
25
28
  }
26
29
 
27
30
  if (type === 'navigation') {
28
- const mapColorPalette = colorPalettes[color] ?? colorPalettes['bluegreenreverse']
31
+ const mapColorPalette =
32
+ colorPalettes[`v${getColorPaletteVersion(config)}`]?.[color] ?? colorPalettes.v1['bluegreenreverse']
29
33
  return generateColorsArray(mapColorPalette[3])
30
34
  }
31
35
 
32
36
  const hash = hashObj(rowObj)
33
37
 
34
38
  if (!legendMemo.current.has(hash)) {
35
- return generateColorsArray()
39
+ return generateColorsArray(DEFAULT_MAP_BACKGROUND)
36
40
  }
37
41
 
38
42
  const idx = legendMemo.current.get(hash)!
39
43
  const disabledIdx = showSpecialClassesLast ? legendSpecialClassLastMemo.current.get(hash) ?? idx : idx
40
44
 
41
45
  if (runtimeLegend.items?.[disabledIdx]?.disabled) {
42
- return generateColorsArray()
46
+ return generateColorsArray(DEFAULT_MAP_BACKGROUND)
43
47
  }
44
48
 
45
49
  const legendBinColor = runtimeLegend.items.find(o => o.bin === idx)?.color
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Builds CSS class names for the main map section container
3
+ */
4
+ export const buildSectionClassNames = (viewport: string, headerColor: string, hasError: boolean): string => {
5
+ const classes = ['cove-component__content', 'cdc-map-inner-container', viewport, headerColor]
6
+ if (hasError) classes.push('type-map--has-error')
7
+ return classes.join(' ')
8
+ }
@@ -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 = [
@@ -42,3 +29,14 @@ export const GEOCODE_TYPES = {
42
29
  } as const
43
30
 
44
31
  export const DEFAULT_MAP_BACKGROUND = '#DFE1E2'
32
+
33
+ // Component constants
34
+ export const LOGO_MAX_WIDTH = '50px'
35
+
36
+ // CSV Parsing Configuration
37
+ export const CSV_PARSE_CONFIG = {
38
+ header: true,
39
+ dynamicTyping: true,
40
+ skipEmptyLines: true,
41
+ encoding: 'utf-8'
42
+ } as const
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Determines if the data table should be shown based on current state
3
+ */
4
+ export const shouldShowDataTable = (config: any, table: any, general: any, loading: boolean): boolean => {
5
+ return !config?.runtime?.editorErrorMessage.length && table.forceDisplay && general.type !== 'navigation' && !loading
6
+ }
@@ -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 {
73
+ // Apply titleCase to unrecognized values (e.g., "DISTRICT OF COLUMBIA" -> "District of Columbia")
59
74
  return titleCase(value)
60
75
  }
61
76
  }
@@ -14,8 +14,13 @@ import _ from 'lodash'
14
14
  import * as d3 from 'd3'
15
15
 
16
16
  // Cdc
17
- import colorPalettes from '@cdc/core/data/colorPalettes'
17
+ import { mapColorPalettes as colorPalettes } from '@cdc/core/data/colorPalettes'
18
18
  import { supportedCountries } from '../data/supported-geos'
19
+ import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
20
+ import { v2ColorDistribution } from '@cdc/core/helpers/palettes/colorDistributions'
21
+
22
+ // Types
23
+ import { MapConfig, DataRow, RuntimeFilters } from '../types/MapConfig'
19
24
 
20
25
  type LegendItem = {
21
26
  special?: boolean
@@ -34,11 +39,11 @@ export type GeneratedLegend = {
34
39
  }
35
40
 
36
41
  export const generateRuntimeLegend = (
37
- configObj,
38
- runtimeData: object[],
42
+ configObj: MapConfig,
43
+ runtimeData: DataRow[],
39
44
  hash: string,
40
- setConfig: Function,
41
- runtimeFilters: object[],
45
+ setConfig: (newMapConfig: MapConfig) => void,
46
+ runtimeFilters: RuntimeFilters,
42
47
  legendMemo: React.MutableRefObject<Map<string, number>>,
43
48
  legendSpecialClassLastMemo: React.MutableRefObject<Map<string, number>>
44
49
  ): GeneratedLegend | [] => {
@@ -331,8 +336,35 @@ export const generateRuntimeLegend = (
331
336
  numberOfRows -= chunkAmt
332
337
  }
333
338
  } else {
334
- let colors = colorPalettes[configObj.color]
335
- let colorRange = colors.slice(0, legend.numberOfItems)
339
+ const paletteName = configObj.general?.palette?.name || configObj.color
340
+ const version = getColorPaletteVersion(configObj)
341
+ let colors = colorPalettes?.[`v${version}`]?.[paletteName]
342
+ // Fallback to a default palette if none is selected or found
343
+ if (!colors) {
344
+ const defaultPalette = version === 1 ? 'sequential_blue_green' : 'sequential_blue'
345
+ colors = colorPalettes?.[`v${version}`]?.[defaultPalette]
346
+ }
347
+
348
+ if (!colors) {
349
+ console.warn('No color palette found, using fallback colors')
350
+ colors = ['#d3d3d3', '#a0a0a0', '#707070', '#404040'] // Gray fallback
351
+ }
352
+
353
+ // Check if we should use v2 distribution logic for better contrast
354
+ const isSequentialOrDivergent =
355
+ paletteName && (paletteName.includes('sequential') || paletteName.includes('divergent'))
356
+ const useV2Distribution =
357
+ version === 2 && isSequentialOrDivergent && colors.length === 9 && legend.numberOfItems <= 9
358
+
359
+ let colorRange
360
+ if (useV2Distribution && v2ColorDistribution[legend.numberOfItems]) {
361
+ // Use strategic color distribution for v2 sequential/divergent palettes
362
+ const distributionIndices = v2ColorDistribution[legend.numberOfItems]
363
+ colorRange = distributionIndices.map(index => colors[index])
364
+ } else {
365
+ // Use existing logic for v1 palettes and other cases
366
+ colorRange = colors.slice(0, legend.numberOfItems)
367
+ }
336
368
 
337
369
  const getDomain = () => {
338
370
  // backwards compatibility
@@ -559,8 +591,10 @@ export const generateRuntimeLegend = (
559
591
  return result
560
592
  } catch (e) {
561
593
  console.error(e)
562
- return []
594
+ return {
595
+ fromHash: null,
596
+ runtimeDataHash: null,
597
+ items: []
598
+ }
563
599
  }
564
600
  }
565
-
566
- export default generateRuntimeLegend
@@ -2,12 +2,13 @@ import { hashObj } from './hashObj'
2
2
  import { MapConfig } from '../types/MapConfig'
3
3
 
4
4
  export const generateRuntimeLegendHash = (config: MapConfig, runtimeFilters) => {
5
+ const { name: paletteName } = config.general.palette
5
6
  return hashObj({
6
7
  unified: config.legend.unified ?? false,
7
8
  equalNumberOptIn: config.general.equalNumberOptIn ?? false,
8
9
  specialClassesLast: config.legend.showSpecialClassesLast ?? false,
9
- color: config.color,
10
- customColors: config.customColors,
10
+ color: paletteName,
11
+ customColors: config.general?.palette?.customColors,
11
12
  numberOfItems: config.legend.numberOfItems,
12
13
  type: config.legend.type,
13
14
  separateZero: config.legend.separateZero ?? false,
@@ -16,6 +17,7 @@ export const generateRuntimeLegendHash = (config: MapConfig, runtimeFilters) =>
16
17
  specialClasses: config.legend.specialClasses,
17
18
  geoType: config.general.geoType,
18
19
  data: config.data,
20
+ palette: config.general.palette,
19
21
  filters: {
20
22
  ...config.filters
21
23
  },
@@ -8,7 +8,7 @@ type ColumnNames = {
8
8
  categoricalColumnName: string | null
9
9
  } | null
10
10
 
11
- export const getColumnNames = (columns?: Pick<MapConfig, 'columns'>): ColumnNames => {
11
+ export const getColumnNames = (columns?: MapConfig['columns']): ColumnNames => {
12
12
  if (!columns) return null
13
13
  const geoColumnName = columns.geo?.name || null
14
14
  const primaryColumnName = columns.primary?.name || null
@@ -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
  }
@@ -0,0 +1,33 @@
1
+ import { MapConfig } from '../types/MapConfig'
2
+
3
+ interface PatternInfo {
4
+ pattern?: string
5
+ dataKey: string
6
+ size?: string
7
+ patternIndex: number
8
+ color?: string
9
+ }
10
+
11
+ export const getPatternForRow = (rowObj: Record<string, any>, config: MapConfig): PatternInfo | null => {
12
+ if (!config.map?.patterns || !rowObj) {
13
+ return null
14
+ }
15
+
16
+ // Find a pattern that matches this row's data
17
+ for (let i = 0; i < config.map.patterns.length; i++) {
18
+ const patternData = config.map.patterns[i]
19
+ const hasMatchingValues = patternData.dataValue === rowObj[patternData.dataKey]
20
+
21
+ if (hasMatchingValues) {
22
+ return {
23
+ pattern: patternData.pattern,
24
+ dataKey: patternData.dataKey,
25
+ size: patternData.size,
26
+ patternIndex: i,
27
+ color: patternData.color
28
+ }
29
+ }
30
+ }
31
+
32
+ return null
33
+ }
@@ -3,9 +3,12 @@ import { supportedStatesFipsCodes } from '../data/supported-geos'
3
3
 
4
4
  export const getStatesPicked = (config, runtimeData) => {
5
5
  const stateNames = getFilterControllingStatesPicked(config, runtimeData)
6
-
7
- return stateNames.map(stateName => ({
8
- fipsCode: Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === stateName),
9
- stateName
10
- }))
6
+ return stateNames.map(stateName => {
7
+ const fipsCode = Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === stateName)
8
+ if (!fipsCode) console.error(`State name "${stateName}" not found.`)
9
+ return {
10
+ fipsCode,
11
+ stateName
12
+ }
13
+ })
11
14
  }
@@ -1,9 +1,8 @@
1
1
  export { addUIDs } from './addUIDs'
2
2
  export { applyColorToLegend } from './applyColorToLegend'
3
- export { colorDistributions } from './colorDistributions'
4
3
  export { displayGeoName } from './displayGeoName'
5
4
  export { formatLegendLocation } from './formatLegendLocation'
6
- export { generateColorsArray } from './generateColorsArray'
5
+ export { generateColorsArray } from '@cdc/core/helpers/generateColorsArray'
7
6
  export { generateRuntimeLegendHash } from './generateRuntimeLegendHash'
8
7
  export { getGeoStrokeColor, getGeoFillColor } from './colors'
9
8
  export { getUniqueValues } from './getUniqueValues'
@@ -11,6 +10,7 @@ export { handleMapAriaLabels } from './handleMapAriaLabels'
11
10
  export { handleMapTabbing } from './handleMapTabbing'
12
11
  export { hashObj } from './hashObj'
13
12
  export { indexOfIgnoreType } from './indexOfIgnoreType'
13
+ export { isLegendItemDisabled } from './isLegendItemDisabled'
14
14
  export { navigationHandler } from './navigationHandler'
15
15
  export { resetLegendToggles } from './resetLegendToggles'
16
16
  export { setBinNumbers } from './setBinNumbers'
@@ -19,4 +19,4 @@ export { titleCase as toTitleCase } from './toTitleCase'
19
19
  export { titleCase } from './titleCase'
20
20
  export { validateFipsCodeLength } from './validateFipsCodeLength'
21
21
  export { getMapContainerClasses } from './getMapContainerClasses'
22
- export { SVG_HEIGHT, SVG_WIDTH, SVG_PADDING, SVG_VIEWBOX, HEADER_COLORS, MAX_ZOOM_LEVEL } from './constants'
22
+ export { SVG_HEIGHT, SVG_WIDTH, SVG_PADDING, SVG_VIEWBOX, MAX_ZOOM_LEVEL, DEFAULT_MAP_BACKGROUND } from './constants'
@@ -0,0 +1,16 @@
1
+ import { hashObj } from './hashObj'
2
+
3
+ export const isLegendItemDisabled = (
4
+ dataForCheck: any,
5
+ runtimeLegend: any,
6
+ legendMemo: React.MutableRefObject<Map<number, number>>,
7
+ legendSpecialClassLastMemo: React.MutableRefObject<Map<number, number>>,
8
+ config: any
9
+ ): boolean => {
10
+ if (!dataForCheck || !runtimeLegend?.items) return false
11
+ const hash = hashObj(dataForCheck)
12
+ if (!legendMemo.current.has(hash)) return false
13
+ const idx = legendMemo.current.get(hash)
14
+ const disabledIdx = config.legend.showSpecialClassesLast ? legendSpecialClassLastMemo.current.get(hash) ?? idx : idx
15
+ return runtimeLegend.items[disabledIdx]?.disabled || false
16
+ }
@@ -0,0 +1,40 @@
1
+ import { publish } from '@cdc/core/helpers/events'
2
+ import { MapConfig } from '../types/MapConfig'
3
+ import MapActions from '../store/map.actions'
4
+ import { Dispatch } from 'react'
5
+
6
+ /**
7
+ * Publishes 'cove_loaded' only after the map SVG is rendered in the DOM.
8
+ * Checks immediately, then uses a MutationObserver as a fallback for async rendering.
9
+ * Update the mapSvg ref if the map container changes.
10
+ */
11
+ export const observeMapSvgLoaded = (
12
+ mapSvgRef: React.RefObject<HTMLElement>,
13
+ config: MapConfig,
14
+ coveLoadedHasRan: boolean,
15
+ dispatch: Dispatch<MapActions>
16
+ ): (() => void) => {
17
+ // Immediate check in case SVG is already present
18
+ const svgEl = mapSvgRef.current?.querySelector('svg')
19
+ if (svgEl && svgEl.childNodes.length > 0) {
20
+ publish('cove_loaded', { config })
21
+ dispatch({ type: 'SET_COVE_LOADED_HAS_RAN', payload: true })
22
+ return () => {}
23
+ }
24
+
25
+ // Fallback to observer for async SVG rendering
26
+ const observer = new MutationObserver(() => {
27
+ const svgEl = mapSvgRef.current?.querySelector('svg')
28
+ if (svgEl && svgEl.childNodes.length > 0) {
29
+ publish('cove_loaded', { config })
30
+ dispatch({ type: 'SET_COVE_LOADED_HAS_RAN', payload: true })
31
+ observer.disconnect()
32
+ }
33
+ })
34
+
35
+ if (mapSvgRef.current) {
36
+ observer.observe(mapSvgRef.current, { childList: true, subtree: true })
37
+ }
38
+
39
+ return () => observer.disconnect()
40
+ }
@@ -1,5 +1,6 @@
1
1
  import _ from 'lodash'
2
- export const resetLegendToggles = (runtimeLegend, setRuntimeLegend) => {
2
+
3
+ export const resetLegendToggles = (runtimeLegend, dispatch) => {
3
4
  const legendCopy = _.cloneDeep(runtimeLegend)
4
5
 
5
6
  legendCopy.items.forEach(legendItem => {
@@ -9,5 +10,5 @@ export const resetLegendToggles = (runtimeLegend, setRuntimeLegend) => {
9
10
 
10
11
  legendCopy.runtimeDataHash = runtimeLegend.runtimeDataHash
11
12
 
12
- setRuntimeLegend(legendCopy)
13
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legendCopy })
13
14
  }