@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.
- package/.claude/settings.local.json +30 -0
- package/dist/cdcmap.js +54263 -52600
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -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 +35 -34
- package/package.json +26 -5
- package/src/CdcMap.tsx +23 -8
- package/src/CdcMapComponent.tsx +215 -309
- 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.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +15 -5
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/equal-number.json +1109 -0
- package/src/_stories/_mock/us-bubble-cities.json +306 -0
- package/src/components/BubbleList.tsx +16 -12
- package/src/components/CityList.tsx +85 -107
- package/src/components/DataTable.tsx +37 -9
- package/src/components/EditorPanel/components/EditorPanel.tsx +177 -165
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
- package/src/components/Geo.tsx +2 -0
- package/src/components/Legend/components/Legend.tsx +109 -73
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/NavigationMenu.tsx +11 -2
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
- package/src/components/UsaMap/components/UsaMap.County.tsx +111 -37
- package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +6 -6
- package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
- package/src/components/UsaMap/helpers/map.ts +2 -2
- package/src/components/WorldMap/WorldMap.tsx +113 -25
- 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 +143 -130
- package/src/data/supported-geos.js +17 -2
- package/src/helpers/applyColorToLegend.ts +116 -20
- package/src/helpers/applyLegendToRow.ts +10 -6
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -0
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +1 -1
- package/src/helpers/generateRuntimeLegend.ts +44 -8
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getPatternForRow.ts +36 -0
- package/src/helpers/getStatesPicked.ts +8 -5
- package/src/helpers/index.ts +11 -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/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useGeoClickHandler.ts +35 -1
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useStateZoom.tsx +25 -6
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +0 -2
- package/src/store/map.reducer.ts +17 -6
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +23 -14
- package/src/types/MapContext.ts +0 -7
- package/src/types/runtimeLegend.ts +17 -1
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- 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
|
@@ -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 '
|
|
4
|
-
import {
|
|
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,
|
|
21
|
-
const { geoType, palette } = general
|
|
22
|
-
|
|
23
|
-
const
|
|
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
|
-
|
|
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(
|
|
38
|
-
|
|
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'))
|
|
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(
|
|
47
|
-
? Math.max(
|
|
48
|
-
: Object.keys(
|
|
49
|
-
const distributionArray =
|
|
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[
|
|
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
|
-
|
|
149
|
+
// Final fallback
|
|
150
|
+
return mapColorPalette[0] || '#d3d3d3'
|
|
55
151
|
}
|
|
@@ -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
|
@@ -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
|
+
}
|
|
@@ -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,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:
|
|
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,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
|
-
|
|
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,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 {
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
14
|
+
dispatch({ type: 'SET_RUNTIME_LEGEND', payload: runtimeLegendCopy })
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|