@cdc/map 4.26.2 → 4.26.3

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 (65) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcmap-vr9HZwRt.es.js +6 -0
  3. package/dist/cdcmap.js +26781 -24615
  4. package/examples/private/annotation-bug.json +642 -0
  5. package/package.json +3 -3
  6. package/src/CdcMap.tsx +3 -14
  7. package/src/CdcMapComponent.tsx +214 -159
  8. package/src/_stories/CdcMap.Defaults.stories.tsx +76 -0
  9. package/src/_stories/CdcMap.Editor.stories.tsx +187 -14
  10. package/src/_stories/CdcMap.stories.tsx +11 -1
  11. package/src/_stories/Map.HTMLInDataTable.stories.tsx +385 -0
  12. package/src/_stories/_mock/multi-state-show-unselected.json +82 -0
  13. package/src/cdcMapComponent.styles.css +2 -2
  14. package/src/components/Annotation/Annotation.Draggable.styles.css +4 -4
  15. package/src/components/Annotation/AnnotationDropdown.styles.css +1 -1
  16. package/src/components/Annotation/AnnotationList.styles.css +13 -13
  17. package/src/components/EditorPanel/components/EditorPanel.tsx +426 -58
  18. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings-style.css +1 -1
  19. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +5 -2
  20. package/src/components/EditorPanel/components/editorPanel.styles.css +34 -24
  21. package/src/components/Legend/components/Legend.tsx +9 -4
  22. package/src/components/Legend/components/LegendGroup/legend.group.css +5 -5
  23. package/src/components/Legend/components/index.scss +2 -3
  24. package/src/components/NavigationMenu.tsx +2 -1
  25. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  26. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +32 -17
  27. package/src/components/UsaMap/components/TerritoriesSection.tsx +3 -2
  28. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +13 -8
  29. package/src/components/UsaMap/components/UsaMap.County.tsx +410 -183
  30. package/src/components/UsaMap/components/UsaMap.Region.styles.css +1 -1
  31. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +2 -2
  32. package/src/components/UsaMap/components/UsaMap.State.tsx +13 -8
  33. package/src/components/WorldMap/WorldMap.tsx +10 -13
  34. package/src/components/WorldMap/data/world-topo-updated.json +1 -0
  35. package/src/components/WorldMap/data/world-topo.json +1 -1
  36. package/src/components/WorldMap/worldMap.styles.css +1 -1
  37. package/src/components/ZoomControls.tsx +49 -18
  38. package/src/components/zoomControls.styles.css +27 -11
  39. package/src/data/initial-state.js +14 -5
  40. package/src/data/legacy-defaults.ts +8 -0
  41. package/src/data/supported-geos.js +19 -0
  42. package/src/helpers/colors.ts +2 -1
  43. package/src/helpers/dataTableHelpers.ts +56 -0
  44. package/src/helpers/displayGeoName.ts +19 -11
  45. package/src/helpers/getMapContainerClasses.ts +8 -2
  46. package/src/helpers/getMatchingPatternForRow.ts +67 -0
  47. package/src/helpers/getPatternForRow.ts +11 -18
  48. package/src/helpers/tests/dataTableHelpers.test.ts +78 -0
  49. package/src/helpers/tests/displayGeoName.test.ts +17 -0
  50. package/src/helpers/tests/getMatchingPatternForRow.test.ts +150 -0
  51. package/src/helpers/tests/getPatternForRow.test.ts +140 -2
  52. package/src/helpers/urlDataHelpers.ts +7 -1
  53. package/src/hooks/useResizeObserver.ts +36 -22
  54. package/src/hooks/useTooltip.test.tsx +64 -0
  55. package/src/hooks/useTooltip.ts +28 -8
  56. package/src/scss/editor-panel.scss +1 -1
  57. package/src/scss/main.scss +140 -6
  58. package/src/scss/map.scss +9 -4
  59. package/src/store/map.actions.ts +2 -0
  60. package/src/store/map.reducer.ts +4 -0
  61. package/src/test/CdcMap.test.jsx +2 -2
  62. package/src/types/MapConfig.ts +22 -4
  63. package/src/types/MapContext.ts +3 -1
  64. package/dist/cdcmap-Cf9_fbQf.es.js +0 -6
  65. package/src/helpers/componentHelpers.ts +0 -8
@@ -356,7 +356,7 @@ export const GeneralSectionTests: Story = {
356
356
  expect(showTitleCheckbox).toBeTruthy()
357
357
 
358
358
  const getTitleVisibility = () => {
359
- const titleElement = canvasElement.querySelector('.cove-title, header.cove-component__header')
359
+ const titleElement = canvasElement.querySelector('.cove-title, header.cove-visualization__header')
360
360
  return {
361
361
  isPresent: Boolean(titleElement)
362
362
  }
@@ -414,7 +414,7 @@ export const GeneralSectionTests: Story = {
414
414
 
415
415
  const getTitleStyleVisual = () => {
416
416
  const coveTitleElement = canvasElement.querySelector('.cove-title')
417
- const legacyTitleElement = canvasElement.querySelector('header.cove-component__header')
417
+ const legacyTitleElement = canvasElement.querySelector('header.cove-visualization__header')
418
418
 
419
419
  // For modern titles, check for h2 (large) or h3 (small) elements
420
420
  const hasH2 = Boolean(coveTitleElement?.querySelector('h2'))
@@ -1738,7 +1738,7 @@ export const FiltersSectionTests: Story = {
1738
1738
  const labelText = labels.join(',')
1739
1739
 
1740
1740
  // Verify filter dropdown is rendered
1741
- const vizContainer = canvasElement.querySelector('.cdc-open-viz-module')
1741
+ const vizContainer = canvasElement.querySelector('.cove-visualization')
1742
1742
  const filterSelect = Array.from(vizContainer?.querySelectorAll('select') || []).find(select => {
1743
1743
  const options = Array.from(select.options).map(opt => opt.value)
1744
1744
  return options.includes('Alabama')
@@ -2189,22 +2189,29 @@ export const VisualSectionTests: StoryObj<typeof CdcMap> = {
2189
2189
  await performAndAssert(
2190
2190
  'Header Theme → Select purple theme',
2191
2191
  () => {
2192
- const innerContainer = canvasElement.querySelector('.cdc-map-inner-container')
2193
- const currentTheme = Array.from(innerContainer?.classList || []).find(cls => cls.startsWith('theme-'))
2192
+ // Check via the HeaderThemeSelector's selected button state
2193
+ const colorPalettes = canvasElement.querySelectorAll('.color-palette')
2194
+ const themeColorPalette = Array.from(colorPalettes).find(palette =>
2195
+ Array.from(palette.querySelectorAll('button')).some(btn => btn.classList.contains('theme-purple'))
2196
+ )
2197
+ const selectedThemeBtn = themeColorPalette?.querySelector('button.selected') as HTMLElement | null
2194
2198
  return {
2195
- currentTheme: currentTheme || ''
2199
+ currentTheme: selectedThemeBtn
2200
+ ? Array.from(selectedThemeBtn.classList).find(cls => cls.startsWith('theme-')) || ''
2201
+ : ''
2196
2202
  }
2197
2203
  },
2198
2204
  async () => {
2199
- // Find the color palette and click on the purple theme button
2200
- const colorPalette = canvasElement.querySelector('.color-palette')
2201
- const purpleTheme = Array.from(colorPalette?.querySelectorAll('button') || []).find(button =>
2202
- button.classList.contains('theme-purple')
2203
- ) as HTMLElement
2205
+ // Find the header theme selector and click purple
2206
+ const colorPalettes = canvasElement.querySelectorAll('.color-palette')
2207
+ const themeColorPalette = Array.from(colorPalettes).find(palette =>
2208
+ Array.from(palette.querySelectorAll('button')).some(btn => btn.classList.contains('theme-purple'))
2209
+ )
2210
+ const purpleTheme = themeColorPalette?.querySelector('button.theme-purple') as HTMLElement
2204
2211
  await userEvent.click(purpleTheme)
2205
2212
  },
2206
2213
  (before, after) => {
2207
- // After clicking, the header should have the purple theme
2214
+ // After clicking, the purple theme button should be selected
2208
2215
  return after.currentTheme === 'theme-purple'
2209
2216
  }
2210
2217
  )
@@ -2220,7 +2227,7 @@ export const VisualSectionTests: StoryObj<typeof CdcMap> = {
2220
2227
  await performAndAssert(
2221
2228
  'Show Title → Toggle off',
2222
2229
  () => {
2223
- const titleElement = canvasElement.querySelector('.cove-title, header.cove-component__header')
2230
+ const titleElement = canvasElement.querySelector('.cove-title, header.cove-visualization__header')
2224
2231
  return {
2225
2232
  isPresent: Boolean(titleElement)
2226
2233
  }
@@ -2237,7 +2244,7 @@ export const VisualSectionTests: StoryObj<typeof CdcMap> = {
2237
2244
  await performAndAssert(
2238
2245
  'Show Title → Toggle back on',
2239
2246
  () => {
2240
- const titleElement = canvasElement.querySelector('.cove-title, header.cove-component__header')
2247
+ const titleElement = canvasElement.querySelector('.cove-title, header.cove-visualization__header')
2241
2248
  return {
2242
2249
  isPresent: Boolean(titleElement)
2243
2250
  }
@@ -2742,6 +2749,108 @@ export const PatternSettingsSectionTests: Story = {
2742
2749
  (before, after) => after.patternPathCount > 0
2743
2750
  )
2744
2751
 
2752
+ await performAndAssert(
2753
+ 'Pattern Settings → Broad match with blank dataKey (value 55)',
2754
+ () => {
2755
+ const allSvgs = canvasElement.querySelectorAll('svg')
2756
+ let patternPathCount = 0
2757
+
2758
+ allSvgs.forEach(svg => {
2759
+ const allPaths = Array.from(svg.querySelectorAll('path'))
2760
+ patternPathCount += allPaths.filter(path => path.getAttribute('fill')?.startsWith('url(#')).length
2761
+ })
2762
+
2763
+ return { patternPathCount }
2764
+ },
2765
+ async () => {
2766
+ const dataKeySelect = canvasElement.querySelector('select[name^="pattern-dataKey--"]') as HTMLSelectElement
2767
+ const dataValueInput = canvasElement.querySelector('input[id^="pattern-dataValue--"]') as HTMLInputElement
2768
+ if (!dataKeySelect || !dataValueInput) throw new Error('Pattern data controls not found')
2769
+
2770
+ await userEvent.selectOptions(dataKeySelect, '')
2771
+ await userEvent.clear(dataValueInput)
2772
+ await userEvent.type(dataValueInput, '55')
2773
+ await userEvent.tab()
2774
+ },
2775
+ (before, after) => after.patternPathCount > 0
2776
+ )
2777
+
2778
+ await performAndAssert(
2779
+ 'Pattern Settings → Specific match beats broad match',
2780
+ () => {
2781
+ const allSvgs = canvasElement.querySelectorAll('svg')
2782
+ const patternTypeById: Record<string, 'lines' | 'circles' | 'waves' | 'unknown'> = {}
2783
+ const appliedRatePatternTypes = new Set<string>()
2784
+
2785
+ allSvgs.forEach(svg => {
2786
+ const patterns = svg.querySelectorAll('pattern')
2787
+ patterns.forEach(pattern => {
2788
+ const patternId = pattern.getAttribute('id')
2789
+ if (!patternId) return
2790
+
2791
+ if (pattern.querySelector('circle')) patternTypeById[patternId] = 'circles'
2792
+ else if (pattern.querySelector('line')) patternTypeById[patternId] = 'lines'
2793
+ else if (pattern.querySelector('path')) patternTypeById[patternId] = 'waves'
2794
+ else patternTypeById[patternId] = 'unknown'
2795
+ })
2796
+
2797
+ const ratePatternNodes = Array.from(svg.querySelectorAll('path.pattern-geoKey--Rate')).filter(node =>
2798
+ node.getAttribute('fill')?.startsWith('url(#')
2799
+ )
2800
+
2801
+ ratePatternNodes.forEach(node => {
2802
+ const fill = node.getAttribute('fill')
2803
+ const match = fill?.match(/^url\(#(.+)\)$/)
2804
+ const patternId = match?.[1]
2805
+ if (!patternId) return
2806
+ appliedRatePatternTypes.add(patternTypeById[patternId] || 'unknown')
2807
+ })
2808
+ })
2809
+
2810
+ return {
2811
+ hasRateCircle: appliedRatePatternTypes.has('circles'),
2812
+ hasRateWave: appliedRatePatternTypes.has('waves')
2813
+ }
2814
+ },
2815
+ async () => {
2816
+ const firstDataKey = canvasElement.querySelector('select[name="pattern-dataKey--0"]') as HTMLSelectElement
2817
+ const firstDataValue = canvasElement.querySelector('input[id="pattern-dataValue--0"]') as HTMLInputElement
2818
+ const firstPatternType = canvasElement.querySelector('select[name="pattern-type--0"]') as HTMLSelectElement
2819
+ if (!firstDataKey || !firstDataValue || !firstPatternType) {
2820
+ throw new Error('First pattern controls not found')
2821
+ }
2822
+ await userEvent.selectOptions(firstDataKey, 'Rate')
2823
+ await userEvent.clear(firstDataValue)
2824
+ await userEvent.type(firstDataValue, '55')
2825
+ await userEvent.selectOptions(firstPatternType, 'circles')
2826
+
2827
+ const buttons = Array.from(canvasElement.querySelectorAll('button'))
2828
+ const addPatternButton = buttons.find(btn => btn.textContent?.includes('Add Geo Pattern'))
2829
+ if (!addPatternButton) throw new Error('Add Geo Pattern button not found')
2830
+ await userEvent.click(addPatternButton)
2831
+
2832
+ const accordionButtons = Array.from(canvasElement.querySelectorAll('.accordion__button'))
2833
+ const selectColumnButtons = accordionButtons.filter(btn => btn.textContent?.includes('Select Column'))
2834
+ const secondPatternAccordionButton = selectColumnButtons[selectColumnButtons.length - 1] as HTMLElement
2835
+ if (!secondPatternAccordionButton) throw new Error('Second pattern accordion not found')
2836
+ await userEvent.click(secondPatternAccordionButton)
2837
+
2838
+ const secondDataKey = canvasElement.querySelector('select[name="pattern-dataKey--1"]') as HTMLSelectElement
2839
+ const secondDataValue = canvasElement.querySelector('input[id="pattern-dataValue--1"]') as HTMLInputElement
2840
+ const secondPatternType = canvasElement.querySelector('select[name="pattern-type--1"]') as HTMLSelectElement
2841
+
2842
+ if (!secondDataKey || !secondDataValue || !secondPatternType) {
2843
+ throw new Error('Second pattern controls not found')
2844
+ }
2845
+
2846
+ await userEvent.selectOptions(secondDataKey, '')
2847
+ await userEvent.clear(secondDataValue)
2848
+ await userEvent.type(secondDataValue, '55')
2849
+ await userEvent.selectOptions(secondPatternType, 'waves')
2850
+ },
2851
+ (before, after) => after.hasRateCircle && !after.hasRateWave
2852
+ )
2853
+
2745
2854
  await performAndAssert(
2746
2855
  'Pattern Settings → Pattern remains after hover',
2747
2856
  () => {
@@ -3135,6 +3244,70 @@ export const SmallMultiplesSectionTests: Story = {
3135
3244
  }
3136
3245
  }
3137
3246
 
3247
+ export const WorldDataMapZoomControlsTest: Story = {
3248
+ args: {
3249
+ ...DEFAULT_ARGS
3250
+ },
3251
+ play: async ({ canvasElement }) => {
3252
+ const canvas = within(canvasElement)
3253
+
3254
+ await waitForEditor(canvas)
3255
+ await waitForPresence('.map-container', canvasElement)
3256
+ await openAccordion(canvas, 'Type')
3257
+
3258
+ const getZoomControlsState = () => {
3259
+ const zoomControls = canvasElement.querySelector('.zoom-controls')
3260
+ const zoomInButton = canvasElement.querySelector('button[aria-label="Zoom In"]')
3261
+ const zoomOutButton = canvasElement.querySelector('button[aria-label="Zoom Out"]')
3262
+ const mapContainer = canvasElement.querySelector('.map-container')
3263
+
3264
+ return {
3265
+ hasZoomControls: Boolean(zoomControls),
3266
+ hasZoomInButton: Boolean(zoomInButton),
3267
+ hasZoomOutButton: Boolean(zoomOutButton),
3268
+ mapClasses: mapContainer ? Array.from(mapContainer.classList) : []
3269
+ }
3270
+ }
3271
+
3272
+ const worldButton = Array.from(canvasElement.querySelectorAll('.geo-buttons button')).find(button =>
3273
+ button.textContent?.trim().toLowerCase().includes('world')
3274
+ ) as HTMLButtonElement
3275
+ expect(worldButton).toBeTruthy()
3276
+
3277
+ await performAndAssert(
3278
+ 'World Data Map → Switch geo type to world',
3279
+ getZoomControlsState,
3280
+ async () => {
3281
+ await userEvent.click(worldButton)
3282
+ },
3283
+ (before, after) => !before.mapClasses.includes('world') && after.mapClasses.includes('world')
3284
+ )
3285
+
3286
+ const mapTypeSelect = canvas.getByLabelText(/Map Type/i) as HTMLSelectElement
3287
+ await performAndAssert(
3288
+ 'World Data Map → Switch type to data',
3289
+ getZoomControlsState,
3290
+ async () => {
3291
+ await userEvent.selectOptions(mapTypeSelect, 'data')
3292
+ },
3293
+ (_before, after) => after.mapClasses.includes('world')
3294
+ )
3295
+
3296
+ const allowMapZoomingCheckbox = canvas.getByLabelText(/Allow Map Zooming/i) as HTMLInputElement
3297
+ expect(allowMapZoomingCheckbox).toBeTruthy()
3298
+
3299
+ await performAndAssert(
3300
+ 'World Data Map → Enable map zooming',
3301
+ getZoomControlsState,
3302
+ async () => {
3303
+ await userEvent.click(allowMapZoomingCheckbox)
3304
+ },
3305
+ (before, after) =>
3306
+ !before.hasZoomControls && after.hasZoomControls && after.hasZoomInButton && after.hasZoomOutButton
3307
+ )
3308
+ }
3309
+ }
3310
+
3138
3311
  // ==========================================================================
3139
3312
  // MULTI-COUNTRY MAP TESTS
3140
3313
  // ==========================================================================
@@ -5,6 +5,7 @@ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
5
5
  import EqualNumberOptInExample from './_mock/DEV-7286.json'
6
6
  import EqualNumberMap from './_mock/equal-number.json'
7
7
  import MultiState from './_mock/multi-state.json'
8
+ import MultiStateShowUnselected from './_mock/multi-state-show-unselected.json'
8
9
  import MultiCountry from './_mock/multi-country.json'
9
10
  import MultiCountryHide from './_mock/multi-country-hide.json'
10
11
  import SingleStateWithFilters from './_mock/DEV-8942.json'
@@ -42,7 +43,7 @@ const testMapRendering = async (canvasElement: HTMLElement, storyName: string) =
42
43
  })
43
44
 
44
45
  await step('Verify COVE module wrapper is present', async () => {
45
- const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
46
+ const coveModule = canvasElement.querySelector('.cove-visualization')
46
47
  expect(coveModule).toBeInTheDocument()
47
48
  })
48
49
  }
@@ -149,6 +150,15 @@ export const Multi_State: Story = {
149
150
  }
150
151
  }
151
152
 
153
+ export const Multi_State_Show_Unselected: Story = {
154
+ args: {
155
+ config: MultiStateShowUnselected
156
+ },
157
+ play: async ({ canvasElement }) => {
158
+ await assertVisualizationRendered(canvasElement)
159
+ }
160
+ }
161
+
152
162
  export const Multi_Country: Story = {
153
163
  args: {
154
164
  config: MultiCountry
@@ -0,0 +1,385 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import CdcMapComponent from '../CdcMapComponent'
3
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
4
+ import usaStateGradient from './_mock/usa-state-gradient.json'
5
+
6
+ const meta: Meta<typeof CdcMapComponent> = {
7
+ title: 'Components/Templates/Map/HTML in Data Table',
8
+ component: CdcMapComponent,
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'Demonstrates map data tables with colored legend circles rendered inline next to location names, and HTML content in a notes column.'
14
+ }
15
+ }
16
+ }
17
+ }
18
+
19
+ export default meta
20
+
21
+ type Story = StoryObj<typeof CdcMapComponent>
22
+
23
+ const data = [
24
+ { STATE: 'Overall', Rate: '55', Location: 'Vehicle', URL: 'https://www.cdc.gov', Notes: '' },
25
+ {
26
+ STATE: 'Alabama',
27
+ Rate: 130,
28
+ Location: 'Vehicle',
29
+ URL: 'https://www.cdc.gov/',
30
+ Notes: 'Rate increased <strong>8%</strong> from prior year'
31
+ },
32
+ {
33
+ STATE: 'Alaska',
34
+ Rate: 40,
35
+ Location: 'Work',
36
+ URL: 'https://www.cdc.gov/',
37
+ Notes: 'Remote regions excluded from <em>survey</em>'
38
+ },
39
+ { STATE: 'American Samoa', Rate: 55, Location: 'Home', URL: 'https://www.cdc.gov/', Notes: '' },
40
+ {
41
+ STATE: 'Arizona',
42
+ Rate: 0,
43
+ Location: 'School',
44
+ URL: 'https://www.cdc.gov/',
45
+ Notes: '<span style="color:#d54309;">⚠ Below target</span> — outreach underway'
46
+ },
47
+ {
48
+ STATE: 'Arkansas',
49
+ Rate: 60,
50
+ Location: 'School',
51
+ URL: 'https://www.cdc.gov/',
52
+ Notes: 'See <a href="https://www.cdc.gov" target="_blank">CDC report</a> for details'
53
+ },
54
+ {
55
+ STATE: 'California',
56
+ Rate: 30,
57
+ Location: 'Home',
58
+ URL: 'https://www.cdc.gov/',
59
+ Notes: '<strong>Leading state</strong> in program participation'
60
+ },
61
+ {
62
+ STATE: 'Colorado',
63
+ Rate: 40,
64
+ Location: 'Vehicle',
65
+ URL: 'https://www.cdc.gov/',
66
+ Notes: 'Data validated by <em>state health dept</em>'
67
+ },
68
+ {
69
+ STATE: 'Connecticut',
70
+ Rate: 55,
71
+ Location: 'Home',
72
+ URL: 'https://www.cdc.gov/',
73
+ Notes: '<span style="color:#2e8540;">✓ Target achieved</span>'
74
+ },
75
+ { STATE: 'Deleware', Rate: '57', Location: 'Home', URL: 'https://www.cdc.gov/', Notes: '' },
76
+ { STATE: 'DC', Rate: 60, Location: 'Home', URL: 'https://www.cdc.gov/', Notes: '' },
77
+ {
78
+ STATE: 'Florida',
79
+ Rate: 30,
80
+ Location: 'Work',
81
+ URL: 'https://www.cdc.gov/',
82
+ Notes: 'Excludes <em>federal facilities</em>'
83
+ },
84
+ {
85
+ STATE: 'Georgia',
86
+ Rate: 40,
87
+ Location: 'Work',
88
+ URL: 'https://www.cdc.gov/',
89
+ Notes: 'Improved <strong>12 points</strong> since 2021'
90
+ },
91
+ {
92
+ STATE: 'Hawaii',
93
+ Rate: 57,
94
+ Location: 'School',
95
+ URL: 'https://www.cdc.gov/',
96
+ Notes: 'Island populations surveyed <em>separately</em>'
97
+ },
98
+ {
99
+ STATE: 'Idaho',
100
+ Rate: 60,
101
+ Location: 'School',
102
+ URL: 'https://www.cdc.gov/',
103
+ Notes: 'Rural adjustment applied — see <strong>methodology</strong>'
104
+ },
105
+ {
106
+ STATE: 'Illinois',
107
+ Rate: 30,
108
+ Location: 'Work',
109
+ URL: 'https://www.cdc.gov/',
110
+ Notes: 'Chicago metro reported <em>independently</em>'
111
+ },
112
+ {
113
+ STATE: 'Indiana',
114
+ Rate: 40,
115
+ Location: 'Vehicle',
116
+ URL: 'https://www.cdc.gov/',
117
+ Notes: 'Rate includes <strong>all age groups</strong>'
118
+ },
119
+ {
120
+ STATE: 'Iowa',
121
+ Rate: 55,
122
+ Location: 'Home',
123
+ URL: 'https://www.cdc.gov/',
124
+ Notes: 'Consistent with <em>national average</em>'
125
+ },
126
+ {
127
+ STATE: 'Kansas',
128
+ Rate: 57,
129
+ Location: 'Home',
130
+ URL: 'https://www.cdc.gov/',
131
+ Notes: 'Data from <strong>Q3 2023</strong> survey'
132
+ },
133
+ {
134
+ STATE: 'Kentucky',
135
+ Rate: 60,
136
+ Location: 'NA',
137
+ URL: 'https://www.cdc.gov/',
138
+ Notes: '<span style="color:#d54309;">Below regional median</span>'
139
+ },
140
+ {
141
+ STATE: 'Louisiana',
142
+ Rate: 30,
143
+ Location: 'Vehicle',
144
+ URL: 'https://www.cdc.gov/',
145
+ Notes: 'Post-storm data collection <em>delayed</em>'
146
+ },
147
+ {
148
+ STATE: 'Maine',
149
+ Rate: 40,
150
+ Location: 'Work',
151
+ URL: 'https://www.cdc.gov/',
152
+ Notes: 'Coastal counties weighted <strong>separately</strong>'
153
+ },
154
+ { STATE: 'Marshall Islands', Rate: 55, Location: 'Home', URL: 'https://www.cdc.gov/', Notes: '' },
155
+ {
156
+ STATE: 'Maryland',
157
+ Rate: 57,
158
+ Location: 'School',
159
+ URL: 'https://www.cdc.gov/',
160
+ Notes: 'Includes DC metro area <em>overlap</em>'
161
+ },
162
+ {
163
+ STATE: 'Massachusetts',
164
+ Rate: 60,
165
+ Location: 'School',
166
+ URL: 'https://www.cdc.gov/',
167
+ Notes: '<span style="color:#2e8540;">✓ Highest in region</span>'
168
+ },
169
+ {
170
+ STATE: 'Michigan',
171
+ Rate: 12,
172
+ Location: 'Work',
173
+ URL: 'https://www.cdc.gov/',
174
+ Notes: '<span style="color:#d54309;">⚠ Significant decline</span> noted'
175
+ },
176
+ { STATE: 'Micronesia', Rate: 65, Location: 'Vehicle', URL: 'https://www.cdc.gov/', Notes: '' },
177
+ {
178
+ STATE: 'Minnesota',
179
+ Rate: 55,
180
+ Location: 'Home',
181
+ URL: 'https://www.cdc.gov/',
182
+ Notes: 'Tribal nations surveyed <em>separately</em>'
183
+ },
184
+ {
185
+ STATE: 'Mississippi',
186
+ Rate: 57,
187
+ Location: 'Home',
188
+ URL: 'https://www.cdc.gov/',
189
+ Notes: 'Additional outreach <strong>planned for 2024</strong>'
190
+ },
191
+ {
192
+ STATE: 'Montana',
193
+ Rate: 60,
194
+ Location: 'Home',
195
+ URL: 'https://www.cdc.gov/',
196
+ Notes: 'Large geographic area — <em>sample size limited</em>'
197
+ },
198
+ { STATE: 'Montana', Rate: 30, Location: 'Vehicle', URL: 'https://www.cdc.gov/', Notes: '' },
199
+ {
200
+ STATE: 'Nebraska',
201
+ Rate: 40,
202
+ Location: 'Work',
203
+ URL: 'https://www.cdc.gov/',
204
+ Notes: 'Rate stable over <strong>3-year period</strong>'
205
+ },
206
+ {
207
+ STATE: 'Nevada',
208
+ Rate: 55,
209
+ Location: 'Home',
210
+ URL: 'https://www.cdc.gov/',
211
+ Notes: 'Las Vegas metro excluded from <em>rural rate</em>'
212
+ },
213
+ {
214
+ STATE: 'New Hampshire',
215
+ Rate: 57,
216
+ Location: 'School',
217
+ URL: 'https://www.cdc.gov/',
218
+ Notes: 'Highest participation in <strong>New England</strong>'
219
+ },
220
+ {
221
+ STATE: 'New Jersey',
222
+ Rate: 60,
223
+ Location: 'School',
224
+ URL: 'https://www.cdc.gov/',
225
+ Notes: 'Dense population — <em>high confidence interval</em>'
226
+ },
227
+ {
228
+ STATE: 'New Mexico',
229
+ Rate: 12,
230
+ Location: 'Work',
231
+ URL: 'https://www.cdc.gov/',
232
+ Notes: '<span style="color:#d54309;">Below target</span> — tribal programs expanding'
233
+ },
234
+ {
235
+ STATE: 'New York',
236
+ Rate: 40,
237
+ Location: 'Vehicle',
238
+ URL: 'https://www.cdc.gov/',
239
+ Notes: 'NYC data reported <em>separately</em>'
240
+ },
241
+ {
242
+ STATE: 'North Carolina',
243
+ Rate: 55,
244
+ Location: 'Home',
245
+ URL: 'https://www.cdc.gov/',
246
+ Notes: 'Urban/rural split — see <strong>supplemental table</strong>'
247
+ },
248
+ {
249
+ STATE: 'North Dakota',
250
+ Rate: 57,
251
+ Location: 'Home',
252
+ URL: 'https://www.cdc.gov/',
253
+ Notes: 'Oil field worker population <em>adjusted</em>'
254
+ },
255
+ { STATE: 'Northern Mariana Islands', Rate: '60', Location: 'Home', URL: 'https://www.cdc.gov/', Notes: '' },
256
+ {
257
+ STATE: 'Ohio',
258
+ Rate: 88,
259
+ Location: 'Vehicle',
260
+ URL: 'https://www.cdc.gov/',
261
+ Notes: '<span style="color:#2e8540;">✓ Above national average</span>'
262
+ },
263
+ {
264
+ STATE: 'Oklahoma',
265
+ Rate: 40,
266
+ Location: 'Work',
267
+ URL: 'https://www.cdc.gov/',
268
+ Notes: 'Native American populations <em>surveyed separately</em>'
269
+ },
270
+ {
271
+ STATE: 'Oregon',
272
+ Rate: 55,
273
+ Location: 'Home',
274
+ URL: 'https://www.cdc.gov/',
275
+ Notes: 'Coast vs inland rates <strong>vary significantly</strong>'
276
+ },
277
+ { STATE: 'Palau', Rate: 15, Location: 'School', URL: 'https://www.cdc.gov/', Notes: '' },
278
+ {
279
+ STATE: 'Pennsylvania',
280
+ Rate: 60,
281
+ Location: 'School',
282
+ URL: 'https://www.cdc.gov/',
283
+ Notes: 'Philadelphia metro weighted <em>separately</em>'
284
+ },
285
+ { STATE: 'Puerto Rico', Rate: 30, Location: 'Work', URL: 'https://www.cdc.gov/', Notes: '' },
286
+ {
287
+ STATE: 'Rhode Island',
288
+ Rate: 40,
289
+ Location: 'Vehicle',
290
+ URL: 'https://www.cdc.gov/',
291
+ Notes: '<strong>Smallest state</strong> — high confidence in data'
292
+ },
293
+ {
294
+ STATE: 'South Carolina',
295
+ Rate: 55,
296
+ Location: 'Home',
297
+ URL: 'https://www.cdc.gov/',
298
+ Notes: 'Rate improved after <em>2022 initiative</em>'
299
+ },
300
+ {
301
+ STATE: 'South Dakota',
302
+ Rate: 86,
303
+ Location: 'Home',
304
+ URL: 'https://www.cdc.gov/',
305
+ Notes: '<span style="color:#2e8540;">✓ Above target</span>'
306
+ },
307
+ {
308
+ STATE: 'Tennessee',
309
+ Rate: 60,
310
+ Location: 'Home',
311
+ URL: 'https://www.cdc.gov/',
312
+ Notes: 'Nashville metro drives <strong>overall rate</strong>'
313
+ },
314
+ {
315
+ STATE: 'Texas',
316
+ Rate: 30,
317
+ Location: 'Vehicle',
318
+ URL: 'https://www.cdc.gov/',
319
+ Notes: 'Border region data <em>collected separately</em>'
320
+ },
321
+ { STATE: 'Utah', Rate: 54, Location: 'Work', URL: 'https://www.cdc.gov/', Notes: '' },
322
+ {
323
+ STATE: 'Vermont',
324
+ Rate: 40,
325
+ Location: 'Home',
326
+ URL: 'https://www.cdc.gov/',
327
+ Notes: 'Near-complete <em>population coverage</em>'
328
+ },
329
+ { STATE: 'Virgin Islands', Rate: 55, Location: 'School', URL: 'https://www.cdc.gov/', Notes: '' },
330
+ {
331
+ STATE: 'Virginia',
332
+ Rate: 57,
333
+ Location: 'School',
334
+ URL: 'https://www.cdc.gov/',
335
+ Notes: 'Northern VA excluded — see <em>DC metro</em>'
336
+ },
337
+ {
338
+ STATE: 'Washington',
339
+ Rate: 62,
340
+ Location: 'Work',
341
+ URL: 'https://www.cdc.gov/',
342
+ Notes: '<span style="color:#2e8540;">✓ Strong program outcomes</span>'
343
+ },
344
+ {
345
+ STATE: 'West Virginia',
346
+ Rate: 25,
347
+ Location: 'Vehicle',
348
+ URL: 'https://www.cdc.gov/',
349
+ Notes: '<span style="color:#d54309;">⚠ Lowest in region</span>'
350
+ },
351
+ {
352
+ STATE: 'Wyoming',
353
+ Rate: 43,
354
+ Location: 'Vehicle',
355
+ URL: 'https://www.cdc.gov/',
356
+ Notes: 'Smallest population — <strong>widest confidence interval</strong>'
357
+ }
358
+ ]
359
+
360
+ export const USAStateMap: Story = {
361
+ name: 'USA State Map',
362
+ args: {
363
+ config: {
364
+ ...usaStateGradient,
365
+ data,
366
+ table: {
367
+ ...usaStateGradient.table,
368
+ expanded: true
369
+ },
370
+ columns: {
371
+ ...usaStateGradient.columns,
372
+ Notes: {
373
+ name: 'Notes',
374
+ label: 'Notes',
375
+ dataTable: true,
376
+ tooltip: false
377
+ }
378
+ }
379
+ },
380
+ isEditor: true
381
+ },
382
+ play: async ({ canvasElement }) => {
383
+ await assertVisualizationRendered(canvasElement)
384
+ }
385
+ }