@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getMatchingPatternForRow } from '../getMatchingPatternForRow'
|
|
3
|
+
import { PatternSelection } from '../../types/MapConfig'
|
|
4
|
+
|
|
5
|
+
describe('getMatchingPatternForRow', () => {
|
|
6
|
+
it('uses the last matching specific pattern when multiple specific patterns match', () => {
|
|
7
|
+
const row = { Rate: 55 }
|
|
8
|
+
const patterns: PatternSelection[] = [
|
|
9
|
+
{
|
|
10
|
+
dataKey: 'Rate',
|
|
11
|
+
dataValue: '55',
|
|
12
|
+
pattern: 'lines',
|
|
13
|
+
size: 'small',
|
|
14
|
+
color: '#111',
|
|
15
|
+
label: '',
|
|
16
|
+
contrastCheck: true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
dataKey: 'Rate',
|
|
20
|
+
dataValue: '55',
|
|
21
|
+
pattern: 'circles',
|
|
22
|
+
size: 'medium',
|
|
23
|
+
color: '#000',
|
|
24
|
+
label: '',
|
|
25
|
+
contrastCheck: true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const result = getMatchingPatternForRow(row, patterns)
|
|
30
|
+
|
|
31
|
+
expect(result?.patternIndex).toBe(1)
|
|
32
|
+
expect(result?.pattern.pattern).toBe('circles')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('matches specific dataKey patterns first', () => {
|
|
36
|
+
const row = { Rate: 55, Status: '55' }
|
|
37
|
+
const patterns: PatternSelection[] = [
|
|
38
|
+
{
|
|
39
|
+
dataKey: '',
|
|
40
|
+
dataValue: '55',
|
|
41
|
+
pattern: 'lines',
|
|
42
|
+
size: 'small',
|
|
43
|
+
color: '#111',
|
|
44
|
+
label: '',
|
|
45
|
+
contrastCheck: true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
dataKey: 'Rate',
|
|
49
|
+
dataValue: '55',
|
|
50
|
+
pattern: 'circles',
|
|
51
|
+
size: 'medium',
|
|
52
|
+
color: '#000',
|
|
53
|
+
label: '',
|
|
54
|
+
contrastCheck: true
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const result = getMatchingPatternForRow(row, patterns)
|
|
59
|
+
|
|
60
|
+
expect(result?.patternIndex).toBe(1)
|
|
61
|
+
expect(result?.matchedDataKey).toBe('Rate')
|
|
62
|
+
expect(result?.pattern.pattern).toBe('circles')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('falls back to broad matching when dataKey is blank', () => {
|
|
66
|
+
const row = { State: 'Colorado', Rate: 55 }
|
|
67
|
+
const patterns: PatternSelection[] = [
|
|
68
|
+
{
|
|
69
|
+
dataKey: '',
|
|
70
|
+
dataValue: 'Colorado',
|
|
71
|
+
pattern: 'waves',
|
|
72
|
+
size: 'large',
|
|
73
|
+
color: '#333',
|
|
74
|
+
label: '',
|
|
75
|
+
contrastCheck: true
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
const result = getMatchingPatternForRow(row, patterns)
|
|
80
|
+
|
|
81
|
+
expect(result?.patternIndex).toBe(0)
|
|
82
|
+
expect(result?.matchedDataKey).toBe('State')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('does not match when dataValue is empty', () => {
|
|
86
|
+
const row = { State: 'Colorado', Rate: 55 }
|
|
87
|
+
const patterns: PatternSelection[] = [
|
|
88
|
+
{
|
|
89
|
+
dataKey: '',
|
|
90
|
+
dataValue: ' ',
|
|
91
|
+
pattern: 'waves',
|
|
92
|
+
size: 'large',
|
|
93
|
+
color: '#333',
|
|
94
|
+
label: '',
|
|
95
|
+
contrastCheck: true
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
expect(getMatchingPatternForRow(row, patterns)).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('supports numeric string and number matching for broad patterns', () => {
|
|
103
|
+
const row = { Rate: 55 }
|
|
104
|
+
const patterns: PatternSelection[] = [
|
|
105
|
+
{
|
|
106
|
+
dataKey: '',
|
|
107
|
+
dataValue: '55',
|
|
108
|
+
pattern: 'waves',
|
|
109
|
+
size: 'large',
|
|
110
|
+
color: '#333',
|
|
111
|
+
label: '',
|
|
112
|
+
contrastCheck: true
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
const result = getMatchingPatternForRow(row, patterns)
|
|
117
|
+
|
|
118
|
+
expect(result?.matchedDataKey).toBe('Rate')
|
|
119
|
+
expect(result?.patternIndex).toBe(0)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('uses the last matching broad pattern when multiple broad patterns match', () => {
|
|
123
|
+
const row = { Category: 'Target' }
|
|
124
|
+
const patterns: PatternSelection[] = [
|
|
125
|
+
{
|
|
126
|
+
dataKey: '',
|
|
127
|
+
dataValue: 'Target',
|
|
128
|
+
pattern: 'lines',
|
|
129
|
+
size: 'small',
|
|
130
|
+
color: '#111',
|
|
131
|
+
label: '',
|
|
132
|
+
contrastCheck: true
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
dataKey: '',
|
|
136
|
+
dataValue: 'Target',
|
|
137
|
+
pattern: 'waves',
|
|
138
|
+
size: 'large',
|
|
139
|
+
color: '#333',
|
|
140
|
+
label: '',
|
|
141
|
+
contrastCheck: true
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
const result = getMatchingPatternForRow(row, patterns)
|
|
146
|
+
|
|
147
|
+
expect(result?.patternIndex).toBe(1)
|
|
148
|
+
expect(result?.pattern.pattern).toBe('waves')
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -4,8 +4,24 @@ import { getPatternForRow } from '../getPatternForRow'
|
|
|
4
4
|
const baseConfig: any = {
|
|
5
5
|
map: {
|
|
6
6
|
patterns: [
|
|
7
|
-
{
|
|
8
|
-
|
|
7
|
+
{
|
|
8
|
+
dataKey: 'Percent Vaccinated',
|
|
9
|
+
dataValue: '99.99',
|
|
10
|
+
pattern: 'circles',
|
|
11
|
+
size: 'medium',
|
|
12
|
+
color: '#000',
|
|
13
|
+
label: '',
|
|
14
|
+
contrastCheck: true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
dataKey: 'Status',
|
|
18
|
+
dataValue: 'NA',
|
|
19
|
+
pattern: 'lines',
|
|
20
|
+
size: 'small',
|
|
21
|
+
color: '#111',
|
|
22
|
+
label: '',
|
|
23
|
+
contrastCheck: true
|
|
24
|
+
}
|
|
9
25
|
]
|
|
10
26
|
}
|
|
11
27
|
}
|
|
@@ -36,4 +52,126 @@ describe('getPatternForRow', () => {
|
|
|
36
52
|
const row = { 'Percent Vaccinated': 75.06, Status: '*' }
|
|
37
53
|
expect(getPatternForRow(row, baseConfig)).toBeNull()
|
|
38
54
|
})
|
|
55
|
+
|
|
56
|
+
it('matches blank dataKey patterns against any row value', () => {
|
|
57
|
+
const config = {
|
|
58
|
+
map: {
|
|
59
|
+
patterns: [
|
|
60
|
+
{
|
|
61
|
+
dataKey: '',
|
|
62
|
+
dataValue: 'Colorado',
|
|
63
|
+
pattern: 'waves',
|
|
64
|
+
size: 'large',
|
|
65
|
+
color: '#222',
|
|
66
|
+
label: '',
|
|
67
|
+
contrastCheck: true
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const row = { STATE: 'Colorado', Rate: 55 }
|
|
73
|
+
const result = getPatternForRow(row, config)
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
pattern: 'waves',
|
|
77
|
+
dataKey: 'STATE',
|
|
78
|
+
size: 'large',
|
|
79
|
+
patternIndex: 0,
|
|
80
|
+
color: '#222'
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('prioritizes specific dataKey matches over broad matches', () => {
|
|
85
|
+
const config = {
|
|
86
|
+
map: {
|
|
87
|
+
patterns: [
|
|
88
|
+
{
|
|
89
|
+
dataKey: '',
|
|
90
|
+
dataValue: '55',
|
|
91
|
+
pattern: 'lines',
|
|
92
|
+
size: 'small',
|
|
93
|
+
color: '#111',
|
|
94
|
+
label: '',
|
|
95
|
+
contrastCheck: true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
dataKey: 'Rate',
|
|
99
|
+
dataValue: '55',
|
|
100
|
+
pattern: 'circles',
|
|
101
|
+
size: 'medium',
|
|
102
|
+
color: '#000',
|
|
103
|
+
label: '',
|
|
104
|
+
contrastCheck: true
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const row = { Rate: 55, Category: '55' }
|
|
110
|
+
const result = getPatternForRow(row, config)
|
|
111
|
+
|
|
112
|
+
expect(result).toEqual({
|
|
113
|
+
pattern: 'circles',
|
|
114
|
+
dataKey: 'Rate',
|
|
115
|
+
size: 'medium',
|
|
116
|
+
patternIndex: 1,
|
|
117
|
+
color: '#000'
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('does not match blank dataKey patterns with empty values', () => {
|
|
122
|
+
const config = {
|
|
123
|
+
map: {
|
|
124
|
+
patterns: [
|
|
125
|
+
{
|
|
126
|
+
dataKey: '',
|
|
127
|
+
dataValue: ' ',
|
|
128
|
+
pattern: 'lines',
|
|
129
|
+
size: 'small',
|
|
130
|
+
color: '#111',
|
|
131
|
+
label: '',
|
|
132
|
+
contrastCheck: true
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const row = { STATE: 'Colorado' }
|
|
138
|
+
expect(getPatternForRow(row, config)).toBeNull()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('returns the last matching specific pattern to preserve dominant visual overlay behavior', () => {
|
|
142
|
+
const config = {
|
|
143
|
+
map: {
|
|
144
|
+
patterns: [
|
|
145
|
+
{
|
|
146
|
+
dataKey: 'Rate',
|
|
147
|
+
dataValue: '55',
|
|
148
|
+
pattern: 'lines',
|
|
149
|
+
size: 'small',
|
|
150
|
+
color: '#111',
|
|
151
|
+
label: '',
|
|
152
|
+
contrastCheck: true
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
dataKey: 'Rate',
|
|
156
|
+
dataValue: '55',
|
|
157
|
+
pattern: 'waves',
|
|
158
|
+
size: 'large',
|
|
159
|
+
color: '#222',
|
|
160
|
+
label: '',
|
|
161
|
+
contrastCheck: true
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const row = { Rate: 55 }
|
|
167
|
+
const result = getPatternForRow(row, config)
|
|
168
|
+
|
|
169
|
+
expect(result).toEqual({
|
|
170
|
+
pattern: 'waves',
|
|
171
|
+
dataKey: 'Rate',
|
|
172
|
+
size: 'large',
|
|
173
|
+
patternIndex: 1,
|
|
174
|
+
color: '#222'
|
|
175
|
+
})
|
|
176
|
+
})
|
|
39
177
|
})
|
|
@@ -5,6 +5,7 @@ import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
|
|
|
5
5
|
import { MapConfig } from '../types/MapConfig'
|
|
6
6
|
import { CSV_PARSE_CONFIG } from './constants'
|
|
7
7
|
import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
|
|
8
|
+
import { extractDataAndMetadata } from '@cdc/core/helpers/extractDataAndMetadata'
|
|
8
9
|
|
|
9
10
|
const buildQueryString = (params: Record<string, string>): string =>
|
|
10
11
|
Object.keys(params)
|
|
@@ -34,6 +35,7 @@ export const reloadURLData = async (config: MapConfig, setConfig: (config: MapCo
|
|
|
34
35
|
|
|
35
36
|
let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${buildQueryString(qsParams)}`
|
|
36
37
|
let data
|
|
38
|
+
let dataMetadata: Record<string, string> = {}
|
|
37
39
|
|
|
38
40
|
try {
|
|
39
41
|
const regex = /(?:\.([^.]+))?$/
|
|
@@ -47,7 +49,10 @@ export const reloadURLData = async (config: MapConfig, setConfig: (config: MapCo
|
|
|
47
49
|
return parsedCsv.data
|
|
48
50
|
})
|
|
49
51
|
} else if ('json' === ext || isSolrJson(dataUrlFinal)) {
|
|
50
|
-
|
|
52
|
+
const json = await fetch(dataUrlFinal).then(response => response.json())
|
|
53
|
+
const extracted = extractDataAndMetadata(json)
|
|
54
|
+
data = extracted.data
|
|
55
|
+
dataMetadata = extracted.dataMetadata
|
|
51
56
|
} else {
|
|
52
57
|
data = []
|
|
53
58
|
}
|
|
@@ -64,6 +69,7 @@ export const reloadURLData = async (config: MapConfig, setConfig: (config: MapCo
|
|
|
64
69
|
|
|
65
70
|
const newConfig = cloneConfig(config)
|
|
66
71
|
newConfig.data = data
|
|
72
|
+
newConfig.dataMetadata = dataMetadata
|
|
67
73
|
newConfig.runtimeDataUrl = dataUrlFinal
|
|
68
74
|
|
|
69
75
|
setConfig(newConfig)
|
|
@@ -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]) {
|
|
@@ -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',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback,
|
|
1
|
+
import { useState, useCallback, useRef } from 'react'
|
|
2
2
|
import { type ViewPort } from '@cdc/core/types/ViewPort'
|
|
3
3
|
import { type DimensionsType } from '@cdc/core/types/Dimensions'
|
|
4
4
|
import { EDITOR_WIDTH } from '@cdc/core/helpers/constants'
|
|
@@ -11,37 +11,51 @@ export const useResizeObserver = (isEditor: boolean) => {
|
|
|
11
11
|
const [vizViewport, setVizViewport] = useState<ViewPort>(null)
|
|
12
12
|
const [container, setContainer] = useState<HTMLElement | null>(null)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Keep isEditor fresh in the observer callback without recreating the observer
|
|
15
|
+
const isEditorRef = useRef(isEditor)
|
|
16
|
+
isEditorRef.current = isEditor
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
width = editorIsOpen ? width - EDITOR_WIDTH : width
|
|
18
|
+
const resizeObserverRef = useRef<ResizeObserver | null>(null)
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const outerContainerRef = useCallback(node => {
|
|
21
|
+
// Tear down any previous observer (node swap or unmount)
|
|
22
|
+
if (resizeObserverRef.current) {
|
|
23
|
+
resizeObserverRef.current.disconnect()
|
|
24
|
+
resizeObserverRef.current = null
|
|
25
|
+
}
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
if (node !== null) {
|
|
28
|
+
resizeObserverRef.current = new ResizeObserver(entries => {
|
|
29
|
+
for (let entry of entries) {
|
|
30
|
+
let { width, height } = entry.contentRect
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
const editorIsOpen = isEditorRef.current && !!document.querySelector('.editor-panel:not(.hidden)')
|
|
33
|
+
width = editorIsOpen ? width - EDITOR_WIDTH : width
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
})
|
|
35
|
+
// Account for 1rem padding in editor mode
|
|
36
|
+
width = width - (isEditorRef.current ? 36 : 0)
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
if (node !== null) {
|
|
35
|
-
resizeObserver.observe(node)
|
|
36
|
-
}
|
|
37
|
-
setContainer(node)
|
|
38
|
+
const newViewport = getViewport(width)
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
setCurrentViewport(newViewport)
|
|
41
|
+
setVizViewport(newViewport)
|
|
42
|
+
setDimensions([width, height])
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
resizeObserverRef.current.observe(node)
|
|
41
46
|
}
|
|
47
|
+
|
|
48
|
+
setContainer(node)
|
|
42
49
|
}, [])
|
|
43
50
|
|
|
44
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
resizeObserver: resizeObserverRef.current,
|
|
53
|
+
dimensions,
|
|
54
|
+
currentViewport,
|
|
55
|
+
vizViewport,
|
|
56
|
+
outerContainerRef,
|
|
57
|
+
container
|
|
58
|
+
}
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
export default useResizeObserver
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import useTooltip from './useTooltip'
|
|
3
|
+
|
|
4
|
+
const supportedStatesFipsCodes = {
|
|
5
|
+
'01': 'Alabama'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const createConfig = (hideGeoColumnInTooltip: boolean) => ({
|
|
9
|
+
general: {
|
|
10
|
+
geoType: 'world',
|
|
11
|
+
type: 'map',
|
|
12
|
+
hideGeoColumnInTooltip,
|
|
13
|
+
hidePrimaryColumnInTooltip: false,
|
|
14
|
+
geoLabelOverride: ''
|
|
15
|
+
},
|
|
16
|
+
columns: {
|
|
17
|
+
geo: {
|
|
18
|
+
name: 'Country',
|
|
19
|
+
label: 'Location',
|
|
20
|
+
tooltip: true,
|
|
21
|
+
displayColumn: ''
|
|
22
|
+
},
|
|
23
|
+
primary: {
|
|
24
|
+
name: 'Value',
|
|
25
|
+
label: 'Value',
|
|
26
|
+
tooltip: true
|
|
27
|
+
},
|
|
28
|
+
navigate: {
|
|
29
|
+
name: ''
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
legend: {
|
|
33
|
+
specialClasses: []
|
|
34
|
+
},
|
|
35
|
+
tooltips: {
|
|
36
|
+
noDataLabel: 'No Data'
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('useTooltip', () => {
|
|
41
|
+
it('hides the geography column label in the tooltip body when configured', () => {
|
|
42
|
+
const row = { Country: 'ssd', Value: 10 }
|
|
43
|
+
|
|
44
|
+
const { result: visibleResult } = renderHook(() =>
|
|
45
|
+
useTooltip({
|
|
46
|
+
config: createConfig(false),
|
|
47
|
+
supportedStatesFipsCodes
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
const { result: hiddenResult } = renderHook(() =>
|
|
51
|
+
useTooltip({
|
|
52
|
+
config: createConfig(true),
|
|
53
|
+
supportedStatesFipsCodes
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const visibleTooltip = visibleResult.current.buildTooltip(row, 'South Sudan')
|
|
58
|
+
const hiddenTooltip = hiddenResult.current.buildTooltip(row, 'South Sudan')
|
|
59
|
+
|
|
60
|
+
expect(visibleTooltip).toContain('Location: South Sudan')
|
|
61
|
+
expect(hiddenTooltip).not.toContain('Location: South Sudan')
|
|
62
|
+
expect(hiddenTooltip).toContain('<li class="tooltip-body">South Sudan</li>')
|
|
63
|
+
})
|
|
64
|
+
})
|
package/src/hooks/useTooltip.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
|
|
2
2
|
import { displayGeoName } from '../helpers/displayGeoName'
|
|
3
|
+
import { MapConfig } from '../types/MapConfig'
|
|
4
|
+
import { supportedStatesFipsCodes } from '../data/supported-geos'
|
|
3
5
|
|
|
4
|
-
const useTooltip =
|
|
5
|
-
const { config, supportedStatesFipsCodes } = props
|
|
6
|
-
|
|
6
|
+
const useTooltip = (config: MapConfig) => {
|
|
7
7
|
/**
|
|
8
8
|
* On county maps there's a need to append the state name
|
|
9
9
|
* @param {String} toolTipText - previous tooltip text to build upon
|
|
@@ -11,24 +11,33 @@ const useTooltip = props => {
|
|
|
11
11
|
* @returns {String} toolTipText - new toolTipText
|
|
12
12
|
*/
|
|
13
13
|
const handleTooltipStateNameColumn = (toolTipText, row) => {
|
|
14
|
-
const { geoType, type, hideGeoColumnInTooltip } = config.general
|
|
14
|
+
const { geoType, type, hideGeoColumnInTooltip, showHSABoundaries } = config.general
|
|
15
15
|
if (geoType === 'us-county' && type !== 'us-geocode') {
|
|
16
16
|
let stateFipsCode = row[config.columns.geo.name].substring(0, 2)
|
|
17
17
|
const stateName = supportedStatesFipsCodes[stateFipsCode]
|
|
18
|
-
toolTipText +=
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
toolTipText +=
|
|
19
|
+
hideGeoColumnInTooltip || showHSABoundaries
|
|
20
|
+
? `<strong>${stateName}</strong><br/>`
|
|
21
|
+
: `<strong>Location: ${stateName}</strong><br/>`
|
|
21
22
|
}
|
|
22
23
|
return toolTipText
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
const getHSAColumnText = (config: MapConfig, row: Object) => {
|
|
27
|
+
const { hsa } = config.columns
|
|
28
|
+
if (!hsa || !config.general.showHSABoundaries) return ''
|
|
29
|
+
const hsaDescription = row[hsa.name]
|
|
30
|
+
return `<p class="tooltip-heading">HSA: ${hsaDescription}</p>`
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* On county and state maps, adds the ability to hide the geo column name (prefix)
|
|
27
35
|
* @param {String} geoName - feature name
|
|
28
36
|
* @returns {String} text to be appended to toolTipText
|
|
29
37
|
*/
|
|
30
|
-
const handleTooltipGeoColumn = (geoName: string) => {
|
|
38
|
+
const handleTooltipGeoColumn = (geoName: string, row?) => {
|
|
31
39
|
const { hideGeoColumnInTooltip } = config.general as { hideGeoColumnInTooltip: boolean }
|
|
40
|
+
const displayOverride = row?.[config.columns.geo?.displayColumn]
|
|
32
41
|
|
|
33
42
|
const handleTooltipPrefix = (toolTipText: string) => {
|
|
34
43
|
const { geoType, geoLabelOverride } = config.general
|
|
@@ -54,8 +63,11 @@ const useTooltip = props => {
|
|
|
54
63
|
|
|
55
64
|
const prefix = handleTooltipPrefix('')
|
|
56
65
|
|
|
57
|
-
if (hideGeoColumnInTooltip) return `<strong>${displayGeoName(geoName)}</strong>`
|
|
58
|
-
return `<p class="tooltip-heading" style="text-transform: none;">${prefix}${displayGeoName(
|
|
66
|
+
if (hideGeoColumnInTooltip) return `<strong>${displayGeoName(geoName, displayOverride)}</strong>`
|
|
67
|
+
return `<p class="tooltip-heading" style="text-transform: none;">${prefix}${displayGeoName(
|
|
68
|
+
geoName,
|
|
69
|
+
displayOverride
|
|
70
|
+
)}</p>`
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
/**
|
|
@@ -74,13 +86,29 @@ const useTooltip = props => {
|
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
const handleTooltipPrimaryColumn = (tooltipValue, column) => {
|
|
77
|
-
const { hidePrimaryColumnInTooltip } = config.general as {
|
|
89
|
+
const { hidePrimaryColumnInTooltip, hideGeoColumnInTooltip } = config.general as {
|
|
90
|
+
hidePrimaryColumnInTooltip: boolean
|
|
91
|
+
hideGeoColumnInTooltip: boolean
|
|
92
|
+
}
|
|
78
93
|
let tooltipPrefix = column.label?.length > 0 ? column.label : ''
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
const hidePrimaryLabel = column.name === config.columns.primary.name && hidePrimaryColumnInTooltip
|
|
95
|
+
const hideGeoLabel = column.name === config.columns.geo.name && hideGeoColumnInTooltip
|
|
96
|
+
|
|
97
|
+
if (hidePrimaryLabel || hideGeoLabel || !tooltipPrefix) return `<li class="tooltip-body">${tooltipValue}</li>`
|
|
81
98
|
return `<li class="tooltip-body">${tooltipPrefix}: ${tooltipValue}</li>`
|
|
82
99
|
}
|
|
83
100
|
|
|
101
|
+
const formatTooltipColumnValue = (row, column, columnKey) => {
|
|
102
|
+
if (!row) return config.tooltips?.noDataLabel || 'No Data'
|
|
103
|
+
|
|
104
|
+
if (column.name === config.columns.geo.name) {
|
|
105
|
+
const displayOverride = row?.[config.columns.geo?.displayColumn]
|
|
106
|
+
return displayGeoName(row[column.name], displayOverride)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return displayDataAsText(row[column.name], columnKey, config)
|
|
110
|
+
}
|
|
111
|
+
|
|
84
112
|
/**
|
|
85
113
|
*
|
|
86
114
|
* @param {String} toolTipText - previous tooltipText to build upon
|
|
@@ -106,7 +134,7 @@ const useTooltip = props => {
|
|
|
106
134
|
let tooltipValue = handleTooltipSpecialClassText(specialClasses, column, row, '', columnKey)
|
|
107
135
|
|
|
108
136
|
if (!tooltipValue) {
|
|
109
|
-
tooltipValue = row
|
|
137
|
+
tooltipValue = formatTooltipColumnValue(row, column, columnKey)
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
toolTipText += handleTooltipPrimaryColumn(tooltipValue, column)
|
|
@@ -124,8 +152,11 @@ const useTooltip = props => {
|
|
|
124
152
|
// Handle County Location Columns
|
|
125
153
|
toolTipText += handleTooltipStateNameColumn(toolTipText, row)
|
|
126
154
|
|
|
155
|
+
// Handle HSA Column
|
|
156
|
+
toolTipText += getHSAColumnText(config, row)
|
|
157
|
+
|
|
127
158
|
// Handle Columns > Data Column In tooltips
|
|
128
|
-
toolTipText += handleTooltipGeoColumn(geoName)
|
|
159
|
+
toolTipText += handleTooltipGeoColumn(geoName, row)
|
|
129
160
|
|
|
130
161
|
// Handle Columns > Geography Column In tooltips
|
|
131
162
|
toolTipText = handleTooltipColumns(toolTipText, row)
|