@cdc/map 4.25.8 → 4.25.10

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 (84) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/dist/cdcmap.js +54263 -52600
  3. package/examples/private/c.json +290 -0
  4. package/examples/private/canvas-city-hover.json +787 -0
  5. package/examples/private/d.json +345 -0
  6. package/examples/private/g.json +1 -0
  7. package/examples/private/h.json +105911 -0
  8. package/examples/private/measles-data.json +378 -0
  9. package/examples/private/measles.json +211 -0
  10. package/examples/private/north-dakota.json +1132 -0
  11. package/examples/private/state-with-pattern.json +883 -0
  12. package/index.html +35 -34
  13. package/package.json +26 -5
  14. package/src/CdcMap.tsx +23 -8
  15. package/src/CdcMapComponent.tsx +215 -309
  16. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  17. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  18. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  19. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  20. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.stories.tsx +15 -5
  22. package/src/_stories/GoogleMap.stories.tsx +2 -2
  23. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  24. package/src/_stories/_mock/equal-number.json +1109 -0
  25. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  26. package/src/components/BubbleList.tsx +16 -12
  27. package/src/components/CityList.tsx +85 -107
  28. package/src/components/DataTable.tsx +37 -9
  29. package/src/components/EditorPanel/components/EditorPanel.tsx +177 -165
  30. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  32. package/src/components/Geo.tsx +2 -0
  33. package/src/components/Legend/components/Legend.tsx +109 -73
  34. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  35. package/src/components/MapContainer.tsx +52 -0
  36. package/src/components/MapControls.tsx +44 -0
  37. package/src/components/NavigationMenu.tsx +11 -2
  38. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  39. package/src/components/UsaMap/components/UsaMap.County.tsx +111 -37
  40. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  41. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +6 -6
  42. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  43. package/src/components/UsaMap/helpers/map.ts +2 -2
  44. package/src/components/WorldMap/WorldMap.tsx +113 -25
  45. package/src/components/ZoomControls.tsx +6 -9
  46. package/src/context/LegendMemoContext.tsx +30 -0
  47. package/src/context.ts +1 -40
  48. package/src/data/initial-state.js +143 -130
  49. package/src/data/supported-geos.js +17 -2
  50. package/src/helpers/applyColorToLegend.ts +116 -20
  51. package/src/helpers/applyLegendToRow.ts +10 -6
  52. package/src/helpers/componentHelpers.ts +8 -0
  53. package/src/helpers/constants.ts +12 -0
  54. package/src/helpers/dataTableHelpers.ts +6 -0
  55. package/src/helpers/displayGeoName.ts +1 -1
  56. package/src/helpers/generateRuntimeLegend.ts +44 -8
  57. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  58. package/src/helpers/getColumnNames.ts +1 -1
  59. package/src/helpers/getPatternForRow.ts +36 -0
  60. package/src/helpers/getStatesPicked.ts +8 -5
  61. package/src/helpers/index.ts +11 -3
  62. package/src/helpers/isLegendItemDisabled.ts +16 -0
  63. package/src/helpers/mapObserverHelpers.ts +40 -0
  64. package/src/helpers/resetLegendToggles.ts +3 -2
  65. package/src/helpers/toggleLegendActive.ts +6 -11
  66. package/src/helpers/urlDataHelpers.ts +70 -0
  67. package/src/hooks/useGeoClickHandler.ts +35 -1
  68. package/src/hooks/useLegendMemo.ts +17 -0
  69. package/src/hooks/useMapLayers.tsx +5 -4
  70. package/src/hooks/useStateZoom.tsx +25 -6
  71. package/src/hooks/useTooltip.ts +1 -2
  72. package/src/index.jsx +0 -2
  73. package/src/store/map.reducer.ts +17 -6
  74. package/src/test/CdcMap.test.jsx +11 -0
  75. package/src/types/MapConfig.ts +23 -14
  76. package/src/types/MapContext.ts +0 -7
  77. package/src/types/runtimeLegend.ts +17 -1
  78. package/vite.config.js +2 -7
  79. package/vitest.config.ts +16 -0
  80. package/src/coreStyles_map.scss +0 -3
  81. package/src/helpers/colorDistributions.ts +0 -12
  82. package/src/helpers/generateColorsArray.ts +0 -14
  83. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  84. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
@@ -1,7 +1,44 @@
1
- import colorPalettes from '@cdc/core/data/colorPalettes'
1
+ import { mapColorPalettes as colorPalettes } from '@cdc/core/data/colorPalettes'
2
2
  import chroma from 'chroma-js'
3
- import { type MapConfig } from '@cdc/map/src/types/MapConfig'
4
- import { colorDistributions } from './colorDistributions'
3
+ import { type MapConfig } from '../types/MapConfig'
4
+ import { mapV1ColorDistribution } from '@cdc/core/helpers/palettes/colorDistributions'
5
+ import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
6
+
7
+ // Palette name migrations from v1 to v2
8
+ const mapPaletteNameMigrations = {
9
+ yelloworangered: 'sequential_yellow_orange_red',
10
+ yelloworangebrown: 'sequential_yellow_orange_brown',
11
+ pinkpurple: 'sequential_pink_purple',
12
+ pinkpurplereverse: 'sequential_pink_purplereverse',
13
+ bluegreen: 'sequential_blue_green',
14
+ bluegreenreverse: 'sequential_blue_greenreverse',
15
+ orangered: 'sequential_orange_red',
16
+ orangeredreverse: 'sequential_orange_redreverse',
17
+ red: 'sequential_red',
18
+ redreverse: 'sequential_redreverse',
19
+ greenblue: 'sequential_green_blue',
20
+ greenbluereverse: 'sequential_green_bluereverse',
21
+ yelloworangeredreverse: 'sequential_yellow_orange_redreverse',
22
+ yelloworangebrownreverse: 'sequential_yellow_orange_brownreverse',
23
+ yellowpurple: 'divergent_yellow_purple',
24
+ yellowpurplereverse: 'divergent_yellow_purplereverse',
25
+ qualitative1: 'qualitative1',
26
+ qualitative2: 'qualitative2',
27
+ qualitative3: 'qualitative3',
28
+ qualitative4: 'qualitative4',
29
+ qualitative9: 'qualitative9',
30
+ 'sequential-blue-2(MPX)': 'sequential_blue_extended',
31
+ 'sequential-blue-2(MPX)reverse': 'sequential_blue_extendedreverse',
32
+ 'sequential-orange(MPX)': 'sequential_orange_extended',
33
+ 'sequential-orange(MPX)reverse': 'sequential_orange_extendedreverse',
34
+ colorblindsafe: 'colorblindsafe',
35
+ qualitative1reverse: 'qualitative1reverse',
36
+ qualitative2reverse: 'qualitative2reverse',
37
+ qualitative3reverse: 'qualitative3reverse',
38
+ qualitative4reverse: 'qualitative4reverse',
39
+ qualitative9reverse: 'qualitative9reverse',
40
+ colorblindsafereverse: 'colorblindsafereverse'
41
+ }
5
42
 
6
43
  type LegendItem = {
7
44
  special: boolean
@@ -17,39 +54,98 @@ type LegendItem = {
17
54
  export const applyColorToLegend = (legendIdx: number, config: MapConfig, result: LegendItem[] = []): string => {
18
55
  if (!config) throw new Error('Config is required')
19
56
 
20
- const { legend, customColors, general, color } = config
21
- const { geoType, palette } = general
22
- const specialClasses = legend?.specialClasses ?? []
23
- const mapColorPalette = customColors ?? colorPalettes[color] ?? colorPalettes['bluegreen']
57
+ const { legend, general } = config
58
+ const { geoType, palette = { name: 'bluegreen', isReversed: false } } = general
59
+ // Support both migrated (general.palette.name) and legacy (config.color) palette locations
60
+ const version = getColorPaletteVersion(config)
61
+ let color = general?.palette?.name || config.color || palette.name || 'bluegreen'
62
+
63
+ // Apply palette migration if needed (v1 name -> v2 name)
64
+ if (mapPaletteNameMigrations[color]) {
65
+ color = mapPaletteNameMigrations[color]
66
+ }
67
+
68
+ // Try multiple approaches to find the palette
69
+ let mapColorPalette = general?.palette?.customColors
70
+
71
+ if (!mapColorPalette) {
72
+ // Try the detected version first
73
+ mapColorPalette = colorPalettes?.[`v${version}`]?.[color]
74
+ }
75
+
76
+ if (!mapColorPalette) {
77
+ // Try v2 explicitly
78
+ mapColorPalette = colorPalettes?.v2?.[color]
79
+ }
80
+
81
+ if (!mapColorPalette) {
82
+ // Try v1 with original color name
83
+ const originalColor = general?.palette?.name || config.color || palette.name || 'bluegreen'
84
+ mapColorPalette = colorPalettes?.v1?.[originalColor]
85
+ }
86
+
87
+ if (!mapColorPalette) {
88
+ // Final fallback
89
+ mapColorPalette = colorPalettes.v1['bluegreen']
90
+ }
24
91
 
25
92
  // Handle Region Maps need for a 10th color
26
93
  if (geoType === 'us-region' && mapColorPalette.length < 10 && mapColorPalette.length > 8) {
27
- const newColor = chroma(mapColorPalette[palette.isReversed ? 0 : 8])
94
+ const newColor = chroma(mapColorPalette[config.general.palette.isReversed ? 0 : 8])
28
95
  .darken(0.75)
29
96
  .hex()
30
- palette.isReversed ? mapColorPalette.unshift(newColor) : mapColorPalette.push(newColor)
97
+ config.general.palette.isReversed ? mapColorPalette.unshift(newColor) : mapColorPalette.push(newColor)
31
98
  }
32
99
 
33
- const colorIdx = legendIdx - specialClasses.length
100
+ // Count actual special classes in the result array
101
+ const actualSpecialClassCount = result.filter(item => item.special).length
102
+ const colorIdx = legendIdx - actualSpecialClassCount
34
103
 
35
104
  // Handle special classes coloring
36
105
  if (result[legendIdx]?.special) {
37
- const specialClassColors = chroma.scale(['#D4D4D4', '#939393']).colors(specialClasses.length)
38
- return specialClassColors[legendIdx]
106
+ const specialClassColors = chroma.scale(['#D4D4D4', '#939393']).colors(actualSpecialClassCount)
107
+ const specialClassIdx = result.slice(0, legendIdx + 1).filter(item => item.special).length - 1
108
+ return specialClassColors[specialClassIdx]
39
109
  }
40
110
 
41
111
  // Use qualitative color palettes directly
42
- if (color.includes('qualitative')) return mapColorPalette[colorIdx]
112
+ if (color.includes('qualitative')) {
113
+ return mapColorPalette[colorIdx]
114
+ }
115
+
116
+ // Determine color distribution based on non-special items
117
+ // For numeric legends, use the configured numberOfItems for consistent color distribution
118
+ // For category legends, use the actual result length
119
+ const isNumericLegend = legend && ['equalnumber', 'equalinterval'].includes(legend.type)
120
+ const nonSpecialItemCount = isNumericLegend
121
+ ? (legend.numberOfItems || result.length)
122
+ : result.length - actualSpecialClassCount
43
123
 
44
- // Determine color distribution
45
124
  const amt =
46
- Math.max(result.length - specialClasses.length, 1) < 10
47
- ? Math.max(result.length - specialClasses.length, 1)
48
- : Object.keys(colorDistributions).length
49
- const distributionArray = colorDistributions[amt] ?? []
125
+ Math.max(nonSpecialItemCount, 1) < 10
126
+ ? Math.max(nonSpecialItemCount, 1)
127
+ : Object.keys(mapV1ColorDistribution).length
128
+ const distributionArray = mapV1ColorDistribution[amt] ?? []
129
+
130
+ // Safety check to ensure mapColorPalette exists and is an array
131
+ if (!mapColorPalette || !Array.isArray(mapColorPalette) || mapColorPalette.length === 0) {
132
+ console.warn('No valid color palette found, returning gray fallback')
133
+ return '#d3d3d3'
134
+ }
50
135
 
51
136
  const specificColor =
52
- distributionArray[legendIdx - specialClasses.length] ?? mapColorPalette[colorIdx] ?? mapColorPalette.at(-1)
137
+ distributionArray[colorIdx] ?? mapColorPalette[colorIdx] ?? mapColorPalette[mapColorPalette.length - 1]
138
+
139
+ if (typeof specificColor === 'number') {
140
+ return specificColor < mapColorPalette.length
141
+ ? mapColorPalette[specificColor]
142
+ : mapColorPalette[mapColorPalette.length - 1]
143
+ }
144
+
145
+ if (typeof specificColor === 'string') {
146
+ return specificColor
147
+ }
53
148
 
54
- return mapColorPalette[specificColor]
149
+ // Final fallback
150
+ return mapColorPalette[0] || '#d3d3d3'
55
151
  }
@@ -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
+ }
@@ -42,3 +42,15 @@ export const GEOCODE_TYPES = {
42
42
  } as const
43
43
 
44
44
  export const DEFAULT_MAP_BACKGROUND = '#DFE1E2'
45
+
46
+ // Component constants
47
+ export const LOGO_MAX_WIDTH = '50px'
48
+ export const STORYBOOK_PORT = 6006
49
+
50
+ // CSV Parsing Configuration
51
+ export const CSV_PARSE_CONFIG = {
52
+ header: true,
53
+ dynamicTyping: true,
54
+ skipEmptyLines: true,
55
+ encoding: 'utf-8'
56
+ } 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
+ }
@@ -56,6 +56,6 @@ export const displayGeoName = (key: string, convertFipsCodes = true): string =>
56
56
  if (value?.length === 2 || value === 'U.S. Virgin Islands') {
57
57
  return value
58
58
  } else {
59
- return titleCase(value)
59
+ return value
60
60
  }
61
61
  }
@@ -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,7 +591,11 @@ 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
601
 
@@ -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,36 @@
1
+ import { MapConfig } from '../types/MapConfig'
2
+
3
+ export interface PatternInfo {
4
+ pattern?: string
5
+ dataKey: string
6
+ size?: string
7
+ patternIndex: number
8
+ color?: string
9
+ }
10
+
11
+ export const getPatternForRow = (
12
+ rowObj: Record<string, any>,
13
+ config: MapConfig
14
+ ): PatternInfo | null => {
15
+ if (!config.map?.patterns || !rowObj) {
16
+ return null
17
+ }
18
+
19
+ // Find a pattern that matches this row's data
20
+ for (let i = 0; i < config.map.patterns.length; i++) {
21
+ const patternData = config.map.patterns[i]
22
+ const hasMatchingValues = patternData.dataValue === rowObj[patternData.dataKey]
23
+
24
+ if (hasMatchingValues) {
25
+ return {
26
+ pattern: patternData.pattern,
27
+ dataKey: patternData.dataKey,
28
+ size: patternData.size,
29
+ patternIndex: i,
30
+ color: patternData.color
31
+ }
32
+ }
33
+ }
34
+
35
+ return null
36
+ }
@@ -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,12 @@ 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 {
23
+ SVG_HEIGHT,
24
+ SVG_WIDTH,
25
+ SVG_PADDING,
26
+ SVG_VIEWBOX,
27
+ HEADER_COLORS,
28
+ MAX_ZOOM_LEVEL,
29
+ DEFAULT_MAP_BACKGROUND
30
+ } 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
  }
@@ -1,12 +1,6 @@
1
1
  import _ from 'lodash'
2
2
 
3
- export const toggleLegendActive = (
4
- i: number,
5
- legendLabel: string,
6
- runtimeLegend,
7
- setRuntimeLegend,
8
- setAccessibleStatus: (message: string) => void
9
- ) => {
3
+ export const toggleLegendActive = (i: number, legendLabel: string, runtimeLegend, dispatch) => {
10
4
  const runtimeLegendCopy = _.cloneDeep(runtimeLegend)
11
5
 
12
6
  // Create and toggle the new value
@@ -17,9 +11,10 @@ export const toggleLegendActive = (
17
11
 
18
12
  runtimeLegendCopy['disabledAmt'] = newValue ? disabledAmt + 1 : disabledAmt - 1
19
13
 
20
- setRuntimeLegend(runtimeLegendCopy)
14
+ dispatch({ type: 'SET_RUNTIME_LEGEND', payload: runtimeLegendCopy })
21
15
 
22
- setAccessibleStatus(
23
- `Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`
24
- )
16
+ dispatch({
17
+ type: 'SET_ACCESSIBLE_STATUS',
18
+ payload: `Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`
19
+ })
25
20
  }
@@ -0,0 +1,70 @@
1
+ import Papa from 'papaparse'
2
+ import _ from 'lodash'
3
+ import { DataTransform } from '@cdc/core/helpers/DataTransform'
4
+ import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
5
+ import { MapConfig } from '../types/MapConfig'
6
+ import { CSV_PARSE_CONFIG } from './constants'
7
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
8
+
9
+ export const buildQueryString = (params: Record<string, string>): string =>
10
+ Object.keys(params)
11
+ .map((param, i) => {
12
+ let qs = i === 0 ? '?' : '&'
13
+ qs += param + '='
14
+ qs += params[param]
15
+ return qs
16
+ })
17
+ .join('')
18
+
19
+ export const reloadURLData = async (config: MapConfig, setConfig: (config: MapConfig) => void): Promise<void> => {
20
+ if (!config.dataUrl) return
21
+
22
+ const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl, window.location.origin)
23
+ let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
24
+
25
+ let isUpdateNeeded = false
26
+ config.filters.forEach(filter => {
27
+ if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
28
+ qsParams[filter.queryParameter] = filter.active
29
+ isUpdateNeeded = true
30
+ }
31
+ })
32
+
33
+ if (!isUpdateNeeded) return
34
+
35
+ let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${buildQueryString(qsParams)}`
36
+ let data
37
+
38
+ try {
39
+ const regex = /(?:\.([^.]+))?$/
40
+ const ext = regex.exec(dataUrl.pathname)[1]
41
+
42
+ if ('csv' === ext || isSolrCsv(dataUrlFinal)) {
43
+ data = await fetch(dataUrlFinal)
44
+ .then(response => response.text())
45
+ .then(responseText => {
46
+ const parsedCsv = Papa.parse(responseText, CSV_PARSE_CONFIG)
47
+ return parsedCsv.data
48
+ })
49
+ } else if ('json' === ext || isSolrJson(dataUrlFinal)) {
50
+ data = await fetch(dataUrlFinal).then(response => response.json())
51
+ } else {
52
+ data = []
53
+ }
54
+ } catch (e) {
55
+ console.error(`Cannot parse URL: ${dataUrlFinal}`, e)
56
+ data = []
57
+ }
58
+
59
+ if (config.dataDescription) {
60
+ const transform = new DataTransform()
61
+ data = transform.autoStandardize(data)
62
+ data = transform.developerStandardize(data, config.dataDescription)
63
+ }
64
+
65
+ const newConfig = cloneConfig(config)
66
+ newConfig.data = data
67
+ newConfig.runtimeDataUrl = dataUrlFinal
68
+
69
+ setConfig(newConfig)
70
+ }