@cdc/map 4.24.2 → 4.24.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 (38) hide show
  1. package/dist/cdcmap.js +33089 -32197
  2. package/examples/508.json +548 -0
  3. package/examples/default-county.json +0 -28
  4. package/examples/default-hex.json +110 -13
  5. package/examples/default-usa.json +69 -28
  6. package/examples/example-city-state.json +51 -17
  7. package/examples/hex-colors.json +507 -0
  8. package/examples/usa-special-class-legend.json +501 -0
  9. package/index.html +10 -9
  10. package/package.json +3 -3
  11. package/src/CdcMap.tsx +200 -125
  12. package/src/components/BubbleList.jsx +16 -6
  13. package/src/components/CityList.jsx +64 -12
  14. package/src/components/DataTable.jsx +7 -7
  15. package/src/components/EditorPanel/components/EditorPanel.tsx +1457 -1367
  16. package/src/components/EditorPanel/components/Error.tsx +12 -0
  17. package/src/components/EditorPanel/components/HexShapeSettings.tsx +16 -1
  18. package/src/components/EditorPanel/components/Panel.PatternSettings-style.css +5 -0
  19. package/src/components/EditorPanel/components/Panel.PatternSettings.tsx +18 -1
  20. package/src/components/Legend/components/Legend.tsx +80 -15
  21. package/src/components/Legend/components/LegendItem.Hex.tsx +1 -1
  22. package/src/components/Legend/components/index.scss +31 -5
  23. package/src/components/UsaMap/components/HexIcon.tsx +41 -0
  24. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +38 -19
  25. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +11 -22
  26. package/src/components/UsaMap/components/UsaMap.County.tsx +7 -4
  27. package/src/components/UsaMap/components/UsaMap.Region.tsx +13 -38
  28. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +5 -3
  29. package/src/components/UsaMap/components/UsaMap.State.tsx +77 -60
  30. package/src/components/UsaMap/helpers/patternSizes.tsx +5 -0
  31. package/src/components/WorldMap/components/WorldMap.jsx +20 -3
  32. package/src/data/initial-state.js +3 -0
  33. package/src/data/supported-geos.js +2 -1
  34. package/src/hooks/useMapLayers.tsx +2 -2
  35. package/src/scss/editor-panel.scss +4 -12
  36. package/src/scss/main.scss +3 -1
  37. package/src/scss/map.scss +1 -1
  38. package/src/types/MapConfig.ts +7 -0
@@ -0,0 +1,12 @@
1
+ const Error = ({ state }) => {
2
+ return (
3
+ <section className='waiting'>
4
+ <section className='waiting-container'>
5
+ <h3>Error With Configuration</h3>
6
+ <p>{state.runtime.editorErrorMessage}</p>
7
+ </section>
8
+ </section>
9
+ )
10
+ }
11
+
12
+ export default Error
@@ -4,7 +4,7 @@ import parse from 'html-react-parser'
4
4
  import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight } from 'react-icons/ai'
5
5
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
6
6
 
7
- const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'None']
7
+ const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'Arrow Left', 'None']
8
8
 
9
9
  // todo: Move duplicated operators to CORE
10
10
  export const DATA_OPERATOR_LESS = '<'
@@ -221,6 +221,21 @@ const HexSettingShapeColumns = props => {
221
221
  {[DATA_OPERATOR_EQUAL].map(option => {
222
222
  return <option value={option}>{option}</option>
223
223
  })}
224
+ {[DATA_OPERATOR_NOTEQUAL].map(option => {
225
+ return <option value={option}>{option}</option>
226
+ })}
227
+ {[DATA_OPERATOR_LESS].map(option => {
228
+ return <option value={option}>{option}</option>
229
+ })}
230
+ {[DATA_OPERATOR_GREATER].map(option => {
231
+ return <option value={option}>{option}</option>
232
+ })}
233
+ {[DATA_OPERATOR_LESSEQUAL].map(option => {
234
+ return <option value={option}>{option}</option>
235
+ })}
236
+ {[DATA_OPERATOR_GREATEREQUAL].map(option => {
237
+ return <option value={option}>{option}</option>
238
+ })}
224
239
  </select>
225
240
  </div>
226
241
  <div className='cove-accordion__panel-col cove-input'>
@@ -0,0 +1,5 @@
1
+ .cdc-open-viz-module {
2
+ .pattern-input__color {
3
+ margin-top: 1rem;
4
+ }
5
+ }
@@ -3,6 +3,9 @@ import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, Acc
3
3
  import ConfigContext from '../../../context'
4
4
  import { type MapContext } from '../../../types/MapContext'
5
5
  import Button from '@cdc/core/components/elements/Button'
6
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
7
+ import Icon from '@cdc/core/components/ui/Icon'
8
+ import './Panel.PatternSettings-style.css'
6
9
 
7
10
  type PanelProps = {
8
11
  name: string
@@ -32,7 +35,7 @@ const PatternSettings = ({ name }: PanelProps) => {
32
35
  }
33
36
 
34
37
  /** Updates the map pattern at a given index */
35
- const handleUpdateGeoPattern = (value: string, index: number, keyToUpdate: 'dataKey' | 'pattern' | 'dataValue' | 'size' | 'label') => {
38
+ const handleUpdateGeoPattern = (value: string, index: number, keyToUpdate: 'dataKey' | 'pattern' | 'dataValue' | 'size' | 'label' | 'color') => {
36
39
  const updatedPatterns = [...state.map.patterns]
37
40
  updatedPatterns[index] = { ...updatedPatterns[index], [keyToUpdate]: value }
38
41
 
@@ -120,6 +123,20 @@ const PatternSettings = ({ name }: PanelProps) => {
120
123
  </option>
121
124
  ))}
122
125
  </select>
126
+ <div className='pattern-input__color'>
127
+ <label htmlFor='patternColor'>
128
+ Pattern Color
129
+ <Tooltip style={{ textTransform: 'none' }}>
130
+ <Tooltip.Target>
131
+ <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
132
+ </Tooltip.Target>
133
+ <Tooltip.Content>
134
+ <p>{`If this setting is used, it is the reponsibility of the visualization author to verify the visualization colors meet WCAG 3:1 contrast ratios.`}</p>
135
+ </Tooltip.Content>
136
+ </Tooltip>
137
+ <input type='text' value={pattern.color || ''} id='patternColor' name='patternColor' onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'color')} />
138
+ </label>
139
+ </div>
123
140
  <Button onClick={e => handleRemovePattern(patternIndex)} className='btn btn--remove warn'>
124
141
  Remove Pattern
125
142
  </Button>
@@ -1,18 +1,26 @@
1
1
  //TODO: Move legends to core
2
- import { useContext } from 'react'
2
+ import { forwardRef, useContext } from 'react'
3
3
  import parse from 'html-react-parser'
4
- import chroma from 'chroma-js'
5
4
 
6
5
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
7
6
  import LegendCircle from '@cdc/core/components/LegendCircle'
8
7
  import LegendItemHex from './LegendItem.Hex'
8
+ import Button from '@cdc/core/components/elements/Button'
9
9
 
10
10
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
11
11
  import ConfigContext from '../../../context'
12
12
  import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
13
+ import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from '@visx/glyph'
14
+ import { Group } from '@visx/group'
13
15
  import './index.scss'
14
16
 
15
- const Legend = () => {
17
+ type LegendProps = {
18
+ skipId: string
19
+ }
20
+
21
+ const Legend = forwardRef((props, ref) => {
22
+ const { skipId } = props
23
+
16
24
  // prettier-ignore
17
25
  const {
18
26
  displayDataAsText,
@@ -26,6 +34,7 @@ const Legend = () => {
26
34
  } = useContext(ConfigContext)
27
35
 
28
36
  const { legend } = state
37
+ const fontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? { fontSize: '11px' } : null
29
38
 
30
39
  // Toggles if a legend is active and being applied to the map and data table.
31
40
  const toggleLegendActive = (i, legendLabel) => {
@@ -87,14 +96,22 @@ const Legend = () => {
87
96
  return (
88
97
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
89
98
  <li
99
+ style={fontSize}
90
100
  className={handleListItemClass().join(' ')}
91
101
  key={idx}
92
102
  title={`Legend item ${legendLabel} - Click to disable`}
93
103
  onClick={() => {
94
104
  toggleLegendActive(idx, legendLabel)
95
105
  }}
106
+ onKeyDown={e => {
107
+ if (e.key === 'Enter') {
108
+ e.preventDefault()
109
+ toggleLegendActive(idx, legendLabel)
110
+ }
111
+ }}
112
+ tabIndex={0}
96
113
  >
97
- <LegendCircle fill={entry.color} /> <span className='label'>{legendLabel}</span>
114
+ <LegendCircle viewport={viewport} fill={entry.color} /> <span>{legendLabel}</span>
98
115
  </li>
99
116
  )
100
117
  })
@@ -114,7 +131,7 @@ const Legend = () => {
114
131
 
115
132
  legendItems.push(
116
133
  <>
117
- <li className={`legend-container__li legend-container__li--geo-pattern`}>
134
+ <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}>
118
135
  <span className='legend-item' style={{ border: 'unset' }}>
119
136
  <svg width={legendSize} height={legendSize}>
120
137
  {pattern === 'waves' && <PatternWaves id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 10} width={sizes[size] ?? 10} fill={defaultPatternColor} />}
@@ -136,23 +153,39 @@ const Legend = () => {
136
153
  const { legendClasses } = useDataVizClasses(state, viewport)
137
154
 
138
155
  const handleReset = e => {
139
- e.preventDefault()
156
+ const legend = ref.current
157
+ if (e) {
158
+ e.preventDefault()
159
+ }
140
160
  resetLegendToggles()
141
161
  setAccessibleStatus('Legend has been reset, please reference the data table to see updated values.')
162
+ if (legend) {
163
+ legend.focus()
164
+ }
165
+ }
166
+
167
+ 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)`} />
168
+
169
+ const cityStyleShapes = {
170
+ pin: pin,
171
+ circle: <GlyphCircle color='#000' size={150} />,
172
+ square: <GlyphSquare color='#000' size={150} />,
173
+ diamond: <GlyphDiamond color='#000' size={150} />,
174
+ star: <GlyphStar color='#000' size={150} />,
175
+ triangle: <GlyphTriangle color='#000' size={150} />
142
176
  }
143
177
 
144
178
  return (
145
179
  <ErrorBoundary component='Sidebar'>
146
180
  <div className='legends'>
147
- <aside id='legend' className={legendClasses.aside.join(' ') || ''} role='region' aria-label='Legend'>
181
+ <aside id={skipId || 'legend'} className={legendClasses.aside.join(' ') || ''} role='region' aria-label='Legend' tabIndex={0} ref={ref}>
148
182
  <section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
149
- {runtimeLegend.disabledAmt > 0 && (
150
- <button onClick={handleReset} className={legendClasses.resetButton.join(' ') || ''}>
151
- Clear
152
- </button>
183
+ {legend.title && <h3 className={legendClasses.title.join(' ') || ''}>{parse(legend.title)}</h3>}
184
+ {legend.dynamicDescription === false && legend.description && (
185
+ <p style={fontSize} className={legendClasses.description.join(' ') || ''}>
186
+ {parse(legend.description)}
187
+ </p>
153
188
  )}
154
- {legend.title && <span className={legendClasses.title.join(' ') || ''}>{parse(legend.title)}</span>}
155
- {legend.dynamicDescription === false && legend.description && <p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>}
156
189
  {legend.dynamicDescription === true &&
157
190
  runtimeFilters.map((filter, idx) => {
158
191
  const lookupStr = `${idx},${filter.values.indexOf(String(filter.active))}`
@@ -162,7 +195,7 @@ const Legend = () => {
162
195
 
163
196
  if (desc.length > 0) {
164
197
  return (
165
- <p key={`dynamic-description-${lookupStr}`} className={`dynamic-legend-description-${lookupStr}`}>
198
+ <p style={fontSize} key={`dynamic-description-${lookupStr}`} className={`dynamic-legend-description-${lookupStr}`}>
166
199
  {desc}
167
200
  </p>
168
201
  )
@@ -172,12 +205,44 @@ const Legend = () => {
172
205
  <ul className={legendClasses.ul.join(' ') || ''} aria-label='Legend items'>
173
206
  {legendList()}
174
207
  </ul>
208
+ {(state.visual.additionalCityStyles.some(c => c.label) || state.visual.cityStyleLabel) && (
209
+ <>
210
+ <hr />
211
+ <div className={legendClasses.div.join(' ') || ''}>
212
+ {state.visual.cityStyleLabel && (
213
+ <div>
214
+ <svg>
215
+ <Group top={state.visual.cityStyle === 'pin' ? 19 : state.visual.cityStyle === 'triangle' ? 13 : 11} left={10}>
216
+ {cityStyleShapes[state.visual.cityStyle.toLowerCase()]}
217
+ </Group>
218
+ </svg>
219
+ <p>{state.visual.cityStyleLabel}</p>
220
+ </div>
221
+ )}
222
+
223
+ {state.visual.additionalCityStyles.map(
224
+ ({ shape, label }) =>
225
+ label && (
226
+ <div>
227
+ <svg>
228
+ <Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
229
+ {cityStyleShapes[shape.toLowerCase()]}
230
+ </Group>
231
+ </svg>
232
+ <p style={fontSize}>{label}</p>
233
+ </div>
234
+ )
235
+ )}
236
+ </div>
237
+ </>
238
+ )}
239
+ {runtimeLegend.disabledAmt > 0 && <Button onClick={handleReset}>Reset</Button>}
175
240
  </section>
176
241
  </aside>
177
242
  {state.hexMap.shapeGroups?.length > 0 && state.hexMap.type === 'shapes' && state.general.displayAsHex && <LegendItemHex state={state} runtimeLegend={runtimeLegend} viewport={viewport} />}
178
243
  </div>
179
244
  </ErrorBoundary>
180
245
  )
181
- }
246
+ })
182
247
 
183
248
  export default Legend
@@ -27,7 +27,7 @@ const LegendItemHex = props => {
27
27
  return (
28
28
  <aside id='legend' className={legendClasses.aside.join(' ')} role='region' aria-label='Legend' tabIndex={0}>
29
29
  <section className={legendClasses.section.join(' ')} aria-label='Map Legend'>
30
- {shapeGroup.legendTitle && <span className={legendClasses.title.join(' ')}>{parse(shapeGroup.legendTitle)}</span>}
30
+ {shapeGroup.legendTitle && <h3 className={legendClasses.title.join(' ')}>{parse(shapeGroup.legendTitle)}</h3>}
31
31
  {shapeGroup.legendDescription && <p className={legendClasses.description.join(' ')}>{parse(shapeGroup.legendDescription)}</p>}
32
32
 
33
33
  <ul className={legendClasses.ul.join(' ')} aria-label='Legend items' style={{ listStyle: 'none' }}>
@@ -89,17 +89,14 @@
89
89
  }
90
90
  .legend-container__reset-button {
91
91
  font-size: 0.75em;
92
- color: rgba(0, 0, 0, 0.6);
93
- position: absolute;
94
92
  right: 1em;
95
- background: rgba(0, 0, 0, 0.1);
96
93
  text-transform: uppercase;
97
94
  transition: 0.2s all;
98
95
  padding: 0.2em 0.5em;
99
96
  border: rgba(0, 0, 0, 0.2) 1px solid;
97
+ padding: 0.375rem;
100
98
  &:hover {
101
99
  text-decoration: none;
102
- background: rgba(0, 0, 0, 0.15);
103
100
  transition: 0.2s all;
104
101
  }
105
102
  }
@@ -118,7 +115,7 @@
118
115
  flex-direction: column;
119
116
  }
120
117
 
121
- &:not(.vertical-sorted, .legend-container__ul--single-column) {
118
+ &:not(.vertical-sorted, .legend-container__ul--single-column, .single-row) {
122
119
  width: 100%;
123
120
  @include breakpoint(sm) {
124
121
  .legend-container__li {
@@ -231,5 +228,34 @@
231
228
  }
232
229
  }
233
230
  }
231
+ & .shape-container {
232
+ &.single-row {
233
+ display: flex;
234
+ flex-direction: row;
235
+ align-items: center;
236
+ justify-content: flex-start;
237
+ flex-wrap: wrap;
238
+
239
+ & > div {
240
+ margin-right: 2em;
241
+ }
242
+ & > div:last-child {
243
+ margin-right: 0;
244
+ }
245
+ }
246
+
247
+ & > div {
248
+ display: flex;
249
+ flex-direction: row;
250
+ }
251
+ & div > svg {
252
+ width: 25px;
253
+ height: 32px;
254
+ }
255
+ & div > p {
256
+ white-space: nowrap;
257
+ margin-top: 1px;
258
+ }
259
+ }
234
260
  }
235
261
  }
@@ -0,0 +1,41 @@
1
+ import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight, AiOutlineArrowLeft } from 'react-icons/ai'
2
+ import { Group } from '@visx/group'
3
+
4
+ type HexIconProps = {
5
+ item: {
6
+ operator: '=' | '≠' | '<' | '>' | '<=' | '>='
7
+ shape: 'Arrow Up' | 'Arrow Down' | 'Arrow Right' | 'Arrow Left'
8
+ key: string
9
+ value: string
10
+ }
11
+ index: number
12
+ centroid: [number, number]
13
+ iconSize: number
14
+ textColor: string
15
+ isTerritory: boolean
16
+ }
17
+
18
+ const HexIcon: React.FC<HexIconProps> = props => {
19
+ const { item, index, centroid, iconSize, textColor = '#000', isTerritory } = props
20
+ if (!centroid) return
21
+
22
+ if (isTerritory) {
23
+ return (
24
+ <Group style={{ transform: `translate(36%, 50%)` }} key={`territory-hex--${index}`}>
25
+ {item.shape === 'Arrow Down' && <AiOutlineArrowDown size={12} stroke='none' fontWeight={100} />}
26
+ {item.shape === 'Arrow Up' && <AiOutlineArrowUp size={12} stroke='none' fontWeight={100} />}
27
+ {item.shape === 'Arrow Right' && <AiOutlineArrowRight size={12} stroke='none' fontWeight={100} />}
28
+ {item.shape === 'Arrow Left' && <AiOutlineArrowLeft size={12} stroke='none' fontWeight={100} />}
29
+ </Group>
30
+ )
31
+ }
32
+ return (
33
+ <Group top={centroid[1] - 5} left={centroid[0] - iconSize} color={textColor} textAnchor='start' key={`hex--${item.key}-${item.value}-${index}`}>
34
+ {item.shape === 'Arrow Down' && <AiOutlineArrowDown />}
35
+ {item.shape === 'Arrow Up' && <AiOutlineArrowUp />}
36
+ {item.shape === 'Arrow Right' && <AiOutlineArrowRight />}
37
+ {item.shape === 'Arrow Left' && <AiOutlineArrowLeft />}
38
+ </Group>
39
+ )
40
+ }
41
+ export default HexIcon
@@ -2,10 +2,9 @@ import { useContext } from 'react'
2
2
  import { geoCentroid, geoPath } from 'd3-geo'
3
3
  import ConfigContext from './../../../../context'
4
4
  import { MapContext } from './../../../../types/MapContext'
5
- import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight } from 'react-icons/ai'
6
- import { Group } from '@visx/group'
5
+ import HexIcon from '../HexIcon'
7
6
  import { Text } from '@visx/text'
8
- import chroma from 'chroma-js'
7
+ import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
9
8
 
10
9
  const offsets = {
11
10
  'US-VT': [50, -8],
@@ -52,16 +51,41 @@ const TerritoryHexagon = ({ label, text, stroke, strokeWidth, textColor, territo
52
51
  <>
53
52
  {state.hexMap.shapeGroups.map((group, groupIndex) => {
54
53
  return group.items.map((item, itemIndex) => {
55
- if (item.operator === '=') {
56
- if (geoData[item.key] === item.value || Number(geoData[item.key]) === Number(item.value)) {
57
- return (
58
- <Group style={{ transform: `translate(36%, 50%)`, fill: 'currentColor' }} key={`territory-hex--${itemIndex}`}>
59
- {item.shape === 'Arrow Down' && <AiOutlineArrowDown size={12} stroke='none' fontWeight={100} />}
60
- {item.shape === 'Arrow Up' && <AiOutlineArrowUp size={12} stroke='none' fontWeight={100} />}
61
- {item.shape === 'Arrow Right' && <AiOutlineArrowRight size={12} stroke='none' fontWeight={100} />}
62
- </Group>
63
- )
64
- }
54
+ switch (item.operator) {
55
+ case '=':
56
+ if (geoData[item.key] === item.value || Number(geoData[item.key]) === Number(item.value)) {
57
+ return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
58
+ }
59
+ break
60
+ case '':
61
+ if (geoData[item.key] !== item.value && Number(geoData[item.key]) !== Number(item.value)) {
62
+ return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
63
+ }
64
+ break
65
+ case '<':
66
+ if (Number(geoData[item.key]) < Number(item.value)) {
67
+ return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
68
+ }
69
+ break
70
+ case '>':
71
+ if (Number(geoData[item.key]) > Number(item.value)) {
72
+ return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
73
+ }
74
+ break
75
+ case '<=':
76
+ if (Number(geoData[item.key]) <= Number(item.value)) {
77
+ return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
78
+ }
79
+ break
80
+ case '>=':
81
+ if (item.operator === '>=') {
82
+ if (Number(geoData[item.key]) >= Number(item.value)) {
83
+ return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
84
+ }
85
+ }
86
+ break
87
+ default:
88
+ break
65
89
  }
66
90
  })
67
91
  })}
@@ -71,12 +95,7 @@ const TerritoryHexagon = ({ label, text, stroke, strokeWidth, textColor, territo
71
95
 
72
96
  if (undefined === abbr) return null
73
97
 
74
- let textColor = '#FFF'
75
-
76
- // Dynamic text color
77
- if (chroma.contrast(textColor, bgColor) < 3.5) {
78
- textColor = '#202020' // dark gray
79
- }
98
+ let textColor = getContrastColor('#FFF', bgColor)
80
99
 
81
100
  // always make HI black since it is off to the side
82
101
  if (abbr === 'US-HI') {
@@ -1,58 +1,47 @@
1
1
  import { useContext } from 'react'
2
2
  import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
3
- import chroma from 'chroma-js'
4
3
  import ConfigContext from './../../../../context'
5
4
  import { type MapContext } from '../../../../types/MapContext'
6
- // todo: move this somewhere that makes better sense for pattern sizes.
7
- const sizes = {
8
- small: '8',
9
- medium: '10',
10
- large: '12'
11
- }
5
+ import { patternSizes } from './../../helpers/patternSizes'
6
+ import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
12
7
 
13
8
  const TerritoryRectangle = ({ label, text, stroke, strokeWidth, textColor, hasPattern, territory, ...props }) => {
14
9
  const { state, supportedTerritories } = useContext<MapContext>(ConfigContext)
15
10
 
16
11
  return (
17
12
  <svg viewBox='0 0 45 28'>
18
- <g {...props} strokeLinejoin='round'>
13
+ <g {...props} strokeLinejoin='round' tabIndex={-1}>
19
14
  <path
20
15
  stroke={stroke}
21
16
  strokeWidth={strokeWidth}
22
17
  d='M40,0.5 C41.2426407,0.5 42.3676407,1.00367966 43.1819805,1.81801948 C43.9963203,2.63235931 44.5,3.75735931 44.5,5 L44.5,5 L44.5,23 C44.5,24.2426407 43.9963203,25.3676407 43.1819805,26.1819805 C42.3676407,26.9963203 41.2426407,27.5 40,27.5 L40,27.5 L5,27.5 C3.75735931,27.5 2.63235931,26.9963203 1.81801948,26.1819805 C1.00367966,25.3676407 0.5,24.2426407 0.5,23 L0.5,23 L0.5,5 C0.5,3.75735931 1.00367966,2.63235931 1.81801948,1.81801948 C2.63235931,1.00367966 3.75735931,0.5 5,0.5 L5,0.5 Z'
23
18
  {...props}
24
19
  />
25
- <text textAnchor='middle' dominantBaseline='middle' x='50%' y='54%' fill={text} style={{ stroke: textColor, strokeWidth: 1 }} className='territory-text' paint-order='stroke'>
20
+ <text textAnchor='middle' dominantBaseline='middle' x='50%' y='54%' fill={text} style={{ stroke: textColor, strokeWidth: 1 }} className='territory-text' paintOrder='stroke'>
26
21
  {label}
27
22
  </text>
28
23
 
29
24
  {state.map.patterns.map((patternData, patternIndex) => {
30
- let defaultPatternColor = 'black'
31
-
32
- const hasMatchingValues = supportedTerritories[territory].includes(patternData?.dataValue)
33
- console.log('has match', patternData)
34
-
35
- if (chroma.contrast(defaultPatternColor, props.style.fill) < 3.5) {
36
- defaultPatternColor = 'white'
37
- }
25
+ const patternColor = getContrastColor('#FFF', props.style.fill)
26
+ const hasMatchingValues = supportedTerritories[territory]?.includes(patternData?.dataValue)
38
27
 
39
28
  if (!hasMatchingValues) return null
40
29
  if (!patternData.pattern) return null
41
30
 
42
31
  return (
43
32
  <>
44
- {patternData?.pattern === 'waves' && <PatternWaves id={`territory-${patternData?.dataKey}--${patternIndex}`} height={sizes[patternData?.size] ?? 10} width={sizes[patternData?.size] ?? 10} fill={defaultPatternColor} complement />}
45
- {patternData?.pattern === 'circles' && <PatternCircles id={`territory-${patternData?.dataKey}--${patternIndex}`} height={sizes[patternData?.size] ?? 10} width={sizes[patternData?.size] ?? 10} fill={defaultPatternColor} complement />}
46
- {patternData?.pattern === 'lines' && <PatternLines id={`territory-${patternData?.dataKey}--${patternIndex}`} height={sizes[patternData?.size] ?? 6} width={sizes[patternData?.size] ?? 6} stroke={defaultPatternColor} strokeWidth={1} orientation={['diagonalRightToLeft']} />}
33
+ {patternData?.pattern === 'waves' && <PatternWaves id={`territory-${patternData?.dataKey}--${patternIndex}`} height={patternSizes[patternData?.size] ?? 10} width={patternSizes[patternData?.size] ?? 10} fill={patternColor} complement />}
34
+ {patternData?.pattern === 'circles' && <PatternCircles id={`territory-${patternData?.dataKey}--${patternIndex}`} height={patternSizes[patternData?.size] ?? 10} width={patternSizes[patternData?.size] ?? 10} fill={patternColor} complement />}
35
+ {patternData?.pattern === 'lines' && <PatternLines id={`territory-${patternData?.dataKey}--${patternIndex}`} height={patternSizes[patternData?.size] ?? 6} width={patternSizes[patternData?.size] ?? 6} stroke={patternColor} strokeWidth={1} orientation={['diagonalRightToLeft']} />}
47
36
  <path
48
37
  stroke={stroke}
49
38
  strokeWidth={strokeWidth}
50
39
  d='M40,0.5 C41.2426407,0.5 42.3676407,1.00367966 43.1819805,1.81801948 C43.9963203,2.63235931 44.5,3.75735931 44.5,5 L44.5,5 L44.5,23 C44.5,24.2426407 43.9963203,25.3676407 43.1819805,26.1819805 C42.3676407,26.9963203 41.2426407,27.5 40,27.5 L40,27.5 L5,27.5 C3.75735931,27.5 2.63235931,26.9963203 1.81801948,26.1819805 C1.00367966,25.3676407 0.5,24.2426407 0.5,23 L0.5,23 L0.5,5 C0.5,3.75735931 1.00367966,2.63235931 1.81801948,1.81801948 C2.63235931,1.00367966 3.75735931,0.5 5,0.5 L5,0.5 Z'
51
40
  fill={`url(#territory-${patternData?.dataKey}--${patternIndex})`}
52
- color='white'
41
+ color={patternData ? 'white' : textColor}
53
42
  className={[`territory-pattern-${patternData.dataKey}`, `territory-pattern-${patternData.dataKey}--${patternData.dataValue}`].join(' ')}
54
43
  />
55
- <text textAnchor='middle' dominantBaseline='middle' x='50%' y='54%' fill={text} style={{ stroke: patternData ? 'black' : textColor, strokeWidth: patternData ? 6 : 0 }} className='territory-text' paint-order='stroke'>
44
+ <text textAnchor='middle' dominantBaseline='middle' x='50%' y='54%' fill={text} style={{ fill: patternData ? 'white' : 'black', stroke: patternData ? 'black' : textColor, strokeWidth: patternData ? 6 : 0 }} className='territory-text' paint-order='stroke'>
56
45
  {label}
57
46
  </text>
58
47
  </>
@@ -127,7 +127,9 @@ const CountyMap = props => {
127
127
  handleMapAriaLabels,
128
128
  runtimeLegend,
129
129
  state,
130
- runtimeFilters
130
+ runtimeFilters,
131
+ tooltipId,
132
+ isEditor
131
133
  } = useContext(ConfigContext)
132
134
 
133
135
  // CREATE STATE LINES
@@ -138,7 +140,7 @@ const CountyMap = props => {
138
140
 
139
141
  const pathGenerator = geoPath().projection(geoAlbersUsaTerritories())
140
142
 
141
- const { featureArray } = useMapLayers(state, '', pathGenerator, false)
143
+ const { featureArray } = useMapLayers(state, '', pathGenerator, tooltipId)
142
144
 
143
145
  useEffect(() => {
144
146
  if (containerEl) {
@@ -322,7 +324,7 @@ const CountyMap = props => {
322
324
 
323
325
  tooltipRef.current.style.display = 'block'
324
326
  tooltipRef.current.style.top = e.clientY + 'px'
325
- tooltipRef.current.style.left = e.clientX + 'px'
327
+ tooltipRef.current.style.left = isEditor ? Number(e.clientX - 350) + 'px' : e.clientX + 'px'
326
328
  tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
327
329
  tooltipRef.current.setAttribute('data-index', countyIndex)
328
330
  } else {
@@ -520,8 +522,9 @@ const CountyMap = props => {
520
522
  tooltipRef.current.setAttribute('data-index', null)
521
523
  }}
522
524
  onClick={canvasClick}
525
+ className='county-map-canvas'
523
526
  ></canvas>
524
- <div ref={tooltipRef} id='canvas-tooltip' className='tooltip' style={{ background: `rgba(255,255,255,${state.tooltips.opacity / 100})` }}></div>
527
+ <div ref={tooltipRef} id={`tooltip__${tooltipId}`} className='tooltip' style={{ background: `rgba(255,255,255,${state.tooltips.opacity / 100})` }}></div>
525
528
  <button className={`btn btn--reset`} onClick={onReset} ref={resetButton} tabIndex='0'>
526
529
  Reset Zoom
527
530
  </button>