@cdc/map 4.26.2 → 4.26.3
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/LICENSE +201 -0
- package/dist/cdcmap-vr9HZwRt.es.js +6 -0
- package/dist/cdcmap.js +26781 -24615
- package/examples/private/annotation-bug.json +642 -0
- package/package.json +3 -3
- package/src/CdcMap.tsx +3 -14
- package/src/CdcMapComponent.tsx +214 -159
- package/src/_stories/CdcMap.Defaults.stories.tsx +76 -0
- package/src/_stories/CdcMap.Editor.stories.tsx +187 -14
- package/src/_stories/CdcMap.stories.tsx +11 -1
- package/src/_stories/Map.HTMLInDataTable.stories.tsx +385 -0
- package/src/_stories/_mock/multi-state-show-unselected.json +82 -0
- package/src/cdcMapComponent.styles.css +2 -2
- package/src/components/Annotation/Annotation.Draggable.styles.css +4 -4
- package/src/components/Annotation/AnnotationDropdown.styles.css +1 -1
- package/src/components/Annotation/AnnotationList.styles.css +13 -13
- package/src/components/EditorPanel/components/EditorPanel.tsx +426 -58
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings-style.css +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +5 -2
- package/src/components/EditorPanel/components/editorPanel.styles.css +34 -24
- package/src/components/Legend/components/Legend.tsx +9 -4
- package/src/components/Legend/components/LegendGroup/legend.group.css +5 -5
- package/src/components/Legend/components/index.scss +2 -3
- package/src/components/NavigationMenu.tsx +2 -1
- package/src/components/SmallMultiples/SmallMultiples.css +5 -5
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +32 -17
- package/src/components/UsaMap/components/TerritoriesSection.tsx +3 -2
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +13 -8
- package/src/components/UsaMap/components/UsaMap.County.tsx +410 -183
- package/src/components/UsaMap/components/UsaMap.Region.styles.css +1 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +2 -2
- package/src/components/UsaMap/components/UsaMap.State.tsx +13 -8
- package/src/components/WorldMap/WorldMap.tsx +10 -13
- package/src/components/WorldMap/data/world-topo-updated.json +1 -0
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/components/WorldMap/worldMap.styles.css +1 -1
- package/src/components/ZoomControls.tsx +49 -18
- package/src/components/zoomControls.styles.css +27 -11
- package/src/data/initial-state.js +14 -5
- package/src/data/legacy-defaults.ts +8 -0
- package/src/data/supported-geos.js +19 -0
- package/src/helpers/colors.ts +2 -1
- package/src/helpers/dataTableHelpers.ts +56 -0
- package/src/helpers/displayGeoName.ts +19 -11
- package/src/helpers/getMapContainerClasses.ts +8 -2
- package/src/helpers/getMatchingPatternForRow.ts +67 -0
- package/src/helpers/getPatternForRow.ts +11 -18
- package/src/helpers/tests/dataTableHelpers.test.ts +78 -0
- package/src/helpers/tests/displayGeoName.test.ts +17 -0
- package/src/helpers/tests/getMatchingPatternForRow.test.ts +150 -0
- package/src/helpers/tests/getPatternForRow.test.ts +140 -2
- package/src/helpers/urlDataHelpers.ts +7 -1
- package/src/hooks/useResizeObserver.ts +36 -22
- package/src/hooks/useTooltip.test.tsx +64 -0
- package/src/hooks/useTooltip.ts +28 -8
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/main.scss +140 -6
- package/src/scss/map.scss +9 -4
- package/src/store/map.actions.ts +2 -0
- package/src/store/map.reducer.ts +4 -0
- package/src/test/CdcMap.test.jsx +2 -2
- package/src/types/MapConfig.ts +22 -4
- package/src/types/MapContext.ts +3 -1
- package/dist/cdcmap-Cf9_fbQf.es.js +0 -6
- package/src/helpers/componentHelpers.ts +0 -8
|
@@ -13,27 +13,58 @@ type ZoomControlsProps = {
|
|
|
13
13
|
const ZoomControls: React.FC<ZoomControlsProps> = ({ handleZoomIn, handleZoomOut, handleZoomReset }) => {
|
|
14
14
|
const { config, setRuntimeData, position } = useContext<MapContext>(ConfigContext)
|
|
15
15
|
if (!config.general.allowMapZoom) return
|
|
16
|
+
|
|
17
|
+
const isUsGeocodeMap = config.general.type === 'us-geocode'
|
|
18
|
+
const shouldShowUsGeocodeReset = isUsGeocodeMap && position.zoom > 1
|
|
19
|
+
const shouldShowBottomReset = config.general.geoType === 'single-state' || config.general.type === 'bubble'
|
|
20
|
+
|
|
21
|
+
if (!isUsGeocodeMap) {
|
|
22
|
+
return (
|
|
23
|
+
<div className='zoom-controls' data-html2canvas-ignore='true'>
|
|
24
|
+
<button onClick={() => handleZoomIn(position)} aria-label='Zoom In'>
|
|
25
|
+
<svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
|
|
26
|
+
<line x1='12' y1='5' x2='12' y2='19' />
|
|
27
|
+
<line x1='5' y1='12' x2='19' y2='12' />
|
|
28
|
+
</svg>
|
|
29
|
+
</button>
|
|
30
|
+
<button onClick={() => handleZoomOut(position)} aria-label='Zoom Out'>
|
|
31
|
+
<svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
|
|
32
|
+
<line x1='5' y1='12' x2='19' y2='12' />
|
|
33
|
+
</svg>
|
|
34
|
+
</button>
|
|
35
|
+
{shouldShowBottomReset && (
|
|
36
|
+
<button onClick={() => handleZoomReset(setRuntimeData)} className='reset' aria-label='Reset Zoom'>
|
|
37
|
+
Reset Zoom
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
return (
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{(config.general.type === 'world-geocode' ||
|
|
30
|
-
config.general.geoType === 'single-state' ||
|
|
31
|
-
config.general.type === 'bubble') && (
|
|
32
|
-
<button onClick={() => handleZoomReset(setRuntimeData)} className='reset' aria-label='Reset Zoom'>
|
|
33
|
-
Reset Zoom
|
|
45
|
+
<>
|
|
46
|
+
<div className='zoom-controls' data-html2canvas-ignore='true'>
|
|
47
|
+
<button onClick={() => handleZoomIn(position)} aria-label='Zoom In'>
|
|
48
|
+
<svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
|
|
49
|
+
<line x1='12' y1='5' x2='12' y2='19' />
|
|
50
|
+
<line x1='5' y1='12' x2='19' y2='12' />
|
|
51
|
+
</svg>
|
|
52
|
+
</button>
|
|
53
|
+
<button onClick={() => handleZoomOut(position)} aria-label='Zoom Out'>
|
|
54
|
+
<svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
|
|
55
|
+
<line x1='5' y1='12' x2='19' y2='12' />
|
|
56
|
+
</svg>
|
|
34
57
|
</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{shouldShowUsGeocodeReset && (
|
|
61
|
+
<div className='zoom-controls zoom-controls--top-right' data-html2canvas-ignore='true'>
|
|
62
|
+
<button onClick={() => handleZoomReset(setRuntimeData)} className='reset' aria-label='Reset Zoom'>
|
|
63
|
+
Reset Zoom
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
35
66
|
)}
|
|
36
|
-
|
|
67
|
+
</>
|
|
37
68
|
)
|
|
38
69
|
}
|
|
39
70
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
.zoom-controls {
|
|
2
|
-
display: flex;
|
|
3
|
-
position: absolute;
|
|
4
2
|
bottom: 2em;
|
|
3
|
+
display: flex;
|
|
5
4
|
left: 1em;
|
|
5
|
+
position: absolute;
|
|
6
6
|
z-index: 4;
|
|
7
7
|
> button.reset {
|
|
8
|
-
margin-left: 5px;
|
|
9
8
|
background: rgba(0, 0, 0, 0.65);
|
|
10
|
-
transition: 0.2s all;
|
|
11
9
|
color: #fff;
|
|
10
|
+
margin-left: 5px;
|
|
11
|
+
transition: 0.2s all;
|
|
12
12
|
&:hover {
|
|
13
13
|
background: rgba(0, 0, 0, 0.8);
|
|
14
14
|
transition: 0.2s all;
|
|
@@ -21,17 +21,17 @@
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
> button:not(.reset) {
|
|
24
|
-
display: flex;
|
|
25
24
|
align-items: center;
|
|
25
|
+
background: rgba(0, 0, 0, 0.65);
|
|
26
|
+
border: 0;
|
|
27
|
+
border-radius: 100%;
|
|
28
|
+
color: #fff;
|
|
29
|
+
display: flex;
|
|
30
|
+
height: 1.75em;
|
|
26
31
|
justify-content: center;
|
|
27
32
|
padding: 0.2em;
|
|
28
|
-
height: 1.75em;
|
|
29
|
-
width: 1.75em;
|
|
30
|
-
background: rgba(0, 0, 0, 0.65);
|
|
31
33
|
transition: 0.2s all;
|
|
32
|
-
|
|
33
|
-
border-radius: 100%;
|
|
34
|
-
border: 0;
|
|
34
|
+
width: 1.75em;
|
|
35
35
|
&:hover {
|
|
36
36
|
background: rgba(0, 0, 0, 0.8);
|
|
37
37
|
transition: 0.2s all;
|
|
@@ -51,3 +51,19 @@
|
|
|
51
51
|
margin-right: 0.25em;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
.zoom-controls--top-right {
|
|
56
|
+
bottom: auto;
|
|
57
|
+
left: auto;
|
|
58
|
+
right: 1em;
|
|
59
|
+
top: 1em;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.zoom-controls--top-right > button.reset {
|
|
63
|
+
background: #005eaa;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.zoom-controls--top-right > button.reset:hover,
|
|
67
|
+
.zoom-controls--top-right > button.reset:focus {
|
|
68
|
+
background: #004b88;
|
|
69
|
+
}
|
|
@@ -41,6 +41,7 @@ const createInitialState = () => {
|
|
|
41
41
|
allowMapZoom: true,
|
|
42
42
|
hideGeoColumnInTooltip: false,
|
|
43
43
|
hidePrimaryColumnInTooltip: false,
|
|
44
|
+
hideUnselectedStates: true,
|
|
44
45
|
statesPicked: []
|
|
45
46
|
},
|
|
46
47
|
type: 'map',
|
|
@@ -49,7 +50,8 @@ const createInitialState = () => {
|
|
|
49
50
|
name: 'FIPS Codes',
|
|
50
51
|
label: 'Location',
|
|
51
52
|
tooltip: false,
|
|
52
|
-
dataTable: true
|
|
53
|
+
dataTable: true,
|
|
54
|
+
displayColumn: ''
|
|
53
55
|
},
|
|
54
56
|
primary: {
|
|
55
57
|
dataTable: true,
|
|
@@ -76,14 +78,14 @@ const createInitialState = () => {
|
|
|
76
78
|
showSpecialClassesLast: false,
|
|
77
79
|
dynamicDescription: false,
|
|
78
80
|
type: 'equalnumber',
|
|
79
|
-
numberOfItems:
|
|
80
|
-
position: '
|
|
81
|
+
numberOfItems: 5,
|
|
82
|
+
position: 'top',
|
|
81
83
|
title: '',
|
|
82
|
-
style: '
|
|
84
|
+
style: 'gradient',
|
|
83
85
|
subStyle: 'linear blocks',
|
|
84
86
|
tickRotation: '',
|
|
85
87
|
singleColumnLegend: false,
|
|
86
|
-
hideBorder:
|
|
88
|
+
hideBorder: true,
|
|
87
89
|
groupBy: ''
|
|
88
90
|
},
|
|
89
91
|
filters: [],
|
|
@@ -114,6 +116,13 @@ const createInitialState = () => {
|
|
|
114
116
|
editorErrorMessage: []
|
|
115
117
|
},
|
|
116
118
|
visual: {
|
|
119
|
+
border: false,
|
|
120
|
+
borderColorTheme: false,
|
|
121
|
+
accent: false,
|
|
122
|
+
background: false,
|
|
123
|
+
hideBackgroundColor: false,
|
|
124
|
+
tp5Treatment: false,
|
|
125
|
+
tp5Background: false,
|
|
117
126
|
minBubbleSize: 1,
|
|
118
127
|
maxBubbleSize: 20,
|
|
119
128
|
extraBubbleBorder: false,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Preserves the OLD default values for properties changed in initial-state.js.
|
|
2
|
+
// When the backfill loop fills a missing property, it uses these values instead
|
|
3
|
+
// of the current defaults so that existing configs aren't visually affected.
|
|
4
|
+
//
|
|
5
|
+
// See backfillDefaults() in @cdc/core for the shared fill logic.
|
|
6
|
+
export const LEGACY_MAP_DEFAULTS: Record<string, Record<string, unknown>> = {
|
|
7
|
+
legend: { style: 'circles', position: 'side', numberOfItems: 3, hideBorder: false }
|
|
8
|
+
}
|
|
@@ -493,7 +493,9 @@ export const supportedCountries = {
|
|
|
493
493
|
SOM: ['Somalia', 'Somaliland'],
|
|
494
494
|
ZAF: ['South Africa'],
|
|
495
495
|
SGS: ['South Georgia and the South Sandwich Islands', 'S. Geo. and the Is.'],
|
|
496
|
+
SSD: ['South Sudan', 'S. Sudan'],
|
|
496
497
|
SDS: ['South Sudan', 'S. Sudan'],
|
|
498
|
+
XSV: ['Svalbard'],
|
|
497
499
|
ESP: ['Spain'],
|
|
498
500
|
LKA: ['Sri Lanka'],
|
|
499
501
|
SDN: ['Sudan'],
|
|
@@ -531,11 +533,28 @@ export const supportedCountries = {
|
|
|
531
533
|
VIR: ['Virgin Islands (U.S.)', 'U.S. Virgin Is.'],
|
|
532
534
|
WLF: ['Wallis and Futuna', 'Wallis and Futuna Is.'],
|
|
533
535
|
KOS: ['Kosovo'],
|
|
536
|
+
XKS: ['Kosovo'],
|
|
534
537
|
SAH: ['Western Sahara', 'W. Sahara'],
|
|
535
538
|
YEM: ['Yemen'],
|
|
536
539
|
ZMB: ['Zambia'],
|
|
537
540
|
ZWE: ['Zimbabwe'],
|
|
538
541
|
IOT: ['British Indian Ocean Territory'],
|
|
542
|
+
CPT: ['Clipperton Island'],
|
|
543
|
+
XAC: ['Ashmore and Cartier Islands'],
|
|
544
|
+
XBK: ['Baker Island'],
|
|
545
|
+
XCS: ['Coral Sea Islands'],
|
|
546
|
+
XHO: ['Howland Island'],
|
|
547
|
+
XJA: ['Johnston Atoll'],
|
|
548
|
+
XJM: ['Jan Mayen'],
|
|
549
|
+
XJV: ['Jarvis Island'],
|
|
550
|
+
XKR: ['Kingman Reef'],
|
|
551
|
+
XMW: ['Midway Islands'],
|
|
552
|
+
XNV: ['Navassa Island'],
|
|
553
|
+
XPL: ['Palmyra Atoll'],
|
|
554
|
+
XPR: ['Paracel Islands'],
|
|
555
|
+
XQZ: ['Akrotiri'],
|
|
556
|
+
XSP: ['Spratly Islands'],
|
|
557
|
+
XWK: ['Wake Island'],
|
|
539
558
|
Alaska: ['Alaska'],
|
|
540
559
|
Hawaii: ['Hawaii'],
|
|
541
560
|
Sardinia: ['Sardinia'],
|
package/src/helpers/colors.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { MapConfig } from '../types/MapConfig'
|
|
|
11
11
|
export const getGeoStrokeColor = (config: MapConfig) => {
|
|
12
12
|
const bodyStyles = getComputedStyle(document.body)
|
|
13
13
|
if (config.general.geoBorderColor === 'darkGray') {
|
|
14
|
-
|
|
14
|
+
const isCountyMap = config.general.geoType === 'us-county'
|
|
15
|
+
return bodyStyles.getPropertyValue(isCountyMap ? '--colors-gray-cool-70' : '--cool-gray-90')
|
|
15
16
|
} else {
|
|
16
17
|
return bodyStyles.getPropertyValue('--white')
|
|
17
18
|
}
|
|
@@ -1,6 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stateFipsToTwoDigit as stateFipsToAbbreviation,
|
|
3
|
+
supportedStatesFipsCodes as supportedStateCodes
|
|
4
|
+
} from '../data/supported-geos'
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Determines if the data table should be shown based on current state
|
|
3
8
|
*/
|
|
4
9
|
export const shouldShowDataTable = (config: any, table: any, general: any, loading: boolean): boolean => {
|
|
5
10
|
return !config?.runtime?.editorErrorMessage.length && table.forceDisplay && general.type !== 'navigation' && !loading
|
|
6
11
|
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Filters county runtime data to a selected state code for data table display.
|
|
15
|
+
* Keeps the original non-enumerable fromHash metadata when present.
|
|
16
|
+
*/
|
|
17
|
+
export const filterCountyTableRuntimeDataByStateCode = (runtimeData: any, stateCode: string, config?: any) => {
|
|
18
|
+
if (!runtimeData || runtimeData.init || !stateCode) return runtimeData
|
|
19
|
+
|
|
20
|
+
const filtered = {}
|
|
21
|
+
const stateName = supportedStateCodes[stateCode]
|
|
22
|
+
const stateAbbreviation = stateFipsToAbbreviation[stateCode]
|
|
23
|
+
const normalizedSelectedStateCode = String(stateCode).replace(/^0+/, '')
|
|
24
|
+
const paddedSelectedStateCode = normalizedSelectedStateCode.padStart(2, '0')
|
|
25
|
+
const stateColumnNames = Object.values(config?.columns || {})
|
|
26
|
+
.map((column: any) => column?.name)
|
|
27
|
+
.filter((name: string) => !!name && /(state|territory|fips)/i.test(name))
|
|
28
|
+
|
|
29
|
+
if (runtimeData.fromHash !== undefined) {
|
|
30
|
+
Object.defineProperty(filtered, 'fromHash', {
|
|
31
|
+
value: runtimeData.fromHash
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Object.keys(runtimeData).forEach(uid => {
|
|
36
|
+
const row = runtimeData[uid]
|
|
37
|
+
const uidPrefix = String(uid).slice(0, 2)
|
|
38
|
+
const normalizedUidPrefix = uidPrefix.startsWith('0') ? uidPrefix.slice(1) : uidPrefix
|
|
39
|
+
const matchesUidPrefix =
|
|
40
|
+
uidPrefix === paddedSelectedStateCode || normalizedUidPrefix === normalizedSelectedStateCode
|
|
41
|
+
const matchesStateColumn = stateColumnNames.some((columnName: string) => {
|
|
42
|
+
const rawValue = row?.[columnName]
|
|
43
|
+
if (rawValue === undefined || rawValue === null) return false
|
|
44
|
+
|
|
45
|
+
const value = String(rawValue).trim()
|
|
46
|
+
const normalizedValueStateCode = value.replace(/^0+/, '')
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
(stateName && value.toLowerCase() === String(stateName).toLowerCase()) ||
|
|
50
|
+
(stateAbbreviation && value.toUpperCase() === String(stateAbbreviation).toUpperCase()) ||
|
|
51
|
+
value === stateCode ||
|
|
52
|
+
normalizedValueStateCode === normalizedSelectedStateCode
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (matchesUidPrefix || matchesStateColumn) {
|
|
57
|
+
filtered[uid] = runtimeData[uid]
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return filtered
|
|
62
|
+
}
|
|
@@ -16,30 +16,38 @@ import {
|
|
|
16
16
|
* Converts a geographic key to its display name.
|
|
17
17
|
*
|
|
18
18
|
* @param {string} key - The geographic key to convert.
|
|
19
|
-
* @param {
|
|
19
|
+
* @param {string} [displayOverride] - If provided, returns this value immediately (used for translated/alternate display names).
|
|
20
20
|
* @returns {string} - The display name for the geographic key.
|
|
21
21
|
*/
|
|
22
|
-
export const displayGeoName = (key: string,
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
export const displayGeoName = (key: string, displayOverride?: string): string => {
|
|
23
|
+
const rawKey = String(key || '')
|
|
24
|
+
const trimmedOverride = typeof displayOverride === 'string' ? displayOverride.trim() : ''
|
|
25
|
+
const normalizedKey = rawKey.toUpperCase()
|
|
26
|
+
const normalizedOverride = trimmedOverride.toUpperCase()
|
|
27
|
+
|
|
28
|
+
if (trimmedOverride && normalizedOverride !== normalizedKey) {
|
|
29
|
+
return trimmedOverride
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let value = rawKey
|
|
25
33
|
let wasLookedUp = false
|
|
26
34
|
|
|
27
35
|
// Map to first item in values array which is the preferred label
|
|
28
|
-
if (stateKeys.includes(
|
|
29
|
-
value = titleCase(supportedStates[
|
|
36
|
+
if (stateKeys.includes(normalizedKey)) {
|
|
37
|
+
value = titleCase(supportedStates[normalizedKey][0])
|
|
30
38
|
wasLookedUp = true
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
if (territoryKeys.includes(
|
|
34
|
-
value = titleCase(supportedTerritories[
|
|
41
|
+
if (territoryKeys.includes(normalizedKey)) {
|
|
42
|
+
value = titleCase(supportedTerritories[normalizedKey][0])
|
|
35
43
|
wasLookedUp = true
|
|
36
44
|
if (value === 'U.s. Virgin Islands') {
|
|
37
45
|
value = 'U.S. Virgin Islands'
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
if (countryKeys.includes(
|
|
42
|
-
value = titleCase(supportedCountries[
|
|
49
|
+
if (countryKeys.includes(normalizedKey)) {
|
|
50
|
+
value = titleCase(supportedCountries[normalizedKey][0])
|
|
43
51
|
wasLookedUp = true
|
|
44
52
|
}
|
|
45
53
|
|
|
@@ -64,7 +72,7 @@ export const displayGeoName = (key: string, convertFipsCodes = true): string =>
|
|
|
64
72
|
wasLookedUp = true
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
if (cityKeys.includes(
|
|
75
|
+
if (cityKeys.includes(normalizedKey)) {
|
|
68
76
|
value = titleCase(String(value) || '')
|
|
69
77
|
wasLookedUp = true
|
|
70
78
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { type MapConfig } from './../types/MapConfig'
|
|
2
2
|
import { isMultiCountryActive } from './getCountriesPicked'
|
|
3
|
+
import { isBelowBreakpoint } from '@cdc/core/helpers/viewports'
|
|
3
4
|
|
|
4
|
-
export const getMapContainerClasses = (state: MapConfig, modal) => {
|
|
5
|
+
export const getMapContainerClasses = (state: MapConfig, modal, currentViewport?: string) => {
|
|
5
6
|
const { general } = state
|
|
7
|
+
const legendWrapping =
|
|
8
|
+
(state.legend?.position === 'left' || state.legend?.position === 'right') &&
|
|
9
|
+
currentViewport &&
|
|
10
|
+
(currentViewport === 'md' || isBelowBreakpoint('md', currentViewport))
|
|
6
11
|
|
|
7
12
|
let mapContainerClasses = [
|
|
8
13
|
'map-container',
|
|
9
|
-
|
|
14
|
+
'visualization-container',
|
|
15
|
+
legendWrapping ? 'legend-wrapped-bottom' : state.legend?.position,
|
|
10
16
|
state.general.type,
|
|
11
17
|
state.general.geoType,
|
|
12
18
|
'outline-none',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PatternSelection } from '../types/MapConfig'
|
|
2
|
+
import { patternValuesMatch } from './patternMatching'
|
|
3
|
+
|
|
4
|
+
export type MatchedPattern = {
|
|
5
|
+
pattern: PatternSelection
|
|
6
|
+
patternIndex: number
|
|
7
|
+
matchedDataKey: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const hasPatternValue = (value: unknown): boolean => String(value ?? '').trim() !== ''
|
|
11
|
+
|
|
12
|
+
export const getMatchingPatternForRow = (
|
|
13
|
+
rowObj: Record<string, any> | null | undefined,
|
|
14
|
+
patterns: PatternSelection[] | null | undefined
|
|
15
|
+
): MatchedPattern | null => {
|
|
16
|
+
if (!rowObj || !Array.isArray(patterns) || patterns.length === 0) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// First pass: specific key matches always win over broad matches.
|
|
21
|
+
// If multiple specific patterns match, keep the last configured one to
|
|
22
|
+
// preserve prior "last overlay wins" map behavior.
|
|
23
|
+
let lastSpecificMatch: MatchedPattern | null = null
|
|
24
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
25
|
+
const pattern = patterns[i]
|
|
26
|
+
const dataKey = pattern?.dataKey ?? ''
|
|
27
|
+
|
|
28
|
+
if (String(dataKey).trim() === '' || !hasPatternValue(pattern?.dataValue)) {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (patternValuesMatch(pattern.dataValue, rowObj[dataKey])) {
|
|
33
|
+
lastSpecificMatch = {
|
|
34
|
+
pattern,
|
|
35
|
+
patternIndex: i,
|
|
36
|
+
matchedDataKey: dataKey
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (lastSpecificMatch) {
|
|
41
|
+
return lastSpecificMatch
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Second pass: broad matches (blank dataKey) compare against all row values.
|
|
45
|
+
// If multiple broad patterns match, keep the last configured one.
|
|
46
|
+
const rowEntries = Object.entries(rowObj)
|
|
47
|
+
let lastBroadMatch: MatchedPattern | null = null
|
|
48
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
49
|
+
const pattern = patterns[i]
|
|
50
|
+
|
|
51
|
+
if (String(pattern?.dataKey ?? '').trim() !== '' || !hasPatternValue(pattern?.dataValue)) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [rowKey, rowValue] of rowEntries) {
|
|
56
|
+
if (patternValuesMatch(pattern.dataValue, rowValue)) {
|
|
57
|
+
lastBroadMatch = {
|
|
58
|
+
pattern,
|
|
59
|
+
patternIndex: i,
|
|
60
|
+
matchedDataKey: rowKey
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return lastBroadMatch
|
|
67
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MapConfig } from '../types/MapConfig'
|
|
2
|
-
import {
|
|
2
|
+
import { getMatchingPatternForRow } from './getMatchingPatternForRow'
|
|
3
3
|
|
|
4
4
|
interface PatternInfo {
|
|
5
5
|
pattern?: string
|
|
@@ -10,25 +10,18 @@ interface PatternInfo {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const getPatternForRow = (rowObj: Record<string, any>, config: MapConfig): PatternInfo | null => {
|
|
13
|
-
|
|
13
|
+
const matchedPattern = getMatchingPatternForRow(rowObj, config.map?.patterns)
|
|
14
|
+
|
|
15
|
+
if (!matchedPattern) {
|
|
14
16
|
return null
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
pattern: patternData.pattern,
|
|
25
|
-
dataKey: patternData.dataKey,
|
|
26
|
-
size: patternData.size,
|
|
27
|
-
patternIndex: i,
|
|
28
|
-
color: patternData.color
|
|
29
|
-
}
|
|
30
|
-
}
|
|
19
|
+
return {
|
|
20
|
+
pattern: matchedPattern.pattern.pattern,
|
|
21
|
+
// Broad matches resolve to the row key that matched, so IDs/classes stay stable.
|
|
22
|
+
dataKey: matchedPattern.matchedDataKey,
|
|
23
|
+
size: matchedPattern.pattern.size,
|
|
24
|
+
patternIndex: matchedPattern.patternIndex,
|
|
25
|
+
color: matchedPattern.pattern.color
|
|
31
26
|
}
|
|
32
|
-
|
|
33
|
-
return null
|
|
34
27
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { filterCountyTableRuntimeDataByStateCode } from '../dataTableHelpers'
|
|
3
|
+
|
|
4
|
+
describe('filterCountyTableRuntimeDataByStateCode', () => {
|
|
5
|
+
it('filters county rows by selected state fips prefix', () => {
|
|
6
|
+
const runtimeData = {
|
|
7
|
+
'06001': { uid: '06001', value: 1 },
|
|
8
|
+
'06013': { uid: '06013', value: 2 },
|
|
9
|
+
'12001': { uid: '12001', value: 3 }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const filtered = filterCountyTableRuntimeDataByStateCode(runtimeData, '06')
|
|
13
|
+
|
|
14
|
+
expect(Object.keys(filtered)).toEqual(['06001', '06013'])
|
|
15
|
+
expect(filtered['06001'].value).toBe(1)
|
|
16
|
+
expect(filtered['12001']).toBeUndefined()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('filters county rows when state fips is provided without leading zero', () => {
|
|
20
|
+
const runtimeData = {
|
|
21
|
+
'06001': { uid: '06001', value: 1 },
|
|
22
|
+
'06013': { uid: '06013', value: 2 },
|
|
23
|
+
'12001': { uid: '12001', value: 3 }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const filtered = filterCountyTableRuntimeDataByStateCode(runtimeData, '6')
|
|
27
|
+
|
|
28
|
+
expect(Object.keys(filtered)).toEqual(['06001', '06013'])
|
|
29
|
+
expect(filtered['06001'].value).toBe(1)
|
|
30
|
+
expect(filtered['12001']).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('preserves non-enumerable fromHash metadata', () => {
|
|
34
|
+
const runtimeData = {
|
|
35
|
+
'06001': { uid: '06001', value: 1 },
|
|
36
|
+
'12001': { uid: '12001', value: 2 }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Object.defineProperty(runtimeData, 'fromHash', {
|
|
40
|
+
value: 12345,
|
|
41
|
+
enumerable: false
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const filtered = filterCountyTableRuntimeDataByStateCode(runtimeData, '06')
|
|
45
|
+
|
|
46
|
+
expect(filtered.fromHash).toBe(12345)
|
|
47
|
+
expect(Object.keys(filtered)).toEqual(['06001'])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('filters us-geocode rows by configured state column when uid is not county fips', () => {
|
|
51
|
+
const runtimeData = {
|
|
52
|
+
'ID:2472': { uid: 'ID:2472', 'State/Territory': 'Alabama' },
|
|
53
|
+
'ID:1010': { uid: 'ID:1010', 'State/Territory': 'California' },
|
|
54
|
+
'ID:2020': { uid: 'ID:2020', 'State/Territory': 'California' }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const config = {
|
|
58
|
+
columns: {
|
|
59
|
+
additionalColumn1: { name: 'State/Territory' }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filtered = filterCountyTableRuntimeDataByStateCode(runtimeData, '06', config)
|
|
64
|
+
|
|
65
|
+
expect(Object.keys(filtered)).toEqual(['ID:1010', 'ID:2020'])
|
|
66
|
+
expect(filtered['ID:2472']).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns original runtime data when state fips is empty', () => {
|
|
70
|
+
const runtimeData = {
|
|
71
|
+
'06001': { uid: '06001', value: 1 }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const filtered = filterCountyTableRuntimeDataByStateCode(runtimeData, '')
|
|
75
|
+
|
|
76
|
+
expect(filtered).toBe(runtimeData)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { displayGeoName } from '../displayGeoName'
|
|
2
|
+
|
|
3
|
+
describe('displayGeoName', () => {
|
|
4
|
+
it('resolves lowercase world iso codes to country names', () => {
|
|
5
|
+
expect(displayGeoName('ssd')).toBe('South Sudan')
|
|
6
|
+
expect(displayGeoName('usa')).toBe('United States of America')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('prefers the provided display override', () => {
|
|
10
|
+
expect(displayGeoName('ssd', 'Custom South Sudan')).toBe('Custom South Sudan')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('ignores display overrides that only restate the raw code with different casing', () => {
|
|
14
|
+
expect(displayGeoName('ssd', 'Ssd')).toBe('South Sudan')
|
|
15
|
+
expect(displayGeoName('SSD', 'ssd')).toBe('South Sudan')
|
|
16
|
+
})
|
|
17
|
+
})
|