@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.
- package/.claude/agents/typescript-organizer.md +118 -0
- package/.claude/settings.local.json +30 -0
- package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
- package/dist/cdcmap.js +56991 -53706
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/d.json +345 -0
- package/examples/private/g.json +1 -0
- package/examples/private/h.json +105911 -0
- package/examples/private/measles-data.json +378 -0
- package/examples/private/measles.json +211 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/state-with-pattern.json +883 -0
- package/index.html +36 -34
- package/package.json +26 -5
- package/src/CdcMap.tsx +23 -8
- package/src/CdcMapComponent.tsx +238 -308
- package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
- package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
- package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
- package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
- package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
- package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +37 -9
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/column-wrap-test.json +265 -0
- package/src/_stories/_mock/equal-number.json +1109 -0
- package/src/_stories/_mock/multi-country-hide.json +78 -0
- package/src/_stories/_mock/multi-country.json +95 -0
- package/src/_stories/_mock/multi-state.json +887 -20403
- package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
- package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
- package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
- package/src/_stories/_mock/us-bubble-cities.json +306 -0
- package/src/_stories/_mock/usa-state-gradient.json +2 -4
- package/src/components/BubbleList.tsx +17 -13
- package/src/components/CityList.tsx +85 -107
- package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/Geo.tsx +22 -3
- package/src/components/Legend/components/Legend.tsx +76 -40
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/Legend/components/index.scss +1 -1
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/NavigationMenu.tsx +27 -15
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
- package/src/components/SmallMultiples/index.tsx +3 -0
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
- package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
- package/src/components/UsaMap/helpers/map.ts +4 -4
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +193 -35
- package/src/components/ZoomControls.tsx +6 -9
- package/src/context/LegendMemoContext.tsx +30 -0
- package/src/context.ts +1 -40
- package/src/data/initial-state.js +153 -130
- package/src/data/supported-geos.js +25 -78
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +140 -20
- package/src/helpers/applyLegendToRow.ts +10 -6
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -14
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +18 -3
- package/src/helpers/generateRuntimeLegend.ts +44 -10
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +33 -0
- package/src/helpers/getStatesPicked.ts +8 -5
- package/src/helpers/index.ts +3 -3
- package/src/helpers/isLegendItemDisabled.ts +16 -0
- package/src/helpers/mapObserverHelpers.ts +40 -0
- package/src/helpers/resetLegendToggles.ts +3 -2
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- package/src/helpers/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +36 -2
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +5 -2
- package/src/hooks/useStateZoom.tsx +30 -8
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +1 -2
- package/src/scss/editor-panel.scss +4 -440
- package/src/scss/main.scss +1 -1
- package/src/scss/map.scss +12 -15
- package/src/store/map.actions.ts +7 -7
- package/src/store/map.reducer.ts +17 -6
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +46 -18
- package/src/types/MapContext.ts +6 -7
- package/src/types/runtimeLegend.ts +17 -1
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- package/src/components/DataTable.tsx +0 -385
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/coreStyles_map.scss +0 -3
- package/src/helpers/colorDistributions.ts +0 -12
- package/src/helpers/generateColorsArray.ts +0 -14
- package/src/helpers/tests/generateColorsArray.test.ts +0 -18
- package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
- package/src/hooks/useActiveElement.ts +0 -19
- package/src/scss/mixins.scss +0 -47
- package/src/types/Annotations.ts +0 -24
- /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { generateColorsArray
|
|
2
|
-
import
|
|
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,
|
|
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 =
|
|
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
|
+
}
|
package/src/helpers/constants.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
export const SVG_WIDTH = 880
|
|
2
2
|
export const SVG_HEIGHT = 500
|
|
3
|
-
export const SVG_PADDING =
|
|
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
|
-
|
|
56
|
-
|
|
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:
|
|
42
|
+
configObj: MapConfig,
|
|
43
|
+
runtimeData: DataRow[],
|
|
39
44
|
hash: string,
|
|
40
|
-
setConfig:
|
|
41
|
-
runtimeFilters:
|
|
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
|
-
|
|
335
|
-
|
|
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:
|
|
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?:
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -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 '
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
dispatch({ type: 'SET_RUNTIME_LEGEND', payload: legendCopy })
|
|
13
14
|
}
|