@cdc/map 4.25.10 → 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/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 +27405 -25783
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/index.html +2 -1
- package/package.json +4 -4
- package/src/CdcMapComponent.tsx +44 -20
- 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.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.stories.tsx +22 -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 +2 -4
- package/src/components/BubbleList.tsx +1 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +630 -564
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
- 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 +20 -3
- package/src/components/Legend/components/Legend.tsx +34 -34
- package/src/components/Legend/components/index.scss +1 -1
- 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 +23 -4
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +14 -2
- package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +25 -5
- package/src/components/UsaMap/components/UsaMap.State.tsx +26 -3
- 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 +10 -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/constants.ts +1 -15
- package/src/helpers/displayGeoName.ts +19 -4
- package/src/helpers/generateRuntimeLegend.ts +0 -2
- 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 +1 -9
- 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/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 +5 -2
- package/src/hooks/useStateZoom.tsx +5 -2
- 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/types/MapConfig.ts +30 -11
- package/src/types/MapContext.ts +6 -0
- package/src/types/runtimeLegend.ts +1 -1
- package/src/components/DataTable.tsx +0 -413
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- 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
|
@@ -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
|
}
|
|
@@ -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
|
|
@@ -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(() => {
|
|
@@ -62,6 +64,19 @@ const SingleStateMap: React.FC = () => {
|
|
|
62
64
|
const geoStrokeColor = getGeoStrokeColor(config)
|
|
63
65
|
const path = geoPath().projection(projection)
|
|
64
66
|
|
|
67
|
+
const dynamicViewBox = useMemo(() => {
|
|
68
|
+
if (!useDynamicViewbox || !bounds) {
|
|
69
|
+
return SVG_VIEWBOX
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const x = Math.floor(bounds[0][0] - SVG_PADDING)
|
|
73
|
+
const y = Math.floor(bounds[0][1] - SVG_PADDING)
|
|
74
|
+
const width = Math.ceil(bounds[1][0] - bounds[0][0] + SVG_PADDING * 2)
|
|
75
|
+
const height = Math.ceil(bounds[1][1] - bounds[0][1] + SVG_PADDING * 2)
|
|
76
|
+
|
|
77
|
+
return `${x} ${y} ${width} ${height}`
|
|
78
|
+
}, [useDynamicViewbox, bounds])
|
|
79
|
+
|
|
65
80
|
useEffect(() => {
|
|
66
81
|
let currentYear = getCurrentTopoYear(config, runtimeFilters)
|
|
67
82
|
|
|
@@ -80,6 +95,11 @@ const SingleStateMap: React.FC = () => {
|
|
|
80
95
|
)
|
|
81
96
|
}
|
|
82
97
|
|
|
98
|
+
// Early return for small multiples rendering
|
|
99
|
+
if (config.smallMultiples?.mode) {
|
|
100
|
+
return <SmallMultiples />
|
|
101
|
+
}
|
|
102
|
+
|
|
83
103
|
const checkForNoData = () => {
|
|
84
104
|
// If no statesPicked, return true
|
|
85
105
|
if (statesPicked?.every(sp => !sp.fipsCode)) return true
|
|
@@ -131,7 +151,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
131
151
|
<ErrorBoundary component='SingleStateMap'>
|
|
132
152
|
{!!statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
|
|
133
153
|
<svg
|
|
134
|
-
viewBox={
|
|
154
|
+
viewBox={dynamicViewBox}
|
|
135
155
|
preserveAspectRatio='xMinYMin'
|
|
136
156
|
className='svg-container'
|
|
137
157
|
role='img'
|
|
@@ -194,7 +214,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
194
214
|
)}
|
|
195
215
|
{!!statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
|
|
196
216
|
<svg
|
|
197
|
-
viewBox={
|
|
217
|
+
viewBox={dynamicViewBox}
|
|
198
218
|
preserveAspectRatio='xMinYMin'
|
|
199
219
|
className='svg-container'
|
|
200
220
|
role='img'
|
|
@@ -247,7 +267,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
247
267
|
|
|
248
268
|
{checkForNoData() && (
|
|
249
269
|
<svg
|
|
250
|
-
viewBox={
|
|
270
|
+
viewBox={dynamicViewBox}
|
|
251
271
|
preserveAspectRatio='xMinYMin'
|
|
252
272
|
className='svg-container'
|
|
253
273
|
role='img'
|
|
@@ -26,6 +26,8 @@ import { useLegendMemoContext } from '../../../context/LegendMemoContext'
|
|
|
26
26
|
import { MapContext } from '../../../types/MapContext'
|
|
27
27
|
import { checkColorContrast, getContrastColor, outlinedTextColor } from '@cdc/core/helpers/cove/accessibility'
|
|
28
28
|
import TerritoriesSection from './TerritoriesSection'
|
|
29
|
+
import SmallMultiples from '../../SmallMultiples'
|
|
30
|
+
import { useSynchronizedGeographies } from '../../../hooks/useSynchronizedGeographies'
|
|
29
31
|
|
|
30
32
|
import { isMobileStateLabelViewport } from '@cdc/core/helpers/viewports'
|
|
31
33
|
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
@@ -80,6 +82,8 @@ const UsaMap = () => {
|
|
|
80
82
|
mapId,
|
|
81
83
|
logo,
|
|
82
84
|
currentViewport,
|
|
85
|
+
vizViewport,
|
|
86
|
+
dimensions,
|
|
83
87
|
translate,
|
|
84
88
|
runtimeLegend,
|
|
85
89
|
interactionLabel
|
|
@@ -87,6 +91,8 @@ const UsaMap = () => {
|
|
|
87
91
|
|
|
88
92
|
const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
|
|
89
93
|
|
|
94
|
+
const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
|
|
95
|
+
|
|
90
96
|
let isFilterValueSupported = false
|
|
91
97
|
const { general, columns, tooltips, hexMap, map, annotations } = config
|
|
92
98
|
const { displayAsHex } = general
|
|
@@ -165,6 +171,10 @@ const UsaMap = () => {
|
|
|
165
171
|
const geoStrokeColor = getGeoStrokeColor(config)
|
|
166
172
|
const geoFillColor = getGeoFillColor(config)
|
|
167
173
|
|
|
174
|
+
// Chrome needs wider stroke for small maps or it doesn't render the pattern
|
|
175
|
+
const mapWidth = dimensions?.[0] || 880
|
|
176
|
+
const patternLinesStrokeWidth = mapWidth < 200 ? 1.75 : mapWidth < 375 ? 1.25 : 0.75
|
|
177
|
+
|
|
168
178
|
const territories = territoriesData.map((territory, territoryIndex) => {
|
|
169
179
|
const Shape = displayAsHex ? Territory.Hexagon : Territory.Rectangle
|
|
170
180
|
|
|
@@ -190,6 +200,8 @@ const UsaMap = () => {
|
|
|
190
200
|
strokeColor='#fff'
|
|
191
201
|
territoryData={territoryData}
|
|
192
202
|
backgroundColor={styles.fill}
|
|
203
|
+
getSyncProps={getSyncProps}
|
|
204
|
+
syncHandlers={syncHandlers}
|
|
193
205
|
/>
|
|
194
206
|
)
|
|
195
207
|
|
|
@@ -242,6 +254,8 @@ const UsaMap = () => {
|
|
|
242
254
|
territoryData={territoryData}
|
|
243
255
|
tabIndex={-1}
|
|
244
256
|
backgroundColor={styles.fill}
|
|
257
|
+
getSyncProps={getSyncProps}
|
|
258
|
+
syncHandlers={syncHandlers}
|
|
245
259
|
/>
|
|
246
260
|
)
|
|
247
261
|
}
|
|
@@ -256,6 +270,10 @@ const UsaMap = () => {
|
|
|
256
270
|
return <></>
|
|
257
271
|
}
|
|
258
272
|
|
|
273
|
+
if (config.smallMultiples?.mode) {
|
|
274
|
+
return <SmallMultiples />
|
|
275
|
+
}
|
|
276
|
+
|
|
259
277
|
// Constructs and displays markup for all geos on the map (except territories right now)
|
|
260
278
|
const constructGeoJsx = (geographies, projection) => {
|
|
261
279
|
let showLabel = general.displayStateLabels
|
|
@@ -437,6 +455,7 @@ const UsaMap = () => {
|
|
|
437
455
|
return (
|
|
438
456
|
<g data-name={geoName} key={key} tabIndex={-1}>
|
|
439
457
|
<g
|
|
458
|
+
{...getSyncProps(geoKey)}
|
|
440
459
|
className='geo-group'
|
|
441
460
|
style={styles}
|
|
442
461
|
onClick={() => geoClickHandler(geoDisplayName, geoData)}
|
|
@@ -444,7 +463,7 @@ const UsaMap = () => {
|
|
|
444
463
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
445
464
|
data-tooltip-html={tooltip}
|
|
446
465
|
tabIndex={-1}
|
|
447
|
-
onMouseEnter={
|
|
466
|
+
onMouseEnter={e => {
|
|
448
467
|
// Track hover analytics event if this is a new location
|
|
449
468
|
const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
|
|
450
469
|
publishAnalyticsEvent({
|
|
@@ -457,6 +476,10 @@ const UsaMap = () => {
|
|
|
457
476
|
location: geoDisplayName,
|
|
458
477
|
specifics: `location: ${locationName?.toLowerCase()}`
|
|
459
478
|
})
|
|
479
|
+
syncHandlers.onMouseEnter(geoKey, e.clientY)
|
|
480
|
+
}}
|
|
481
|
+
onMouseLeave={() => {
|
|
482
|
+
syncHandlers.onMouseLeave()
|
|
460
483
|
}}
|
|
461
484
|
>
|
|
462
485
|
{/* state path */}
|
|
@@ -499,7 +522,7 @@ const UsaMap = () => {
|
|
|
499
522
|
height={patternSizes[size] ?? 6}
|
|
500
523
|
width={patternSizes[size] ?? 6}
|
|
501
524
|
stroke={patternColor}
|
|
502
|
-
strokeWidth={
|
|
525
|
+
strokeWidth={patternLinesStrokeWidth}
|
|
503
526
|
orientation={['diagonalRightToLeft']}
|
|
504
527
|
/>
|
|
505
528
|
)}
|
|
@@ -590,7 +613,7 @@ const UsaMap = () => {
|
|
|
590
613
|
<text
|
|
591
614
|
x={x}
|
|
592
615
|
y={y}
|
|
593
|
-
fontSize={isMobileStateLabelViewport(
|
|
616
|
+
fontSize={isMobileStateLabelViewport(vizViewport) ? 16 : 13}
|
|
594
617
|
fontWeight={900}
|
|
595
618
|
strokeWidth='1'
|
|
596
619
|
paintOrder='stroke'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { feature } from 'topojson-client'
|
|
2
2
|
import usExtendedGeography from './../data/us-extended-geography.json'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const getCountyTopoURL = year => {
|
|
5
5
|
return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
|
|
6
6
|
}
|
|
7
7
|
|
|
@@ -85,7 +85,7 @@ export const isTopoReady = (topoData, state, runtimeFilters) => {
|
|
|
85
85
|
return topoData?.year && (!currentYear || currentYear === topoData?.year)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
const hasMoreThanFromHash = (data: { [key: string]: any }): boolean => {
|
|
89
89
|
// Get all keys of the data object
|
|
90
90
|
const keys = Object.keys(data)
|
|
91
91
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const drawCircle = (circle, context, state) => {
|
|
2
2
|
const percentOfOriginalSize = 0.75
|
|
3
3
|
const scaleVal = 1
|
|
4
4
|
const adjustedGeoRadius =
|
|
@@ -12,7 +12,8 @@ export const drawCircle = (circle, context, state) => {
|
|
|
12
12
|
context.fill()
|
|
13
13
|
context.stroke()
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
const drawSquare = (square, context, state) => {
|
|
16
17
|
const percentOfOriginalSize = 0.75
|
|
17
18
|
const scaleVal = 1.75
|
|
18
19
|
const sideLength = square.size * scaleVal
|
|
@@ -32,7 +33,7 @@ export const drawSquare = (square, context, state) => {
|
|
|
32
33
|
context.stroke()
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
const drawDiamond = (diamond, context, state) => {
|
|
36
37
|
const percentOfOriginalSize = 0.75
|
|
37
38
|
const scaleVal = 2.2
|
|
38
39
|
const fullSize = diamond.size * scaleVal
|
|
@@ -69,7 +70,8 @@ export const drawDiamond = (diamond, context, state) => {
|
|
|
69
70
|
context.fill()
|
|
70
71
|
context.stroke()
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
|
|
74
|
+
const drawTriangle = (triangle, context, state) => {
|
|
73
75
|
const percentOfOriginalSize = 0.75
|
|
74
76
|
const scaleVal = 2.2
|
|
75
77
|
const baseLength = triangle.size * scaleVal
|
|
@@ -102,7 +104,8 @@ export const drawTriangle = (triangle, context, state) => {
|
|
|
102
104
|
context.fill()
|
|
103
105
|
context.stroke()
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
|
|
108
|
+
const drawStar = (star, context, state) => {
|
|
106
109
|
const percentOfOriginalSize = 0.75
|
|
107
110
|
const scaleVal = 2.2
|
|
108
111
|
const spikes = star.spikes
|
|
@@ -155,7 +158,7 @@ export const drawStar = (star, context, state) => {
|
|
|
155
158
|
context.stroke()
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
|
|
161
|
+
const drawPin = (pin, ctx, state) => {
|
|
159
162
|
const scaleVal = 10
|
|
160
163
|
const percentOfOriginalSize = 0.75
|
|
161
164
|
const baseSize = pin.size * scaleVal
|