@cdc/map 4.26.2 → 4.26.4

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 (118) hide show
  1. package/CONFIG.md +235 -0
  2. package/README.md +70 -24
  3. package/dist/cdcmap-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcmap.js +31260 -27946
  6. package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
  7. package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
  8. package/examples/county-hsa-toggle.json +51993 -0
  9. package/examples/custom-map-layers.json +2 -2
  10. package/examples/default-county.json +3 -3
  11. package/examples/minimal-example.json +69 -0
  12. package/examples/private/annotation-bug.json +642 -0
  13. package/examples/private/css-issue.json +314 -0
  14. package/examples/private/region-breaking.json +1639 -0
  15. package/examples/private/test1.json +27247 -0
  16. package/package.json +4 -4
  17. package/src/CdcMap.tsx +3 -14
  18. package/src/CdcMapComponent.tsx +302 -164
  19. package/src/_stories/CdcMap.Defaults.smoke.stories.tsx +76 -0
  20. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
  21. package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
  22. package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
  23. package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
  24. package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
  25. package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
  26. package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
  27. package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
  28. package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
  29. package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
  30. package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
  31. package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
  32. package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +23 -1
  33. package/src/_stories/Map.HTMLInDataTable.stories.tsx +385 -0
  34. package/src/_stories/_mock/legends/legend-tests.json +3 -3
  35. package/src/_stories/_mock/multi-state-show-unselected.json +82 -0
  36. package/src/cdcMapComponent.styles.css +2 -2
  37. package/src/components/Annotation/Annotation.Draggable.styles.css +4 -4
  38. package/src/components/Annotation/AnnotationDropdown.styles.css +1 -1
  39. package/src/components/Annotation/AnnotationList.styles.css +13 -13
  40. package/src/components/Annotation/AnnotationList.tsx +1 -1
  41. package/src/components/EditorPanel/components/EditorPanel.tsx +905 -416
  42. package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
  44. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings-style.css +1 -1
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +31 -15
  46. package/src/components/EditorPanel/components/editorPanel.styles.css +55 -25
  47. package/src/components/Legend/components/Legend.tsx +12 -7
  48. package/src/components/Legend/components/LegendGroup/legend.group.css +5 -5
  49. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
  50. package/src/components/Legend/components/index.scss +2 -3
  51. package/src/components/NavigationMenu.tsx +2 -1
  52. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  53. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
  54. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +32 -17
  55. package/src/components/UsaMap/components/TerritoriesSection.tsx +3 -2
  56. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +13 -8
  57. package/src/components/UsaMap/components/UsaMap.County.tsx +629 -231
  58. package/src/components/UsaMap/components/UsaMap.Region.styles.css +1 -1
  59. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +2 -2
  60. package/src/components/UsaMap/components/UsaMap.State.tsx +14 -9
  61. package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
  62. package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
  63. package/src/components/WorldMap/WorldMap.tsx +10 -13
  64. package/src/components/WorldMap/data/world-topo-updated.json +1 -0
  65. package/src/components/WorldMap/data/world-topo.json +1 -1
  66. package/src/components/WorldMap/worldMap.styles.css +1 -1
  67. package/src/components/ZoomControls.tsx +49 -18
  68. package/src/components/zoomControls.styles.css +27 -11
  69. package/src/data/initial-state.js +15 -5
  70. package/src/data/legacy-defaults.ts +8 -0
  71. package/src/data/supported-counties.json +1 -1
  72. package/src/data/supported-geos.js +19 -0
  73. package/src/helpers/colors.ts +2 -1
  74. package/src/helpers/countyTerritories.ts +38 -0
  75. package/src/helpers/dataTableHelpers.ts +85 -0
  76. package/src/helpers/displayGeoName.ts +19 -11
  77. package/src/helpers/getMapContainerClasses.ts +8 -2
  78. package/src/helpers/getMatchingPatternForRow.ts +67 -0
  79. package/src/helpers/getPatternForRow.ts +11 -18
  80. package/src/helpers/tests/countyTerritories.test.ts +87 -0
  81. package/src/helpers/tests/dataTableHelpers.test.ts +78 -0
  82. package/src/helpers/tests/displayGeoName.test.ts +17 -0
  83. package/src/helpers/tests/getMatchingPatternForRow.test.ts +150 -0
  84. package/src/helpers/tests/getPatternForRow.test.ts +140 -2
  85. package/src/helpers/urlDataHelpers.ts +7 -1
  86. package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
  87. package/src/hooks/useMapLayers.tsx +1 -1
  88. package/src/hooks/useResizeObserver.ts +36 -22
  89. package/src/hooks/useTooltip.test.tsx +64 -0
  90. package/src/hooks/useTooltip.ts +46 -15
  91. package/src/scss/editor-panel.scss +1 -1
  92. package/src/scss/main.scss +140 -6
  93. package/src/scss/map.scss +9 -4
  94. package/src/store/map.actions.ts +5 -0
  95. package/src/store/map.reducer.ts +13 -0
  96. package/src/test/CdcMap.test.jsx +26 -2
  97. package/src/types/MapConfig.ts +28 -4
  98. package/src/types/MapContext.ts +5 -1
  99. package/topojson-updater/README.txt +1 -1
  100. package/dist/cdcmap-Cf9_fbQf.es.js +0 -6
  101. package/examples/__data__/city-state-data.json +0 -668
  102. package/examples/city-state.json +0 -434
  103. package/examples/default-world-data.json +0 -1450
  104. package/examples/new-cities.json +0 -656
  105. package/src/_stories/CdcMap.Editor.stories.tsx +0 -3475
  106. package/src/helpers/componentHelpers.ts +0 -8
  107. package/topojson-updater/package-lock.json +0 -223
  108. /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
  109. /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
  110. /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
  111. /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
  112. /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
  113. /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
  114. /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
  115. /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
  116. /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
  117. /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
  118. /package/src/_stories/{UsaMap.NoData.stories.tsx → UsaMap.NoData.smoke.stories.tsx} +0 -0
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getMatchingPatternForRow } from '../getMatchingPatternForRow'
3
+ import { PatternSelection } from '../../types/MapConfig'
4
+
5
+ describe('getMatchingPatternForRow', () => {
6
+ it('uses the last matching specific pattern when multiple specific patterns match', () => {
7
+ const row = { Rate: 55 }
8
+ const patterns: PatternSelection[] = [
9
+ {
10
+ dataKey: 'Rate',
11
+ dataValue: '55',
12
+ pattern: 'lines',
13
+ size: 'small',
14
+ color: '#111',
15
+ label: '',
16
+ contrastCheck: true
17
+ },
18
+ {
19
+ dataKey: 'Rate',
20
+ dataValue: '55',
21
+ pattern: 'circles',
22
+ size: 'medium',
23
+ color: '#000',
24
+ label: '',
25
+ contrastCheck: true
26
+ }
27
+ ]
28
+
29
+ const result = getMatchingPatternForRow(row, patterns)
30
+
31
+ expect(result?.patternIndex).toBe(1)
32
+ expect(result?.pattern.pattern).toBe('circles')
33
+ })
34
+
35
+ it('matches specific dataKey patterns first', () => {
36
+ const row = { Rate: 55, Status: '55' }
37
+ const patterns: PatternSelection[] = [
38
+ {
39
+ dataKey: '',
40
+ dataValue: '55',
41
+ pattern: 'lines',
42
+ size: 'small',
43
+ color: '#111',
44
+ label: '',
45
+ contrastCheck: true
46
+ },
47
+ {
48
+ dataKey: 'Rate',
49
+ dataValue: '55',
50
+ pattern: 'circles',
51
+ size: 'medium',
52
+ color: '#000',
53
+ label: '',
54
+ contrastCheck: true
55
+ }
56
+ ]
57
+
58
+ const result = getMatchingPatternForRow(row, patterns)
59
+
60
+ expect(result?.patternIndex).toBe(1)
61
+ expect(result?.matchedDataKey).toBe('Rate')
62
+ expect(result?.pattern.pattern).toBe('circles')
63
+ })
64
+
65
+ it('falls back to broad matching when dataKey is blank', () => {
66
+ const row = { State: 'Colorado', Rate: 55 }
67
+ const patterns: PatternSelection[] = [
68
+ {
69
+ dataKey: '',
70
+ dataValue: 'Colorado',
71
+ pattern: 'waves',
72
+ size: 'large',
73
+ color: '#333',
74
+ label: '',
75
+ contrastCheck: true
76
+ }
77
+ ]
78
+
79
+ const result = getMatchingPatternForRow(row, patterns)
80
+
81
+ expect(result?.patternIndex).toBe(0)
82
+ expect(result?.matchedDataKey).toBe('State')
83
+ })
84
+
85
+ it('does not match when dataValue is empty', () => {
86
+ const row = { State: 'Colorado', Rate: 55 }
87
+ const patterns: PatternSelection[] = [
88
+ {
89
+ dataKey: '',
90
+ dataValue: ' ',
91
+ pattern: 'waves',
92
+ size: 'large',
93
+ color: '#333',
94
+ label: '',
95
+ contrastCheck: true
96
+ }
97
+ ]
98
+
99
+ expect(getMatchingPatternForRow(row, patterns)).toBeNull()
100
+ })
101
+
102
+ it('supports numeric string and number matching for broad patterns', () => {
103
+ const row = { Rate: 55 }
104
+ const patterns: PatternSelection[] = [
105
+ {
106
+ dataKey: '',
107
+ dataValue: '55',
108
+ pattern: 'waves',
109
+ size: 'large',
110
+ color: '#333',
111
+ label: '',
112
+ contrastCheck: true
113
+ }
114
+ ]
115
+
116
+ const result = getMatchingPatternForRow(row, patterns)
117
+
118
+ expect(result?.matchedDataKey).toBe('Rate')
119
+ expect(result?.patternIndex).toBe(0)
120
+ })
121
+
122
+ it('uses the last matching broad pattern when multiple broad patterns match', () => {
123
+ const row = { Category: 'Target' }
124
+ const patterns: PatternSelection[] = [
125
+ {
126
+ dataKey: '',
127
+ dataValue: 'Target',
128
+ pattern: 'lines',
129
+ size: 'small',
130
+ color: '#111',
131
+ label: '',
132
+ contrastCheck: true
133
+ },
134
+ {
135
+ dataKey: '',
136
+ dataValue: 'Target',
137
+ pattern: 'waves',
138
+ size: 'large',
139
+ color: '#333',
140
+ label: '',
141
+ contrastCheck: true
142
+ }
143
+ ]
144
+
145
+ const result = getMatchingPatternForRow(row, patterns)
146
+
147
+ expect(result?.patternIndex).toBe(1)
148
+ expect(result?.pattern.pattern).toBe('waves')
149
+ })
150
+ })
@@ -4,8 +4,24 @@ import { getPatternForRow } from '../getPatternForRow'
4
4
  const baseConfig: any = {
5
5
  map: {
6
6
  patterns: [
7
- { dataKey: 'Percent Vaccinated', dataValue: '99.99', pattern: 'circles', size: 'medium', color: '#000' },
8
- { dataKey: 'Status', dataValue: 'NA', pattern: 'lines', size: 'small', color: '#111' }
7
+ {
8
+ dataKey: 'Percent Vaccinated',
9
+ dataValue: '99.99',
10
+ pattern: 'circles',
11
+ size: 'medium',
12
+ color: '#000',
13
+ label: '',
14
+ contrastCheck: true
15
+ },
16
+ {
17
+ dataKey: 'Status',
18
+ dataValue: 'NA',
19
+ pattern: 'lines',
20
+ size: 'small',
21
+ color: '#111',
22
+ label: '',
23
+ contrastCheck: true
24
+ }
9
25
  ]
10
26
  }
11
27
  }
@@ -36,4 +52,126 @@ describe('getPatternForRow', () => {
36
52
  const row = { 'Percent Vaccinated': 75.06, Status: '*' }
37
53
  expect(getPatternForRow(row, baseConfig)).toBeNull()
38
54
  })
55
+
56
+ it('matches blank dataKey patterns against any row value', () => {
57
+ const config = {
58
+ map: {
59
+ patterns: [
60
+ {
61
+ dataKey: '',
62
+ dataValue: 'Colorado',
63
+ pattern: 'waves',
64
+ size: 'large',
65
+ color: '#222',
66
+ label: '',
67
+ contrastCheck: true
68
+ }
69
+ ]
70
+ }
71
+ }
72
+ const row = { STATE: 'Colorado', Rate: 55 }
73
+ const result = getPatternForRow(row, config)
74
+
75
+ expect(result).toEqual({
76
+ pattern: 'waves',
77
+ dataKey: 'STATE',
78
+ size: 'large',
79
+ patternIndex: 0,
80
+ color: '#222'
81
+ })
82
+ })
83
+
84
+ it('prioritizes specific dataKey matches over broad matches', () => {
85
+ const config = {
86
+ map: {
87
+ patterns: [
88
+ {
89
+ dataKey: '',
90
+ dataValue: '55',
91
+ pattern: 'lines',
92
+ size: 'small',
93
+ color: '#111',
94
+ label: '',
95
+ contrastCheck: true
96
+ },
97
+ {
98
+ dataKey: 'Rate',
99
+ dataValue: '55',
100
+ pattern: 'circles',
101
+ size: 'medium',
102
+ color: '#000',
103
+ label: '',
104
+ contrastCheck: true
105
+ }
106
+ ]
107
+ }
108
+ }
109
+ const row = { Rate: 55, Category: '55' }
110
+ const result = getPatternForRow(row, config)
111
+
112
+ expect(result).toEqual({
113
+ pattern: 'circles',
114
+ dataKey: 'Rate',
115
+ size: 'medium',
116
+ patternIndex: 1,
117
+ color: '#000'
118
+ })
119
+ })
120
+
121
+ it('does not match blank dataKey patterns with empty values', () => {
122
+ const config = {
123
+ map: {
124
+ patterns: [
125
+ {
126
+ dataKey: '',
127
+ dataValue: ' ',
128
+ pattern: 'lines',
129
+ size: 'small',
130
+ color: '#111',
131
+ label: '',
132
+ contrastCheck: true
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ const row = { STATE: 'Colorado' }
138
+ expect(getPatternForRow(row, config)).toBeNull()
139
+ })
140
+
141
+ it('returns the last matching specific pattern to preserve dominant visual overlay behavior', () => {
142
+ const config = {
143
+ map: {
144
+ patterns: [
145
+ {
146
+ dataKey: 'Rate',
147
+ dataValue: '55',
148
+ pattern: 'lines',
149
+ size: 'small',
150
+ color: '#111',
151
+ label: '',
152
+ contrastCheck: true
153
+ },
154
+ {
155
+ dataKey: 'Rate',
156
+ dataValue: '55',
157
+ pattern: 'waves',
158
+ size: 'large',
159
+ color: '#222',
160
+ label: '',
161
+ contrastCheck: true
162
+ }
163
+ ]
164
+ }
165
+ }
166
+ const row = { Rate: 55 }
167
+ const result = getPatternForRow(row, config)
168
+
169
+ expect(result).toEqual({
170
+ pattern: 'waves',
171
+ dataKey: 'Rate',
172
+ size: 'large',
173
+ patternIndex: 1,
174
+ color: '#222'
175
+ })
176
+ })
39
177
  })
@@ -5,6 +5,7 @@ import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
5
5
  import { MapConfig } from '../types/MapConfig'
6
6
  import { CSV_PARSE_CONFIG } from './constants'
7
7
  import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
8
+ import { extractDataAndMetadata } from '@cdc/core/helpers/extractDataAndMetadata'
8
9
 
9
10
  const buildQueryString = (params: Record<string, string>): string =>
10
11
  Object.keys(params)
@@ -34,6 +35,7 @@ export const reloadURLData = async (config: MapConfig, setConfig: (config: MapCo
34
35
 
35
36
  let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${buildQueryString(qsParams)}`
36
37
  let data
38
+ let dataMetadata: Record<string, string> = {}
37
39
 
38
40
  try {
39
41
  const regex = /(?:\.([^.]+))?$/
@@ -47,7 +49,10 @@ export const reloadURLData = async (config: MapConfig, setConfig: (config: MapCo
47
49
  return parsedCsv.data
48
50
  })
49
51
  } else if ('json' === ext || isSolrJson(dataUrlFinal)) {
50
- data = await fetch(dataUrlFinal).then(response => response.json())
52
+ const json = await fetch(dataUrlFinal).then(response => response.json())
53
+ const extracted = extractDataAndMetadata(json)
54
+ data = extracted.data
55
+ dataMetadata = extracted.dataMetadata
51
56
  } else {
52
57
  data = []
53
58
  }
@@ -64,6 +69,7 @@ export const reloadURLData = async (config: MapConfig, setConfig: (config: MapCo
64
69
 
65
70
  const newConfig = cloneConfig(config)
66
71
  newConfig.data = data
72
+ newConfig.dataMetadata = dataMetadata
67
73
  newConfig.runtimeDataUrl = dataUrlFinal
68
74
 
69
75
  setConfig(newConfig)
@@ -1,8 +1,7 @@
1
1
  import { type ReactNode, useContext } from 'react'
2
- import { displayGeoName, navigationHandler } from '../helpers'
2
+ import { navigationHandler } from '../helpers'
3
3
  import ConfigContext from '../context'
4
4
  import useTooltip from './useTooltip'
5
- import { supportedStatesFipsCodes } from './../data/supported-geos'
6
5
  import parse from 'html-react-parser'
7
6
  import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
8
7
  import ExternalIcon from './../images/external-link.svg'
@@ -10,7 +9,7 @@ import ExternalIcon from './../images/external-link.svg'
10
9
  const useApplyTooltipsToGeo = () => {
11
10
  const { config, customNavigationHandler } = useContext(ConfigContext)
12
11
  const navigationColumnName = config.columns.navigate.name
13
- const { buildTooltip } = useTooltip({ config, displayGeoName, supportedStatesFipsCodes })
12
+ const { buildTooltip } = useTooltip(config)
14
13
 
15
14
  const applyTooltipsToGeo = (geoName: string, row: Object, returnType = 'string') => {
16
15
  let toolTipText: string | ReactNode = buildTooltip(row, geoName, '')
@@ -18,7 +17,11 @@ const useApplyTooltipsToGeo = () => {
18
17
  // We convert the markup into JSX and add a navigation link if it's going into a modal.
19
18
  if ('jsx' === returnType) {
20
19
  if (typeof toolTipText === 'string') {
21
- toolTipText = [<div key='modal-content'>{parse(toolTipText)}</div>]
20
+ toolTipText = [
21
+ <div key='modal-content' className='cove-prose'>
22
+ {parse(toolTipText)}
23
+ </div>
24
+ ]
22
25
  }
23
26
 
24
27
  if (config.columns.hasOwnProperty('navigate') && row[navigationColumnName]) {
@@ -53,7 +53,7 @@ export default function useMapLayers(config: MapConfig, setConfig, pathGenerator
53
53
  setConfig(newConfig)
54
54
  }
55
55
 
56
- const handleAddLayer = (e: Event) => {
56
+ const handleAddLayer = (e: MouseEvent<HTMLButtonElement>) => {
57
57
  e.preventDefault()
58
58
  const placeHolderLayer = {
59
59
  name: 'New Custom Layer',
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useEffect } from 'react'
1
+ import { useState, useCallback, useRef } from 'react'
2
2
  import { type ViewPort } from '@cdc/core/types/ViewPort'
3
3
  import { type DimensionsType } from '@cdc/core/types/Dimensions'
4
4
  import { EDITOR_WIDTH } from '@cdc/core/helpers/constants'
@@ -11,37 +11,51 @@ export const useResizeObserver = (isEditor: boolean) => {
11
11
  const [vizViewport, setVizViewport] = useState<ViewPort>(null)
12
12
  const [container, setContainer] = useState<HTMLElement | null>(null)
13
13
 
14
- const resizeObserver = new ResizeObserver(entries => {
15
- for (let entry of entries) {
16
- let { width, height } = entry.contentRect
14
+ // Keep isEditor fresh in the observer callback without recreating the observer
15
+ const isEditorRef = useRef(isEditor)
16
+ isEditorRef.current = isEditor
17
17
 
18
- const editorIsOpen = isEditor && !!document.querySelector('.editor-panel:not(.hidden)')
19
- width = editorIsOpen ? width - EDITOR_WIDTH : width
18
+ const resizeObserverRef = useRef<ResizeObserver | null>(null)
20
19
 
21
- // Account for 1rem padding in editor mode
22
- width = width - (isEditor ? 36 : 0)
20
+ const outerContainerRef = useCallback(node => {
21
+ // Tear down any previous observer (node swap or unmount)
22
+ if (resizeObserverRef.current) {
23
+ resizeObserverRef.current.disconnect()
24
+ resizeObserverRef.current = null
25
+ }
23
26
 
24
- const newViewport = getViewport(width)
27
+ if (node !== null) {
28
+ resizeObserverRef.current = new ResizeObserver(entries => {
29
+ for (let entry of entries) {
30
+ let { width, height } = entry.contentRect
25
31
 
26
- setCurrentViewport(newViewport)
27
- setVizViewport(newViewport)
32
+ const editorIsOpen = isEditorRef.current && !!document.querySelector('.editor-panel:not(.hidden)')
33
+ width = editorIsOpen ? width - EDITOR_WIDTH : width
28
34
 
29
- setDimensions([width, height])
30
- }
31
- })
35
+ // Account for 1rem padding in editor mode
36
+ width = width - (isEditorRef.current ? 36 : 0)
32
37
 
33
- const outerContainerRef = useCallback(node => {
34
- if (node !== null) {
35
- resizeObserver.observe(node)
36
- }
37
- setContainer(node)
38
+ const newViewport = getViewport(width)
38
39
 
39
- return () => {
40
- resizeObserver.disconnect()
40
+ setCurrentViewport(newViewport)
41
+ setVizViewport(newViewport)
42
+ setDimensions([width, height])
43
+ }
44
+ })
45
+ resizeObserverRef.current.observe(node)
41
46
  }
47
+
48
+ setContainer(node)
42
49
  }, [])
43
50
 
44
- return { resizeObserver, dimensions, currentViewport, vizViewport, outerContainerRef, container }
51
+ return {
52
+ resizeObserver: resizeObserverRef.current,
53
+ dimensions,
54
+ currentViewport,
55
+ vizViewport,
56
+ outerContainerRef,
57
+ container
58
+ }
45
59
  }
46
60
 
47
61
  export default useResizeObserver
@@ -0,0 +1,64 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import useTooltip from './useTooltip'
3
+
4
+ const supportedStatesFipsCodes = {
5
+ '01': 'Alabama'
6
+ }
7
+
8
+ const createConfig = (hideGeoColumnInTooltip: boolean) => ({
9
+ general: {
10
+ geoType: 'world',
11
+ type: 'map',
12
+ hideGeoColumnInTooltip,
13
+ hidePrimaryColumnInTooltip: false,
14
+ geoLabelOverride: ''
15
+ },
16
+ columns: {
17
+ geo: {
18
+ name: 'Country',
19
+ label: 'Location',
20
+ tooltip: true,
21
+ displayColumn: ''
22
+ },
23
+ primary: {
24
+ name: 'Value',
25
+ label: 'Value',
26
+ tooltip: true
27
+ },
28
+ navigate: {
29
+ name: ''
30
+ }
31
+ },
32
+ legend: {
33
+ specialClasses: []
34
+ },
35
+ tooltips: {
36
+ noDataLabel: 'No Data'
37
+ }
38
+ })
39
+
40
+ describe('useTooltip', () => {
41
+ it('hides the geography column label in the tooltip body when configured', () => {
42
+ const row = { Country: 'ssd', Value: 10 }
43
+
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
+ )
56
+
57
+ const visibleTooltip = visibleResult.current.buildTooltip(row, 'South Sudan')
58
+ const hiddenTooltip = hiddenResult.current.buildTooltip(row, 'South Sudan')
59
+
60
+ expect(visibleTooltip).toContain('Location: South Sudan')
61
+ expect(hiddenTooltip).not.toContain('Location: South Sudan')
62
+ expect(hiddenTooltip).toContain('<li class="tooltip-body">South Sudan</li>')
63
+ })
64
+ })
@@ -1,9 +1,9 @@
1
1
  import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
2
2
  import { displayGeoName } from '../helpers/displayGeoName'
3
+ import { MapConfig } from '../types/MapConfig'
4
+ import { supportedStatesFipsCodes } from '../data/supported-geos'
3
5
 
4
- const useTooltip = props => {
5
- const { config, supportedStatesFipsCodes } = props
6
-
6
+ const useTooltip = (config: MapConfig) => {
7
7
  /**
8
8
  * On county maps there's a need to append the state name
9
9
  * @param {String} toolTipText - previous tooltip text to build upon
@@ -11,24 +11,33 @@ const useTooltip = props => {
11
11
  * @returns {String} toolTipText - new toolTipText
12
12
  */
13
13
  const handleTooltipStateNameColumn = (toolTipText, row) => {
14
- const { geoType, type, hideGeoColumnInTooltip } = config.general
14
+ const { geoType, type, hideGeoColumnInTooltip, showHSABoundaries } = config.general
15
15
  if (geoType === 'us-county' && type !== 'us-geocode') {
16
16
  let stateFipsCode = row[config.columns.geo.name].substring(0, 2)
17
17
  const stateName = supportedStatesFipsCodes[stateFipsCode]
18
- toolTipText += hideGeoColumnInTooltip
19
- ? `<strong>${stateName}</strong><br/>`
20
- : `<strong>Location: ${stateName}</strong><br/>`
18
+ toolTipText +=
19
+ hideGeoColumnInTooltip || showHSABoundaries
20
+ ? `<strong>${stateName}</strong><br/>`
21
+ : `<strong>Location: ${stateName}</strong><br/>`
21
22
  }
22
23
  return toolTipText
23
24
  }
24
25
 
26
+ const getHSAColumnText = (config: MapConfig, row: Object) => {
27
+ const { hsa } = config.columns
28
+ if (!hsa || !config.general.showHSABoundaries) return ''
29
+ const hsaDescription = row[hsa.name]
30
+ return `<p class="tooltip-heading">HSA: ${hsaDescription}</p>`
31
+ }
32
+
25
33
  /**
26
34
  * On county and state maps, adds the ability to hide the geo column name (prefix)
27
35
  * @param {String} geoName - feature name
28
36
  * @returns {String} text to be appended to toolTipText
29
37
  */
30
- const handleTooltipGeoColumn = (geoName: string) => {
38
+ const handleTooltipGeoColumn = (geoName: string, row?) => {
31
39
  const { hideGeoColumnInTooltip } = config.general as { hideGeoColumnInTooltip: boolean }
40
+ const displayOverride = row?.[config.columns.geo?.displayColumn]
32
41
 
33
42
  const handleTooltipPrefix = (toolTipText: string) => {
34
43
  const { geoType, geoLabelOverride } = config.general
@@ -54,8 +63,11 @@ const useTooltip = props => {
54
63
 
55
64
  const prefix = handleTooltipPrefix('')
56
65
 
57
- if (hideGeoColumnInTooltip) return `<strong>${displayGeoName(geoName)}</strong>`
58
- return `<p class="tooltip-heading" style="text-transform: none;">${prefix}${displayGeoName(geoName)}</p>`
66
+ if (hideGeoColumnInTooltip) return `<strong>${displayGeoName(geoName, displayOverride)}</strong>`
67
+ return `<p class="tooltip-heading" style="text-transform: none;">${prefix}${displayGeoName(
68
+ geoName,
69
+ displayOverride
70
+ )}</p>`
59
71
  }
60
72
 
61
73
  /**
@@ -74,13 +86,29 @@ const useTooltip = props => {
74
86
  }
75
87
 
76
88
  const handleTooltipPrimaryColumn = (tooltipValue, column) => {
77
- const { hidePrimaryColumnInTooltip } = config.general as { hidePrimaryColumnInTooltip: boolean }
89
+ const { hidePrimaryColumnInTooltip, hideGeoColumnInTooltip } = config.general as {
90
+ hidePrimaryColumnInTooltip: boolean
91
+ hideGeoColumnInTooltip: boolean
92
+ }
78
93
  let tooltipPrefix = column.label?.length > 0 ? column.label : ''
79
- if ((column.name === config.columns.primary.name && hidePrimaryColumnInTooltip) || !tooltipPrefix)
80
- return `<li class="tooltip-body">${tooltipValue}</li>`
94
+ const hidePrimaryLabel = column.name === config.columns.primary.name && hidePrimaryColumnInTooltip
95
+ const hideGeoLabel = column.name === config.columns.geo.name && hideGeoColumnInTooltip
96
+
97
+ if (hidePrimaryLabel || hideGeoLabel || !tooltipPrefix) return `<li class="tooltip-body">${tooltipValue}</li>`
81
98
  return `<li class="tooltip-body">${tooltipPrefix}: ${tooltipValue}</li>`
82
99
  }
83
100
 
101
+ const formatTooltipColumnValue = (row, column, columnKey) => {
102
+ if (!row) return config.tooltips?.noDataLabel || 'No Data'
103
+
104
+ if (column.name === config.columns.geo.name) {
105
+ const displayOverride = row?.[config.columns.geo?.displayColumn]
106
+ return displayGeoName(row[column.name], displayOverride)
107
+ }
108
+
109
+ return displayDataAsText(row[column.name], columnKey, config)
110
+ }
111
+
84
112
  /**
85
113
  *
86
114
  * @param {String} toolTipText - previous tooltipText to build upon
@@ -106,7 +134,7 @@ const useTooltip = props => {
106
134
  let tooltipValue = handleTooltipSpecialClassText(specialClasses, column, row, '', columnKey)
107
135
 
108
136
  if (!tooltipValue) {
109
- tooltipValue = row ? displayDataAsText(row[column.name], columnKey, config) : 'No Data'
137
+ tooltipValue = formatTooltipColumnValue(row, column, columnKey)
110
138
  }
111
139
 
112
140
  toolTipText += handleTooltipPrimaryColumn(tooltipValue, column)
@@ -124,8 +152,11 @@ const useTooltip = props => {
124
152
  // Handle County Location Columns
125
153
  toolTipText += handleTooltipStateNameColumn(toolTipText, row)
126
154
 
155
+ // Handle HSA Column
156
+ toolTipText += getHSAColumnText(config, row)
157
+
127
158
  // Handle Columns > Data Column In tooltips
128
- toolTipText += handleTooltipGeoColumn(geoName)
159
+ toolTipText += handleTooltipGeoColumn(geoName, row)
129
160
 
130
161
  // Handle Columns > Geography Column In tooltips
131
162
  toolTipText = handleTooltipColumns(toolTipText, row)
@@ -18,7 +18,7 @@
18
18
  }
19
19
  }
20
20
 
21
- .cdc-open-viz-module {
21
+ .cove-visualization {
22
22
  .editor-panel {
23
23
  .cove-input-group,
24
24
  .cove-accordion__small-inputs {