@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
|
@@ -11,6 +11,7 @@ import CityList from '../CityList'
|
|
|
11
11
|
import BubbleList from '../BubbleList'
|
|
12
12
|
import ZoomControls from '../ZoomControls'
|
|
13
13
|
import { supportedCountries } from '../../data/supported-geos'
|
|
14
|
+
import { getCountriesPicked } from '../../helpers/getCountriesPicked'
|
|
14
15
|
import {
|
|
15
16
|
getGeoFillColor,
|
|
16
17
|
getGeoStrokeColor,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from '../../helpers'
|
|
25
26
|
import useGeoClickHandler from '../../hooks/useGeoClickHandler'
|
|
26
27
|
import useApplyTooltipsToGeo from '../../hooks/useApplyTooltipsToGeo'
|
|
28
|
+
import useCountryZoom from '../../hooks/useCountryZoom'
|
|
27
29
|
import generateRuntimeData from '../../helpers/generateRuntimeData'
|
|
28
30
|
import { applyLegendToRow } from '../../helpers/applyLegendToRow'
|
|
29
31
|
|
|
@@ -33,17 +35,24 @@ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
|
33
35
|
|
|
34
36
|
let projection = geoMercator()
|
|
35
37
|
|
|
38
|
+
const GRAYED_OUT_COLOR = '#d3d3d3'
|
|
39
|
+
|
|
40
|
+
type MapPosition = { coordinates: number[]; zoom: number }
|
|
41
|
+
|
|
36
42
|
const WorldMap = () => {
|
|
37
43
|
// prettier-ignore
|
|
38
44
|
const {
|
|
39
45
|
runtimeData,
|
|
40
|
-
position,
|
|
46
|
+
position: mapPosition,
|
|
41
47
|
config,
|
|
42
48
|
tooltipId,
|
|
43
49
|
runtimeLegend,
|
|
44
50
|
interactionLabel
|
|
45
51
|
} = useContext(ConfigContext)
|
|
46
52
|
|
|
53
|
+
// Type assertion: position from context is actually the map viewport position, not legend position
|
|
54
|
+
const position = mapPosition as unknown as MapPosition
|
|
55
|
+
|
|
47
56
|
const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
|
|
48
57
|
|
|
49
58
|
const { type, allowMapZoom } = config.general
|
|
@@ -51,12 +60,16 @@ const WorldMap = () => {
|
|
|
51
60
|
const [world, setWorld] = useState(null)
|
|
52
61
|
const { geoClickHandler } = useGeoClickHandler()
|
|
53
62
|
const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
|
|
63
|
+
|
|
64
|
+
const { centerOnCountries } = useCountryZoom(world)
|
|
65
|
+
|
|
54
66
|
const dispatch = useContext(MapDispatchContext)
|
|
55
67
|
|
|
56
68
|
useEffect(() => {
|
|
57
69
|
const fetchData = async () => {
|
|
58
70
|
import(/* webpackChunkName: "world-topo" */ './data/world-topo.json').then(topoJSON => {
|
|
59
|
-
|
|
71
|
+
const worldFeatures = feature(topoJSON, topoJSON.objects.countries).features
|
|
72
|
+
setWorld(worldFeatures)
|
|
60
73
|
})
|
|
61
74
|
}
|
|
62
75
|
fetchData()
|
|
@@ -66,6 +79,19 @@ const WorldMap = () => {
|
|
|
66
79
|
return <></>
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
// Filter countries based on selection
|
|
83
|
+
const getFilteredWorld = () => {
|
|
84
|
+
if (!config.general.countriesPicked || config.general.countriesPicked.length === 0) {
|
|
85
|
+
return world // Show all countries if none selected
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Always show all countries when multi-country mode is active
|
|
89
|
+
// Individual country styling will handle hide/grayed-out states
|
|
90
|
+
return world
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const filteredWorld = getFilteredWorld()
|
|
94
|
+
|
|
69
95
|
const handleFiltersReset = () => {
|
|
70
96
|
const newRuntimeData = generateRuntimeData(config)
|
|
71
97
|
publishAnalyticsEvent({
|
|
@@ -89,7 +115,15 @@ const WorldMap = () => {
|
|
|
89
115
|
eventLabel: interactionLabel,
|
|
90
116
|
vizTitle: getVizTitle(config)
|
|
91
117
|
})
|
|
92
|
-
|
|
118
|
+
|
|
119
|
+
// If countries are selected, center on them; otherwise, use default world position
|
|
120
|
+
const countriesPicked = getCountriesPicked(config)
|
|
121
|
+
|
|
122
|
+
if (countriesPicked && countriesPicked.length > 0) {
|
|
123
|
+
centerOnCountries('reset')
|
|
124
|
+
} else {
|
|
125
|
+
dispatch({ type: 'SET_POSITION', payload: { coordinates: [0, 30], zoom: 1 } })
|
|
126
|
+
}
|
|
93
127
|
}
|
|
94
128
|
|
|
95
129
|
const handleZoomIn = position => {
|
|
@@ -189,25 +223,57 @@ const WorldMap = () => {
|
|
|
189
223
|
const geoStrokeColor = getGeoStrokeColor(config)
|
|
190
224
|
const geoFillColor = getGeoFillColor(config)
|
|
191
225
|
|
|
226
|
+
// Check if this country should be greyed out for multi-country selection
|
|
227
|
+
const countriesPicked = getCountriesPicked(config)
|
|
228
|
+
|
|
229
|
+
const isGreyedOut = Boolean(
|
|
230
|
+
countriesPicked.length > 0 &&
|
|
231
|
+
config.general.hideUnselectedCountries !== true &&
|
|
232
|
+
!countriesPicked.some(country => country.iso === geo.properties.iso || country.name === geoDisplayName)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
// Determine visual state for TDD tests
|
|
236
|
+
const isSelected = countriesPicked.some(
|
|
237
|
+
country => country.iso === geo.properties.iso || country.name === geoDisplayName
|
|
238
|
+
)
|
|
239
|
+
const isHidden = countriesPicked.length > 0 && config.general.hideUnselectedCountries === true && !isSelected
|
|
240
|
+
|
|
241
|
+
// Build CSS class names for TDD tests
|
|
242
|
+
let geoClassName = ''
|
|
243
|
+
if (countriesPicked.length > 0) {
|
|
244
|
+
if (isSelected) {
|
|
245
|
+
geoClassName = 'selected'
|
|
246
|
+
} else if (isGreyedOut) {
|
|
247
|
+
geoClassName = 'grayed-out'
|
|
248
|
+
} else if (isHidden) {
|
|
249
|
+
geoClassName = 'hidden'
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
192
253
|
let styles: Record<string, string | Record<string, string>> = {
|
|
193
|
-
fill: geoFillColor,
|
|
194
|
-
cursor: 'default'
|
|
254
|
+
fill: isGreyedOut ? GRAYED_OUT_COLOR : geoFillColor,
|
|
255
|
+
cursor: 'default',
|
|
256
|
+
...(isGreyedOut && { opacity: '0.3' })
|
|
195
257
|
}
|
|
196
258
|
|
|
197
|
-
|
|
259
|
+
// Scale stroke width inversely with zoom level to maintain consistent visual thickness
|
|
260
|
+
// At zoom=1, use base width of 0.9; at zoom=4, use 0.225; etc.
|
|
261
|
+
const baseStrokeWidth = 0.9
|
|
262
|
+
const currentZoom = position?.zoom || 1
|
|
263
|
+
const strokeWidth = baseStrokeWidth / currentZoom
|
|
198
264
|
|
|
199
265
|
// If a legend applies, return it with appropriate information.
|
|
200
266
|
const toolTip = applyTooltipsToGeo(geoDisplayName, geoData)
|
|
201
267
|
if (legendColors && legendColors[0] !== '#000000' && type !== 'bubble') {
|
|
202
268
|
styles = {
|
|
203
269
|
...styles,
|
|
204
|
-
fill: type !== 'world-geocode' ? legendColors[0] : geoFillColor,
|
|
270
|
+
fill: isGreyedOut ? GRAYED_OUT_COLOR : type !== 'world-geocode' ? legendColors[0] : geoFillColor,
|
|
205
271
|
cursor: 'default',
|
|
206
272
|
'&:hover': {
|
|
207
|
-
fill: type !== 'world-geocode' ? legendColors[1] : geoFillColor
|
|
273
|
+
fill: isGreyedOut ? GRAYED_OUT_COLOR : type !== 'world-geocode' ? legendColors[1] : geoFillColor
|
|
208
274
|
},
|
|
209
275
|
'&:active': {
|
|
210
|
-
fill: type !== 'world-geocode' ? legendColors[2] : geoFillColor
|
|
276
|
+
fill: isGreyedOut ? GRAYED_OUT_COLOR : type !== 'world-geocode' ? legendColors[2] : geoFillColor
|
|
211
277
|
}
|
|
212
278
|
}
|
|
213
279
|
|
|
@@ -228,6 +294,7 @@ const WorldMap = () => {
|
|
|
228
294
|
path={path}
|
|
229
295
|
stroke={geoStrokeColor}
|
|
230
296
|
strokeWidth={strokeWidth}
|
|
297
|
+
className={geoClassName}
|
|
231
298
|
onClick={() => geoClickHandler(geoDisplayName, geoData)}
|
|
232
299
|
onMouseEnter={() => {
|
|
233
300
|
// Track hover analytics event if this is a new location
|
|
@@ -245,6 +312,7 @@ const WorldMap = () => {
|
|
|
245
312
|
}}
|
|
246
313
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
247
314
|
data-tooltip-html={toolTip}
|
|
315
|
+
data-country-code={geo.properties.iso}
|
|
248
316
|
tabIndex={-1}
|
|
249
317
|
/>
|
|
250
318
|
)
|
|
@@ -260,6 +328,7 @@ const WorldMap = () => {
|
|
|
260
328
|
strokeWidth={strokeWidth}
|
|
261
329
|
styles={styles}
|
|
262
330
|
path={path}
|
|
331
|
+
className={geoClassName}
|
|
263
332
|
onMouseEnter={() => {
|
|
264
333
|
// Track hover analytics event if this is a new location
|
|
265
334
|
const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
|
|
@@ -276,6 +345,7 @@ const WorldMap = () => {
|
|
|
276
345
|
}}
|
|
277
346
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
278
347
|
data-tooltip-html={toolTip}
|
|
348
|
+
data-country-code={geo.properties.iso}
|
|
279
349
|
/>
|
|
280
350
|
)
|
|
281
351
|
})
|
|
@@ -305,7 +375,7 @@ const WorldMap = () => {
|
|
|
305
375
|
width={SVG_WIDTH}
|
|
306
376
|
height={SVG_HEIGHT}
|
|
307
377
|
>
|
|
308
|
-
<Mercator data={
|
|
378
|
+
<Mercator data={filteredWorld}>{({ features }) => constructGeoJsx(features)}</Mercator>
|
|
309
379
|
</ZoomableGroup>
|
|
310
380
|
</svg>
|
|
311
381
|
) : (
|
|
@@ -319,7 +389,7 @@ const WorldMap = () => {
|
|
|
319
389
|
width={SVG_WIDTH}
|
|
320
390
|
height={SVG_HEIGHT}
|
|
321
391
|
>
|
|
322
|
-
<Mercator data={
|
|
392
|
+
<Mercator data={filteredWorld}>{({ features }) => constructGeoJsx(features)}</Mercator>
|
|
323
393
|
</ZoomableGroup>
|
|
324
394
|
</svg>
|
|
325
395
|
)}
|
|
@@ -140,6 +140,16 @@ const createInitialState = () => {
|
|
|
140
140
|
},
|
|
141
141
|
filterBehavior: 'Filter Change',
|
|
142
142
|
filterIntro: '',
|
|
143
|
+
smallMultiples: {
|
|
144
|
+
mode: '',
|
|
145
|
+
tileColumn: '',
|
|
146
|
+
tilesPerRowDesktop: 2,
|
|
147
|
+
tilesPerRowMobile: 1,
|
|
148
|
+
tileOrderType: 'asc',
|
|
149
|
+
tileOrder: [],
|
|
150
|
+
tileTitles: {},
|
|
151
|
+
synchronizedTooltips: true
|
|
152
|
+
},
|
|
143
153
|
markupVariables: [],
|
|
144
154
|
enableMarkupVariables: false
|
|
145
155
|
}
|
|
@@ -98,74 +98,6 @@ export const supportedRegions = {
|
|
|
98
98
|
'region 10': ['REGION 10', 'R10']
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/**
|
|
102
|
-
* State Name to ISO Code Mapping
|
|
103
|
-
*
|
|
104
|
-
* Maps proper case state names to their corresponding ISO 3166-2 codes.
|
|
105
|
-
* Provides reverse lookup capability for the supportedStates table.
|
|
106
|
-
*
|
|
107
|
-
* Structure: { 'State Name': 'US-XX' }
|
|
108
|
-
* - Key: Proper case state name (e.g., 'California')
|
|
109
|
-
* - Value: ISO 3166-2 state code (e.g., 'US-CA')
|
|
110
|
-
*
|
|
111
|
-
* Used in:
|
|
112
|
-
* - Data processing when state names need to be converted to ISO codes
|
|
113
|
-
* - Validation and normalization of state data
|
|
114
|
-
*/
|
|
115
|
-
export const stateToIso = {
|
|
116
|
-
// States
|
|
117
|
-
Alabama: 'US-AL',
|
|
118
|
-
Alaska: 'US-AK',
|
|
119
|
-
Arizona: 'US-AZ',
|
|
120
|
-
Arkansas: 'US-AR',
|
|
121
|
-
California: 'US-CA',
|
|
122
|
-
Colorado: 'US-CO',
|
|
123
|
-
Connecticut: 'US-CT',
|
|
124
|
-
Delaware: 'US-DE',
|
|
125
|
-
Florida: 'US-FL',
|
|
126
|
-
Georgia: 'US-GA',
|
|
127
|
-
Hawaii: 'US-HI',
|
|
128
|
-
Idaho: 'US-ID',
|
|
129
|
-
Illinois: 'US-IL',
|
|
130
|
-
Indiana: 'US-IN',
|
|
131
|
-
Iowa: 'US-IA',
|
|
132
|
-
Kansas: 'US-KS',
|
|
133
|
-
Kentucky: 'US-KY',
|
|
134
|
-
Louisiana: 'US-LA',
|
|
135
|
-
Maine: 'US-ME',
|
|
136
|
-
Maryland: 'US-MD',
|
|
137
|
-
Massachusetts: 'US-MA',
|
|
138
|
-
Michigan: 'US-MI',
|
|
139
|
-
Minnesota: 'US-MN',
|
|
140
|
-
Mississippi: 'US-MS',
|
|
141
|
-
Missouri: 'US-MO',
|
|
142
|
-
Montana: 'US-MT',
|
|
143
|
-
Nebraska: 'US-NE',
|
|
144
|
-
Nevada: 'US-NV',
|
|
145
|
-
'New Hampshire': 'US-NH',
|
|
146
|
-
'New Jersey': 'US-NJ',
|
|
147
|
-
'New Mexico': 'US-NM',
|
|
148
|
-
'New York': 'US-NY',
|
|
149
|
-
'North Carolina': 'US-NC',
|
|
150
|
-
'North Dakota': 'US-ND',
|
|
151
|
-
Ohio: 'US-OH',
|
|
152
|
-
Oklahoma: 'US-OK',
|
|
153
|
-
Oregon: 'US-OR',
|
|
154
|
-
Pennsylvania: 'US-PA',
|
|
155
|
-
'Rhode Island': 'US-RI',
|
|
156
|
-
'South Carolina': 'US-SC',
|
|
157
|
-
'South Dakota': 'US-SD',
|
|
158
|
-
Tennessee: 'US-TN',
|
|
159
|
-
Texas: 'US-TX',
|
|
160
|
-
Utah: 'US-UT',
|
|
161
|
-
Vermont: 'US-VT',
|
|
162
|
-
Virginia: 'US-VA',
|
|
163
|
-
Washington: 'US-WA',
|
|
164
|
-
'West Virginia': 'US-WV',
|
|
165
|
-
Wisconsin: 'US-WI',
|
|
166
|
-
Wyoming: 'US-WY'
|
|
167
|
-
}
|
|
168
|
-
|
|
169
101
|
/**
|
|
170
102
|
* State FIPS Code to Two-Letter Abbreviation Mapping
|
|
171
103
|
*
|
|
@@ -750,7 +682,7 @@ export const supportedCities = {
|
|
|
750
682
|
'GREAT PLAINS TRIBAL LEADERS HEALTH BOARD': [-103.22444, 44.083054],
|
|
751
683
|
'GREENSBORO': [-79.791977, 36.072636],
|
|
752
684
|
'HENDERSON': [-114.981720, 36.039524],
|
|
753
|
-
'HERSHEY': [-76.6779444, 40.2849997
|
|
685
|
+
'HERSHEY': [-76.6779444, 40.2849997],
|
|
754
686
|
'HIALEAH': [-80.278107, 25.857595],
|
|
755
687
|
'HONOLULU': [-157.858337, 21.306944],
|
|
756
688
|
'HOPI TRIBE': [-110.5035, 35.7833],
|
|
@@ -783,7 +715,7 @@ export const supportedCities = {
|
|
|
783
715
|
'LUBBOCK': [-101.855164, 33.577862],
|
|
784
716
|
'MADISON': [-89.401230, 43.073051],
|
|
785
717
|
'MARION COUNTY, INDIANA': [-86.136543, 39.781029],
|
|
786
|
-
'MARION':[-88.9330556,37.7305556],
|
|
718
|
+
'MARION': [-88.9330556, 37.7305556],
|
|
787
719
|
'MEMPHIS': [-90.048981, 35.149532],
|
|
788
720
|
'MESA': [-111.831474, 33.415184],
|
|
789
721
|
'MIAMI': [-80.191788, 25.761681],
|
|
@@ -808,7 +740,7 @@ export const supportedCities = {
|
|
|
808
740
|
'OLYMPIA': [-122.9382403, 47.0394791],
|
|
809
741
|
'OMAHA': [-95.934502, 41.256538],
|
|
810
742
|
'ORLANDO': [-81.379234, 28.538336],
|
|
811
|
-
'PASADENA':[-95.209099,29.691063],
|
|
743
|
+
'PASADENA': [-95.209099, 29.691063],
|
|
812
744
|
'PHILADELPHIA': [-75.165222, 39.952583],
|
|
813
745
|
'PHOENIX': [-112.074036, 33.448376],
|
|
814
746
|
'PITTSBURGH': [-79.995888, 40.440624],
|
|
@@ -828,9 +760,9 @@ export const supportedCities = {
|
|
|
828
760
|
'SACRAMENTO': [-121.494400, 38.581573],
|
|
829
761
|
'SAINT PAUL': [-93.089958, 44.953705],
|
|
830
762
|
'SALEM, ALABAMA': [-85.2386, 32.5968],
|
|
831
|
-
'SALEM, CONNECTICUT': [-72.2754,41.4904],
|
|
763
|
+
'SALEM, CONNECTICUT': [-72.2754, 41.4904],
|
|
832
764
|
'SALEM, FLORIDA': [-83.4129, 29.8869],
|
|
833
|
-
'SALEM, ILLINOIS':[-88.945618,38.626991],
|
|
765
|
+
'SALEM, ILLINOIS': [-88.945618, 38.626991],
|
|
834
766
|
'SALEM, MASSACHUSETTS': [-70.8955, 42.5197],
|
|
835
767
|
'SALEM, OR': [-123.0351, 44.9429],
|
|
836
768
|
'SALEM, OREGON': [-123.0351, 44.9429],
|
|
@@ -839,19 +771,19 @@ export const supportedCities = {
|
|
|
839
771
|
'SALUDA, VIRGINIA': [-76.5950, 37.6064],
|
|
840
772
|
'SAN ANTONIO': [-98.493629, 29.424122],
|
|
841
773
|
'SAN BENITO': [-97.6311, 26.1326],
|
|
842
|
-
'SAN BERNARDINO':[-117.302399,34.115784],
|
|
774
|
+
'SAN BERNARDINO': [-117.302399, 34.115784],
|
|
843
775
|
'SAN DIEGO': [-117.161087, 32.715736],
|
|
844
776
|
'SAN FRANCISCO': [-122.419418, 37.774929],
|
|
845
777
|
'SAN JOSE': [-121.886330, 37.338207],
|
|
846
778
|
'SANTA ANA': [-117.867653, 33.745472],
|
|
847
|
-
'SANTA CLARA':[-121.955238,37.354107],
|
|
779
|
+
'SANTA CLARA': [-121.955238, 37.354107],
|
|
848
780
|
'SCOTTSDALE': [-111.926048, 33.494171],
|
|
849
781
|
'SEATTLE': [-122.332069, 47.606209],
|
|
850
782
|
'SOUTH PUGET INTERTRIBAL PLANNING AGENCY': [-123.0832, 47.1241],
|
|
851
783
|
'SOUTHCENTRAL FOUNDATION': [-149.7971, 61.1821],
|
|
852
784
|
'SOUTHEAST ALASKA REGIONAL HEALTH CONSORTIUM': [-135.3369, 57.05479],
|
|
853
785
|
'SPOKANE': [-117.426048, 47.658779],
|
|
854
|
-
'ST PAUL': [
|
|
786
|
+
'ST PAUL': [-93.089958, 44.953705],
|
|
855
787
|
'ST. LOUIS': [-90.199402, 38.627003],
|
|
856
788
|
'ST. PETERSBURG': [-82.640289, 27.767601],
|
|
857
789
|
'STOCKTON': [-121.290779, 37.957703],
|
package/src/helpers/addUIDs.ts
CHANGED
|
@@ -25,8 +25,16 @@ const geoLookups: Record<string, GeoLookup> = {
|
|
|
25
25
|
country: { keys: countryKeys, data: supportedCountries }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const memoizedFindUID = (
|
|
28
|
+
const memoizedFindUID = (
|
|
29
|
+
geoName: string,
|
|
30
|
+
type: keyof typeof geoLookups,
|
|
31
|
+
caseInsensitive = false
|
|
32
|
+
): string | undefined => {
|
|
29
33
|
const lookup = geoLookups[type]
|
|
34
|
+
if (caseInsensitive) {
|
|
35
|
+
const lowerGeoName = geoName.toLowerCase()
|
|
36
|
+
return lookup.keys.find(key => lookup.data[key].some(name => name.toLowerCase() === lowerGeoName))
|
|
37
|
+
}
|
|
30
38
|
return lookup.keys.find(key => lookup.data[key].includes(geoName))
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -72,7 +80,10 @@ const handleUSLocation = (row: DataRow, geoColumn: string, displayAsHex: boolean
|
|
|
72
80
|
|
|
73
81
|
const handleWorldLocation = (row: DataRow, geoColumn: string, isWorldGeocodeType: boolean): string | null => {
|
|
74
82
|
const geoName = row[geoColumn]
|
|
75
|
-
|
|
83
|
+
if (!geoName) return null
|
|
84
|
+
|
|
85
|
+
// Use case-insensitive matching for world countries to handle various input formats
|
|
86
|
+
let uid = memoizedFindUID(geoName, 'country', true)
|
|
76
87
|
if (!uid && (isWorldGeocodeType || geoName)) {
|
|
77
88
|
uid = findCityUID(geoName)
|
|
78
89
|
}
|
|
@@ -65,6 +65,30 @@ export const applyColorToLegend = (legendIdx: number, config: MapConfig, result:
|
|
|
65
65
|
color = mapPaletteNameMigrations[color]
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Check for customColorsOrdered first (direct 1-to-1 mapping, no distribution)
|
|
69
|
+
if (general?.palette?.customColorsOrdered && Array.isArray(general.palette.customColorsOrdered)) {
|
|
70
|
+
const customColorsOrdered = general.palette.customColorsOrdered
|
|
71
|
+
|
|
72
|
+
// Count actual special classes in the result array
|
|
73
|
+
const actualSpecialClassCount = result.filter(item => item.special).length
|
|
74
|
+
const colorIdx = legendIdx - actualSpecialClassCount
|
|
75
|
+
|
|
76
|
+
// Handle special classes coloring
|
|
77
|
+
if (result[legendIdx]?.special) {
|
|
78
|
+
const specialClassColors = chroma.scale(['#D4D4D4', '#939393']).colors(actualSpecialClassCount)
|
|
79
|
+
const specialClassIdx = result.slice(0, legendIdx + 1).filter(item => item.special).length - 1
|
|
80
|
+
return specialClassColors[specialClassIdx]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Direct 1-to-1 mapping with customColorsOrdered (no distribution array)
|
|
84
|
+
if (colorIdx >= 0 && colorIdx < customColorsOrdered.length) {
|
|
85
|
+
return customColorsOrdered[colorIdx]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fallback to last color if index out of bounds
|
|
89
|
+
return customColorsOrdered[customColorsOrdered.length - 1] || '#d3d3d3'
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
// Try multiple approaches to find the palette
|
|
69
93
|
let mapColorPalette = general?.palette?.customColors
|
|
70
94
|
|
|
@@ -118,7 +142,7 @@ export const applyColorToLegend = (legendIdx: number, config: MapConfig, result:
|
|
|
118
142
|
// For category legends, use the actual result length
|
|
119
143
|
const isNumericLegend = legend && ['equalnumber', 'equalinterval'].includes(legend.type)
|
|
120
144
|
const nonSpecialItemCount = isNumericLegend
|
|
121
|
-
?
|
|
145
|
+
? legend.numberOfItems || result.length
|
|
122
146
|
: result.length - actualSpecialClassCount
|
|
123
147
|
|
|
124
148
|
const amt =
|
package/src/helpers/constants.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
export const SVG_WIDTH = 880
|
|
2
2
|
export const SVG_HEIGHT = 500
|
|
3
|
-
export const SVG_PADDING =
|
|
3
|
+
export const SVG_PADDING = 25
|
|
4
4
|
export const SVG_VIEWBOX = `0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`
|
|
5
|
-
export const HEADER_COLORS = [
|
|
6
|
-
'theme-blue',
|
|
7
|
-
'theme-purple',
|
|
8
|
-
'theme-brown',
|
|
9
|
-
'theme-teal',
|
|
10
|
-
'theme-pink',
|
|
11
|
-
'theme-orange',
|
|
12
|
-
'theme-slate',
|
|
13
|
-
'theme-indigo',
|
|
14
|
-
'theme-cyan',
|
|
15
|
-
'theme-green',
|
|
16
|
-
'theme-amber'
|
|
17
|
-
]
|
|
18
5
|
export const MAX_ZOOM_LEVEL = 4
|
|
19
6
|
|
|
20
7
|
export const SUPPORTED_DC_NAMES = [
|
|
@@ -45,7 +32,6 @@ export const DEFAULT_MAP_BACKGROUND = '#DFE1E2'
|
|
|
45
32
|
|
|
46
33
|
// Component constants
|
|
47
34
|
export const LOGO_MAX_WIDTH = '50px'
|
|
48
|
-
export const STORYBOOK_PORT = 6006
|
|
49
35
|
|
|
50
36
|
// CSV Parsing Configuration
|
|
51
37
|
export const CSV_PARSE_CONFIG = {
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
supportedTerritories,
|
|
5
5
|
supportedCountries,
|
|
6
6
|
supportedCounties,
|
|
7
|
+
supportedCities,
|
|
7
8
|
stateKeys,
|
|
8
9
|
territoryKeys,
|
|
9
10
|
countryKeys,
|
|
10
|
-
countyKeys
|
|
11
|
+
countyKeys,
|
|
12
|
+
cityKeys
|
|
11
13
|
} from '../data/supported-geos'
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -20,14 +22,17 @@ import {
|
|
|
20
22
|
export const displayGeoName = (key: string, convertFipsCodes = true): string => {
|
|
21
23
|
if (!convertFipsCodes) return key
|
|
22
24
|
let value = key
|
|
25
|
+
let wasLookedUp = false
|
|
23
26
|
|
|
24
27
|
// Map to first item in values array which is the preferred label
|
|
25
28
|
if (stateKeys.includes(value)) {
|
|
26
29
|
value = titleCase(supportedStates[key][0])
|
|
30
|
+
wasLookedUp = true
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
if (territoryKeys.includes(value)) {
|
|
30
34
|
value = titleCase(supportedTerritories[key][0])
|
|
35
|
+
wasLookedUp = true
|
|
31
36
|
if (value === 'U.s. Virgin Islands') {
|
|
32
37
|
value = 'U.S. Virgin Islands'
|
|
33
38
|
}
|
|
@@ -35,10 +40,17 @@ export const displayGeoName = (key: string, convertFipsCodes = true): string =>
|
|
|
35
40
|
|
|
36
41
|
if (countryKeys.includes(value)) {
|
|
37
42
|
value = titleCase(supportedCountries[key][0])
|
|
43
|
+
wasLookedUp = true
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
if (countyKeys.includes(value)) {
|
|
41
47
|
value = titleCase(supportedCounties[key])
|
|
48
|
+
wasLookedUp = true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (cityKeys.includes(value)) {
|
|
52
|
+
value = titleCase(String(value) || '')
|
|
53
|
+
wasLookedUp = true
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
const dict = {
|
|
@@ -51,11 +63,14 @@ export const displayGeoName = (key: string, convertFipsCodes = true): string =>
|
|
|
51
63
|
|
|
52
64
|
if (Object.keys(dict).includes(value)) {
|
|
53
65
|
value = dict[value]
|
|
66
|
+
wasLookedUp = true
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
|
|
69
|
+
// If value was looked up from our dictionaries and needs formatting, or if it's a 2-letter abbreviation, return as-is
|
|
70
|
+
if (value?.length === 2 || value === 'U.S. Virgin Islands' || wasLookedUp) {
|
|
57
71
|
return value
|
|
58
72
|
} else {
|
|
59
|
-
|
|
73
|
+
// Apply titleCase to unrecognized values (e.g., "DISTRICT OF COLUMBIA" -> "District of Columbia")
|
|
74
|
+
return titleCase(value)
|
|
60
75
|
}
|
|
61
76
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { supportedCountries } from '../data/supported-geos'
|
|
2
|
+
import type { MapConfig } from '../types/MapConfig'
|
|
3
|
+
|
|
4
|
+
export interface CountryPickedInfo {
|
|
5
|
+
iso: string
|
|
6
|
+
name: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getCountriesPicked = (config: MapConfig): CountryPickedInfo[] => {
|
|
10
|
+
if (!config.general.countriesPicked || config.general.countriesPicked.length === 0) {
|
|
11
|
+
return []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return config.general.countriesPicked.map(country => {
|
|
15
|
+
// Validate that the ISO code exists in our supported countries
|
|
16
|
+
if (!supportedCountries[country.iso]) {
|
|
17
|
+
console.error(`Country ISO code "${country.iso}" not found in supported countries.`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
iso: country.iso,
|
|
22
|
+
name: country.name
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ISO codes that are in supported-geos.js but don't have geometries in world-topo.json
|
|
29
|
+
* These are filtered out to prevent users from selecting countries that won't render
|
|
30
|
+
*/
|
|
31
|
+
const EXCLUDED_ISOS = new Set([
|
|
32
|
+
// US Territories (not in topology - grouped with USA or missing)
|
|
33
|
+
'ASM',
|
|
34
|
+
'GUM',
|
|
35
|
+
'MNP',
|
|
36
|
+
'VIR',
|
|
37
|
+
// Small territories/islands without separate geometries
|
|
38
|
+
'ALA',
|
|
39
|
+
'AIA',
|
|
40
|
+
'AND',
|
|
41
|
+
'ABW',
|
|
42
|
+
'BES',
|
|
43
|
+
'BMU',
|
|
44
|
+
'BVT',
|
|
45
|
+
'CXR',
|
|
46
|
+
'CCK',
|
|
47
|
+
'COK',
|
|
48
|
+
'CUW',
|
|
49
|
+
'FRO',
|
|
50
|
+
'GGY',
|
|
51
|
+
'HMD',
|
|
52
|
+
'IMN',
|
|
53
|
+
'JEY',
|
|
54
|
+
'LIE',
|
|
55
|
+
'MCO',
|
|
56
|
+
'MSR',
|
|
57
|
+
'NRU',
|
|
58
|
+
'NIU',
|
|
59
|
+
'NFK',
|
|
60
|
+
'PCN',
|
|
61
|
+
'SGS',
|
|
62
|
+
'SJM',
|
|
63
|
+
'TKL',
|
|
64
|
+
'TCA',
|
|
65
|
+
'TUV',
|
|
66
|
+
'VAT',
|
|
67
|
+
'WLF'
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Helper to get all supported countries formatted for dropdown options
|
|
72
|
+
* Filters to only valid ISO 3166-1 alpha-3 codes and removes countries without topology
|
|
73
|
+
*/
|
|
74
|
+
export const getSupportedCountryOptions = () => {
|
|
75
|
+
return Object.keys(supportedCountries)
|
|
76
|
+
.filter(iso => /^[A-Z]{3}$/.test(iso)) // Only proper 3-letter ISO codes
|
|
77
|
+
.filter(iso => !EXCLUDED_ISOS.has(iso)) // Exclude countries without topology
|
|
78
|
+
.map(iso => ({
|
|
79
|
+
value: iso,
|
|
80
|
+
label: supportedCountries[iso][0] // Use the first (primary) name
|
|
81
|
+
}))
|
|
82
|
+
.sort((a, b) => a.label.localeCompare(b.label)) // Sort alphabetically by name
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Helper to determine if the map should show only selected countries
|
|
87
|
+
* Returns true if countries are selected, false if showing all countries
|
|
88
|
+
*/
|
|
89
|
+
export const isMultiCountryActive = (config: MapConfig): boolean => {
|
|
90
|
+
return Boolean(config.general.countriesPicked && config.general.countriesPicked.length > 0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Helper to determine display mode for unselected countries
|
|
95
|
+
* Returns 'hidden' if hideUnselectedCountries is true, 'grayed' if false (default)
|
|
96
|
+
*/
|
|
97
|
+
export const getUnselectedCountryDisplayMode = (config: MapConfig): 'hidden' | 'grayed' | 'normal' => {
|
|
98
|
+
if (!isMultiCountryActive(config)) {
|
|
99
|
+
return 'normal' // Show all countries normally when none are specifically selected
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return config.general.hideUnselectedCountries ? 'hidden' : 'grayed'
|
|
103
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MapConfig } from './../types/MapConfig'
|
|
2
|
+
import { isMultiCountryActive } from './getCountriesPicked'
|
|
2
3
|
|
|
3
4
|
export const getMapContainerClasses = (state: MapConfig, modal) => {
|
|
4
5
|
const { general } = state
|
|
@@ -19,5 +20,11 @@ export const getMapContainerClasses = (state: MapConfig, modal) => {
|
|
|
19
20
|
if (general.type === 'navigation' && true === general.fullBorder) {
|
|
20
21
|
mapContainerClasses.push('full-border')
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
// Add multi-country class when multi-country mode is active
|
|
25
|
+
if (isMultiCountryActive(state)) {
|
|
26
|
+
mapContainerClasses.push('multi-country-selected')
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
return mapContainerClasses
|
|
23
30
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MapConfig } from '../types/MapConfig'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface PatternInfo {
|
|
4
4
|
pattern?: string
|
|
5
5
|
dataKey: string
|
|
6
6
|
size?: string
|
|
@@ -8,10 +8,7 @@ export interface PatternInfo {
|
|
|
8
8
|
color?: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const getPatternForRow = (
|
|
12
|
-
rowObj: Record<string, any>,
|
|
13
|
-
config: MapConfig
|
|
14
|
-
): PatternInfo | null => {
|
|
11
|
+
export const getPatternForRow = (rowObj: Record<string, any>, config: MapConfig): PatternInfo | null => {
|
|
15
12
|
if (!config.map?.patterns || !rowObj) {
|
|
16
13
|
return null
|
|
17
14
|
}
|