@cdc/map 4.26.4 → 4.26.5

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 (44) hide show
  1. package/CONFIG.md +70 -37
  2. package/LICENSE +201 -0
  3. package/README.md +6 -2
  4. package/dist/cdcmap.js +23502 -22964
  5. package/examples/default-county.json +3 -0
  6. package/examples/minimal-example.json +6 -2
  7. package/package.json +3 -3
  8. package/src/CdcMapComponent.tsx +13 -3
  9. package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
  10. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +15 -16
  11. package/src/_stories/CdcMap.FocusVisibility.stories.tsx +87 -0
  12. package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
  13. package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
  14. package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
  15. package/src/_stories/CdcMap.smoke.stories.tsx +48 -0
  16. package/src/_stories/_mock/alt_text_metadata.json +65 -0
  17. package/src/_stories/_mock/world-bubble-reset.json +138 -0
  18. package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
  19. package/src/components/BubbleList.tsx +13 -0
  20. package/src/components/EditorPanel/components/EditorPanel.tsx +134 -0
  21. package/src/components/FilterControls.tsx +21 -0
  22. package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
  23. package/src/components/UsaMap/components/UsaMap.County.tsx +39 -9
  24. package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
  25. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
  26. package/src/components/UsaMap/components/UsaMap.State.tsx +9 -2
  27. package/src/components/WorldMap/WorldMap.tsx +37 -4
  28. package/src/components/ZoomableGroup.tsx +23 -3
  29. package/src/components/filterControls.styles.css +6 -0
  30. package/src/data/initial-state.js +2 -0
  31. package/src/helpers/countyTerritories.ts +1 -1
  32. package/src/helpers/generateRuntimeFilters.ts +2 -1
  33. package/src/helpers/handleMapAriaLabels.ts +45 -30
  34. package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
  35. package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
  36. package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
  37. package/src/hooks/useGeoClickHandler.ts +13 -1
  38. package/src/hooks/useStateZoom.tsx +39 -20
  39. package/src/hooks/useTooltip.test.tsx +2 -16
  40. package/src/index.jsx +5 -2
  41. package/src/scss/main.scss +6 -21
  42. package/src/scss/map.scss +20 -0
  43. package/src/types/MapConfig.ts +5 -0
  44. package/src/types/MapContext.ts +3 -0
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { shouldAutoResetSingleStateZoom } from '../shouldAutoResetSingleStateZoom'
3
+
4
+ describe('shouldAutoResetSingleStateZoom', () => {
5
+ it('returns false on the first runtime data load', () => {
6
+ expect(
7
+ shouldAutoResetSingleStateZoom({
8
+ isDashboard: true,
9
+ previousRuntimeDataHash: null,
10
+ nextRuntimeDataHash: 123,
11
+ hasDashboardFilters: true
12
+ })
13
+ ).toBe(false)
14
+ })
15
+
16
+ it('returns true when filtered dashboard runtime data changes after initial load', () => {
17
+ expect(
18
+ shouldAutoResetSingleStateZoom({
19
+ isDashboard: true,
20
+ previousRuntimeDataHash: 123,
21
+ nextRuntimeDataHash: 456,
22
+ hasDashboardFilters: true
23
+ })
24
+ ).toBe(true)
25
+ })
26
+
27
+ it('returns false when the runtime data hash is unchanged', () => {
28
+ expect(
29
+ shouldAutoResetSingleStateZoom({
30
+ isDashboard: true,
31
+ previousRuntimeDataHash: 123,
32
+ nextRuntimeDataHash: 123,
33
+ hasDashboardFilters: true
34
+ })
35
+ ).toBe(false)
36
+ })
37
+
38
+ it('returns false outside dashboards', () => {
39
+ expect(
40
+ shouldAutoResetSingleStateZoom({
41
+ isDashboard: false,
42
+ previousRuntimeDataHash: 123,
43
+ nextRuntimeDataHash: 456,
44
+ hasDashboardFilters: true
45
+ })
46
+ ).toBe(false)
47
+ })
48
+
49
+ it('returns false when map zoom is disabled', () => {
50
+ expect(
51
+ shouldAutoResetSingleStateZoom({
52
+ isDashboard: true,
53
+ previousRuntimeDataHash: 123,
54
+ nextRuntimeDataHash: 456,
55
+ hasDashboardFilters: true,
56
+ allowMapZoom: false
57
+ })
58
+ ).toBe(false)
59
+ })
60
+
61
+ it('returns false when dashboard filters are not present on the visualization', () => {
62
+ expect(
63
+ shouldAutoResetSingleStateZoom({
64
+ isDashboard: true,
65
+ previousRuntimeDataHash: 123,
66
+ nextRuntimeDataHash: 456,
67
+ hasDashboardFilters: false
68
+ })
69
+ ).toBe(false)
70
+ })
71
+ })
@@ -9,6 +9,9 @@ const useGeoClickHandler = () => {
9
9
  config: state,
10
10
  setConfig,
11
11
  setSharedFilter,
12
+ clearSharedFilter,
13
+ hasActiveSharedFilter,
14
+ setSharedFilterValue,
12
15
  customNavigationHandler,
13
16
  interactionLabel
14
17
  } = useContext(ConfigContext)
@@ -16,7 +19,16 @@ const useGeoClickHandler = () => {
16
19
 
17
20
  const geoClickHandler = (geoDisplayName: string, geoData: object): void => {
18
21
  if (setSharedFilter) {
19
- setSharedFilter(state.uid, geoData)
22
+ // Get the column name for the filter (from dashboardFilters config)
23
+ const filterColumnName = state.dashboardFilters?.[0]?.columnName || state.columns?.geo?.name
24
+ const clickedValue = filterColumnName ? geoData[filterColumnName] : geoDisplayName
25
+
26
+ // Toggle behavior: if the clicked value matches the current filter value, clear it
27
+ if (hasActiveSharedFilter && setSharedFilterValue === clickedValue && clearSharedFilter) {
28
+ clearSharedFilter(state.uid)
29
+ } else {
30
+ setSharedFilter(state.uid, geoData)
31
+ }
20
32
  }
21
33
 
22
34
  // If world-geocode map zoom to geo point
@@ -19,7 +19,7 @@ interface StateData {
19
19
  }
20
20
 
21
21
  const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
22
- const { config, runtimeData, position, interactionLabel } = useContext<MapContext>(ConfigContext)
22
+ const { config, runtimeData, position, scale, translate, interactionLabel } = useContext<MapContext>(ConfigContext)
23
23
  const dispatch = useContext(MapDispatchContext)
24
24
 
25
25
  // Get statesPicked with memoization
@@ -72,8 +72,42 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
72
72
  return { projection, newProjection, stateCenter, bounds }
73
73
  }, [topoData, statesPicked])
74
74
 
75
+ const resetZoomState = useCallback(
76
+ ({ publishEvent = true }: { publishEvent?: boolean } = {}) => {
77
+ const nextCoordinates = projectionData.stateCenter
78
+ const alreadyAtDefaultPosition =
79
+ position.zoom === 1 &&
80
+ position.coordinates[0] === nextCoordinates[0] &&
81
+ position.coordinates[1] === nextCoordinates[1]
82
+ const alreadyAtDefaultTransform = scale === 1 && translate[0] === 0 && translate[1] === 0
83
+
84
+ if (alreadyAtDefaultPosition && alreadyAtDefaultTransform) return
85
+
86
+ dispatch({ type: 'SET_POSITION', payload: { coordinates: nextCoordinates, zoom: 1 } })
87
+ dispatch({ type: 'SET_TRANSLATE', payload: [0, 0] })
88
+ dispatch({ type: 'SET_SCALE', payload: 1 })
89
+
90
+ if (publishEvent) {
91
+ publishAnalyticsEvent({
92
+ vizType: 'map',
93
+ vizSubType: getVizSubType(config),
94
+ eventType: 'map_reset_zoom_level',
95
+ eventAction: 'click',
96
+ eventLabel: interactionLabel,
97
+ vizTitle: getVizTitle(config)
98
+ })
99
+ }
100
+ },
101
+ [config, dispatch, interactionLabel, position, projectionData.stateCenter, scale, translate]
102
+ )
103
+
75
104
  const setScaleAndTranslate = useCallback(
76
105
  (zoomFunction: string = '') => {
106
+ if (zoomFunction === 'reset') {
107
+ resetZoomState()
108
+ return
109
+ }
110
+
77
111
  const _prevPosition = position
78
112
  let newZoom = _prevPosition.zoom
79
113
  let newCoordinates = _prevPosition.coordinates
@@ -98,27 +132,11 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
98
132
  _prevPosition.coordinates[0] !== 0 && _prevPosition.coordinates[1] !== 0
99
133
  ? _prevPosition.coordinates
100
134
  : projectionData.stateCenter
101
- } else if (zoomFunction === 'reset') {
102
- newZoom = 1
103
- newCoordinates = projectionData.stateCenter
104
135
  }
105
136
 
106
137
  dispatch({ type: 'SET_POSITION', payload: { coordinates: newCoordinates, zoom: newZoom } })
107
-
108
- if (zoomFunction === 'reset') {
109
- dispatch({ type: 'SET_TRANSLATE', payload: [0, 0] }) // needed for state switcher
110
- dispatch({ type: 'SET_SCALE', payload: 1 }) // needed for state switcher
111
- publishAnalyticsEvent({
112
- vizType: 'map',
113
- vizSubType: getVizSubType(config),
114
- eventType: 'map_reset_zoom_level',
115
- eventAction: 'click',
116
- eventLabel: interactionLabel,
117
- vizTitle: getVizTitle(config)
118
- })
119
- }
120
138
  },
121
- [position, projectionData.stateCenter, interactionLabel, dispatch]
139
+ [config, dispatch, interactionLabel, position, projectionData.stateCenter, resetZoomState]
122
140
  )
123
141
 
124
142
  // Essential fix: Remove config from dependencies to prevent infinite loops
@@ -165,9 +183,9 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
165
183
 
166
184
  const handleZoomReset = useCallback(
167
185
  _setRuntimeData => {
168
- setScaleAndTranslate('reset')
186
+ resetZoomState()
169
187
  },
170
- [setScaleAndTranslate]
188
+ [resetZoomState]
171
189
  )
172
190
 
173
191
  return {
@@ -178,6 +196,7 @@ const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
178
196
  handleZoomOut,
179
197
  handleMoveEnd,
180
198
  handleZoomReset,
199
+ resetZoomState,
181
200
  projection: projectionData.projection,
182
201
  bounds: projectionData.bounds
183
202
  }
@@ -1,10 +1,6 @@
1
1
  import { renderHook } from '@testing-library/react'
2
2
  import useTooltip from './useTooltip'
3
3
 
4
- const supportedStatesFipsCodes = {
5
- '01': 'Alabama'
6
- }
7
-
8
4
  const createConfig = (hideGeoColumnInTooltip: boolean) => ({
9
5
  general: {
10
6
  geoType: 'world',
@@ -41,18 +37,8 @@ describe('useTooltip', () => {
41
37
  it('hides the geography column label in the tooltip body when configured', () => {
42
38
  const row = { Country: 'ssd', Value: 10 }
43
39
 
44
- const { result: visibleResult } = renderHook(() =>
45
- useTooltip({
46
- config: createConfig(false),
47
- supportedStatesFipsCodes
48
- })
49
- )
50
- const { result: hiddenResult } = renderHook(() =>
51
- useTooltip({
52
- config: createConfig(true),
53
- supportedStatesFipsCodes
54
- })
55
- )
40
+ const { result: visibleResult } = renderHook(() => useTooltip(createConfig(false) as any))
41
+ const { result: hiddenResult } = renderHook(() => useTooltip(createConfig(true) as any))
56
42
 
57
43
  const visibleTooltip = visibleResult.current.buildTooltip(row, 'South Sudan')
58
44
  const hiddenTooltip = hiddenResult.current.buildTooltip(row, 'South Sudan')
package/src/index.jsx CHANGED
@@ -8,13 +8,16 @@ import CdcMap from './CdcMap'
8
8
 
9
9
  let isEditor = window.location.href.includes('editor=true')
10
10
  let domContainer = document.getElementsByClassName('react-container')[0]
11
+ let configUrl = domContainer.dataset.configUrl
12
+ let injectedConfig = domContainer.coveConfig
11
13
 
12
14
  ReactDOM.createRoot(domContainer).render(
13
15
  <React.StrictMode>
14
16
  <CdcMap
15
17
  isEditor={isEditor}
16
- configUrl={domContainer.attributes['data-config'].value}
17
- interactionLabel={domContainer.attributes['data-config'].value}
18
+ config={injectedConfig}
19
+ configUrl={injectedConfig ? undefined : configUrl}
20
+ interactionLabel={configUrl}
18
21
  containerEl={domContainer}
19
22
  />
20
23
  </React.StrictMode>
@@ -2,6 +2,7 @@
2
2
  @import 'editor-panel';
3
3
  @import '@cdc/core/styles/utils/accessibility';
4
4
  @import '@cdc/core/styles/layout/wrapper-padding';
5
+ @import '@cdc/core/styles/layout/callout';
5
6
 
6
7
  .type-map--has-error {
7
8
  .waiting {
@@ -17,7 +18,7 @@
17
18
  position: relative;
18
19
  display: flex; // Needed for the main content
19
20
 
20
- .loading>div.la-ball-beat {
21
+ .loading > div.la-ball-beat {
21
22
  margin-top: 20%;
22
23
  }
23
24
 
@@ -125,33 +126,17 @@
125
126
  background: transparent;
126
127
 
127
128
  .cdc-callout {
128
- box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
129
- border: 1px solid #dff2f6;
130
- margin: 0;
131
- padding: 1.25rem;
132
- border-radius: 0.25rem;
133
- position: relative;
134
- background: transparent;
135
-
136
- .cdc-callout__flag {
137
- position: absolute;
138
- top: -0.36rem;
139
- right: 1.08rem;
140
- width: 1.84rem;
141
- height: auto;
142
- }
129
+ --cdc-callout-background: transparent;
143
130
 
144
131
  .cove-visualization__title,
145
132
  .cove-visualization__header {
146
133
  background: transparent;
147
134
  border: 0;
148
- color: var(--cool-gray-90, #1f2937);
149
135
  margin: 0 0 1rem;
150
136
  padding: 0;
151
137
  border-radius: 0;
152
138
 
153
139
  h2 {
154
- color: var(--cool-gray-90, #1f2937);
155
140
  font-family: var(--fonts-nunito, var(--app-font-secondary));
156
141
  font-size: 1.1rem;
157
142
  font-weight: 700;
@@ -178,7 +163,7 @@
178
163
  flex-direction: column;
179
164
  row-gap: var(--cove-visualization-section-gap, 1.5rem);
180
165
 
181
- >.legends {
166
+ > .legends {
182
167
  margin-top: var(--cove-visualization-section-gap, 1.5rem) !important;
183
168
  }
184
169
  }
@@ -271,7 +256,7 @@
271
256
  }
272
257
  }
273
258
 
274
- .visualization-container.legend-wrapped-bottom>.legends,
259
+ .visualization-container.legend-wrapped-bottom > .legends,
275
260
  .legends.legend-wrapped-bottom {
276
261
  margin-top: var(--cove-visualization-section-gap, 1.5rem) !important;
277
262
  }
@@ -311,7 +296,7 @@
311
296
  label {
312
297
  flex-grow: 1;
313
298
 
314
- >div.select-heading {
299
+ > div.select-heading {
315
300
  font-size: 1.1em;
316
301
  font-weight: 600;
317
302
  margin-bottom: 0.75em;
package/src/scss/map.scss CHANGED
@@ -5,11 +5,13 @@
5
5
  flex-direction: row;
6
6
  }
7
7
  }
8
+
8
9
  .visualization-container.map {
9
10
  &.side {
10
11
  flex-direction: row;
11
12
  }
12
13
  }
14
+
13
15
  // Bubble Specific
14
16
  .visualization-container.bubble {
15
17
  &.side {
@@ -30,6 +32,7 @@ $medium: 768px;
30
32
  .single-geo {
31
33
  transition: 0.2s fill;
32
34
  cursor: pointer;
35
+
33
36
  &:focus {
34
37
  outline: 0;
35
38
  }
@@ -45,6 +48,20 @@ $medium: 768px;
45
48
  }
46
49
  }
47
50
 
51
+ .svg-container,
52
+ .county,
53
+ .state-path,
54
+ .bubble,
55
+ .geo-point,
56
+ .marker,
57
+ .bubble *,
58
+ .geo-point * {
59
+ &:focus,
60
+ &:focus-visible {
61
+ outline: none;
62
+ }
63
+ }
64
+
48
65
  .territories-label {
49
66
  font-weight: 700;
50
67
  }
@@ -52,6 +69,7 @@ $medium: 768px;
52
69
  // Cities and Territories
53
70
  .territories {
54
71
  gap: 0.5em;
72
+
55
73
  svg {
56
74
  max-width: var(--territory-svg-max-width, 30px);
57
75
  min-width: var(--territory-svg-min-width, 30px);
@@ -60,6 +78,7 @@ $medium: 768px;
60
78
  text {
61
79
  font-size: var(--territory-label-font-size);
62
80
  font-weight: 900;
81
+
63
82
  @include breakpointClass(sm) {
64
83
  font-size: var(--territory-label-font-size-mobile);
65
84
  }
@@ -71,6 +90,7 @@ $medium: 768px;
71
90
  .zoom-controls > button:not(.reset) {
72
91
  height: 2.5em;
73
92
  width: 2.5em;
93
+
74
94
  svg {
75
95
  height: 2.5em;
76
96
  width: 2.5em;
@@ -5,6 +5,7 @@ import { type Version } from '@cdc/core/types/Version'
5
5
  import { type VizFilter } from '@cdc/core/types/VizFilter'
6
6
  import { type Annotation } from '@cdc/core/types/Annotation'
7
7
  import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
8
+ import { AltTextConfig } from '@cdc/core/types/AltText'
8
9
 
9
10
  // Runtime data types
10
11
  export type RuntimeFilters = VizFilter[] & { fromHash?: number }
@@ -110,6 +111,7 @@ export type SmallMultiples = {
110
111
  }
111
112
 
112
113
  export type MapConfig = Visualization & {
114
+ altText?: AltTextConfig
113
115
  annotations: Annotation[]
114
116
  // map color palette
115
117
  color: string
@@ -132,6 +134,7 @@ export type MapConfig = Visualization & {
132
134
  subtext: string
133
135
  introText: string
134
136
  allowMapZoom: boolean
137
+ showClearSelectionButton?: boolean
135
138
  convertFipsCodes: boolean
136
139
  displayAsHex: boolean
137
140
  displayStateLabels: boolean
@@ -226,6 +229,8 @@ export type MapConfig = Visualization & {
226
229
  forceDisplay: boolean
227
230
  indexLabel: string
228
231
  cellMinWidth: string
232
+ search?: boolean
233
+ searchPlaceholder?: string
229
234
  }
230
235
  tooltips: {
231
236
  appearanceType: 'hover' | 'click'
@@ -40,6 +40,9 @@ export type MapContext = {
40
40
  setParentConfig: Function
41
41
  setRuntimeData: Function
42
42
  setFilteredStateCountyCode: (stateCode: string, countyCode?: string) => void
43
+ setSharedFilter?: Function
44
+ clearSharedFilter?: (key: string) => void
45
+ hasActiveSharedFilter?: boolean
43
46
  setSharedFilterValue: Function
44
47
  setConfig: (newState: MapConfig) => MapConfig
45
48
  config: MapConfig