@cdc/map 4.26.3 → 4.26.5
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 +268 -0
- package/README.md +74 -24
- package/dist/cdcmap-CY9IcPSi.es.js +6 -0
- package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
- package/dist/cdcmap.js +29168 -27482
- 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 +6 -3
- package/examples/minimal-example.json +73 -0
- package/examples/private/annotation-bug.json +2 -2
- 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/CdcMapComponent.tsx +107 -14
- package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +600 -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.FocusVisibility.stories.tsx +87 -0
- package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
- package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
- package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
- package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +60 -0
- package/src/_stories/_mock/alt_text_metadata.json +65 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- package/src/_stories/_mock/world-bubble-reset.json +138 -0
- package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
- package/src/components/Annotation/AnnotationList.tsx +1 -1
- package/src/components/BubbleList.tsx +13 -0
- package/src/components/EditorPanel/components/EditorPanel.tsx +637 -382
- 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.tsx +26 -13
- package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
- package/src/components/FilterControls.tsx +21 -0
- package/src/components/Legend/components/Legend.tsx +3 -3
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +309 -108
- package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +10 -3
- 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 +37 -4
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/components/ZoomableGroup.tsx +23 -3
- package/src/components/filterControls.styles.css +6 -0
- package/src/data/initial-state.js +3 -0
- package/src/data/supported-counties.json +1 -1
- package/src/helpers/countyTerritories.ts +38 -0
- package/src/helpers/dataTableHelpers.ts +35 -6
- package/src/helpers/generateRuntimeFilters.ts +2 -1
- package/src/helpers/handleMapAriaLabels.ts +45 -30
- package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
- package/src/helpers/tests/countyTerritories.test.ts +87 -0
- package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
- package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useGeoClickHandler.ts +13 -1
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useStateZoom.tsx +39 -20
- package/src/hooks/useTooltip.test.tsx +2 -16
- package/src/hooks/useTooltip.ts +18 -7
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +6 -21
- package/src/scss/map.scss +20 -0
- package/src/store/map.actions.ts +5 -2
- package/src/store/map.reducer.ts +12 -3
- package/src/test/CdcMap.test.jsx +24 -0
- package/src/types/MapConfig.ts +11 -0
- package/src/types/MapContext.ts +6 -1
- package/topojson-updater/README.txt +1 -1
- package/dist/cdcmap-vr9HZwRt.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 -3648
- 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.Defaults.stories.tsx → CdcMap.Defaults.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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const US_TERRITORY_STATE_FIPS_PREFIXES = new Set(['60', '66', '69', '72', '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,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
stateFipsToTwoDigit as stateFipsToAbbreviation,
|
|
3
|
-
supportedStatesFipsCodes as
|
|
3
|
+
supportedStatesFipsCodes as supportedStateFipsCodes
|
|
4
4
|
} from '../data/supported-geos'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -13,18 +13,46 @@ export const shouldShowDataTable = (config: any, table: any, general: any, loadi
|
|
|
13
13
|
/**
|
|
14
14
|
* Filters county runtime data to a selected state code for data table display.
|
|
15
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.
|
|
16
19
|
*/
|
|
17
20
|
export const filterCountyTableRuntimeDataByStateCode = (runtimeData: any, stateCode: string, config?: any) => {
|
|
18
21
|
if (!runtimeData || runtimeData.init || !stateCode) return runtimeData
|
|
19
22
|
|
|
20
|
-
const
|
|
21
|
-
|
|
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]
|
|
23
28
|
const normalizedSelectedStateCode = String(stateCode).replace(/^0+/, '')
|
|
24
29
|
const paddedSelectedStateCode = normalizedSelectedStateCode.padStart(2, '0')
|
|
25
30
|
const stateColumnNames = Object.values(config?.columns || {})
|
|
26
31
|
.map((column: any) => column?.name)
|
|
27
|
-
.filter((name: string) => !!name && /(state|territory|fips)/i.test(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 = {}
|
|
28
56
|
|
|
29
57
|
if (runtimeData.fromHash !== undefined) {
|
|
30
58
|
Object.defineProperty(filtered, 'fromHash', {
|
|
@@ -38,7 +66,8 @@ export const filterCountyTableRuntimeDataByStateCode = (runtimeData: any, stateC
|
|
|
38
66
|
const normalizedUidPrefix = uidPrefix.startsWith('0') ? uidPrefix.slice(1) : uidPrefix
|
|
39
67
|
const matchesUidPrefix =
|
|
40
68
|
uidPrefix === paddedSelectedStateCode || normalizedUidPrefix === normalizedSelectedStateCode
|
|
41
|
-
|
|
69
|
+
|
|
70
|
+
const matchesStateColumn = allStateColumns.some((columnName: string) => {
|
|
42
71
|
const rawValue = row?.[columnName]
|
|
43
72
|
if (rawValue === undefined || rawValue === null) return false
|
|
44
73
|
|
|
@@ -13,6 +13,7 @@ export const generateRuntimeFilters = (state, hash, runtimeFilters) => {
|
|
|
13
13
|
queryParameter,
|
|
14
14
|
orderedValues,
|
|
15
15
|
active,
|
|
16
|
+
defaultValue,
|
|
16
17
|
values,
|
|
17
18
|
type,
|
|
18
19
|
showDropdown,
|
|
@@ -50,7 +51,7 @@ export const generateRuntimeFilters = (state, hash, runtimeFilters) => {
|
|
|
50
51
|
newFilter.values = values
|
|
51
52
|
newFilter.setByQueryParameter = setByQueryParameter
|
|
52
53
|
handleSorting(newFilter)
|
|
53
|
-
newFilter.active = active ?? values[0] // Default to first found value
|
|
54
|
+
newFilter.active = active ?? defaultValue ?? values[0] // Default to configured defaultValue, then first found value
|
|
54
55
|
newFilter.filterStyle = state.filters[idx].filterStyle ? state.filters[idx].filterStyle : 'dropdown'
|
|
55
56
|
newFilter.showDropdown = showDropdown
|
|
56
57
|
newFilter.subGrouping = state.filters[idx].subGrouping
|
|
@@ -1,36 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
import type { AltTextConfig } from '@cdc/core/types/AltText'
|
|
2
|
+
import { resolveAltTextDescription } from '@cdc/core/helpers/resolveAltTextDescription'
|
|
3
|
+
|
|
4
|
+
const getAutoLabel = (state): string => {
|
|
5
|
+
const {
|
|
6
|
+
general: { title, geoType, statesPicked }
|
|
7
|
+
} = state
|
|
8
|
+
let ariaLabel = ''
|
|
9
|
+
switch (geoType) {
|
|
10
|
+
case 'world':
|
|
11
|
+
ariaLabel += 'World map'
|
|
12
|
+
break
|
|
13
|
+
case 'us':
|
|
14
|
+
ariaLabel += 'United States map'
|
|
15
|
+
break
|
|
16
|
+
case 'us-county':
|
|
17
|
+
ariaLabel += `United States county map`
|
|
18
|
+
break
|
|
19
|
+
case 'single-state':
|
|
20
|
+
ariaLabel += `${statesPicked.map(sp => sp.stateName).join(', ')} county map`
|
|
21
|
+
break
|
|
22
|
+
case 'us-region':
|
|
23
|
+
ariaLabel += `United States HHS Region map`
|
|
24
|
+
break
|
|
25
|
+
default:
|
|
26
|
+
ariaLabel = 'Data visualization container'
|
|
27
|
+
break
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (title) {
|
|
31
|
+
ariaLabel += ` with the title: ${title}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return ariaLabel
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const handleMapAriaLabels = (state: {
|
|
38
|
+
general?: { geoType?: string; title?: string; statesPicked?: { stateName: string }[] }
|
|
39
|
+
altText?: AltTextConfig
|
|
40
|
+
dataMetadata?: Record<string, string>
|
|
41
|
+
}): string => {
|
|
2
42
|
try {
|
|
3
|
-
if (!state.general
|
|
4
|
-
const {
|
|
5
|
-
general: { title, geoType, statesPicked }
|
|
6
|
-
} = state
|
|
7
|
-
let ariaLabel = ''
|
|
8
|
-
switch (geoType) {
|
|
9
|
-
case 'world':
|
|
10
|
-
ariaLabel += 'World map'
|
|
11
|
-
break
|
|
12
|
-
case 'us':
|
|
13
|
-
ariaLabel += 'United States map'
|
|
14
|
-
break
|
|
15
|
-
case 'us-county':
|
|
16
|
-
ariaLabel += `United States county map`
|
|
17
|
-
break
|
|
18
|
-
case 'single-state':
|
|
19
|
-
ariaLabel += `${statesPicked.map(sp => sp.stateName).join(', ')} county map`
|
|
20
|
-
break
|
|
21
|
-
case 'us-region':
|
|
22
|
-
ariaLabel += `United States HHS Region map`
|
|
23
|
-
break
|
|
24
|
-
default:
|
|
25
|
-
ariaLabel = 'Data visualization container'
|
|
26
|
-
break
|
|
27
|
-
}
|
|
43
|
+
if (!state.general?.geoType) throw Error('handleMapAriaLabels: no geoType found in state')
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
45
|
+
const title = getAutoLabel(state)
|
|
46
|
+
const description = resolveAltTextDescription(state.altText, state.dataMetadata)
|
|
32
47
|
|
|
33
|
-
return
|
|
48
|
+
return description ? `${title}. ${description}` : title
|
|
34
49
|
} catch (e) {
|
|
35
50
|
console.error('COVE: ', e.message) // eslint-disable-line
|
|
36
51
|
return 'Data visualization container'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type AutoResetArgs = {
|
|
2
|
+
isDashboard: boolean
|
|
3
|
+
previousRuntimeDataHash: number | null
|
|
4
|
+
nextRuntimeDataHash?: number
|
|
5
|
+
hasDashboardFilters?: boolean
|
|
6
|
+
allowMapZoom?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const shouldAutoResetSingleStateZoom = ({
|
|
10
|
+
isDashboard,
|
|
11
|
+
previousRuntimeDataHash,
|
|
12
|
+
nextRuntimeDataHash,
|
|
13
|
+
hasDashboardFilters = false,
|
|
14
|
+
allowMapZoom = true
|
|
15
|
+
}: AutoResetArgs): boolean => {
|
|
16
|
+
if (!isDashboard || !allowMapZoom) return false
|
|
17
|
+
if (!hasDashboardFilters) return false
|
|
18
|
+
if (previousRuntimeDataHash === null) return false
|
|
19
|
+
if (nextRuntimeDataHash === undefined) return false
|
|
20
|
+
|
|
21
|
+
return previousRuntimeDataHash !== nextRuntimeDataHash
|
|
22
|
+
}
|
|
@@ -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,71 @@
|
|
|
1
|
+
import { handleMapAriaLabels } from '../handleMapAriaLabels'
|
|
2
|
+
|
|
3
|
+
const baseState = {
|
|
4
|
+
general: {
|
|
5
|
+
geoType: 'us',
|
|
6
|
+
title: 'COVID-19 Cases by State',
|
|
7
|
+
statesPicked: []
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('handleMapAriaLabels', () => {
|
|
12
|
+
it('returns auto-generated title for US map', () => {
|
|
13
|
+
const result = handleMapAriaLabels(baseState)
|
|
14
|
+
expect(result).toBe('United States map with the title: COVID-19 Cases by State')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns auto-generated title for world map', () => {
|
|
18
|
+
const result = handleMapAriaLabels({
|
|
19
|
+
general: { geoType: 'world', title: 'Global Data', statesPicked: [] }
|
|
20
|
+
})
|
|
21
|
+
expect(result).toBe('World map with the title: Global Data')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns fallback when geoType is missing', () => {
|
|
25
|
+
const result = handleMapAriaLabels({ general: {} })
|
|
26
|
+
expect(result).toBe('Data visualization container')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('description', () => {
|
|
30
|
+
it('concatenates static description after title', () => {
|
|
31
|
+
const result = handleMapAriaLabels({
|
|
32
|
+
...baseState,
|
|
33
|
+
altText: { type: 'static', value: 'Rates highest in the Southeast.' }
|
|
34
|
+
})
|
|
35
|
+
expect(result).toBe('United States map with the title: COVID-19 Cases by State. Rates highest in the Southeast.')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('concatenates description from metadata after title', () => {
|
|
39
|
+
const result = handleMapAriaLabels({
|
|
40
|
+
...baseState,
|
|
41
|
+
altText: { type: 'metadata', metadataKey: 'altDescription' },
|
|
42
|
+
dataMetadata: { altDescription: 'Map shows elevated rates in southern states.' }
|
|
43
|
+
})
|
|
44
|
+
expect(result).toBe(
|
|
45
|
+
'United States map with the title: COVID-19 Cases by State. Map shows elevated rates in southern states.'
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns title only when metadata key is missing', () => {
|
|
50
|
+
const result = handleMapAriaLabels({
|
|
51
|
+
...baseState,
|
|
52
|
+
altText: { type: 'metadata', metadataKey: 'missing' },
|
|
53
|
+
dataMetadata: { other: 'value' }
|
|
54
|
+
})
|
|
55
|
+
expect(result).toBe('United States map with the title: COVID-19 Cases by State')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns title only when not configured', () => {
|
|
59
|
+
const result = handleMapAriaLabels(baseState)
|
|
60
|
+
expect(result).toBe('United States map with the title: COVID-19 Cases by State')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('returns title only when altText is empty object', () => {
|
|
64
|
+
const result = handleMapAriaLabels({
|
|
65
|
+
...baseState,
|
|
66
|
+
altText: {}
|
|
67
|
+
})
|
|
68
|
+
expect(result).toBe('United States map with the title: COVID-19 Cases by State')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { shouldAutoResetSingleStateZoom } from '../shouldAutoResetSingleStateZoom'
|
|
3
|
+
|
|
4
|
+
describe('shouldAutoResetSingleStateZoom', () => {
|
|
5
|
+
it('returns false on the first runtime data load', () => {
|
|
6
|
+
expect(
|
|
7
|
+
shouldAutoResetSingleStateZoom({
|
|
8
|
+
isDashboard: true,
|
|
9
|
+
previousRuntimeDataHash: null,
|
|
10
|
+
nextRuntimeDataHash: 123,
|
|
11
|
+
hasDashboardFilters: true
|
|
12
|
+
})
|
|
13
|
+
).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns true when filtered dashboard runtime data changes after initial load', () => {
|
|
17
|
+
expect(
|
|
18
|
+
shouldAutoResetSingleStateZoom({
|
|
19
|
+
isDashboard: true,
|
|
20
|
+
previousRuntimeDataHash: 123,
|
|
21
|
+
nextRuntimeDataHash: 456,
|
|
22
|
+
hasDashboardFilters: true
|
|
23
|
+
})
|
|
24
|
+
).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns false when the runtime data hash is unchanged', () => {
|
|
28
|
+
expect(
|
|
29
|
+
shouldAutoResetSingleStateZoom({
|
|
30
|
+
isDashboard: true,
|
|
31
|
+
previousRuntimeDataHash: 123,
|
|
32
|
+
nextRuntimeDataHash: 123,
|
|
33
|
+
hasDashboardFilters: true
|
|
34
|
+
})
|
|
35
|
+
).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns false outside dashboards', () => {
|
|
39
|
+
expect(
|
|
40
|
+
shouldAutoResetSingleStateZoom({
|
|
41
|
+
isDashboard: false,
|
|
42
|
+
previousRuntimeDataHash: 123,
|
|
43
|
+
nextRuntimeDataHash: 456,
|
|
44
|
+
hasDashboardFilters: true
|
|
45
|
+
})
|
|
46
|
+
).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns false when map zoom is disabled', () => {
|
|
50
|
+
expect(
|
|
51
|
+
shouldAutoResetSingleStateZoom({
|
|
52
|
+
isDashboard: true,
|
|
53
|
+
previousRuntimeDataHash: 123,
|
|
54
|
+
nextRuntimeDataHash: 456,
|
|
55
|
+
hasDashboardFilters: true,
|
|
56
|
+
allowMapZoom: false
|
|
57
|
+
})
|
|
58
|
+
).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns false when dashboard filters are not present on the visualization', () => {
|
|
62
|
+
expect(
|
|
63
|
+
shouldAutoResetSingleStateZoom({
|
|
64
|
+
isDashboard: true,
|
|
65
|
+
previousRuntimeDataHash: 123,
|
|
66
|
+
nextRuntimeDataHash: 456,
|
|
67
|
+
hasDashboardFilters: false
|
|
68
|
+
})
|
|
69
|
+
).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type ReactNode, useContext } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { navigationHandler } from '../helpers'
|
|
3
3
|
import ConfigContext from '../context'
|
|
4
4
|
import useTooltip from './useTooltip'
|
|
5
|
-
import { supportedStatesFipsCodes } from './../data/supported-geos'
|
|
6
5
|
import parse from 'html-react-parser'
|
|
7
6
|
import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
|
|
8
7
|
import ExternalIcon from './../images/external-link.svg'
|
|
@@ -10,7 +9,7 @@ import ExternalIcon from './../images/external-link.svg'
|
|
|
10
9
|
const useApplyTooltipsToGeo = () => {
|
|
11
10
|
const { config, customNavigationHandler } = useContext(ConfigContext)
|
|
12
11
|
const navigationColumnName = config.columns.navigate.name
|
|
13
|
-
const { buildTooltip } = useTooltip(
|
|
12
|
+
const { buildTooltip } = useTooltip(config)
|
|
14
13
|
|
|
15
14
|
const applyTooltipsToGeo = (geoName: string, row: Object, returnType = 'string') => {
|
|
16
15
|
let toolTipText: string | ReactNode = buildTooltip(row, geoName, '')
|
|
@@ -18,7 +17,11 @@ const useApplyTooltipsToGeo = () => {
|
|
|
18
17
|
// We convert the markup into JSX and add a navigation link if it's going into a modal.
|
|
19
18
|
if ('jsx' === returnType) {
|
|
20
19
|
if (typeof toolTipText === 'string') {
|
|
21
|
-
toolTipText = [
|
|
20
|
+
toolTipText = [
|
|
21
|
+
<div key='modal-content' className='cove-prose'>
|
|
22
|
+
{parse(toolTipText)}
|
|
23
|
+
</div>
|
|
24
|
+
]
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
if (config.columns.hasOwnProperty('navigate') && row[navigationColumnName]) {
|
|
@@ -9,6 +9,9 @@ const useGeoClickHandler = () => {
|
|
|
9
9
|
config: state,
|
|
10
10
|
setConfig,
|
|
11
11
|
setSharedFilter,
|
|
12
|
+
clearSharedFilter,
|
|
13
|
+
hasActiveSharedFilter,
|
|
14
|
+
setSharedFilterValue,
|
|
12
15
|
customNavigationHandler,
|
|
13
16
|
interactionLabel
|
|
14
17
|
} = useContext(ConfigContext)
|
|
@@ -16,7 +19,16 @@ const useGeoClickHandler = () => {
|
|
|
16
19
|
|
|
17
20
|
const geoClickHandler = (geoDisplayName: string, geoData: object): void => {
|
|
18
21
|
if (setSharedFilter) {
|
|
19
|
-
|
|
22
|
+
// Get the column name for the filter (from dashboardFilters config)
|
|
23
|
+
const filterColumnName = state.dashboardFilters?.[0]?.columnName || state.columns?.geo?.name
|
|
24
|
+
const clickedValue = filterColumnName ? geoData[filterColumnName] : geoDisplayName
|
|
25
|
+
|
|
26
|
+
// Toggle behavior: if the clicked value matches the current filter value, clear it
|
|
27
|
+
if (hasActiveSharedFilter && setSharedFilterValue === clickedValue && clearSharedFilter) {
|
|
28
|
+
clearSharedFilter(state.uid)
|
|
29
|
+
} else {
|
|
30
|
+
setSharedFilter(state.uid, geoData)
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
// If world-geocode map zoom to geo point
|
|
@@ -53,7 +53,7 @@ export default function useMapLayers(config: MapConfig, setConfig, pathGenerator
|
|
|
53
53
|
setConfig(newConfig)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const handleAddLayer = (e:
|
|
56
|
+
const handleAddLayer = (e: MouseEvent<HTMLButtonElement>) => {
|
|
57
57
|
e.preventDefault()
|
|
58
58
|
const placeHolderLayer = {
|
|
59
59
|
name: 'New Custom Layer',
|
|
@@ -19,7 +19,7 @@ interface StateData {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
|
|
22
|
-
const { config, runtimeData, position, interactionLabel } = useContext<MapContext>(ConfigContext)
|
|
22
|
+
const { config, runtimeData, position, scale, translate, interactionLabel } = useContext<MapContext>(ConfigContext)
|
|
23
23
|
const dispatch = useContext(MapDispatchContext)
|
|
24
24
|
|
|
25
25
|
// Get statesPicked with memoization
|
|
@@ -72,8 +72,42 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
|
|
|
72
72
|
return { projection, newProjection, stateCenter, bounds }
|
|
73
73
|
}, [topoData, statesPicked])
|
|
74
74
|
|
|
75
|
+
const resetZoomState = useCallback(
|
|
76
|
+
({ publishEvent = true }: { publishEvent?: boolean } = {}) => {
|
|
77
|
+
const nextCoordinates = projectionData.stateCenter
|
|
78
|
+
const alreadyAtDefaultPosition =
|
|
79
|
+
position.zoom === 1 &&
|
|
80
|
+
position.coordinates[0] === nextCoordinates[0] &&
|
|
81
|
+
position.coordinates[1] === nextCoordinates[1]
|
|
82
|
+
const alreadyAtDefaultTransform = scale === 1 && translate[0] === 0 && translate[1] === 0
|
|
83
|
+
|
|
84
|
+
if (alreadyAtDefaultPosition && alreadyAtDefaultTransform) return
|
|
85
|
+
|
|
86
|
+
dispatch({ type: 'SET_POSITION', payload: { coordinates: nextCoordinates, zoom: 1 } })
|
|
87
|
+
dispatch({ type: 'SET_TRANSLATE', payload: [0, 0] })
|
|
88
|
+
dispatch({ type: 'SET_SCALE', payload: 1 })
|
|
89
|
+
|
|
90
|
+
if (publishEvent) {
|
|
91
|
+
publishAnalyticsEvent({
|
|
92
|
+
vizType: 'map',
|
|
93
|
+
vizSubType: getVizSubType(config),
|
|
94
|
+
eventType: 'map_reset_zoom_level',
|
|
95
|
+
eventAction: 'click',
|
|
96
|
+
eventLabel: interactionLabel,
|
|
97
|
+
vizTitle: getVizTitle(config)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[config, dispatch, interactionLabel, position, projectionData.stateCenter, scale, translate]
|
|
102
|
+
)
|
|
103
|
+
|
|
75
104
|
const setScaleAndTranslate = useCallback(
|
|
76
105
|
(zoomFunction: string = '') => {
|
|
106
|
+
if (zoomFunction === 'reset') {
|
|
107
|
+
resetZoomState()
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
const _prevPosition = position
|
|
78
112
|
let newZoom = _prevPosition.zoom
|
|
79
113
|
let newCoordinates = _prevPosition.coordinates
|
|
@@ -98,27 +132,11 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
|
|
|
98
132
|
_prevPosition.coordinates[0] !== 0 && _prevPosition.coordinates[1] !== 0
|
|
99
133
|
? _prevPosition.coordinates
|
|
100
134
|
: projectionData.stateCenter
|
|
101
|
-
} else if (zoomFunction === 'reset') {
|
|
102
|
-
newZoom = 1
|
|
103
|
-
newCoordinates = projectionData.stateCenter
|
|
104
135
|
}
|
|
105
136
|
|
|
106
137
|
dispatch({ type: 'SET_POSITION', payload: { coordinates: newCoordinates, zoom: newZoom } })
|
|
107
|
-
|
|
108
|
-
if (zoomFunction === 'reset') {
|
|
109
|
-
dispatch({ type: 'SET_TRANSLATE', payload: [0, 0] }) // needed for state switcher
|
|
110
|
-
dispatch({ type: 'SET_SCALE', payload: 1 }) // needed for state switcher
|
|
111
|
-
publishAnalyticsEvent({
|
|
112
|
-
vizType: 'map',
|
|
113
|
-
vizSubType: getVizSubType(config),
|
|
114
|
-
eventType: 'map_reset_zoom_level',
|
|
115
|
-
eventAction: 'click',
|
|
116
|
-
eventLabel: interactionLabel,
|
|
117
|
-
vizTitle: getVizTitle(config)
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
138
|
},
|
|
121
|
-
[position, projectionData.stateCenter,
|
|
139
|
+
[config, dispatch, interactionLabel, position, projectionData.stateCenter, resetZoomState]
|
|
122
140
|
)
|
|
123
141
|
|
|
124
142
|
// Essential fix: Remove config from dependencies to prevent infinite loops
|
|
@@ -165,9 +183,9 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
|
|
|
165
183
|
|
|
166
184
|
const handleZoomReset = useCallback(
|
|
167
185
|
_setRuntimeData => {
|
|
168
|
-
|
|
186
|
+
resetZoomState()
|
|
169
187
|
},
|
|
170
|
-
[
|
|
188
|
+
[resetZoomState]
|
|
171
189
|
)
|
|
172
190
|
|
|
173
191
|
return {
|
|
@@ -178,6 +196,7 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
|
|
|
178
196
|
handleZoomOut,
|
|
179
197
|
handleMoveEnd,
|
|
180
198
|
handleZoomReset,
|
|
199
|
+
resetZoomState,
|
|
181
200
|
projection: projectionData.projection,
|
|
182
201
|
bounds: projectionData.bounds
|
|
183
202
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react'
|
|
2
2
|
import useTooltip from './useTooltip'
|
|
3
3
|
|
|
4
|
-
const supportedStatesFipsCodes = {
|
|
5
|
-
'01': 'Alabama'
|
|
6
|
-
}
|
|
7
|
-
|
|
8
4
|
const createConfig = (hideGeoColumnInTooltip: boolean) => ({
|
|
9
5
|
general: {
|
|
10
6
|
geoType: 'world',
|
|
@@ -41,18 +37,8 @@ describe('useTooltip', () => {
|
|
|
41
37
|
it('hides the geography column label in the tooltip body when configured', () => {
|
|
42
38
|
const row = { Country: 'ssd', Value: 10 }
|
|
43
39
|
|
|
44
|
-
const { result: visibleResult } = renderHook(() =>
|
|
45
|
-
|
|
46
|
-
config: createConfig(false),
|
|
47
|
-
supportedStatesFipsCodes
|
|
48
|
-
})
|
|
49
|
-
)
|
|
50
|
-
const { result: hiddenResult } = renderHook(() =>
|
|
51
|
-
useTooltip({
|
|
52
|
-
config: createConfig(true),
|
|
53
|
-
supportedStatesFipsCodes
|
|
54
|
-
})
|
|
55
|
-
)
|
|
40
|
+
const { result: visibleResult } = renderHook(() => useTooltip(createConfig(false) as any))
|
|
41
|
+
const { result: hiddenResult } = renderHook(() => useTooltip(createConfig(true) as any))
|
|
56
42
|
|
|
57
43
|
const visibleTooltip = visibleResult.current.buildTooltip(row, 'South Sudan')
|
|
58
44
|
const hiddenTooltip = hiddenResult.current.buildTooltip(row, 'South Sudan')
|