@cdc/map 4.25.8 → 4.25.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.claude/agents/typescript-organizer.md +118 -0
  2. package/.claude/settings.local.json +30 -0
  3. package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
  4. package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
  5. package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
  6. package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
  7. package/dist/cdcmap.js +56991 -53706
  8. package/examples/example-city-state.json +9 -1
  9. package/examples/multi-country-centering.json +45 -0
  10. package/examples/private/c.json +290 -0
  11. package/examples/private/canvas-city-hover.json +787 -0
  12. package/examples/private/colors-2.json +221 -0
  13. package/examples/private/colors.json +221 -0
  14. package/examples/private/d.json +345 -0
  15. package/examples/private/g.json +1 -0
  16. package/examples/private/h.json +105911 -0
  17. package/examples/private/measles-data.json +378 -0
  18. package/examples/private/measles.json +211 -0
  19. package/examples/private/north-dakota.json +1132 -0
  20. package/examples/private/state-with-pattern.json +883 -0
  21. package/index.html +36 -34
  22. package/package.json +26 -5
  23. package/src/CdcMap.tsx +23 -8
  24. package/src/CdcMapComponent.tsx +238 -308
  25. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  26. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  27. package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
  28. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  29. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  30. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  31. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  32. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  33. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  34. package/src/_stories/CdcMap.stories.tsx +37 -9
  35. package/src/_stories/GoogleMap.stories.tsx +2 -2
  36. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  37. package/src/_stories/_mock/column-wrap-test.json +265 -0
  38. package/src/_stories/_mock/equal-number.json +1109 -0
  39. package/src/_stories/_mock/multi-country-hide.json +78 -0
  40. package/src/_stories/_mock/multi-country.json +95 -0
  41. package/src/_stories/_mock/multi-state.json +887 -20403
  42. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  43. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  44. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  45. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  46. package/src/_stories/_mock/usa-state-gradient.json +2 -4
  47. package/src/components/BubbleList.tsx +17 -13
  48. package/src/components/CityList.tsx +85 -107
  49. package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
  50. package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
  51. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
  53. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  54. package/src/components/Geo.tsx +22 -3
  55. package/src/components/Legend/components/Legend.tsx +76 -40
  56. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  57. package/src/components/Legend/components/index.scss +1 -1
  58. package/src/components/MapContainer.tsx +52 -0
  59. package/src/components/MapControls.tsx +44 -0
  60. package/src/components/NavigationMenu.tsx +27 -15
  61. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  62. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  63. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  64. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  65. package/src/components/SmallMultiples/index.tsx +3 -0
  66. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
  67. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  68. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  69. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
  70. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
  71. package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
  72. package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
  73. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
  74. package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
  75. package/src/components/UsaMap/helpers/map.ts +4 -4
  76. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  77. package/src/components/WorldMap/WorldMap.tsx +193 -35
  78. package/src/components/ZoomControls.tsx +6 -9
  79. package/src/context/LegendMemoContext.tsx +30 -0
  80. package/src/context.ts +1 -40
  81. package/src/data/initial-state.js +153 -130
  82. package/src/data/supported-geos.js +25 -78
  83. package/src/helpers/addUIDs.ts +13 -2
  84. package/src/helpers/applyColorToLegend.ts +140 -20
  85. package/src/helpers/applyLegendToRow.ts +10 -6
  86. package/src/helpers/componentHelpers.ts +8 -0
  87. package/src/helpers/constants.ts +12 -14
  88. package/src/helpers/dataTableHelpers.ts +6 -0
  89. package/src/helpers/displayGeoName.ts +18 -3
  90. package/src/helpers/generateRuntimeLegend.ts +44 -10
  91. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  92. package/src/helpers/getColumnNames.ts +1 -1
  93. package/src/helpers/getCountriesPicked.ts +103 -0
  94. package/src/helpers/getMapContainerClasses.ts +7 -0
  95. package/src/helpers/getPatternForRow.ts +33 -0
  96. package/src/helpers/getStatesPicked.ts +8 -5
  97. package/src/helpers/index.ts +3 -3
  98. package/src/helpers/isLegendItemDisabled.ts +16 -0
  99. package/src/helpers/mapObserverHelpers.ts +40 -0
  100. package/src/helpers/resetLegendToggles.ts +3 -2
  101. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  102. package/src/helpers/tests/titleCase.test.ts +76 -0
  103. package/src/helpers/titleCase.ts +13 -13
  104. package/src/helpers/toggleLegendActive.ts +6 -11
  105. package/src/helpers/urlDataHelpers.ts +70 -0
  106. package/src/hooks/useCountryZoom.tsx +241 -0
  107. package/src/hooks/useGeoClickHandler.ts +36 -2
  108. package/src/hooks/useLegendMemo.ts +17 -0
  109. package/src/hooks/useMapLayers.tsx +5 -4
  110. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  111. package/src/hooks/useResizeObserver.ts +5 -2
  112. package/src/hooks/useStateZoom.tsx +30 -8
  113. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  114. package/src/hooks/useTooltip.ts +1 -2
  115. package/src/index.jsx +1 -2
  116. package/src/scss/editor-panel.scss +4 -440
  117. package/src/scss/main.scss +1 -1
  118. package/src/scss/map.scss +12 -15
  119. package/src/store/map.actions.ts +7 -7
  120. package/src/store/map.reducer.ts +17 -6
  121. package/src/test/CdcMap.test.jsx +11 -0
  122. package/src/types/MapConfig.ts +46 -18
  123. package/src/types/MapContext.ts +6 -7
  124. package/src/types/runtimeLegend.ts +17 -1
  125. package/vite.config.js +2 -7
  126. package/vitest.config.ts +16 -0
  127. package/src/components/DataTable.tsx +0 -385
  128. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  129. package/src/coreStyles_map.scss +0 -3
  130. package/src/helpers/colorDistributions.ts +0 -12
  131. package/src/helpers/generateColorsArray.ts +0 -14
  132. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  133. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
  134. package/src/hooks/useActiveElement.ts +0 -19
  135. package/src/scss/mixins.scss +0 -47
  136. package/src/types/Annotations.ts +0 -24
  137. /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
@@ -1,10 +1,11 @@
1
- import { useContext, useEffect, useState } from 'react'
1
+ import { useContext, useMemo } from 'react'
2
2
  import { scaleLinear } from 'd3-scale'
3
3
  import { GlyphCircle, GlyphDiamond, GlyphSquare, GlyphStar, GlyphTriangle } from '@visx/glyph'
4
4
  import ConfigContext from '../context'
5
+ import { useLegendMemoContext } from '../context/LegendMemoContext'
5
6
  import { supportedCities } from '../data/supported-geos'
6
7
  import { getFilterControllingStatesPicked } from './UsaMap/helpers/map'
7
- import { displayGeoName, getGeoStrokeColor, SVG_HEIGHT, SVG_PADDING, SVG_WIDTH, titleCase } from '../helpers'
8
+ import { displayGeoName, getGeoStrokeColor, SVG_HEIGHT, SVG_PADDING, SVG_WIDTH, isLegendItemDisabled } from '../helpers'
8
9
  import useGeoClickHandler from '../hooks/useGeoClickHandler'
9
10
  import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
10
11
  import { applyLegendToRow } from '../helpers/applyLegendToRow'
@@ -18,72 +19,86 @@ type CityListProps = {
18
19
  }
19
20
 
20
21
  const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValueSupported, tooltipId, projection }) => {
21
- const {
22
- config,
23
- topoData,
24
- data: runtimeData,
25
- position,
26
- legendMemo,
27
- legendSpecialClassLastMemo,
28
- runtimeLegend
29
- } = useContext(ConfigContext)
22
+ const { config, topoData, runtimeData, position, runtimeLegend } = useContext(ConfigContext)
23
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
30
24
  const { geoClickHandler } = useGeoClickHandler()
31
25
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
32
26
 
33
- const { geoColumnName, latitudeColumnName, longitudeColumnName, primaryColumnName } = getColumnNames(config.columns)
34
- const { additionalCityStyles } = config.visual || []
27
+ const { geoColumnName, latitudeColumnName, longitudeColumnName, primaryColumnName } =
28
+ getColumnNames(config.columns) || {}
35
29
 
36
- if (!projection) return
30
+ // Memoize expensive city data creation
31
+ const citiesData = useMemo(() => {
32
+ if (!runtimeData) return {}
37
33
 
38
- const citiesData = runtimeData
39
- ? Object.keys(runtimeData).reduce((acc, key) => {
40
- const city = runtimeData[key]
34
+ return Object.keys(runtimeData).reduce((acc, key) => {
35
+ const city = runtimeData[key]
36
+ if (city && city[geoColumnName]) {
41
37
  acc[city[geoColumnName]] = city
42
- return acc
43
- }, {})
44
- : {}
38
+ }
39
+ return acc
40
+ }, {})
41
+ }, [runtimeData, geoColumnName])
42
+
43
+ // Memoize bubble size calculation
44
+ const size = useMemo(() => {
45
+ if (config.general.type !== 'bubble' || !runtimeData) {
46
+ return null
47
+ }
45
48
 
46
- if (config.general.type === 'bubble') {
47
- const maxDataValue = Math.max(
48
- ...(runtimeData ? Object.keys(runtimeData).map(key => runtimeData[key][config.columns.primary.name]) : [0])
49
- )
50
- const sortedRuntimeData = Object.values(runtimeData).sort((a, b) =>
51
- a[primaryColumnName] < b[primaryColumnName] ? 1 : -1
52
- )
53
- if (!sortedRuntimeData) return
49
+ const maxVal = Math.max(...Object.keys(runtimeData).map(key => runtimeData[key][config.columns.primary.name]))
50
+
51
+ if (maxVal <= 0) {
52
+ return null
53
+ }
54
+
55
+ return scaleLinear().domain([1, maxVal]).range([config.visual.minBubbleSize, config.visual.maxBubbleSize])
56
+ }, [
57
+ config.general.type,
58
+ config.columns.primary.name,
59
+ config.visual.minBubbleSize,
60
+ config.visual.maxBubbleSize,
61
+ runtimeData
62
+ ])
63
+
64
+ // Get the list of cities to render
65
+ const cityList = useMemo(() => {
66
+ return Object.keys(citiesData).filter(cityName => cityName && citiesData[cityName])
67
+ }, [citiesData])
68
+
69
+ // Early exit for map types that don't use city rendering
70
+ if (!projection) {
71
+ return null
72
+ }
54
73
 
55
- // Set bubble sizes
56
- var size = scaleLinear().domain([1, maxDataValue]).range([config.visual.minBubbleSize, config.visual.maxBubbleSize])
74
+ // Early exit if no cities to render
75
+ if (!cityList.length) {
76
+ return null
57
77
  }
58
- const cityList = Object.keys(citiesData).filter(c => undefined !== c || undefined !== runtimeData[c])
59
- if (!cityList) return true
60
78
 
61
79
  // Cities output
62
80
  return cityList.map((city, i) => {
63
- let geoData: Object
64
- if (runtimeData) {
65
- Object.keys(runtimeData).forEach(key => {
66
- if (city === runtimeData[key][config.columns.geo.name]) {
67
- geoData = runtimeData[key]
68
- }
69
- })
70
- }
81
+ // Get the city data directly from our memoized citiesData
82
+ const geoData = citiesData[city]
83
+
71
84
  if (!geoData) {
72
- geoData = runtimeData ? runtimeData[city] : undefined
85
+ return null
73
86
  }
74
- const cityDisplayName = titleCase(displayGeoName(city))
75
87
 
76
- const legendColors = geoData
77
- ? applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
78
- : runtimeData[city]
79
- ? applyLegendToRow(runtimeData[city], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
80
- : false
88
+ const cityDisplayName = displayGeoName(city)
81
89
 
82
- if (legendColors === false) {
83
- return true
90
+ const legendColors = applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
91
+
92
+ if (!legendColors || legendColors.length === 0) {
93
+ return null
94
+ }
95
+
96
+ // Don't render if legend item is disabled
97
+ if (isLegendItemDisabled(geoData, runtimeLegend, legendMemo, legendSpecialClassLastMemo, config)) {
98
+ return null
84
99
  }
85
100
 
86
- const toolTip = applyTooltipsToGeo(cityDisplayName, geoData || runtimeData[city])
101
+ const toolTip = applyTooltipsToGeo(cityDisplayName, geoData)
87
102
 
88
103
  const radius = config.visual.geoCodeCircleSize || 8
89
104
 
@@ -94,19 +109,21 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
94
109
  const geoStrokeColor = getGeoStrokeColor(config)
95
110
 
96
111
  const pin = (
97
- <path
98
- className='marker'
99
- d='M0,0l-8.8-17.7C-12.1-24.3-7.4-32,0-32h0c7.4,0,12.1,7.7,8.8,14.3L0,0z'
100
- title='Select for more information'
101
- onClick={() => geoClickHandler(cityDisplayName, geoData)}
102
- data-tooltip-id={`tooltip__${tooltipId}`}
103
- data-tooltip-html={toolTip}
104
- transform={`scale(${radius / 7.5})`}
105
- stroke={geoStrokeColor}
106
- strokeWidth={'2px'}
107
- tabIndex='-1'
108
- {...additionalProps}
109
- />
112
+ <g>
113
+ <title>Select for more information</title>
114
+ <path
115
+ className='marker'
116
+ d='M0,0l-8.8-17.7C-12.1-24.3-7.4-32,0-32h0c7.4,0,12.1,7.7,8.8,14.3L0,0z'
117
+ onClick={() => geoClickHandler(cityDisplayName, geoData)}
118
+ data-tooltip-id={`tooltip__${tooltipId}`}
119
+ data-tooltip-html={toolTip}
120
+ transform={`scale(${radius / 7.5})`}
121
+ stroke={geoStrokeColor}
122
+ strokeWidth={'2px'}
123
+ tabIndex={-1}
124
+ {...additionalProps}
125
+ />
126
+ </g>
110
127
  )
111
128
 
112
129
  let transform = ''
@@ -130,7 +147,7 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
130
147
 
131
148
  if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName] && config.general.geoType === 'single-state') {
132
149
  const statesPicked = getFilterControllingStatesPicked(config, runtimeData)
133
- const _statesPickedData = topoData?.states?.find(s => statesPicked.includes(s.properties.name))
150
+ const _statesPickedData = (topoData as any)?.states?.find(s => statesPicked.includes(s.properties.name))
134
151
 
135
152
  const newProjection = projection.fitExtent(
136
153
  [
@@ -141,7 +158,7 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
141
158
  )
142
159
  let coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
143
160
  transform = `translate(${newProjection(coords)}) scale(${
144
- config.visual.geoCodeCircleSize / (position.zoom > 1 ? position.zoom : 1)
161
+ config.visual.geoCodeCircleSize / ((position as any).zoom > 1 ? (position as any).zoom : 1)
145
162
  })`
146
163
  needsPointer = true
147
164
  }
@@ -181,8 +198,7 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
181
198
 
182
199
  const shapeProps = {
183
200
  onClick: () => geoClickHandler(cityDisplayName, geoData),
184
- size: config.general.type === 'bubble' ? size(geoData[primaryColumnName]) : radius * 30,
185
- title: 'Select for more information',
201
+ size: config.general.type === 'bubble' && size ? size(geoData[primaryColumnName]) : radius * 30,
186
202
  'data-tooltip-id': `tooltip__${tooltipId}`,
187
203
  'data-tooltip-html': toolTip,
188
204
  stroke: geoStrokeColor,
@@ -200,48 +216,10 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
200
216
  triangle: <GlyphTriangle {...shapeProps} />
201
217
  }
202
218
 
203
- const cityStyle = Object.values(runtimeData)
204
- .filter(d => additionalCityStyles?.some(style => String(d[style.column]) === String(style.value)))
205
- .map(d => {
206
- const conditionsMatched = additionalCityStyles.find(style => String(d[style.column]) === String(style.value))
207
- return { ...conditionsMatched, ...d }
208
- })
209
- .find(item => {
210
- return Object.keys(item).find(key => item[key] === city)
211
- })
212
-
213
- if (cityStyle !== undefined && cityStyle.shape) {
214
- if (
215
- !geoData?.[longitudeColumnName] &&
216
- !geoData?.[latitudeColumnName] &&
217
- city &&
218
- supportedCities[city.toUpperCase()]
219
- ) {
220
- let translate = `translate(${projection(supportedCities[city.toUpperCase()])})`
221
-
222
- return (
223
- <g key={i} transform={translate} style={styles} className='geo-point' tabIndex={-1}>
224
- {cityStyleShapes[cityStyle.shape.toLowerCase()]}
225
- </g>
226
- )
227
- }
228
-
229
- if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName]) {
230
- const coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
231
- let translate = `translate(${projection(coords)})`
232
-
233
- return (
234
- <g key={i} transform={translate} style={styles} className='geo-point' tabIndex={-1}>
235
- {cityStyleShapes[cityStyle.shape.toLowerCase()]}
236
- </g>
237
- )
238
- }
239
- }
240
- if (legendColors?.[0] === '#000000') return
241
-
219
+ // Render the city marker
242
220
  return (
243
221
  <g key={i} transform={transform} style={styles} className='geo-point' tabIndex={-1}>
244
- {cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
222
+ {cityStyleShapes[config.visual.cityStyle?.toLowerCase() || 'circle']}
245
223
  </g>
246
224
  )
247
225
  })