@cdc/map 4.25.10 → 4.26.1
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/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 +58397 -55987
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/city_styles_variable.json +877 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/map-filter-issue.json +2260 -0
- package/examples/private/map-legend.json +5303 -0
- package/index.html +27 -36
- package/package.json +6 -5
- package/src/CdcMapComponent.tsx +86 -26
- 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 +3426 -0
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.stories.tsx +116 -4
- package/src/_stories/_mock/column-wrap-test.json +265 -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/usa-state-gradient.json +3 -4
- package/src/components/BubbleList.tsx +1 -1
- package/src/components/CityList.tsx +24 -18
- package/src/components/EditorPanel/components/EditorPanel.tsx +2380 -2206
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -19
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +351 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/Geo.tsx +20 -3
- package/src/components/Legend/components/Legend.tsx +58 -75
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +1 -1
- package/src/components/Legend/components/index.scss +23 -6
- package/src/components/NavigationMenu.tsx +16 -13
- 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 +18 -3
- 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 +29 -9
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +7 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +16 -4
- package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +29 -12
- package/src/components/UsaMap/components/UsaMap.State.tsx +30 -5
- package/src/components/UsaMap/helpers/map.ts +2 -2
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +81 -11
- package/src/data/initial-state.js +11 -0
- package/src/data/supported-geos.js +8 -76
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +25 -1
- package/src/helpers/applyLegendToRow.ts +5 -3
- package/src/helpers/constants.ts +3 -15
- package/src/helpers/displayGeoName.ts +22 -4
- package/src/helpers/generateRuntimeFilters.ts +1 -1
- package/src/helpers/generateRuntimeLegend.ts +1 -3
- package/src/helpers/generateRuntimeLegendHash.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +2 -5
- package/src/helpers/index.ts +2 -4
- package/src/helpers/isLegendItemDisabled.ts +2 -2
- package/src/helpers/resetLegendToggles.ts +1 -0
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/hashObj.test.ts +1 -1
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- package/src/helpers/toggleLegendActive.ts +76 -8
- package/src/helpers/urlDataHelpers.ts +1 -1
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +1 -1
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +8 -2
- package/src/hooks/useStateZoom.tsx +7 -4
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/index.jsx +1 -0
- 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/test/CdcMap.test.jsx +1 -1
- package/src/types/MapConfig.ts +32 -11
- package/src/types/MapContext.ts +6 -0
- package/src/types/runtimeLegend.ts +2 -1
- package/LICENSE +0 -201
- package/src/components/DataTable.tsx +0 -413
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/components/MapControls.tsx +0 -44
- package/src/helpers/getUniqueValues.ts +0 -19
- package/src/helpers/hashObj.ts +0 -25
- package/src/hooks/useActiveElement.ts +0 -19
- package/src/hooks/useLegendSeparators.ts +0 -26
- 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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
interface SynchronizedTooltipProps {
|
|
4
|
+
tileTooltipId: string
|
|
5
|
+
opacity: number
|
|
6
|
+
containerRef: RefObject<HTMLElement>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom tooltip component for small multiples that responds to synthetic events
|
|
11
|
+
* This bypasses react-tooltip's lazy initialization issues
|
|
12
|
+
*/
|
|
13
|
+
const SynchronizedTooltip: React.FC<SynchronizedTooltipProps> = ({ tileTooltipId, opacity, containerRef }) => {
|
|
14
|
+
const [tooltipState, setTooltipState] = useState<{
|
|
15
|
+
visible: boolean
|
|
16
|
+
html: string
|
|
17
|
+
x: number
|
|
18
|
+
y: number
|
|
19
|
+
}>({
|
|
20
|
+
visible: false,
|
|
21
|
+
html: '',
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 0
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const tooltipRef = useRef<HTMLDivElement>(null)
|
|
27
|
+
const currentGeoRef = useRef<string | null>(null)
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const handleMouseEnter = (e: MouseEvent) => {
|
|
31
|
+
const target = e.target as HTMLElement
|
|
32
|
+
const tooltipId = target.getAttribute('data-tooltip-id')
|
|
33
|
+
|
|
34
|
+
if (tooltipId === `tooltip__${tileTooltipId}`) {
|
|
35
|
+
const tooltipHtml = target.getAttribute('data-tooltip-html')
|
|
36
|
+
const geoId = target.getAttribute('data-geo-id') || ''
|
|
37
|
+
|
|
38
|
+
// Don't show tooltip if there's no content
|
|
39
|
+
if (!tooltipHtml || tooltipHtml === 'undefined' || tooltipHtml.trim() === '') {
|
|
40
|
+
setTooltipState(prev => ({ ...prev, visible: false }))
|
|
41
|
+
currentGeoRef.current = null
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Only update if we're entering a different geography or showing for first time
|
|
46
|
+
if (geoId !== currentGeoRef.current) {
|
|
47
|
+
currentGeoRef.current = geoId
|
|
48
|
+
|
|
49
|
+
// Position tooltip relative to the geography element's bounding box
|
|
50
|
+
const rect = target.getBoundingClientRect()
|
|
51
|
+
const x = rect.left + rect.width / 2
|
|
52
|
+
const y = rect.top + rect.height / 2
|
|
53
|
+
|
|
54
|
+
setTooltipState({
|
|
55
|
+
visible: true,
|
|
56
|
+
html: tooltipHtml,
|
|
57
|
+
x,
|
|
58
|
+
y
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleMouseLeave = (e: MouseEvent) => {
|
|
65
|
+
const target = e.target as HTMLElement
|
|
66
|
+
const tooltipId = target.getAttribute('data-tooltip-id')
|
|
67
|
+
|
|
68
|
+
if (tooltipId === `tooltip__${tileTooltipId}`) {
|
|
69
|
+
setTooltipState(prev => ({ ...prev, visible: false }))
|
|
70
|
+
currentGeoRef.current = null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const container = containerRef.current
|
|
75
|
+
if (!container) return
|
|
76
|
+
|
|
77
|
+
container.addEventListener('mouseenter', handleMouseEnter, true)
|
|
78
|
+
container.addEventListener('mouseleave', handleMouseLeave, true)
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
container.removeEventListener('mouseenter', handleMouseEnter, true)
|
|
82
|
+
container.removeEventListener('mouseleave', handleMouseLeave, true)
|
|
83
|
+
}
|
|
84
|
+
}, [tileTooltipId, containerRef])
|
|
85
|
+
|
|
86
|
+
if (!tooltipState.visible) return null
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
ref={tooltipRef}
|
|
91
|
+
className='tooltip tooltip-test'
|
|
92
|
+
style={{
|
|
93
|
+
position: 'fixed',
|
|
94
|
+
left: `${tooltipState.x + 10}px`,
|
|
95
|
+
top: `${tooltipState.y + 10}px`,
|
|
96
|
+
background: `rgba(255,255,255, ${opacity / 100})`,
|
|
97
|
+
pointerEvents: 'none',
|
|
98
|
+
zIndex: 9999
|
|
99
|
+
}}
|
|
100
|
+
dangerouslySetInnerHTML={{ __html: tooltipState.html }}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default SynchronizedTooltip
|
|
@@ -8,6 +8,7 @@ import { applyLegendToRow } from '../../../../helpers/applyLegendToRow'
|
|
|
8
8
|
import useGeoClickHandler, { geoClickHandler } from '././../../../../hooks/useGeoClickHandler'
|
|
9
9
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
10
10
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
11
|
+
import { useSynchronizedGeographies } from '../../../../hooks/useSynchronizedGeographies'
|
|
11
12
|
|
|
12
13
|
interface CountyOutputProps {
|
|
13
14
|
counties: any[]
|
|
@@ -23,6 +24,7 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
23
24
|
const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
|
|
24
25
|
const geoFillColor = getGeoFillColor(config)
|
|
25
26
|
const { geoClickHandler } = useGeoClickHandler()
|
|
27
|
+
const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
30
|
<>
|
|
@@ -71,14 +73,16 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
71
73
|
|
|
72
74
|
return (
|
|
73
75
|
<g
|
|
76
|
+
{...getSyncProps(geoKey)}
|
|
74
77
|
key={`key--${county.id}`}
|
|
75
|
-
className={`county county--${geoDisplayName.split(' ').join('')} county--${
|
|
76
|
-
|
|
78
|
+
className={`county county--${geoDisplayName.split(' ').join('')} county--${
|
|
79
|
+
geoData[config.columns.geo.name]
|
|
80
|
+
}`}
|
|
77
81
|
style={styles}
|
|
78
82
|
onClick={() => geoClickHandler(geoDisplayName, geoData)}
|
|
79
83
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
80
84
|
data-tooltip-html={toolTip}
|
|
81
|
-
onMouseEnter={
|
|
85
|
+
onMouseEnter={e => {
|
|
82
86
|
// Track hover analytics event if this is a new location
|
|
83
87
|
const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
|
|
84
88
|
publishAnalyticsEvent({
|
|
@@ -91,6 +95,10 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
91
95
|
location: geoDisplayName,
|
|
92
96
|
specifics: `location: ${locationName?.toLowerCase()}`
|
|
93
97
|
})
|
|
98
|
+
syncHandlers.onMouseEnter(geoKey, e.clientY)
|
|
99
|
+
}}
|
|
100
|
+
onMouseLeave={() => {
|
|
101
|
+
syncHandlers.onMouseLeave()
|
|
94
102
|
}}
|
|
95
103
|
>
|
|
96
104
|
<path
|
|
@@ -105,11 +113,18 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
|
|
|
105
113
|
} else {
|
|
106
114
|
return (
|
|
107
115
|
<g
|
|
116
|
+
{...getSyncProps(geoKey)}
|
|
108
117
|
key={`key--${county.id}`}
|
|
109
118
|
className={`county county--${geoDisplayName.split(' ').join('')}`}
|
|
110
119
|
style={{ fill: geoFillColor }}
|
|
111
120
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
112
121
|
data-tooltip-html={toolTip}
|
|
122
|
+
onMouseEnter={e => {
|
|
123
|
+
syncHandlers.onMouseEnter(geoKey, e.clientY)
|
|
124
|
+
}}
|
|
125
|
+
onMouseLeave={() => {
|
|
126
|
+
syncHandlers.onMouseLeave()
|
|
127
|
+
}}
|
|
113
128
|
>
|
|
114
129
|
<path
|
|
115
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,10 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
18
18
|
territory,
|
|
19
19
|
textColor,
|
|
20
20
|
backgroundColor,
|
|
21
|
+
mapId,
|
|
22
|
+
svgStyle,
|
|
23
|
+
getSyncProps,
|
|
24
|
+
syncHandlers,
|
|
21
25
|
...props
|
|
22
26
|
}) => {
|
|
23
27
|
const { config } = useContext<MapContext>(ConfigContext)
|
|
@@ -25,17 +29,32 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
25
29
|
const rectanglePath =
|
|
26
30
|
'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
31
|
|
|
32
|
+
const geoKey = territory || `US-${label}`
|
|
33
|
+
|
|
28
34
|
return (
|
|
29
|
-
<svg viewBox='0 0 45 29' key={
|
|
35
|
+
<svg viewBox='0 0 45 29' key={geoKey} className={geoKey} style={svgStyle}>
|
|
30
36
|
<g
|
|
37
|
+
{...(getSyncProps ? getSyncProps(geoKey) : {})}
|
|
31
38
|
{...otherProps}
|
|
32
39
|
strokeLinejoin='round'
|
|
33
40
|
tabIndex={-1}
|
|
34
41
|
onClick={handleShapeClick}
|
|
35
42
|
data-tooltip-id={dataTooltipId}
|
|
36
43
|
data-tooltip-html={dataTooltipHtml}
|
|
44
|
+
onMouseEnter={e => {
|
|
45
|
+
syncHandlers?.onMouseEnter(geoKey, e.clientY)
|
|
46
|
+
}}
|
|
47
|
+
onMouseLeave={() => {
|
|
48
|
+
syncHandlers?.onMouseLeave()
|
|
49
|
+
}}
|
|
37
50
|
>
|
|
38
|
-
<path
|
|
51
|
+
<path
|
|
52
|
+
stroke={stroke}
|
|
53
|
+
strokeWidth={strokeWidth}
|
|
54
|
+
d={rectanglePath}
|
|
55
|
+
style={{ pointerEvents: 'none' }}
|
|
56
|
+
{...otherProps}
|
|
57
|
+
/>
|
|
39
58
|
<text
|
|
40
59
|
textAnchor='middle'
|
|
41
60
|
dominantBaseline='middle'
|
|
@@ -45,14 +64,14 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
45
64
|
stroke={strokeColor}
|
|
46
65
|
className='territory-text'
|
|
47
66
|
paintOrder='stroke'
|
|
48
|
-
|
|
67
|
+
style={{ pointerEvents: 'none' }}
|
|
49
68
|
data-tooltip-id={dataTooltipId}
|
|
50
69
|
data-tooltip-html={dataTooltipHtml}
|
|
51
70
|
>
|
|
52
71
|
{label}
|
|
53
72
|
</text>
|
|
54
73
|
|
|
55
|
-
{config.map
|
|
74
|
+
{config.map?.patterns?.map((patternData, patternIndex) => {
|
|
56
75
|
const patternColor = patternData.color || getContrastColor('#FFF', backgroundColor)
|
|
57
76
|
const hasMatchingValues = patternData.dataValue === territoryData?.[patternData.dataKey]
|
|
58
77
|
|
|
@@ -63,7 +82,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
63
82
|
<>
|
|
64
83
|
{patternData?.pattern === 'waves' && (
|
|
65
84
|
<PatternWaves
|
|
66
|
-
id={
|
|
85
|
+
id={`${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
|
|
67
86
|
height={patternSizes[patternData?.size] ?? 10}
|
|
68
87
|
width={patternSizes[patternData?.size] ?? 10}
|
|
69
88
|
fill={patternColor}
|
|
@@ -73,7 +92,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
73
92
|
)}
|
|
74
93
|
{patternData?.pattern === 'circles' && (
|
|
75
94
|
<PatternCircles
|
|
76
|
-
id={
|
|
95
|
+
id={`${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
|
|
77
96
|
height={patternSizes[patternData?.size] ?? 10}
|
|
78
97
|
width={patternSizes[patternData?.size] ?? 10}
|
|
79
98
|
fill={patternColor}
|
|
@@ -84,7 +103,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
84
103
|
)}
|
|
85
104
|
{patternData?.pattern === 'lines' && (
|
|
86
105
|
<PatternLines
|
|
87
|
-
id={
|
|
106
|
+
id={`${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex}`}
|
|
88
107
|
height={patternSizes[patternData?.size] ?? 6}
|
|
89
108
|
width={patternSizes[patternData?.size] ?? 6}
|
|
90
109
|
stroke={patternColor}
|
|
@@ -96,7 +115,8 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
96
115
|
stroke={stroke}
|
|
97
116
|
strokeWidth={strokeWidth}
|
|
98
117
|
d={rectanglePath}
|
|
99
|
-
fill={`url(
|
|
118
|
+
fill={`url(#${mapId}--territory-${territory}-${patternData?.dataKey}--${patternIndex})`}
|
|
119
|
+
style={{ pointerEvents: 'none' }}
|
|
100
120
|
className={[
|
|
101
121
|
`territory-pattern-${patternData?.dataKey}`,
|
|
102
122
|
`territory-pattern-${patternData?.dataKey}--${patternData.dataValue}`
|
|
@@ -111,7 +131,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
111
131
|
stroke={strokeColor}
|
|
112
132
|
className='territory-text'
|
|
113
133
|
paintOrder='stroke'
|
|
114
|
-
|
|
134
|
+
style={{ pointerEvents: 'none' }}
|
|
115
135
|
data-tooltip-id={dataTooltipId}
|
|
116
136
|
data-tooltip-html={dataTooltipHtml}
|
|
117
137
|
>
|
|
@@ -11,4 +11,11 @@ export type TerritoryShape = {
|
|
|
11
11
|
territory: string
|
|
12
12
|
territoryData: object
|
|
13
13
|
textColor: string
|
|
14
|
+
mapId?: string
|
|
15
|
+
svgStyle?: React.CSSProperties
|
|
16
|
+
getSyncProps?: (geoKey: string) => any
|
|
17
|
+
syncHandlers?: {
|
|
18
|
+
onMouseEnter: (geoKey: string, clientY: number) => void
|
|
19
|
+
onMouseLeave: () => void
|
|
20
|
+
}
|
|
14
21
|
}
|
|
@@ -15,7 +15,7 @@ import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
|
|
|
15
15
|
import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
|
|
16
16
|
import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo'
|
|
17
17
|
import { MapConfig } from '../../../types/MapConfig'
|
|
18
|
-
import { DEFAULT_MAP_BACKGROUND } from '../../../helpers/constants'
|
|
18
|
+
import { DEFAULT_MAP_BACKGROUND, DISABLED_MAP_COLOR } from '../../../helpers/constants'
|
|
19
19
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
20
20
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
21
21
|
|
|
@@ -501,8 +501,20 @@ const CountyMap = () => {
|
|
|
501
501
|
const distance = Math.hypot(pixelCoords[0] - x, pixelCoords[1] - y)
|
|
502
502
|
if (
|
|
503
503
|
distance < 15 &&
|
|
504
|
-
applyLegendToRow(
|
|
505
|
-
|
|
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
|
+
)
|
|
506
518
|
) {
|
|
507
519
|
hoveredGeo = runtimeData[runtimeKeys[i]]
|
|
508
520
|
hoveredGeoIndex = i
|
|
@@ -696,7 +708,7 @@ const CountyMap = () => {
|
|
|
696
708
|
? applyLegendToRow(runtimeData[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
|
|
697
709
|
: false
|
|
698
710
|
if (legendValues) {
|
|
699
|
-
if (legendValues?.[0] === '#000000' || legendValues?.[0] ===
|
|
711
|
+
if (legendValues?.[0] === '#000000' || legendValues?.[0] === DISABLED_MAP_COLOR) return
|
|
700
712
|
const shapeType = config.visual.cityStyle.toLowerCase()
|
|
701
713
|
const shapeProperties = createShapeProperties(shapeType, pixelCoords, legendValues, config, geoRadius)
|
|
702
714
|
if (shapeProperties) {
|
|
@@ -12,6 +12,7 @@ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
|
12
12
|
import ConfigContext from '../../../context'
|
|
13
13
|
import { useLegendMemoContext } from '../../../context/LegendMemoContext'
|
|
14
14
|
import Annotation from '../../Annotation'
|
|
15
|
+
import SmallMultiples from '../../SmallMultiples/SmallMultiples'
|
|
15
16
|
|
|
16
17
|
// Data
|
|
17
18
|
import { supportedTerritories } from '../../../data/supported-geos'
|
|
@@ -23,6 +24,7 @@ import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
|
|
|
23
24
|
import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo'
|
|
24
25
|
import './UsaMap.Region.styles.css'
|
|
25
26
|
import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
|
|
27
|
+
import { useSynchronizedGeographies } from '../../../hooks/useSynchronizedGeographies'
|
|
26
28
|
|
|
27
29
|
type TerritoryRectProps = {
|
|
28
30
|
posX?: number
|
|
@@ -59,6 +61,7 @@ const UsaRegionMap = () => {
|
|
|
59
61
|
const [focusedStates, setFocusedStates] = useState(null)
|
|
60
62
|
const { geoClickHandler } = useGeoClickHandler()
|
|
61
63
|
const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
|
|
64
|
+
const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
|
|
62
65
|
const { general } = config
|
|
63
66
|
const { displayStateLabels, territoriesLabel, displayAsHex, type } = general
|
|
64
67
|
const tooltipInteractionType = config.tooltips.appearanceType
|
|
@@ -88,6 +91,11 @@ const UsaRegionMap = () => {
|
|
|
88
91
|
return <></>
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
// Early return for small multiples rendering
|
|
95
|
+
if (config.smallMultiples?.mode) {
|
|
96
|
+
return <SmallMultiples />
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
const geoStrokeColor = getGeoStrokeColor(config)
|
|
92
100
|
const geoFillColor = getGeoFillColor(config)
|
|
93
101
|
|
|
@@ -212,6 +220,7 @@ const UsaRegionMap = () => {
|
|
|
212
220
|
|
|
213
221
|
return (
|
|
214
222
|
<g
|
|
223
|
+
{...getSyncProps(geoKey)}
|
|
215
224
|
key={key}
|
|
216
225
|
className='geo-group'
|
|
217
226
|
style={styles}
|
|
@@ -219,7 +228,7 @@ const UsaRegionMap = () => {
|
|
|
219
228
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
220
229
|
data-tooltip-html={toolTip}
|
|
221
230
|
tabIndex={-1}
|
|
222
|
-
onMouseEnter={
|
|
231
|
+
onMouseEnter={e => {
|
|
223
232
|
// Track hover analytics event if this is a new location
|
|
224
233
|
const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
|
|
225
234
|
publishAnalyticsEvent({
|
|
@@ -232,6 +241,10 @@ const UsaRegionMap = () => {
|
|
|
232
241
|
location: geoDisplayName,
|
|
233
242
|
specifics: `location: ${locationName?.toLowerCase()}`
|
|
234
243
|
})
|
|
244
|
+
syncHandlers.onMouseEnter(geoKey, e.clientY)
|
|
245
|
+
}}
|
|
246
|
+
onMouseLeave={() => {
|
|
247
|
+
syncHandlers.onMouseLeave()
|
|
235
248
|
}}
|
|
236
249
|
>
|
|
237
250
|
<path tabIndex={-1} className='single-geo' stroke={geoStrokeColor} strokeWidth={1} d={path} />
|
|
@@ -13,6 +13,7 @@ import ZoomControls from '../../ZoomControls'
|
|
|
13
13
|
import { MapContext } from '../../../types/MapContext'
|
|
14
14
|
import useStateZoom from '../../../hooks/useStateZoom'
|
|
15
15
|
import { Text } from '@visx/text'
|
|
16
|
+
import SmallMultiples from '../../SmallMultiples/SmallMultiples'
|
|
16
17
|
|
|
17
18
|
import './UsaMap.SingleState.styles.css'
|
|
18
19
|
|
|
@@ -37,11 +38,12 @@ const SingleStateMap: React.FC = () => {
|
|
|
37
38
|
position,
|
|
38
39
|
topoData,
|
|
39
40
|
scale,
|
|
40
|
-
translate
|
|
41
|
+
translate,
|
|
42
|
+
useDynamicViewbox
|
|
41
43
|
} = useContext<MapContext>(ConfigContext)
|
|
42
44
|
|
|
43
45
|
const dispatch = useContext(MapDispatchContext)
|
|
44
|
-
const { handleMoveEnd, handleZoomIn, handleZoomOut, handleZoomReset, projection } = useStateZoom(topoData)
|
|
46
|
+
const { handleMoveEnd, handleZoomIn, handleZoomOut, handleZoomReset, projection, bounds } = useStateZoom(topoData)
|
|
45
47
|
|
|
46
48
|
// Memoize statesPicked to prevent creating new arrays on every render
|
|
47
49
|
const statesPicked = useMemo(() => {
|
|
@@ -56,12 +58,22 @@ const SingleStateMap: React.FC = () => {
|
|
|
56
58
|
|
|
57
59
|
const { geoClickHandler } = useGeoClickHandler()
|
|
58
60
|
|
|
59
|
-
const cityListProjection = geoAlbersUsaTerritories()
|
|
60
|
-
.translate([SVG_WIDTH / 2, SVG_HEIGHT / 2])
|
|
61
|
-
.scale(1)
|
|
62
61
|
const geoStrokeColor = getGeoStrokeColor(config)
|
|
63
62
|
const path = geoPath().projection(projection)
|
|
64
63
|
|
|
64
|
+
const dynamicViewBox = useMemo(() => {
|
|
65
|
+
if (!useDynamicViewbox || !bounds) {
|
|
66
|
+
return SVG_VIEWBOX
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const x = Math.floor(bounds[0][0] - SVG_PADDING)
|
|
70
|
+
const y = Math.floor(bounds[0][1] - SVG_PADDING)
|
|
71
|
+
const width = Math.ceil(bounds[1][0] - bounds[0][0] + SVG_PADDING * 2)
|
|
72
|
+
const height = Math.ceil(bounds[1][1] - bounds[0][1] + SVG_PADDING * 2)
|
|
73
|
+
|
|
74
|
+
return `${x} ${y} ${width} ${height}`
|
|
75
|
+
}, [useDynamicViewbox, bounds])
|
|
76
|
+
|
|
65
77
|
useEffect(() => {
|
|
66
78
|
let currentYear = getCurrentTopoYear(config, runtimeFilters)
|
|
67
79
|
|
|
@@ -80,6 +92,11 @@ const SingleStateMap: React.FC = () => {
|
|
|
80
92
|
)
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
// Early return for small multiples rendering
|
|
96
|
+
if (config.smallMultiples?.mode) {
|
|
97
|
+
return <SmallMultiples />
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
const checkForNoData = () => {
|
|
84
101
|
// If no statesPicked, return true
|
|
85
102
|
if (statesPicked?.every(sp => !sp.fipsCode)) return true
|
|
@@ -112,10 +129,10 @@ const SingleStateMap: React.FC = () => {
|
|
|
112
129
|
/>
|
|
113
130
|
)
|
|
114
131
|
|
|
115
|
-
// Push city list
|
|
132
|
+
// Push city list - use projection from useStateZoom which is fitted to ALL selected states
|
|
116
133
|
geosJsx.push(
|
|
117
134
|
<CityList
|
|
118
|
-
projection={
|
|
135
|
+
projection={projection}
|
|
119
136
|
key='cities'
|
|
120
137
|
geoClickHandler={geoClickHandler}
|
|
121
138
|
titleCase={titleCase}
|
|
@@ -131,7 +148,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
131
148
|
<ErrorBoundary component='SingleStateMap'>
|
|
132
149
|
{!!statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
|
|
133
150
|
<svg
|
|
134
|
-
viewBox={
|
|
151
|
+
viewBox={dynamicViewBox}
|
|
135
152
|
preserveAspectRatio='xMinYMin'
|
|
136
153
|
className='svg-container'
|
|
137
154
|
role='img'
|
|
@@ -172,7 +189,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
172
189
|
statesToShow
|
|
173
190
|
]}
|
|
174
191
|
>
|
|
175
|
-
{({ features
|
|
192
|
+
{({ features }) => {
|
|
176
193
|
return (
|
|
177
194
|
<g
|
|
178
195
|
id='mapGroup'
|
|
@@ -183,7 +200,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
183
200
|
data-scale=''
|
|
184
201
|
key='countyMapGroup'
|
|
185
202
|
>
|
|
186
|
-
{constructGeoJsx(features
|
|
203
|
+
{constructGeoJsx(features)}
|
|
187
204
|
</g>
|
|
188
205
|
)
|
|
189
206
|
}}
|
|
@@ -194,7 +211,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
194
211
|
)}
|
|
195
212
|
{!!statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
|
|
196
213
|
<svg
|
|
197
|
-
viewBox={
|
|
214
|
+
viewBox={dynamicViewBox}
|
|
198
215
|
preserveAspectRatio='xMinYMin'
|
|
199
216
|
className='svg-container'
|
|
200
217
|
role='img'
|
|
@@ -247,7 +264,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
247
264
|
|
|
248
265
|
{checkForNoData() && (
|
|
249
266
|
<svg
|
|
250
|
-
viewBox={
|
|
267
|
+
viewBox={dynamicViewBox}
|
|
251
268
|
preserveAspectRatio='xMinYMin'
|
|
252
269
|
className='svg-container'
|
|
253
270
|
role='img'
|