@cdc/map 4.25.8 → 4.25.11
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/.claude/agents/typescript-organizer.md +118 -0
- package/.claude/settings.local.json +30 -0
- package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
- package/dist/cdcmap.js +56991 -53706
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/d.json +345 -0
- package/examples/private/g.json +1 -0
- package/examples/private/h.json +105911 -0
- package/examples/private/measles-data.json +378 -0
- package/examples/private/measles.json +211 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/state-with-pattern.json +883 -0
- package/index.html +36 -34
- package/package.json +26 -5
- package/src/CdcMap.tsx +23 -8
- package/src/CdcMapComponent.tsx +238 -308
- package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
- package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
- package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
- package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
- package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
- package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +37 -9
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/column-wrap-test.json +265 -0
- package/src/_stories/_mock/equal-number.json +1109 -0
- package/src/_stories/_mock/multi-country-hide.json +78 -0
- package/src/_stories/_mock/multi-country.json +95 -0
- package/src/_stories/_mock/multi-state.json +887 -20403
- package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
- package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
- package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
- package/src/_stories/_mock/us-bubble-cities.json +306 -0
- package/src/_stories/_mock/usa-state-gradient.json +2 -4
- package/src/components/BubbleList.tsx +17 -13
- package/src/components/CityList.tsx +85 -107
- package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/Geo.tsx +22 -3
- package/src/components/Legend/components/Legend.tsx +76 -40
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/Legend/components/index.scss +1 -1
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/NavigationMenu.tsx +27 -15
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
- package/src/components/SmallMultiples/index.tsx +3 -0
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
- package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
- package/src/components/UsaMap/helpers/map.ts +4 -4
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +193 -35
- package/src/components/ZoomControls.tsx +6 -9
- package/src/context/LegendMemoContext.tsx +30 -0
- package/src/context.ts +1 -40
- package/src/data/initial-state.js +153 -130
- package/src/data/supported-geos.js +25 -78
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +140 -20
- package/src/helpers/applyLegendToRow.ts +10 -6
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -14
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +18 -3
- package/src/helpers/generateRuntimeLegend.ts +44 -10
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +33 -0
- package/src/helpers/getStatesPicked.ts +8 -5
- package/src/helpers/index.ts +3 -3
- package/src/helpers/isLegendItemDisabled.ts +16 -0
- package/src/helpers/mapObserverHelpers.ts +40 -0
- package/src/helpers/resetLegendToggles.ts +3 -2
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- package/src/helpers/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +36 -2
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +5 -2
- package/src/hooks/useStateZoom.tsx +30 -8
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +1 -2
- package/src/scss/editor-panel.scss +4 -440
- package/src/scss/main.scss +1 -1
- package/src/scss/map.scss +12 -15
- package/src/store/map.actions.ts +7 -7
- package/src/store/map.reducer.ts +17 -6
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +46 -18
- package/src/types/MapContext.ts +6 -7
- package/src/types/runtimeLegend.ts +17 -1
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- package/src/components/DataTable.tsx +0 -385
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/coreStyles_map.scss +0 -3
- package/src/helpers/colorDistributions.ts +0 -12
- package/src/helpers/generateColorsArray.ts +0 -14
- package/src/helpers/tests/generateColorsArray.test.ts +0 -18
- package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
- package/src/hooks/useActiveElement.ts +0 -19
- package/src/scss/mixins.scss +0 -47
- package/src/types/Annotations.ts +0 -24
- /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import React, { useContext } from 'react'
|
|
1
|
+
import React, { useContext, useState } from 'react'
|
|
2
2
|
import ConfigContext from '../../../../context'
|
|
3
|
+
import { useLegendMemoContext } from '../../../../context/LegendMemoContext'
|
|
3
4
|
import { MapContext } from '../../../../types/MapContext'
|
|
4
5
|
import { getGeoFillColor, displayGeoName } from '../../../../helpers'
|
|
5
6
|
import useApplyTooltipsToGeo from '../../../../hooks/useApplyTooltipsToGeo'
|
|
6
7
|
import { applyLegendToRow } from '../../../../helpers/applyLegendToRow'
|
|
7
8
|
import useGeoClickHandler, { geoClickHandler } from '././../../../../hooks/useGeoClickHandler'
|
|
9
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
10
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
11
|
+
import { useSynchronizedGeographies } from '../../../../hooks/useSynchronizedGeographies'
|
|
8
12
|
|
|
9
13
|
interface CountyOutputProps {
|
|
10
14
|
counties: any[]
|
|
@@ -15,14 +19,16 @@ interface CountyOutputProps {
|
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoStrokeColor, tooltipId }) => {
|
|
18
|
-
const { config,
|
|
22
|
+
const { config, runtimeData, runtimeLegend, interactionLabel } = useContext<MapContext>(ConfigContext)
|
|
23
|
+
const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
|
|
19
24
|
const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
|
|
20
25
|
const geoFillColor = getGeoFillColor(config)
|
|
21
26
|
const { geoClickHandler } = useGeoClickHandler()
|
|
27
|
+
const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
|
|
22
28
|
|
|
23
29
|
return (
|
|
24
30
|
<>
|
|
25
|
-
{counties.map(county => {
|
|
31
|
+
{counties.map((county, countyIndex) => {
|
|
26
32
|
// Map the name from the geo data with the appropriate key for the processed data
|
|
27
33
|
const geoKey = county.id
|
|
28
34
|
|
|
@@ -30,7 +36,7 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
30
36
|
|
|
31
37
|
const countyPath = path(county)
|
|
32
38
|
|
|
33
|
-
const geoData =
|
|
39
|
+
const geoData = runtimeData[county.id]
|
|
34
40
|
let legendColors
|
|
35
41
|
|
|
36
42
|
// Once we receive data for this geographic item, setup variables.
|
|
@@ -67,6 +73,7 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
67
73
|
|
|
68
74
|
return (
|
|
69
75
|
<g
|
|
76
|
+
{...getSyncProps(geoKey)}
|
|
70
77
|
key={`key--${county.id}`}
|
|
71
78
|
className={`county county--${geoDisplayName.split(' ').join('')} county--${
|
|
72
79
|
geoData[config.columns.geo.name]
|
|
@@ -75,6 +82,24 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
75
82
|
onClick={() => geoClickHandler(geoDisplayName, geoData)}
|
|
76
83
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
77
84
|
data-tooltip-html={toolTip}
|
|
85
|
+
onMouseEnter={e => {
|
|
86
|
+
// Track hover analytics event if this is a new location
|
|
87
|
+
const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
|
|
88
|
+
publishAnalyticsEvent({
|
|
89
|
+
vizType: config.type,
|
|
90
|
+
vizSubType: getVizSubType(config),
|
|
91
|
+
eventType: `map_hover`,
|
|
92
|
+
eventAction: 'hover',
|
|
93
|
+
eventLabel: interactionLabel,
|
|
94
|
+
vizTitle: getVizTitle(config),
|
|
95
|
+
location: geoDisplayName,
|
|
96
|
+
specifics: `location: ${locationName?.toLowerCase()}`
|
|
97
|
+
})
|
|
98
|
+
syncHandlers.onMouseEnter(geoKey, e.clientY)
|
|
99
|
+
}}
|
|
100
|
+
onMouseLeave={() => {
|
|
101
|
+
syncHandlers.onMouseLeave()
|
|
102
|
+
}}
|
|
78
103
|
>
|
|
79
104
|
<path
|
|
80
105
|
tabIndex={-1}
|
|
@@ -88,11 +113,18 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
88
113
|
} else {
|
|
89
114
|
return (
|
|
90
115
|
<g
|
|
116
|
+
{...getSyncProps(geoKey)}
|
|
91
117
|
key={`key--${county.id}`}
|
|
92
118
|
className={`county county--${geoDisplayName.split(' ').join('')}`}
|
|
93
119
|
style={{ fill: geoFillColor }}
|
|
94
120
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
95
121
|
data-tooltip-html={toolTip}
|
|
122
|
+
onMouseEnter={e => {
|
|
123
|
+
syncHandlers.onMouseEnter(geoKey, e.clientY)
|
|
124
|
+
}}
|
|
125
|
+
onMouseLeave={() => {
|
|
126
|
+
syncHandlers.onMouseLeave()
|
|
127
|
+
}}
|
|
96
128
|
>
|
|
97
129
|
<path
|
|
98
130
|
tabIndex={-1}
|
|
@@ -12,7 +12,7 @@ type TerritoriesSectionProps = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, logo, config, territoriesData }) => {
|
|
15
|
-
const { currentViewport } = useContext<MapContext>(ConfigContext)
|
|
15
|
+
const { currentViewport, vizViewport } = useContext<MapContext>(ConfigContext)
|
|
16
16
|
|
|
17
17
|
// filter territioriesData into the two groups below
|
|
18
18
|
const freelyAssociatedKeys = territoriesData.filter(territory => {
|
|
@@ -38,7 +38,7 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
|
|
|
38
38
|
return a.props.label.localeCompare(b.props.label)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
const isMobileViewport = isMobileTerritoryViewport(
|
|
41
|
+
const isMobileViewport = isMobileTerritoryViewport(vizViewport)
|
|
42
42
|
const SVG_GAP = 9
|
|
43
43
|
const SVG_WIDTH = isMobileViewport ? 30 : 45
|
|
44
44
|
|
|
@@ -55,10 +55,18 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
|
|
|
55
55
|
<div className='d-flex flex-wrap' style={{ columnGap: '1.5rem' }}>
|
|
56
56
|
{(usTerritories.length > 0 || config.general.territoriesAlwaysShow) && (
|
|
57
57
|
<div>
|
|
58
|
-
<span className='territories-label'
|
|
58
|
+
<span className='territories-label' style={{ fontSize: isMobileViewport ? '0.8rem' : '1rem' }}>
|
|
59
|
+
U.S. territories
|
|
60
|
+
</span>
|
|
59
61
|
<span
|
|
60
|
-
className={
|
|
61
|
-
style={
|
|
62
|
+
className={`${isMobileViewport ? 'mt-1 mb-3' : 'mt-2 mb-4'} d-flex territories`}
|
|
63
|
+
style={
|
|
64
|
+
{
|
|
65
|
+
minWidth: `${usTerritories.length * SVG_WIDTH + (usTerritories.length - 1) * SVG_GAP}px`,
|
|
66
|
+
'--territory-svg-max-width': `${SVG_WIDTH}px`,
|
|
67
|
+
'--territory-svg-min-width': `${SVG_WIDTH}px`
|
|
68
|
+
} as React.CSSProperties
|
|
69
|
+
}
|
|
62
70
|
>
|
|
63
71
|
{usTerritories}
|
|
64
72
|
</span>
|
|
@@ -66,14 +74,20 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
|
|
|
66
74
|
)}
|
|
67
75
|
{(freelyAssociatedStates.length > 0 || config.general.territoriesAlwaysShow) && (
|
|
68
76
|
<div>
|
|
69
|
-
<span className='territories-label'
|
|
77
|
+
<span className='territories-label' style={{ fontSize: isMobileViewport ? '0.8rem' : '1rem' }}>
|
|
78
|
+
Freely associated states
|
|
79
|
+
</span>
|
|
70
80
|
<span
|
|
71
|
-
className={
|
|
72
|
-
style={
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
className={`${isMobileViewport ? 'mt-1 mb-3' : 'mt-2 mb-4'} d-flex territories`}
|
|
82
|
+
style={
|
|
83
|
+
{
|
|
84
|
+
minWidth: `${
|
|
85
|
+
freelyAssociatedStates.length * SVG_WIDTH + (freelyAssociatedStates.length - 1) * SVG_GAP
|
|
86
|
+
}px`,
|
|
87
|
+
'--territory-svg-max-width': `${SVG_WIDTH}px`,
|
|
88
|
+
'--territory-svg-min-width': `${SVG_WIDTH}px`
|
|
89
|
+
} as React.CSSProperties
|
|
90
|
+
}
|
|
77
91
|
>
|
|
78
92
|
{freelyAssociatedStates}
|
|
79
93
|
</span>
|
|
@@ -43,12 +43,17 @@ const TerritoryHexagon = ({
|
|
|
43
43
|
territory,
|
|
44
44
|
territoryData,
|
|
45
45
|
textColor,
|
|
46
|
+
getSyncProps,
|
|
47
|
+
syncHandlers,
|
|
46
48
|
...props
|
|
47
49
|
}) => {
|
|
48
50
|
const { config } = useContext<MapContext>(ConfigContext)
|
|
49
51
|
|
|
50
52
|
const isHex = config.general.displayAsHex
|
|
51
53
|
|
|
54
|
+
// Construct geography key: use territory prop if available, otherwise construct from label
|
|
55
|
+
const geoKey = territory || `US-${label}`
|
|
56
|
+
|
|
52
57
|
// Labels
|
|
53
58
|
const hexagonLabel = (geo, bgColor = '#FFFFFF', projection) => {
|
|
54
59
|
let centroid = projection ? projection(geoCentroid(geo)) : [22, 17.5]
|
|
@@ -133,7 +138,14 @@ const TerritoryHexagon = ({
|
|
|
133
138
|
fontSize={14}
|
|
134
139
|
x={'50%'}
|
|
135
140
|
y={y}
|
|
136
|
-
style={{
|
|
141
|
+
style={{
|
|
142
|
+
fill: 'currentColor',
|
|
143
|
+
stroke: strokeColor,
|
|
144
|
+
fontWeight: 900,
|
|
145
|
+
opacity: 1,
|
|
146
|
+
fillOpacity: 1,
|
|
147
|
+
pointerEvents: 'none'
|
|
148
|
+
}}
|
|
137
149
|
paintOrder='stroke'
|
|
138
150
|
textAnchor='middle'
|
|
139
151
|
verticalAnchor='middle'
|
|
@@ -143,7 +155,9 @@ const TerritoryHexagon = ({
|
|
|
143
155
|
>
|
|
144
156
|
{abbr.substring(3)}
|
|
145
157
|
</Text>
|
|
146
|
-
{config.general.displayAsHex &&
|
|
158
|
+
{config.general.displayAsHex &&
|
|
159
|
+
config.hexMap.type === 'shapes' &&
|
|
160
|
+
getArrowDirection(territoryData, geo, true)}
|
|
147
161
|
</>
|
|
148
162
|
)
|
|
149
163
|
}
|
|
@@ -151,7 +165,7 @@ const TerritoryHexagon = ({
|
|
|
151
165
|
let [dx, dy] = offsets[abbr]
|
|
152
166
|
|
|
153
167
|
return (
|
|
154
|
-
<g>
|
|
168
|
+
<g style={{ pointerEvents: 'none' }}>
|
|
155
169
|
<line
|
|
156
170
|
x1={centroid[0]}
|
|
157
171
|
y1={centroid[1]}
|
|
@@ -179,11 +193,23 @@ const TerritoryHexagon = ({
|
|
|
179
193
|
|
|
180
194
|
return (
|
|
181
195
|
<svg viewBox='-1 -1 46 53' className='territory-wrapper--hex'>
|
|
182
|
-
<g
|
|
196
|
+
<g
|
|
197
|
+
{...(getSyncProps ? getSyncProps(geoKey) : {})}
|
|
198
|
+
{...props}
|
|
199
|
+
data-tooltip-html={dataTooltipHtml}
|
|
200
|
+
data-tooltip-id={dataTooltipId}
|
|
201
|
+
onClick={handleShapeClick}
|
|
202
|
+
>
|
|
183
203
|
<polygon
|
|
184
204
|
stroke={stroke}
|
|
185
205
|
strokeWidth={strokeWidth}
|
|
186
206
|
points='22 0 44 12.702 44 38.105 22 50.807 0 38.105 0 12.702'
|
|
207
|
+
onMouseEnter={e => {
|
|
208
|
+
syncHandlers?.onMouseEnter(geoKey, e.clientY)
|
|
209
|
+
}}
|
|
210
|
+
onMouseLeave={() => {
|
|
211
|
+
syncHandlers?.onMouseLeave()
|
|
212
|
+
}}
|
|
187
213
|
/>
|
|
188
214
|
{config.general.displayAsHex && hexagonLabel(territoryData, stroke, false)}
|
|
189
215
|
</g>
|
|
@@ -18,6 +18,9 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
18
18
|
territory,
|
|
19
19
|
textColor,
|
|
20
20
|
backgroundColor,
|
|
21
|
+
svgStyle,
|
|
22
|
+
getSyncProps,
|
|
23
|
+
syncHandlers,
|
|
21
24
|
...props
|
|
22
25
|
}) => {
|
|
23
26
|
const { config } = useContext<MapContext>(ConfigContext)
|
|
@@ -25,17 +28,32 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
25
28
|
const rectanglePath =
|
|
26
29
|
'M42,0.5 C42.8284271,0.5 43.5,1.17157288 43.5,2 L43.5,2 L43.5,26 C43.5,26.8284271 42.8284271,27.5 42,27.5 L42,27.5 L3,27.5 C2.17157288,27.5 1.5,26.8284271 1.5,26 L1.5,26 L1.5,2 C1.5,1.17157288 2.17157288,0.5 3,0.5 L3,0.5 Z'
|
|
27
30
|
|
|
31
|
+
const geoKey = territory || `US-${label}`
|
|
32
|
+
|
|
28
33
|
return (
|
|
29
|
-
<svg viewBox='0 0 45 29' key={
|
|
34
|
+
<svg viewBox='0 0 45 29' key={geoKey} className={geoKey} style={svgStyle}>
|
|
30
35
|
<g
|
|
36
|
+
{...(getSyncProps ? getSyncProps(geoKey) : {})}
|
|
31
37
|
{...otherProps}
|
|
32
38
|
strokeLinejoin='round'
|
|
33
39
|
tabIndex={-1}
|
|
34
40
|
onClick={handleShapeClick}
|
|
35
41
|
data-tooltip-id={dataTooltipId}
|
|
36
42
|
data-tooltip-html={dataTooltipHtml}
|
|
43
|
+
onMouseEnter={e => {
|
|
44
|
+
syncHandlers?.onMouseEnter(geoKey, e.clientY)
|
|
45
|
+
}}
|
|
46
|
+
onMouseLeave={() => {
|
|
47
|
+
syncHandlers?.onMouseLeave()
|
|
48
|
+
}}
|
|
37
49
|
>
|
|
38
|
-
<path
|
|
50
|
+
<path
|
|
51
|
+
stroke={stroke}
|
|
52
|
+
strokeWidth={strokeWidth}
|
|
53
|
+
d={rectanglePath}
|
|
54
|
+
style={{ pointerEvents: 'none' }}
|
|
55
|
+
{...otherProps}
|
|
56
|
+
/>
|
|
39
57
|
<text
|
|
40
58
|
textAnchor='middle'
|
|
41
59
|
dominantBaseline='middle'
|
|
@@ -45,7 +63,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
45
63
|
stroke={strokeColor}
|
|
46
64
|
className='territory-text'
|
|
47
65
|
paintOrder='stroke'
|
|
48
|
-
|
|
66
|
+
style={{ pointerEvents: 'none' }}
|
|
49
67
|
data-tooltip-id={dataTooltipId}
|
|
50
68
|
data-tooltip-html={dataTooltipHtml}
|
|
51
69
|
>
|
|
@@ -97,6 +115,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
97
115
|
strokeWidth={strokeWidth}
|
|
98
116
|
d={rectanglePath}
|
|
99
117
|
fill={`url(#territory-${territory}-${patternData?.dataKey}--${patternIndex})`}
|
|
118
|
+
style={{ pointerEvents: 'none' }}
|
|
100
119
|
className={[
|
|
101
120
|
`territory-pattern-${patternData?.dataKey}`,
|
|
102
121
|
`territory-pattern-${patternData?.dataKey}--${patternData.dataValue}`
|
|
@@ -111,7 +130,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
111
130
|
stroke={strokeColor}
|
|
112
131
|
className='territory-text'
|
|
113
132
|
paintOrder='stroke'
|
|
114
|
-
|
|
133
|
+
style={{ pointerEvents: 'none' }}
|
|
115
134
|
data-tooltip-id={dataTooltipId}
|
|
116
135
|
data-tooltip-html={dataTooltipHtml}
|
|
117
136
|
>
|
|
@@ -11,4 +11,10 @@ export type TerritoryShape = {
|
|
|
11
11
|
territory: string
|
|
12
12
|
territoryData: object
|
|
13
13
|
textColor: string
|
|
14
|
+
svgStyle?: React.CSSProperties
|
|
15
|
+
getSyncProps?: (geoKey: string) => any
|
|
16
|
+
syncHandlers?: {
|
|
17
|
+
onMouseEnter: (geoKey: string, clientY: number) => void
|
|
18
|
+
onMouseLeave: () => void
|
|
19
|
+
}
|
|
14
20
|
}
|
|
@@ -7,14 +7,17 @@ import Loading from '@cdc/core/components/Loading'
|
|
|
7
7
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
8
8
|
import useMapLayers from '../../../hooks/useMapLayers'
|
|
9
9
|
import ConfigContext from '../../../context'
|
|
10
|
+
import { useLegendMemoContext } from '../../../context/LegendMemoContext'
|
|
10
11
|
import { drawShape, createShapeProperties } from '../helpers/shapes'
|
|
11
|
-
import { getGeoStrokeColor, handleMapAriaLabels, displayGeoName } from '../../../helpers'
|
|
12
|
+
import { getGeoStrokeColor, handleMapAriaLabels, displayGeoName, isLegendItemDisabled } from '../../../helpers'
|
|
13
|
+
import { supportedStatesFipsCodes } from '../../../data/supported-geos'
|
|
12
14
|
import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
|
|
13
15
|
import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
|
|
14
16
|
import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo'
|
|
15
17
|
import { MapConfig } from '../../../types/MapConfig'
|
|
16
18
|
import { DEFAULT_MAP_BACKGROUND } from '../../../helpers/constants'
|
|
17
19
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
20
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
18
21
|
|
|
19
22
|
const getCountyTopoURL = year => {
|
|
20
23
|
return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
|
|
@@ -131,18 +134,18 @@ const CountyMap = () => {
|
|
|
131
134
|
const {
|
|
132
135
|
container,
|
|
133
136
|
containerEl,
|
|
134
|
-
|
|
137
|
+
runtimeData,
|
|
135
138
|
runtimeFilters,
|
|
136
139
|
runtimeLegend,
|
|
137
140
|
setConfig,
|
|
138
141
|
config,
|
|
139
142
|
tooltipId,
|
|
140
143
|
tooltipRef,
|
|
141
|
-
|
|
142
|
-
legendSpecialClassLastMemo,
|
|
143
|
-
configUrl
|
|
144
|
+
interactionLabel
|
|
144
145
|
} = useContext(ConfigContext)
|
|
145
146
|
|
|
147
|
+
const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
|
|
148
|
+
|
|
146
149
|
// CREATE STATE LINES
|
|
147
150
|
const geoStrokeColor = getGeoStrokeColor(config)
|
|
148
151
|
const { geoClickHandler } = useGeoClickHandler()
|
|
@@ -200,7 +203,7 @@ const CountyMap = () => {
|
|
|
200
203
|
const canvasRef = useRef()
|
|
201
204
|
|
|
202
205
|
// If runtimeData is not defined, show loader
|
|
203
|
-
if (!
|
|
206
|
+
if (!runtimeData || !isTopoReady(topoData, config, runtimeFilters)) {
|
|
204
207
|
return (
|
|
205
208
|
<div style={{ height: 300 }}>
|
|
206
209
|
<Loading />
|
|
@@ -208,11 +211,18 @@ const CountyMap = () => {
|
|
|
208
211
|
)
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
const runtimeKeys = Object.keys(
|
|
214
|
+
const runtimeKeys = Object.keys(runtimeData)
|
|
212
215
|
const lineWidth = 1
|
|
213
216
|
|
|
214
217
|
const onReset = () => {
|
|
215
|
-
publishAnalyticsEvent(
|
|
218
|
+
publishAnalyticsEvent({
|
|
219
|
+
vizType: config.type,
|
|
220
|
+
vizSubType: getVizSubType(config),
|
|
221
|
+
eventType: 'map_reset_zoom_level',
|
|
222
|
+
eventAction: 'click',
|
|
223
|
+
eventLabel: interactionLabel,
|
|
224
|
+
vizTitle: getVizTitle(config)
|
|
225
|
+
})
|
|
216
226
|
setConfig({
|
|
217
227
|
...config,
|
|
218
228
|
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
@@ -256,8 +266,8 @@ const CountyMap = () => {
|
|
|
256
266
|
break
|
|
257
267
|
}
|
|
258
268
|
}
|
|
259
|
-
if (county &&
|
|
260
|
-
geoClickHandler(displayGeoName(county.id),
|
|
269
|
+
if (county && runtimeData[county.id]) {
|
|
270
|
+
geoClickHandler(displayGeoName(county.id), runtimeData[county.id])
|
|
261
271
|
}
|
|
262
272
|
}
|
|
263
273
|
|
|
@@ -271,19 +281,36 @@ const CountyMap = () => {
|
|
|
271
281
|
|
|
272
282
|
// Redraw with focus on state
|
|
273
283
|
setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState), feature: clickedState })
|
|
274
|
-
publishAnalyticsEvent(
|
|
275
|
-
|
|
284
|
+
publishAnalyticsEvent({
|
|
285
|
+
vizType: config.type,
|
|
286
|
+
vizSubType: getVizSubType(config),
|
|
287
|
+
eventType: `zoom_in`,
|
|
288
|
+
eventAction: 'click',
|
|
289
|
+
eventLabel: interactionLabel,
|
|
290
|
+
vizTitle: getVizTitle(config),
|
|
291
|
+
specifics: `zoom_level: 3, location: ${clickedState.properties.name}`
|
|
292
|
+
})
|
|
276
293
|
}
|
|
277
294
|
if (config.general.type === 'us-geocode') {
|
|
278
295
|
const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
279
296
|
let clickedGeo
|
|
280
297
|
for (let i = 0; i < runtimeKeys.length; i++) {
|
|
281
298
|
const pixelCoords = topoData.projection([
|
|
282
|
-
|
|
283
|
-
|
|
299
|
+
runtimeData[runtimeKeys[i]][config.columns.longitude.name],
|
|
300
|
+
runtimeData[runtimeKeys[i]][config.columns.latitude.name]
|
|
284
301
|
])
|
|
285
|
-
if (
|
|
286
|
-
|
|
302
|
+
if (
|
|
303
|
+
pixelCoords &&
|
|
304
|
+
Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius &&
|
|
305
|
+
!isLegendItemDisabled(
|
|
306
|
+
runtimeData[runtimeKeys[i]],
|
|
307
|
+
runtimeLegend,
|
|
308
|
+
legendMemo,
|
|
309
|
+
legendSpecialClassLastMemo,
|
|
310
|
+
config
|
|
311
|
+
)
|
|
312
|
+
) {
|
|
313
|
+
clickedGeo = runtimeData[runtimeKeys[i]]
|
|
287
314
|
break
|
|
288
315
|
}
|
|
289
316
|
}
|
|
@@ -324,7 +351,7 @@ const CountyMap = () => {
|
|
|
324
351
|
if (
|
|
325
352
|
!isNaN(currentTooltipIndex) &&
|
|
326
353
|
applyLegendToRow(
|
|
327
|
-
|
|
354
|
+
runtimeData[topoData.mapData[currentTooltipIndex].id],
|
|
328
355
|
config,
|
|
329
356
|
runtimeLegend,
|
|
330
357
|
legendMemo,
|
|
@@ -332,7 +359,7 @@ const CountyMap = () => {
|
|
|
332
359
|
)
|
|
333
360
|
) {
|
|
334
361
|
context.fillStyle = applyLegendToRow(
|
|
335
|
-
|
|
362
|
+
runtimeData[topoData.mapData[currentTooltipIndex].id],
|
|
336
363
|
config,
|
|
337
364
|
runtimeLegend,
|
|
338
365
|
legendMemo,
|
|
@@ -369,10 +396,10 @@ const CountyMap = () => {
|
|
|
369
396
|
}
|
|
370
397
|
|
|
371
398
|
// If the hovered county is found, show the tooltip for that county, otherwise hide the tooltip
|
|
372
|
-
if (county &&
|
|
373
|
-
if (applyLegendToRow(
|
|
399
|
+
if (county && runtimeData[county.id]) {
|
|
400
|
+
if (applyLegendToRow(runtimeData[county.id], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)) {
|
|
374
401
|
let fillColor = applyLegendToRow(
|
|
375
|
-
|
|
402
|
+
runtimeData[county.id],
|
|
376
403
|
config,
|
|
377
404
|
runtimeLegend,
|
|
378
405
|
legendMemo,
|
|
@@ -389,6 +416,24 @@ const CountyMap = () => {
|
|
|
389
416
|
context.stroke()
|
|
390
417
|
}
|
|
391
418
|
|
|
419
|
+
// Track hover analytics event if this is a new location
|
|
420
|
+
if (isNaN(currentTooltipIndex) || currentTooltipIndex !== countyIndex) {
|
|
421
|
+
const countyName = displayGeoName(county.id).replace(/[^a-zA-Z0-9]/g, ' ')
|
|
422
|
+
const stateFips = county.id.slice(0, 2)
|
|
423
|
+
const stateName = supportedStatesFipsCodes[stateFips]?.replace(/[^a-zA-Z0-9]/g, '_') || 'unknown'
|
|
424
|
+
const locationName = `${countyName}, ${stateName}`
|
|
425
|
+
publishAnalyticsEvent({
|
|
426
|
+
vizType: config.type,
|
|
427
|
+
vizSubType: getVizSubType(config),
|
|
428
|
+
eventType: `map_hover`,
|
|
429
|
+
eventAction: 'hover',
|
|
430
|
+
eventLabel: interactionLabel,
|
|
431
|
+
vizTitle: getVizTitle(config),
|
|
432
|
+
location: displayGeoName(county.id),
|
|
433
|
+
specifics: `location: ${locationName?.toLowerCase()}`
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
|
|
392
437
|
tooltipRef.current.style.display = 'block'
|
|
393
438
|
tooltipRef.current.style.top = tooltipY + 'px'
|
|
394
439
|
if (tooltipX > containerBounds.width / 2) {
|
|
@@ -398,7 +443,7 @@ const CountyMap = () => {
|
|
|
398
443
|
tooltipRef.current.style.transform = 'translate(0, -50%)'
|
|
399
444
|
tooltipRef.current.style.left = tooltipX + 5 + 'px'
|
|
400
445
|
}
|
|
401
|
-
tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id),
|
|
446
|
+
tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), runtimeData[county.id])
|
|
402
447
|
tooltipRef.current.setAttribute('data-index', countyIndex)
|
|
403
448
|
} else {
|
|
404
449
|
tooltipRef.current.style.display = 'none'
|
|
@@ -409,8 +454,8 @@ const CountyMap = () => {
|
|
|
409
454
|
// Handle geo map hover
|
|
410
455
|
if (!isNaN(currentTooltipIndex)) {
|
|
411
456
|
const pixelCoords = topoData.projection([
|
|
412
|
-
|
|
413
|
-
|
|
457
|
+
runtimeData[runtimeKeys[currentTooltipIndex]][config.columns.longitude.name],
|
|
458
|
+
runtimeData[runtimeKeys[currentTooltipIndex]][config.columns.latitude.name]
|
|
414
459
|
])
|
|
415
460
|
if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
|
|
416
461
|
return // The user is still hovering over the previous geo point, don't redraw tooltip
|
|
@@ -424,17 +469,30 @@ const CountyMap = () => {
|
|
|
424
469
|
let hoveredGeoIndex
|
|
425
470
|
for (let i = 0; i < runtimeKeys.length; i++) {
|
|
426
471
|
const pixelCoords = topoData.projection([
|
|
427
|
-
|
|
428
|
-
|
|
472
|
+
runtimeData[runtimeKeys[i]][config.columns.longitude.name],
|
|
473
|
+
runtimeData[runtimeKeys[i]][config.columns.latitude.name]
|
|
429
474
|
])
|
|
430
475
|
let includedShapes = ['circle', 'diamond', 'star', 'triangle', 'square'].includes(config.visual.cityStyle)
|
|
431
476
|
if (
|
|
432
477
|
includedShapes &&
|
|
433
478
|
pixelCoords &&
|
|
434
479
|
Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius &&
|
|
435
|
-
applyLegendToRow(
|
|
480
|
+
applyLegendToRow(
|
|
481
|
+
runtimeData[runtimeKeys[i]],
|
|
482
|
+
config,
|
|
483
|
+
runtimeLegend,
|
|
484
|
+
legendMemo,
|
|
485
|
+
legendSpecialClassLastMemo
|
|
486
|
+
) &&
|
|
487
|
+
!isLegendItemDisabled(
|
|
488
|
+
runtimeData[runtimeKeys[i]],
|
|
489
|
+
runtimeLegend,
|
|
490
|
+
legendMemo,
|
|
491
|
+
legendSpecialClassLastMemo,
|
|
492
|
+
config
|
|
493
|
+
)
|
|
436
494
|
) {
|
|
437
|
-
hoveredGeo =
|
|
495
|
+
hoveredGeo = runtimeData[runtimeKeys[i]]
|
|
438
496
|
hoveredGeoIndex = i
|
|
439
497
|
break
|
|
440
498
|
}
|
|
@@ -443,9 +501,22 @@ const CountyMap = () => {
|
|
|
443
501
|
const distance = Math.hypot(pixelCoords[0] - x, pixelCoords[1] - y)
|
|
444
502
|
if (
|
|
445
503
|
distance < 15 &&
|
|
446
|
-
applyLegendToRow(
|
|
504
|
+
applyLegendToRow(
|
|
505
|
+
runtimeData[runtimeKeys[i]],
|
|
506
|
+
config,
|
|
507
|
+
runtimeLegend,
|
|
508
|
+
legendMemo,
|
|
509
|
+
legendSpecialClassLastMemo
|
|
510
|
+
) &&
|
|
511
|
+
!isLegendItemDisabled(
|
|
512
|
+
runtimeData[runtimeKeys[i]],
|
|
513
|
+
runtimeLegend,
|
|
514
|
+
legendMemo,
|
|
515
|
+
legendSpecialClassLastMemo,
|
|
516
|
+
config
|
|
517
|
+
)
|
|
447
518
|
) {
|
|
448
|
-
hoveredGeo =
|
|
519
|
+
hoveredGeo = runtimeData[runtimeKeys[i]]
|
|
449
520
|
hoveredGeoIndex = i
|
|
450
521
|
break
|
|
451
522
|
}
|
|
@@ -453,6 +524,21 @@ const CountyMap = () => {
|
|
|
453
524
|
}
|
|
454
525
|
|
|
455
526
|
if (hoveredGeo) {
|
|
527
|
+
// Track hover analytics event if this is a new location
|
|
528
|
+
if (isNaN(currentTooltipIndex) || currentTooltipIndex !== hoveredGeoIndex) {
|
|
529
|
+
const locationName = displayGeoName(hoveredGeo[config.columns.geo.name]).replace(/[^a-zA-Z0-9]/g, '_')
|
|
530
|
+
publishAnalyticsEvent({
|
|
531
|
+
vizType: config.type,
|
|
532
|
+
vizSubType: getVizSubType(config),
|
|
533
|
+
eventType: `map_hover`,
|
|
534
|
+
eventAction: 'hover',
|
|
535
|
+
eventLabel: interactionLabel,
|
|
536
|
+
vizTitle: getVizTitle(config),
|
|
537
|
+
location: displayGeoName(hoveredGeo[config.columns.geo.name]),
|
|
538
|
+
specifics: `location: ${locationName?.toLowerCase()}`
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
456
542
|
tooltipRef.current.style.display = 'block'
|
|
457
543
|
tooltipRef.current.style.top = tooltipY + 'px'
|
|
458
544
|
if (tooltipX > containerBounds.width / 2) {
|
|
@@ -529,7 +615,7 @@ const CountyMap = () => {
|
|
|
529
615
|
if (!focus.id && config.general.type === 'us-geocode' && geo.id.length > 2) return
|
|
530
616
|
|
|
531
617
|
// Gets numeric data associated with the topo data for this state/county
|
|
532
|
-
const geoData =
|
|
618
|
+
const geoData = runtimeData[geo.id]
|
|
533
619
|
|
|
534
620
|
// Renders state/county
|
|
535
621
|
const legendValues =
|
|
@@ -574,7 +660,7 @@ const CountyMap = () => {
|
|
|
574
660
|
context.strokeStyle = geoStrokeColor
|
|
575
661
|
const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
576
662
|
const { additionalCityStyles } = config.visual || []
|
|
577
|
-
const cityStyles = Object.values(
|
|
663
|
+
const cityStyles = Object.values(runtimeData)
|
|
578
664
|
.filter(d => additionalCityStyles.some(style => String(d[style.column]) === String(style.value)))
|
|
579
665
|
.map(d => {
|
|
580
666
|
const conditionsMatched = additionalCityStyles.find(
|
|
@@ -592,7 +678,7 @@ const CountyMap = () => {
|
|
|
592
678
|
|
|
593
679
|
if (cityPixelCoords) {
|
|
594
680
|
const legendValues = applyLegendToRow(
|
|
595
|
-
|
|
681
|
+
runtimeData[city?.value],
|
|
596
682
|
config,
|
|
597
683
|
runtimeLegend,
|
|
598
684
|
legendMemo,
|
|
@@ -613,13 +699,13 @@ const CountyMap = () => {
|
|
|
613
699
|
const citiesList = new Set(cityStyles.map(item => item.value))
|
|
614
700
|
|
|
615
701
|
const pixelCoords = topoData.projection([
|
|
616
|
-
|
|
617
|
-
|
|
702
|
+
runtimeData[key][config.columns.longitude.name],
|
|
703
|
+
runtimeData[key][config.columns.latitude.name]
|
|
618
704
|
])
|
|
619
705
|
if (pixelCoords && !citiesList.has(key)) {
|
|
620
706
|
const legendValues =
|
|
621
|
-
|
|
622
|
-
? applyLegendToRow(
|
|
707
|
+
runtimeData[key] !== undefined
|
|
708
|
+
? applyLegendToRow(runtimeData[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
|
|
623
709
|
: false
|
|
624
710
|
if (legendValues) {
|
|
625
711
|
if (legendValues?.[0] === '#000000' || legendValues?.[0] === DEFAULT_MAP_BACKGROUND) return
|