@cdc/map 4.26.2 → 4.26.4
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/CONFIG.md +235 -0
- package/README.md +70 -24
- package/dist/cdcmap-CY9IcPSi.es.js +6 -0
- package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
- package/dist/cdcmap.js +31260 -27946
- package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
- package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
- package/examples/county-hsa-toggle.json +51993 -0
- package/examples/custom-map-layers.json +2 -2
- package/examples/default-county.json +3 -3
- package/examples/minimal-example.json +69 -0
- package/examples/private/annotation-bug.json +642 -0
- package/examples/private/css-issue.json +314 -0
- package/examples/private/region-breaking.json +1639 -0
- package/examples/private/test1.json +27247 -0
- package/package.json +4 -4
- package/src/CdcMap.tsx +3 -14
- package/src/CdcMapComponent.tsx +302 -164
- package/src/_stories/CdcMap.Defaults.smoke.stories.tsx +76 -0
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
- package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
- package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
- package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
- package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
- package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
- package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
- package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
- package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
- package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
- package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +23 -1
- package/src/_stories/Map.HTMLInDataTable.stories.tsx +385 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- 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/Annotation/AnnotationList.tsx +1 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +905 -416
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings-style.css +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +31 -15
- package/src/components/EditorPanel/components/editorPanel.styles.css +55 -25
- package/src/components/Legend/components/Legend.tsx +12 -7
- package/src/components/Legend/components/LegendGroup/legend.group.css +5 -5
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- 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/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- 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 +629 -231
- 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 +14 -9
- package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
- package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
- 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 +15 -5
- package/src/data/legacy-defaults.ts +8 -0
- package/src/data/supported-counties.json +1 -1
- package/src/data/supported-geos.js +19 -0
- package/src/helpers/colors.ts +2 -1
- package/src/helpers/countyTerritories.ts +38 -0
- package/src/helpers/dataTableHelpers.ts +85 -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/countyTerritories.test.ts +87 -0
- 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/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useResizeObserver.ts +36 -22
- package/src/hooks/useTooltip.test.tsx +64 -0
- package/src/hooks/useTooltip.ts +46 -15
- 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 +5 -0
- package/src/store/map.reducer.ts +13 -0
- package/src/test/CdcMap.test.jsx +26 -2
- package/src/types/MapConfig.ts +28 -4
- package/src/types/MapContext.ts +5 -1
- package/topojson-updater/README.txt +1 -1
- package/dist/cdcmap-Cf9_fbQf.es.js +0 -6
- package/examples/__data__/city-state-data.json +0 -668
- package/examples/city-state.json +0 -434
- package/examples/default-world-data.json +0 -1450
- package/examples/new-cities.json +0 -656
- package/src/_stories/CdcMap.Editor.stories.tsx +0 -3475
- package/src/helpers/componentHelpers.ts +0 -8
- package/topojson-updater/package-lock.json +0 -223
- /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
- /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
- /package/src/_stories/{UsaMap.NoData.stories.tsx → UsaMap.NoData.smoke.stories.tsx} +0 -0
|
@@ -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
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const US_TERRITORY_STATE_FIPS_PREFIXES = new Set(['60', '66', '69', '78'])
|
|
2
|
+
|
|
3
|
+
export type CountyTerritoryVisibility = {
|
|
4
|
+
showTerritories: boolean
|
|
5
|
+
statePrefixes: Set<string>
|
|
6
|
+
countyIds: Set<string>
|
|
7
|
+
key: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const getCountyTerritoryVisibility = (
|
|
11
|
+
territoriesAlwaysShow: boolean | undefined,
|
|
12
|
+
runtimeData?: Record<string, any>
|
|
13
|
+
): CountyTerritoryVisibility => {
|
|
14
|
+
const countyIds = new Set<string>()
|
|
15
|
+
const statePrefixes = new Set<string>()
|
|
16
|
+
|
|
17
|
+
if (runtimeData) {
|
|
18
|
+
Object.keys(runtimeData).forEach(key => {
|
|
19
|
+
if (key.length <= 2) return
|
|
20
|
+
|
|
21
|
+
const statePrefix = key.slice(0, 2)
|
|
22
|
+
if (!US_TERRITORY_STATE_FIPS_PREFIXES.has(statePrefix)) return
|
|
23
|
+
|
|
24
|
+
countyIds.add(key)
|
|
25
|
+
statePrefixes.add(statePrefix)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const showTerritories = territoriesAlwaysShow !== false && countyIds.size > 0
|
|
30
|
+
const key = `${showTerritories}:${showTerritories ? Array.from(countyIds).sort().join(',') : ''}`
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
showTerritories,
|
|
34
|
+
statePrefixes,
|
|
35
|
+
countyIds,
|
|
36
|
+
key
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,6 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stateFipsToTwoDigit as stateFipsToAbbreviation,
|
|
3
|
+
supportedStatesFipsCodes as supportedStateFipsCodes
|
|
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
|
+
* Fail-safe: if no recognizable state columns exist in the data, returns original
|
|
17
|
+
* data unfiltered (avoids breaking misconfigured datasets). If valid state columns
|
|
18
|
+
* exist but a state has no data, returns empty result as expected.
|
|
19
|
+
*/
|
|
20
|
+
export const filterCountyTableRuntimeDataByStateCode = (runtimeData: any, stateCode: string, config?: any) => {
|
|
21
|
+
if (!runtimeData || runtimeData.init || !stateCode) return runtimeData
|
|
22
|
+
|
|
23
|
+
const runtimeKeys = Object.keys(runtimeData)
|
|
24
|
+
if (runtimeKeys.length === 0) return runtimeData
|
|
25
|
+
|
|
26
|
+
const stateName = supportedStateFipsCodes?.[stateCode]
|
|
27
|
+
const stateAbbreviation = stateFipsToAbbreviation?.[stateCode]
|
|
28
|
+
const normalizedSelectedStateCode = String(stateCode).replace(/^0+/, '')
|
|
29
|
+
const paddedSelectedStateCode = normalizedSelectedStateCode.padStart(2, '0')
|
|
30
|
+
const stateColumnNames = Object.values(config?.columns || {})
|
|
31
|
+
.map((column: any) => column?.name)
|
|
32
|
+
.filter((name: string) => !!name && /(state|territory|fips|jurisdiction)/i.test(name))
|
|
33
|
+
|
|
34
|
+
// Also check common state field names directly in the data rows
|
|
35
|
+
const commonStateFieldNames = [
|
|
36
|
+
'jurisdiction',
|
|
37
|
+
'state',
|
|
38
|
+
'State',
|
|
39
|
+
'state_name',
|
|
40
|
+
'stateName',
|
|
41
|
+
'State/Territory',
|
|
42
|
+
'state_territory_name'
|
|
43
|
+
]
|
|
44
|
+
const allStateColumns = [...new Set([...stateColumnNames, ...commonStateFieldNames])]
|
|
45
|
+
|
|
46
|
+
// Fail-safe: check if UIDs look like county FIPS codes (5 digits) OR if any state column exists in the data
|
|
47
|
+
const hasCountyFipsUids = runtimeKeys.some(uid => /^\d{5}$/.test(String(uid)))
|
|
48
|
+
const hasStateColumn = runtimeKeys.some(uid => allStateColumns.some(col => runtimeData[uid]?.[col] !== undefined))
|
|
49
|
+
|
|
50
|
+
// If data has neither county FIPS UIDs nor any recognizable state columns, don't filter
|
|
51
|
+
if (!hasCountyFipsUids && !hasStateColumn) {
|
|
52
|
+
return runtimeData
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const filtered = {}
|
|
56
|
+
|
|
57
|
+
if (runtimeData.fromHash !== undefined) {
|
|
58
|
+
Object.defineProperty(filtered, 'fromHash', {
|
|
59
|
+
value: runtimeData.fromHash
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Object.keys(runtimeData).forEach(uid => {
|
|
64
|
+
const row = runtimeData[uid]
|
|
65
|
+
const uidPrefix = String(uid).slice(0, 2)
|
|
66
|
+
const normalizedUidPrefix = uidPrefix.startsWith('0') ? uidPrefix.slice(1) : uidPrefix
|
|
67
|
+
const matchesUidPrefix =
|
|
68
|
+
uidPrefix === paddedSelectedStateCode || normalizedUidPrefix === normalizedSelectedStateCode
|
|
69
|
+
|
|
70
|
+
const matchesStateColumn = allStateColumns.some((columnName: string) => {
|
|
71
|
+
const rawValue = row?.[columnName]
|
|
72
|
+
if (rawValue === undefined || rawValue === null) return false
|
|
73
|
+
|
|
74
|
+
const value = String(rawValue).trim()
|
|
75
|
+
const normalizedValueStateCode = value.replace(/^0+/, '')
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
(stateName && value.toLowerCase() === String(stateName).toLowerCase()) ||
|
|
79
|
+
(stateAbbreviation && value.toUpperCase() === String(stateAbbreviation).toUpperCase()) ||
|
|
80
|
+
value === stateCode ||
|
|
81
|
+
normalizedValueStateCode === normalizedSelectedStateCode
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (matchesUidPrefix || matchesStateColumn) {
|
|
86
|
+
filtered[uid] = runtimeData[uid]
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return filtered
|
|
91
|
+
}
|
|
@@ -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,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getCountyTerritoryVisibility } from '../countyTerritories'
|
|
3
|
+
|
|
4
|
+
describe('countyTerritories', () => {
|
|
5
|
+
it('collects visible territory prefixes and county ids from runtime data', () => {
|
|
6
|
+
const runtimeData = {
|
|
7
|
+
'06001': { uid: '06001', value: 1 },
|
|
8
|
+
'72001': { uid: '72001', value: 2 }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const visibility = getCountyTerritoryVisibility(true, runtimeData)
|
|
12
|
+
|
|
13
|
+
expect(visibility.showTerritories).toBe(true)
|
|
14
|
+
expect(Array.from(visibility.statePrefixes).sort()).toEqual(['72'])
|
|
15
|
+
expect(Array.from(visibility.countyIds).sort()).toEqual(['72001'])
|
|
16
|
+
expect(visibility.key).toBe('true:72001')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('collects multiple territory prefixes and ignores non-territory counties', () => {
|
|
20
|
+
const runtimeData = {
|
|
21
|
+
'72001': { uid: '72001', value: 1 },
|
|
22
|
+
'72003': { uid: '72003', value: 2 },
|
|
23
|
+
'78010': { uid: '78010', value: 3 },
|
|
24
|
+
'06001': { uid: '06001', value: 4 }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const visibility = getCountyTerritoryVisibility(true, runtimeData)
|
|
28
|
+
|
|
29
|
+
expect(Array.from(visibility.statePrefixes).sort()).toEqual(['72', '78'])
|
|
30
|
+
expect(Array.from(visibility.countyIds).sort()).toEqual(['72001', '72003', '78010'])
|
|
31
|
+
expect(visibility.key).toBe('true:72001,72003,78010')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('hides county territories when the config flag is disabled even if territory data exists', () => {
|
|
35
|
+
const runtimeData = {
|
|
36
|
+
'72001': { uid: '72001', value: 1 }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const visibility = getCountyTerritoryVisibility(false, runtimeData)
|
|
40
|
+
|
|
41
|
+
expect(visibility.showTerritories).toBe(false)
|
|
42
|
+
expect(Array.from(visibility.statePrefixes).sort()).toEqual(['72'])
|
|
43
|
+
expect(Array.from(visibility.countyIds).sort()).toEqual(['72001'])
|
|
44
|
+
expect(visibility.key).toBe('false:')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('hides county territories when the config flag is enabled but no territory data exists', () => {
|
|
48
|
+
const runtimeData = {
|
|
49
|
+
'06001': { uid: '06001', value: 1 }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const visibility = getCountyTerritoryVisibility(true, runtimeData)
|
|
53
|
+
|
|
54
|
+
expect(visibility.showTerritories).toBe(false)
|
|
55
|
+
expect(Array.from(visibility.statePrefixes).sort()).toEqual([])
|
|
56
|
+
expect(Array.from(visibility.countyIds).sort()).toEqual([])
|
|
57
|
+
expect(visibility.key).toBe('false:')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('treats an omitted config flag as enabled by default but still hides territories when no territory data exists', () => {
|
|
61
|
+
const runtimeData = {
|
|
62
|
+
'06001': { uid: '06001', value: 1 }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const visibility = getCountyTerritoryVisibility(undefined, runtimeData)
|
|
66
|
+
|
|
67
|
+
expect(visibility.showTerritories).toBe(false)
|
|
68
|
+
expect(Array.from(visibility.statePrefixes)).toEqual([])
|
|
69
|
+
expect(Array.from(visibility.countyIds)).toEqual([])
|
|
70
|
+
expect(visibility.key).toBe('false:')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('changes the key when visible territory county ids change within the same territory prefix', () => {
|
|
74
|
+
const firstVisibility = getCountyTerritoryVisibility(true, {
|
|
75
|
+
'72001': { uid: '72001', value: 1 }
|
|
76
|
+
})
|
|
77
|
+
const secondVisibility = getCountyTerritoryVisibility(true, {
|
|
78
|
+
'72003': { uid: '72003', value: 1 }
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(Array.from(firstVisibility.statePrefixes)).toEqual(['72'])
|
|
82
|
+
expect(Array.from(secondVisibility.statePrefixes)).toEqual(['72'])
|
|
83
|
+
expect(firstVisibility.key).toBe('true:72001')
|
|
84
|
+
expect(secondVisibility.key).toBe('true:72003')
|
|
85
|
+
expect(firstVisibility.key).not.toBe(secondVisibility.key)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -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
|
+
})
|