@cdc/map 4.24.7 → 4.24.9

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 (46) hide show
  1. package/dist/cdcmap.js +40720 -38422
  2. package/examples/county-year.csv +10 -0
  3. package/examples/default-geocode.json +44 -10
  4. package/examples/default-patterns.json +0 -2
  5. package/examples/default-single-state.json +279 -108
  6. package/examples/map-issue-3.json +646 -0
  7. package/examples/single-state-filter.json +153 -0
  8. package/index.html +9 -6
  9. package/package.json +3 -3
  10. package/src/CdcMap.tsx +322 -126
  11. package/src/_stories/CdcMap.stories.tsx +7 -0
  12. package/src/_stories/_mock/DEV-8942.json +270 -0
  13. package/src/components/Annotation/AnnotationDropdown.tsx +1 -0
  14. package/src/components/{BubbleList.jsx → BubbleList.tsx} +1 -1
  15. package/src/components/{CityList.jsx → CityList.tsx} +28 -2
  16. package/src/components/{DataTable.jsx → DataTable.tsx} +2 -2
  17. package/src/components/EditorPanel/components/EditorPanel.tsx +647 -127
  18. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -22
  19. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +61 -11
  20. package/src/components/Legend/components/Legend.tsx +125 -36
  21. package/src/components/Legend/components/index.scss +42 -42
  22. package/src/components/Modal.tsx +25 -0
  23. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +74 -0
  24. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +29 -0
  25. package/src/components/UsaMap/components/SingleState/index.tsx +9 -0
  26. package/src/components/UsaMap/components/UsaMap.County.tsx +84 -33
  27. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +173 -206
  28. package/src/components/UsaMap/components/UsaMap.State.tsx +161 -26
  29. package/src/components/UsaMap/data/us-extended-geography.json +1 -0
  30. package/src/components/UsaMap/helpers/map.ts +111 -0
  31. package/src/components/WorldMap/WorldMap.tsx +17 -32
  32. package/src/components/ZoomControls.tsx +41 -0
  33. package/src/data/initial-state.js +7 -1
  34. package/src/data/supported-geos.js +15 -4
  35. package/src/helpers/generateRuntimeLegendHash.ts +2 -2
  36. package/src/hooks/useStateZoom.tsx +157 -0
  37. package/src/hooks/{useZoomPan.js → useZoomPan.ts} +6 -5
  38. package/src/scss/editor-panel.scss +0 -4
  39. package/src/scss/main.scss +23 -1
  40. package/src/scss/map.scss +8 -0
  41. package/src/types/MapConfig.ts +9 -1
  42. package/src/types/MapContext.ts +14 -2
  43. package/src/components/Modal.jsx +0 -22
  44. /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
  45. /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
  46. /package/src/components/{ZoomableGroup.jsx → ZoomableGroup.tsx} +0 -0
@@ -101,8 +101,6 @@ const PanelAnnotate: React.FC = props => {
101
101
  return (
102
102
  <Accordion>
103
103
  <Accordion.Section title={props.name}>
104
- <p>Dragging state: {isDraggingAnnotation ? 'Dragging' : 'Not dragging'}</p>
105
-
106
104
  <label>
107
105
  Show Annotation Dropdown
108
106
  <input
@@ -225,26 +223,6 @@ const PanelAnnotate: React.FC = props => {
225
223
  }}
226
224
  />
227
225
  </label>
228
- <label>
229
- Associated Series:
230
- <select
231
- onChange={e => {
232
- const updatedAnnotations = [...config?.annotations]
233
- updatedAnnotations[index].seriesKey = e.target.value
234
- updateConfig({
235
- ...config,
236
- annotations: updatedAnnotations
237
- })
238
- }}
239
- >
240
- <option key='none' value='none'>
241
- None
242
- </option>
243
- {getColumns(false).map((column, columnIndex) => {
244
- return <option>{column}</option>
245
- })}
246
- </select>
247
- </label>
248
226
 
249
227
  <label>
250
228
  Connection Type:
@@ -6,13 +6,19 @@ import Button from '@cdc/core/components/elements/Button'
6
6
  import Tooltip from '@cdc/core/components/ui/Tooltip'
7
7
  import Icon from '@cdc/core/components/ui/Icon'
8
8
  import './Panel.PatternSettings-style.css'
9
+ import Alert from '@cdc/core/components/Alert'
10
+
11
+ // topojson helpers for checking color contrasts
12
+ import { feature } from 'topojson-client'
13
+ import { checkColorContrast, getContrastColor, getColorContrast } from '@cdc/core/helpers/cove/accessibility'
14
+ import topoJSON from '../../../UsaMap/data/us-topo.json'
9
15
 
10
16
  type PanelProps = {
11
17
  name: string
12
18
  }
13
19
 
14
20
  const PatternSettings = ({ name }: PanelProps) => {
15
- const { state, setState } = useContext<MapContext>(ConfigContext)
21
+ const { state, setState, applyLegendToRow, runtimeData } = useContext<MapContext>(ConfigContext)
16
22
  const defaultPattern = 'circles'
17
23
  const patternTypes = ['circles', 'waves', 'lines']
18
24
 
@@ -24,7 +30,7 @@ const PatternSettings = ({ name }: PanelProps) => {
24
30
  /** Updates the map config with a new pattern item */
25
31
  const handleAddGeoPattern = () => {
26
32
  let patterns = [...state.map.patterns]
27
- patterns.push({ dataKey: '', pattern: defaultPattern })
33
+ patterns.push({ dataKey: '', pattern: defaultPattern, contrastCheck: true })
28
34
  setState({
29
35
  ...state,
30
36
  map: {
@@ -36,16 +42,56 @@ const PatternSettings = ({ name }: PanelProps) => {
36
42
 
37
43
  /** Updates the map pattern at a given index */
38
44
  const handleUpdateGeoPattern = (value: string, index: number, keyToUpdate: 'dataKey' | 'pattern' | 'dataValue' | 'size' | 'label' | 'color') => {
45
+ const { features: unitedStates } = feature(topoJSON, topoJSON.objects.states)
39
46
  const updatedPatterns = [...state.map.patterns]
47
+
48
+ // Update the specific pattern with the new value
40
49
  updatedPatterns[index] = { ...updatedPatterns[index], [keyToUpdate]: value }
41
50
 
42
- setState({
43
- ...state,
51
+ // Iterate over each state feature
52
+ unitedStates.forEach(geo => {
53
+ const geoKey = geo.properties.iso
54
+ if (!geoKey || !runtimeData) return
55
+
56
+ const legendColors = runtimeData[geoKey] ? applyLegendToRow(runtimeData[geoKey]) : undefined
57
+ const geoData = runtimeData[geoKey]
58
+ if (!geoData) return
59
+
60
+ // Iterate over each pattern
61
+ state.map.patterns.forEach((patternData, patternIndex) => {
62
+ const hasMatchingValues = patternData.dataValue === geoData[patternData.dataKey]
63
+ if (!hasMatchingValues) return
64
+
65
+ const currentFill = legendColors[0]
66
+ const patternColor = keyToUpdate === 'color' && value !== '' ? value : getContrastColor('#000', currentFill)
67
+ const contrastCheck = checkColorContrast(currentFill, patternColor)
68
+
69
+ // Log a warning if the contrast check fails
70
+ if (!contrastCheck) {
71
+ console.warn(`COVE: pattern contrast check failed on ${geoData?.[state.columns.geo.name]} for ${patternData.dataKey} with:
72
+ pattern color: ${patternColor}
73
+ contrast: ${getColorContrast(currentFill, patternColor)}
74
+ `)
75
+ }
76
+
77
+ updatedPatterns[index] = { ...updatedPatterns[index], [keyToUpdate]: value, contrastCheck }
78
+ })
79
+ })
80
+
81
+ const editorErrorMessage = updatedPatterns.some(pattern => pattern.contrastCheck === false) ? 'One or more patterns do not pass the WCAG 2.1 contrast ratio of 3:1.' : ''
82
+
83
+ // Update the state with the new patterns and error message
84
+ setState(prevState => ({
85
+ ...prevState,
44
86
  map: {
45
- ...state.map,
87
+ ...prevState.map,
46
88
  patterns: updatedPatterns
89
+ },
90
+ runtime: {
91
+ ...prevState.runtime,
92
+ editorErrorMessage
47
93
  }
48
- })
94
+ }))
49
95
  }
50
96
 
51
97
  const handleRemovePattern = index => {
@@ -60,12 +106,19 @@ const PatternSettings = ({ name }: PanelProps) => {
60
106
  })
61
107
  }
62
108
 
109
+ const checkPatternContrasts = () => {
110
+ return state.map.patterns.every(pattern => pattern.contrastCheck !== false)
111
+ }
112
+
63
113
  return (
64
114
  <AccordionItem>
65
115
  <AccordionItemHeading>
66
116
  <AccordionItemButton>{name}</AccordionItemButton>
67
117
  </AccordionItemHeading>
68
118
  <AccordionItemPanel>
119
+ {patterns.length > 0 && <Alert type={checkPatternContrasts() ? 'success' : 'danger'} message='Pattern colors must comply with <br /> <a href="https://www.w3.org/TR/WCAG21/">WCAG 2.1</a> 3:1 contrast ratio.' />}
120
+ <br />
121
+
69
122
  {patterns &&
70
123
  patterns.map((pattern, patternIndex) => {
71
124
  const dataValueOptions = [...new Set(data?.map(d => d?.[pattern?.dataKey]))]
@@ -77,13 +130,14 @@ const PatternSettings = ({ name }: PanelProps) => {
77
130
  dataKeyOptions.sort()
78
131
 
79
132
  return (
80
- <Accordion allowZeroExpanded>
133
+ <Accordion allowZeroExpanded key={`accordion-pattern--${patternIndex}`}>
81
134
  <AccordionItem>
82
135
  <AccordionItemHeading>
83
136
  <AccordionItemButton>{pattern.dataKey ? `${pattern.dataKey}: ${pattern.dataValue ?? 'No Value'}` : 'Select Column'}</AccordionItemButton>
84
137
  </AccordionItemHeading>
85
138
  <AccordionItemPanel>
86
139
  <>
140
+ {pattern.contrastCheck ?? true ? <Alert type='success' message='This pattern passes contrast checks' /> : <Alert type='danger' message='Error: <a href="https://webaim.org/resources/contrastchecker/" target="_blank"> Review Color Contrast</a>' />}{' '}
87
141
  <label htmlFor={`pattern-dataKey--${patternIndex}`}>Data Key:</label>
88
142
  <select id={`pattern-dataKey--${patternIndex}`} value={pattern.dataKey !== '' ? pattern.dataKey : 'Select'} onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'dataKey')}>
89
143
  {/* TODO: sort these? */}
@@ -95,17 +149,14 @@ const PatternSettings = ({ name }: PanelProps) => {
95
149
  )
96
150
  })}
97
151
  </select>
98
-
99
152
  <label htmlFor={`pattern-dataValue--${patternIndex}`}>
100
153
  Data Value:
101
154
  <input type='text' onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'dataValue')} id={`pattern-dataValue--${patternIndex}`} value={pattern.dataValue === '' ? '' : pattern.dataValue} />
102
155
  </label>
103
-
104
156
  <label htmlFor={`pattern-label--${patternIndex}`}>
105
157
  Label (optional):
106
158
  <input type='text' onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'label')} id={`pattern-dataValue--${patternIndex}`} value={pattern.label === '' ? '' : pattern.label} />
107
159
  </label>
108
-
109
160
  <label htmlFor={`pattern-type--${patternIndex}`}>Pattern Type:</label>
110
161
  <select id={`pattern-type--${patternIndex}`} value={pattern?.pattern} onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'pattern')}>
111
162
  {patternTypes.map((patternName, index) => (
@@ -114,7 +165,6 @@ const PatternSettings = ({ name }: PanelProps) => {
114
165
  </option>
115
166
  ))}
116
167
  </select>
117
-
118
168
  <label htmlFor={`pattern-size--${patternIndex}`}>Pattern Size:</label>
119
169
  <select id={`pattern-size--${patternIndex}`} value={pattern?.size} onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'size')}>
120
170
  {['small', 'medium', 'large'].map((size, index) => (
@@ -1,9 +1,13 @@
1
1
  //TODO: Move legends to core
2
- import { forwardRef, useContext } from 'react'
2
+ import { forwardRef, useContext, useId } from 'react'
3
3
  import parse from 'html-react-parser'
4
4
 
5
+ //types
6
+ import { DimensionsType } from '@cdc/core/types/Dimensions'
7
+
5
8
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
- import LegendCircle from '@cdc/core/components/LegendCircle'
9
+ import LegendShape from '@cdc/core/components/LegendShape'
10
+ import LegendGradient from '@cdc/core/components/Legend/Legend.Gradient'
7
11
  import LegendItemHex from './LegendItem.Hex'
8
12
  import Button from '@cdc/core/components/elements/Button'
9
13
 
@@ -11,18 +15,21 @@ import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
11
15
  import ConfigContext from '../../../context'
12
16
  import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
13
17
  import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from '@visx/glyph'
18
+ import { type ViewportSize } from '../../../types/MapConfig'
14
19
  import { Group } from '@visx/group'
15
20
  import './index.scss'
16
21
 
17
22
  type LegendProps = {
18
23
  skipId: string
24
+ currentViewport: ViewportSize
25
+ dimensions: DimensionsType
19
26
  }
20
27
 
21
- const Legend = forwardRef((props, ref) => {
22
- const { skipId } = props
28
+ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
29
+ const { skipId, currentViewport, dimensions } = props
23
30
 
24
- // prettier-ignore
25
31
  const {
32
+ // prettier-ignore
26
33
  displayDataAsText,
27
34
  resetLegendToggles,
28
35
  runtimeFilters,
@@ -31,6 +38,8 @@ const Legend = forwardRef((props, ref) => {
31
38
  setRuntimeLegend,
32
39
  state,
33
40
  viewport,
41
+ getTextWidth,
42
+ mapId
34
43
  } = useContext(ConfigContext)
35
44
 
36
45
  const { legend } = state
@@ -51,17 +60,15 @@ const Legend = forwardRef((props, ref) => {
51
60
 
52
61
  setRuntimeLegend(newLegend)
53
62
 
54
- setAccessibleStatus(`Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`)
63
+ setAccessibleStatus(
64
+ `Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`
65
+ )
55
66
  }
56
-
57
- const legendList = () => {
58
- let legendItems
59
-
60
- legendItems = runtimeLegend.map((entry, idx) => {
67
+ const getFormattedLegendItems = () => {
68
+ return runtimeLegend.map((entry, idx) => {
61
69
  const entryMax = displayDataAsText(entry.max, 'primary')
62
70
 
63
71
  const entryMin = displayDataAsText(entry.min, 'primary')
64
-
65
72
  let formattedText = `${entryMin}${entryMax !== entryMin ? ` - ${entryMax}` : ''}`
66
73
 
67
74
  // If interval, add some formatting
@@ -69,8 +76,6 @@ const Legend = forwardRef((props, ref) => {
69
76
  formattedText = `${entryMin} - < ${entryMax}`
70
77
  }
71
78
 
72
- const { disabled } = entry
73
-
74
79
  if (legend.type === 'category') {
75
80
  formattedText = displayDataAsText(entry.value, 'primary')
76
81
  }
@@ -85,31 +90,49 @@ const Legend = forwardRef((props, ref) => {
85
90
  legendLabel = entry.label || entry.value
86
91
  }
87
92
 
93
+ return {
94
+ color: entry.color,
95
+ label: legendLabel,
96
+ disabled: entry.disabled,
97
+ special: entry.hasOwnProperty('special'),
98
+ value: [entry.min, entry.max]
99
+ }
100
+ })
101
+ }
102
+
103
+ const legendList = () => {
104
+ const formattedItems = getFormattedLegendItems()
105
+ let legendItems
106
+
107
+ legendItems = formattedItems.map((item, idx) => {
88
108
  const handleListItemClass = () => {
89
109
  let classes = ['legend-container__li']
90
- if (disabled) classes.push('legend-container__li--disabled')
91
- if (entry.hasOwnProperty('special')) classes.push('legend-container__li--special-class')
92
- return classes
110
+ if (item.disabled) classes.push('legend-container__li--disabled')
111
+ if (item.special) classes.push('legend-container__li--special-class')
112
+ return classes.join(' ')
93
113
  }
94
114
 
95
115
  return (
96
116
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
97
117
  <li
98
- className={handleListItemClass().join(' ')}
118
+ className={handleListItemClass()}
99
119
  key={idx}
100
- title={`Legend item ${legendLabel} - Click to disable`}
101
- onClick={() => {
102
- toggleLegendActive(idx, legendLabel)
103
- }}
120
+ title={`Legend item ${item.label} - Click to disable`}
121
+ onClick={() => toggleLegendActive(idx, item.label)}
104
122
  onKeyDown={e => {
105
123
  if (e.key === 'Enter') {
106
124
  e.preventDefault()
107
- toggleLegendActive(idx, legendLabel)
125
+ toggleLegendActive(idx, item.label)
108
126
  }
109
127
  }}
110
128
  tabIndex={0}
111
129
  >
112
- <LegendCircle viewport={viewport} fill={entry.color} /> <span>{legendLabel}</span>
130
+ <LegendShape
131
+ shape={state.legend.style === 'boxes' ? 'square' : 'circle'}
132
+ viewport={viewport}
133
+ fill={item.color}
134
+ />
135
+ <span>{item.label}</span>
113
136
  </li>
114
137
  )
115
138
  })
@@ -129,13 +152,48 @@ const Legend = forwardRef((props, ref) => {
129
152
 
130
153
  legendItems.push(
131
154
  <>
132
- <li className={`legend-container__li legend-container__li--geo-pattern`} aria-label='You are on a pattern button. We dont support toggling patterns on this legend at the moment, but provide the area as being focusable for congruity.' tabIndex={0}>
155
+ <li
156
+ className={`legend-container__li legend-container__li--geo-pattern`}
157
+ aria-label='You are on a pattern button. We dont support toggling patterns on this legend at the moment, but provide the area as being focusable for congruity.'
158
+ tabIndex={0}
159
+ >
133
160
  <span className='legend-item' style={{ border: 'unset' }}>
134
161
  <svg width={legendSize} height={legendSize}>
135
- {pattern === 'waves' && <PatternWaves id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 10} width={sizes[size] ?? 10} fill={defaultPatternColor} />}
136
- {pattern === 'circles' && <PatternCircles id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 10} width={sizes[size] ?? 10} fill={defaultPatternColor} />}
137
- {pattern === 'lines' && <PatternLines id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 6} width={sizes[size] ?? 10} stroke={defaultPatternColor} strokeWidth={2} orientation={['diagonalRightToLeft']} />}
138
- <circle id={dataKey} fill={`url(#${dataKey}--${patternDataIndex})`} r={legendSize / 2} cx={legendSize / 2} cy={legendSize / 2} stroke='#0000004d' strokeWidth={1} />
162
+ {pattern === 'waves' && (
163
+ <PatternWaves
164
+ id={`${mapId}--${dataKey}--${patternDataIndex}`}
165
+ height={sizes[size] ?? 10}
166
+ width={sizes[size] ?? 10}
167
+ fill={defaultPatternColor}
168
+ />
169
+ )}
170
+ {pattern === 'circles' && (
171
+ <PatternCircles
172
+ id={`${mapId}--${dataKey}--${patternDataIndex}`}
173
+ height={sizes[size] ?? 10}
174
+ width={sizes[size] ?? 10}
175
+ fill={defaultPatternColor}
176
+ />
177
+ )}
178
+ {pattern === 'lines' && (
179
+ <PatternLines
180
+ id={`${mapId}--${dataKey}--${patternDataIndex}`}
181
+ height={sizes[size] ?? 6}
182
+ width={sizes[size] ?? 10}
183
+ stroke={defaultPatternColor}
184
+ strokeWidth={2}
185
+ orientation={['diagonalRightToLeft']}
186
+ />
187
+ )}
188
+ <circle
189
+ id={dataKey}
190
+ fill={`url(#${mapId}--${dataKey}--${patternDataIndex})`}
191
+ r={legendSize / 2}
192
+ cx={legendSize / 2}
193
+ cy={legendSize / 2}
194
+ stroke='#0000004d'
195
+ strokeWidth={1}
196
+ />
139
197
  </svg>
140
198
  </span>
141
199
  <p style={{ lineHeight: '22.4px' }}>{patternData.label || patternData.dataValue || ''}</p>
@@ -147,7 +205,6 @@ const Legend = forwardRef((props, ref) => {
147
205
 
148
206
  return legendItems
149
207
  }
150
-
151
208
  const { legendClasses } = useDataVizClasses(state, viewport)
152
209
 
153
210
  const handleReset = e => {
@@ -162,7 +219,15 @@ const Legend = forwardRef((props, ref) => {
162
219
  }
163
220
  }
164
221
 
165
- const pin = <path className='marker' 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' strokeWidth={2} stroke={'black'} transform={`scale(0.5)`} />
222
+ const pin = (
223
+ <path
224
+ className='marker'
225
+ 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'
226
+ strokeWidth={2}
227
+ stroke={'black'}
228
+ transform={`scale(0.5)`}
229
+ />
230
+ )
166
231
 
167
232
  const cityStyleShapes = {
168
233
  pin: pin,
@@ -176,10 +241,19 @@ const Legend = forwardRef((props, ref) => {
176
241
  return (
177
242
  <ErrorBoundary component='Sidebar'>
178
243
  <div className='legends'>
179
- <aside id={skipId || 'legend'} className={legendClasses.aside.join(' ') || ''} role='region' aria-label='Legend' tabIndex={0} ref={ref}>
244
+ <aside
245
+ id={skipId || 'legend'}
246
+ className={legendClasses.aside.join(' ') || ''}
247
+ role='region'
248
+ aria-label='Legend'
249
+ tabIndex={0}
250
+ ref={ref}
251
+ >
180
252
  <section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
181
253
  {legend.title && <h3 className={legendClasses.title.join(' ') || ''}>{parse(legend.title)}</h3>}
182
- {legend.dynamicDescription === false && legend.description && <p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>}
254
+ {legend.dynamicDescription === false && legend.description && (
255
+ <p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>
256
+ )}
183
257
  {legend.dynamicDescription === true &&
184
258
  runtimeFilters.map((filter, idx) => {
185
259
  const lookupStr = `${idx},${filter.values.indexOf(String(filter.active))}`
@@ -196,8 +270,18 @@ const Legend = forwardRef((props, ref) => {
196
270
  }
197
271
  return true
198
272
  })}
273
+
274
+ <LegendGradient
275
+ labels={getFormattedLegendItems().map(item => item?.label) ?? []}
276
+ colors={getFormattedLegendItems().map(item => item?.color) ?? []}
277
+ values={getFormattedLegendItems().map(item => item?.value) ?? []}
278
+ dimensions={dimensions}
279
+ currentViewport={currentViewport}
280
+ config={state}
281
+ getTextWidth={getTextWidth}
282
+ />
199
283
  <ul className={legendClasses.ul.join(' ') || ''} aria-label='Legend items'>
200
- {legendList()}
284
+ {state.legend.style === 'gradient' ? '' : legendList()}
201
285
  </ul>
202
286
  {(state.visual.additionalCityStyles.some(c => c.label) || state.visual.cityStyleLabel) && (
203
287
  <>
@@ -206,7 +290,10 @@ const Legend = forwardRef((props, ref) => {
206
290
  {state.visual.cityStyleLabel && (
207
291
  <div>
208
292
  <svg>
209
- <Group top={state.visual.cityStyle === 'pin' ? 19 : state.visual.cityStyle === 'triangle' ? 13 : 11} left={10}>
293
+ <Group
294
+ top={state.visual.cityStyle === 'pin' ? 19 : state.visual.cityStyle === 'triangle' ? 13 : 11}
295
+ left={10}
296
+ >
210
297
  {cityStyleShapes[state.visual.cityStyle.toLowerCase()]}
211
298
  </Group>
212
299
  </svg>
@@ -233,7 +320,9 @@ const Legend = forwardRef((props, ref) => {
233
320
  {runtimeLegend.disabledAmt > 0 && <Button onClick={handleReset}>Reset</Button>}
234
321
  </section>
235
322
  </aside>
236
- {state.hexMap.shapeGroups?.length > 0 && state.hexMap.type === 'shapes' && state.general.displayAsHex && <LegendItemHex state={state} runtimeLegend={runtimeLegend} viewport={viewport} />}
323
+ {state.hexMap.shapeGroups?.length > 0 && state.hexMap.type === 'shapes' && state.general.displayAsHex && (
324
+ <LegendItemHex state={state} runtimeLegend={runtimeLegend} viewport={viewport} />
325
+ )}
237
326
  </div>
238
327
  </ErrorBoundary>
239
328
  )
@@ -17,8 +17,10 @@
17
17
  background-color: #fff;
18
18
  z-index: 6;
19
19
  border-top: $lightGray 1px solid;
20
+
20
21
  @include breakpointClass(md) {
21
- &.bottom {
22
+ &.bottom,
23
+ &.top {
22
24
  border: $lightGray 1px solid;
23
25
  }
24
26
  &.side {
@@ -35,8 +37,12 @@
35
37
  right: 1em;
36
38
 
37
39
  ul.vertical-sorted {
40
+ display: block;
38
41
  column-count: 2;
39
42
  column-fill: balance;
43
+ & > li {
44
+ white-space: nowrap;
45
+ }
40
46
  }
41
47
 
42
48
  ul:not(.vertical-sorted) {
@@ -47,29 +53,17 @@
47
53
  flex-wrap: wrap;
48
54
  }
49
55
  }
56
+ &.no-border {
57
+ border: none;
58
+ }
50
59
 
51
- &.bottom {
60
+ &.bottom,
61
+ &.top {
52
62
  ul.legend-container__ul.vertical-sorted {
53
63
  display: block;
54
64
  column-count: 2;
55
65
  column-fill: balance;
56
66
  }
57
-
58
- ul.legend-container__ul {
59
- display: flex;
60
- flex-direction: row;
61
- flex-wrap: wrap;
62
-
63
- li {
64
- width: 50%;
65
- }
66
- }
67
-
68
- ul.single-row {
69
- display: block;
70
- column-count: initial;
71
- column-fill: auto;
72
- }
73
67
  }
74
68
  }
75
69
 
@@ -103,9 +97,12 @@
103
97
  p {
104
98
  line-height: 1.4em;
105
99
  }
106
- .legend-container__ul {
100
+ .legend-container__ul:not(.single-row) {
107
101
  list-style: none;
108
102
  padding-top: 1em;
103
+ display: grid;
104
+ grid-template-columns: 1fr 1fr;
105
+
109
106
  button {
110
107
  font-size: unset;
111
108
  background: transparent;
@@ -132,6 +129,7 @@
132
129
  transition: 0.1s opacity;
133
130
  display: flex;
134
131
  cursor: pointer;
132
+ white-space: nowrap;
135
133
  flex-grow: 1;
136
134
 
137
135
  &.legend-container__li--disabled {
@@ -139,6 +137,29 @@
139
137
  }
140
138
  }
141
139
  }
140
+ .legend-container__ul.single-row {
141
+ width: 100%;
142
+ list-style: none;
143
+ display: flex;
144
+ flex-direction: row;
145
+ align-items: center;
146
+ justify-content: flex-start;
147
+ flex-wrap: wrap;
148
+
149
+ & > li {
150
+ margin-right: 1em;
151
+ margin-bottom: 1em;
152
+ white-space: nowrap;
153
+ display: flex;
154
+ justify-content: center;
155
+ align-items: center;
156
+ vertical-align: middle;
157
+
158
+ & svg {
159
+ vertical-align: baseline;
160
+ }
161
+ }
162
+ }
142
163
  }
143
164
 
144
165
  .bottom .legend-container__ul--single-column:not(.vertical-sorted) {
@@ -150,6 +171,7 @@
150
171
 
151
172
  .legend-container__li {
152
173
  width: 100%;
174
+ white-space: nowrap;
153
175
  }
154
176
  }
155
177
 
@@ -175,30 +197,8 @@
175
197
  }
176
198
  }
177
199
 
178
- &.bottom.single-row {
179
- width: 100%;
180
- .legend-container ul {
181
- flex-direction: row;
182
- align-items: baseline;
183
- justify-content: flex-start;
184
- flex-wrap: wrap;
185
- li {
186
- justify-items: center;
187
- line-break: loose;
188
- align-items: center;
189
- width: auto;
190
- padding-right: 1em;
191
- padding-bottom: 1em;
192
- display: inline-block;
193
- & > span {
194
- margin: 0 !important;
195
- }
196
- }
197
- }
198
- }
199
-
200
200
  @include breakpointClass(sm) {
201
- .legend-container ul {
201
+ .legend-container ul:not(.single-row) {
202
202
  align-items: flex-start;
203
203
  justify-content: space-between;
204
204
  li {
@@ -0,0 +1,25 @@
1
+ import { useContext } from 'react'
2
+ import LegendShape from '@cdc/core/components/LegendShape'
3
+ import ConfigContext from '../context'
4
+ import Icon from '@cdc/core/components/ui/Icon'
5
+
6
+ const Modal = () => {
7
+ const { applyTooltipsToGeo, capitalize, applyLegendToRow, viewport, type, content } = useContext(ConfigContext)
8
+
9
+ const tooltip = applyTooltipsToGeo(content.geoName, content.keyedData, 'jsx')
10
+
11
+ const legendColors = applyLegendToRow(content.keyedData)
12
+
13
+ return (
14
+ <section
15
+ className={capitalize ? 'modal-content tooltip capitalize ' + viewport : 'modal-content tooltip ' + viewport}
16
+ aria-hidden='true'
17
+ >
18
+ {type === 'data' && <LegendShape fill={legendColors[0]} />}
19
+ <div className='content'>{tooltip}</div>
20
+ <Icon display='close' alt='Close Modal' size={20} color='#000' className='modal-close' />
21
+ </section>
22
+ )
23
+ }
24
+
25
+ export default Modal