@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.
Files changed (118) hide show
  1. package/CONFIG.md +235 -0
  2. package/README.md +70 -24
  3. package/dist/cdcmap-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcmap.js +31260 -27946
  6. package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
  7. package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
  8. package/examples/county-hsa-toggle.json +51993 -0
  9. package/examples/custom-map-layers.json +2 -2
  10. package/examples/default-county.json +3 -3
  11. package/examples/minimal-example.json +69 -0
  12. package/examples/private/annotation-bug.json +642 -0
  13. package/examples/private/css-issue.json +314 -0
  14. package/examples/private/region-breaking.json +1639 -0
  15. package/examples/private/test1.json +27247 -0
  16. package/package.json +4 -4
  17. package/src/CdcMap.tsx +3 -14
  18. package/src/CdcMapComponent.tsx +302 -164
  19. package/src/_stories/CdcMap.Defaults.smoke.stories.tsx +76 -0
  20. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
  21. package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
  22. package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
  23. package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
  24. package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
  25. package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
  26. package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
  27. package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
  28. package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
  29. package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
  30. package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
  31. package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
  32. package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +23 -1
  33. package/src/_stories/Map.HTMLInDataTable.stories.tsx +385 -0
  34. package/src/_stories/_mock/legends/legend-tests.json +3 -3
  35. package/src/_stories/_mock/multi-state-show-unselected.json +82 -0
  36. package/src/cdcMapComponent.styles.css +2 -2
  37. package/src/components/Annotation/Annotation.Draggable.styles.css +4 -4
  38. package/src/components/Annotation/AnnotationDropdown.styles.css +1 -1
  39. package/src/components/Annotation/AnnotationList.styles.css +13 -13
  40. package/src/components/Annotation/AnnotationList.tsx +1 -1
  41. package/src/components/EditorPanel/components/EditorPanel.tsx +905 -416
  42. package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
  44. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings-style.css +1 -1
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +31 -15
  46. package/src/components/EditorPanel/components/editorPanel.styles.css +55 -25
  47. package/src/components/Legend/components/Legend.tsx +12 -7
  48. package/src/components/Legend/components/LegendGroup/legend.group.css +5 -5
  49. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
  50. package/src/components/Legend/components/index.scss +2 -3
  51. package/src/components/NavigationMenu.tsx +2 -1
  52. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  53. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
  54. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +32 -17
  55. package/src/components/UsaMap/components/TerritoriesSection.tsx +3 -2
  56. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +13 -8
  57. package/src/components/UsaMap/components/UsaMap.County.tsx +629 -231
  58. package/src/components/UsaMap/components/UsaMap.Region.styles.css +1 -1
  59. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +2 -2
  60. package/src/components/UsaMap/components/UsaMap.State.tsx +14 -9
  61. package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
  62. package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
  63. package/src/components/WorldMap/WorldMap.tsx +10 -13
  64. package/src/components/WorldMap/data/world-topo-updated.json +1 -0
  65. package/src/components/WorldMap/data/world-topo.json +1 -1
  66. package/src/components/WorldMap/worldMap.styles.css +1 -1
  67. package/src/components/ZoomControls.tsx +49 -18
  68. package/src/components/zoomControls.styles.css +27 -11
  69. package/src/data/initial-state.js +15 -5
  70. package/src/data/legacy-defaults.ts +8 -0
  71. package/src/data/supported-counties.json +1 -1
  72. package/src/data/supported-geos.js +19 -0
  73. package/src/helpers/colors.ts +2 -1
  74. package/src/helpers/countyTerritories.ts +38 -0
  75. package/src/helpers/dataTableHelpers.ts +85 -0
  76. package/src/helpers/displayGeoName.ts +19 -11
  77. package/src/helpers/getMapContainerClasses.ts +8 -2
  78. package/src/helpers/getMatchingPatternForRow.ts +67 -0
  79. package/src/helpers/getPatternForRow.ts +11 -18
  80. package/src/helpers/tests/countyTerritories.test.ts +87 -0
  81. package/src/helpers/tests/dataTableHelpers.test.ts +78 -0
  82. package/src/helpers/tests/displayGeoName.test.ts +17 -0
  83. package/src/helpers/tests/getMatchingPatternForRow.test.ts +150 -0
  84. package/src/helpers/tests/getPatternForRow.test.ts +140 -2
  85. package/src/helpers/urlDataHelpers.ts +7 -1
  86. package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
  87. package/src/hooks/useMapLayers.tsx +1 -1
  88. package/src/hooks/useResizeObserver.ts +36 -22
  89. package/src/hooks/useTooltip.test.tsx +64 -0
  90. package/src/hooks/useTooltip.ts +46 -15
  91. package/src/scss/editor-panel.scss +1 -1
  92. package/src/scss/main.scss +140 -6
  93. package/src/scss/map.scss +9 -4
  94. package/src/store/map.actions.ts +5 -0
  95. package/src/store/map.reducer.ts +13 -0
  96. package/src/test/CdcMap.test.jsx +26 -2
  97. package/src/types/MapConfig.ts +28 -4
  98. package/src/types/MapContext.ts +5 -1
  99. package/topojson-updater/README.txt +1 -1
  100. package/dist/cdcmap-Cf9_fbQf.es.js +0 -6
  101. package/examples/__data__/city-state-data.json +0 -668
  102. package/examples/city-state.json +0 -434
  103. package/examples/default-world-data.json +0 -1450
  104. package/examples/new-cities.json +0 -656
  105. package/src/_stories/CdcMap.Editor.stories.tsx +0 -3475
  106. package/src/helpers/componentHelpers.ts +0 -8
  107. package/topojson-updater/package-lock.json +0 -223
  108. /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
  109. /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
  110. /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
  111. /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
  112. /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
  113. /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
  114. /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
  115. /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
  116. /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
  117. /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
  118. /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'],
@@ -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
- return bodyStyles.getPropertyValue('--cool-gray-90')
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 {boolean} [convertFipsCodes=true] - Whether to convert FIPS codes.
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, convertFipsCodes = true): string => {
23
- if (!convertFipsCodes) return key
24
- let value = key
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(value)) {
29
- value = titleCase(supportedStates[key][0])
36
+ if (stateKeys.includes(normalizedKey)) {
37
+ value = titleCase(supportedStates[normalizedKey][0])
30
38
  wasLookedUp = true
31
39
  }
32
40
 
33
- if (territoryKeys.includes(value)) {
34
- value = titleCase(supportedTerritories[key][0])
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(value)) {
42
- value = titleCase(supportedCountries[key][0])
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(value)) {
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
- state.legend?.position,
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 { patternValuesMatch } from './patternMatching'
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
- if (!config.map?.patterns || !rowObj) {
13
+ const matchedPattern = getMatchingPatternForRow(rowObj, config.map?.patterns)
14
+
15
+ if (!matchedPattern) {
14
16
  return null
15
17
  }
16
18
 
17
- // Find a pattern that matches this row's data
18
- for (let i = 0; i < config.map.patterns.length; i++) {
19
- const patternData = config.map.patterns[i]
20
- const hasMatchingValues = patternValuesMatch(patternData.dataValue, rowObj[patternData.dataKey])
21
-
22
- if (hasMatchingValues) {
23
- return {
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
+ })