@cdc/map 4.24.4 → 4.24.7
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.
- package/dist/cdcmap.js +39011 -34350
- package/examples/annotation/index.json +552 -0
- package/examples/annotation/usa-map.json +900 -0
- package/index.html +4 -3
- package/package.json +6 -5
- package/src/CdcMap.tsx +57 -85
- package/src/_stories/CdcMap.stories.tsx +7 -0
- package/src/_stories/_mock/DEV-7286.json +165 -0
- package/src/components/Annotation/Annotation.Draggable.styles.css +18 -0
- package/src/components/Annotation/Annotation.Draggable.tsx +152 -0
- package/src/components/Annotation/AnnotationDropdown.styles.css +14 -0
- package/src/components/Annotation/AnnotationDropdown.tsx +69 -0
- package/src/components/Annotation/AnnotationList.styles.css +45 -0
- package/src/components/Annotation/AnnotationList.tsx +42 -0
- package/src/components/Annotation/index.tsx +11 -0
- package/src/components/BubbleList.jsx +2 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +3 -2
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +358 -0
- package/src/components/EditorPanel/components/{Panel.PatternSettings.tsx → Panels/Panel.PatternSettings.tsx} +2 -2
- package/src/components/EditorPanel/components/{Panels.tsx → Panels/index.tsx} +3 -0
- package/src/components/Legend/components/Legend.tsx +3 -9
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +4 -3
- package/src/components/UsaMap/components/UsaMap.County.tsx +48 -21
- package/src/components/UsaMap/components/UsaMap.Region.tsx +2 -0
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +2 -0
- package/src/components/UsaMap/components/UsaMap.State.tsx +28 -19
- package/src/components/WorldMap/{components/WorldMap.jsx → WorldMap.tsx} +8 -9
- package/src/components/WorldMap/index.tsx +1 -1
- package/src/context.ts +2 -1
- package/src/data/initial-state.js +4 -1
- package/src/helpers/generateColorsArray.ts +13 -0
- package/src/helpers/generateRuntimeLegendHash.ts +23 -0
- package/src/helpers/getUniqueValues.ts +19 -0
- package/src/helpers/hashObj.ts +25 -0
- package/src/helpers/tests/generateColorsArray.test.ts +18 -0
- package/src/helpers/tests/generateRuntimeLegendHash.test.ts +11 -0
- package/src/helpers/tests/hashObj.test.ts +10 -0
- package/src/scss/map.scss +6 -3
- package/src/types/MapContext.ts +2 -0
- package/LICENSE +0 -201
- package/src/test/CdcMap.test.jsx +0 -19
- /package/src/components/EditorPanel/components/{Panel.PatternSettings-style.css → Panels/Panel.PatternSettings-style.css} +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
// CDC Core
|
|
4
|
+
import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
|
|
5
|
+
import Accordion from '@cdc/core/components/ui/Accordion'
|
|
6
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
7
|
+
import { MapContext } from '../../../../types/MapContext'
|
|
8
|
+
import ConfigContext from '../../../../context'
|
|
9
|
+
|
|
10
|
+
// types
|
|
11
|
+
// styles
|
|
12
|
+
|
|
13
|
+
const PanelAnnotate: React.FC = props => {
|
|
14
|
+
const { state: config, setState: updateConfig, dimensions, isDraggingAnnotation } = useContext<MapContext>(ConfigContext)
|
|
15
|
+
const getColumns = (filter = true) => {
|
|
16
|
+
const columns = {}
|
|
17
|
+
config.data.forEach(row => {
|
|
18
|
+
Object.keys(row).forEach(columnName => (columns[columnName] = true))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
if (filter) {
|
|
22
|
+
Object.keys(columns).forEach(key => {
|
|
23
|
+
if ((config.series && config.series.filter(series => series.dataKey === key).length > 0) || (config.confidenceKeys && Object.keys(config.confidenceKeys).includes(key))) {
|
|
24
|
+
delete columns[key]
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Object.keys(columns)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleAnnotationUpdate = (value, property, index) => {
|
|
33
|
+
const annotations = [...config?.annotations]
|
|
34
|
+
annotations[index][property] = value
|
|
35
|
+
annotations[index].savedDimensions = [dimensions[0] * 0.73, dimensions[1]]
|
|
36
|
+
|
|
37
|
+
updateConfig({
|
|
38
|
+
...config,
|
|
39
|
+
annotations
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleAddAnnotation = () => {
|
|
44
|
+
const svgContainer = document.querySelector('.map-container > section > svg, .map-container > section > canvas')?.getBoundingClientRect()
|
|
45
|
+
const newSvgDims = [svgContainer.width, svgContainer.height]
|
|
46
|
+
|
|
47
|
+
const newAnnotation = {
|
|
48
|
+
text: 'New Annotation',
|
|
49
|
+
snapToNearestPoint: false,
|
|
50
|
+
fontSize: 16,
|
|
51
|
+
show: {
|
|
52
|
+
desktop: true,
|
|
53
|
+
tablet: true,
|
|
54
|
+
mobile: true
|
|
55
|
+
},
|
|
56
|
+
markerType: 'arrow',
|
|
57
|
+
connectorType: 'line',
|
|
58
|
+
colors: {
|
|
59
|
+
label: 'black',
|
|
60
|
+
connector: 'black',
|
|
61
|
+
marker: 'black'
|
|
62
|
+
},
|
|
63
|
+
selected: true,
|
|
64
|
+
anchor: {
|
|
65
|
+
vertical: false,
|
|
66
|
+
horizontal: false
|
|
67
|
+
},
|
|
68
|
+
connectionType: 'line',
|
|
69
|
+
marker: 'arrow',
|
|
70
|
+
edit: {
|
|
71
|
+
subject: true,
|
|
72
|
+
label: true
|
|
73
|
+
},
|
|
74
|
+
seriesKey: '',
|
|
75
|
+
x: Number(newSvgDims?.[0]) / 2,
|
|
76
|
+
y: Number(newSvgDims?.[1] / 2),
|
|
77
|
+
xKey: null,
|
|
78
|
+
yKey: null,
|
|
79
|
+
dx: 0,
|
|
80
|
+
dy: 0,
|
|
81
|
+
opacity: '100',
|
|
82
|
+
savedDimensions: [dimensions[0] * 0.73, dimensions[1]]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const annotations = Array.isArray(config.annotations) ? config.annotations : []
|
|
86
|
+
|
|
87
|
+
updateConfig({
|
|
88
|
+
...config,
|
|
89
|
+
annotations: [...annotations, newAnnotation]
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleRemoveAnnotation = (annotationIndex: number) => {
|
|
94
|
+
const updated = config.annotations.filter((_, index) => index !== annotationIndex)
|
|
95
|
+
updateConfig({
|
|
96
|
+
...config,
|
|
97
|
+
annotations: updated
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Accordion>
|
|
103
|
+
<Accordion.Section title={props.name}>
|
|
104
|
+
<p>Dragging state: {isDraggingAnnotation ? 'Dragging' : 'Not dragging'}</p>
|
|
105
|
+
|
|
106
|
+
<label>
|
|
107
|
+
Show Annotation Dropdown
|
|
108
|
+
<input
|
|
109
|
+
type='checkbox'
|
|
110
|
+
checked={config?.general?.showAnnotationDropdown}
|
|
111
|
+
onClick={e => {
|
|
112
|
+
updateConfig({
|
|
113
|
+
...config,
|
|
114
|
+
general: {
|
|
115
|
+
...config.general,
|
|
116
|
+
showAnnotationDropdown: e.target.checked
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</label>
|
|
122
|
+
|
|
123
|
+
<label>
|
|
124
|
+
Annotation Dropdown Title:
|
|
125
|
+
<input
|
|
126
|
+
type='text'
|
|
127
|
+
style={{ marginBottom: '10px' }}
|
|
128
|
+
value={config?.general?.annotationDropdownText}
|
|
129
|
+
onChange={e => {
|
|
130
|
+
updateConfig({
|
|
131
|
+
...config,
|
|
132
|
+
general: {
|
|
133
|
+
...config.general,
|
|
134
|
+
annotationDropdownText: e.target.value
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
</label>
|
|
140
|
+
|
|
141
|
+
{config?.annotations &&
|
|
142
|
+
config?.annotations.map((annotation, index) => (
|
|
143
|
+
<Accordion>
|
|
144
|
+
<Accordion.Section title={annotation.text ? annotation.text.substring(0, 15) + '...' : `Annotation ${index + 1}`}>
|
|
145
|
+
<div className='annotation-group'>
|
|
146
|
+
<label>
|
|
147
|
+
Annotation Text:
|
|
148
|
+
<textarea rows={5} value={annotation.text} onChange={e => handleAnnotationUpdate(e.target.value, 'text', index)} />
|
|
149
|
+
</label>
|
|
150
|
+
{/* <label>
|
|
151
|
+
Vertical Anchor
|
|
152
|
+
<input
|
|
153
|
+
type='checkbox'
|
|
154
|
+
checked={config?.annotations[index].anchor.vertical}
|
|
155
|
+
onClick={e => {
|
|
156
|
+
const updatedAnnotations = [...config?.annotations]
|
|
157
|
+
updatedAnnotations[index].anchor.vertical = e.target.checked
|
|
158
|
+
updateConfig({
|
|
159
|
+
...config,
|
|
160
|
+
annotations: updatedAnnotations
|
|
161
|
+
})
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
</label>
|
|
165
|
+
<label>
|
|
166
|
+
Horizontal Anchor
|
|
167
|
+
<input
|
|
168
|
+
type='checkbox'
|
|
169
|
+
checked={config?.annotations[index].anchor.horizontal}
|
|
170
|
+
onClick={e => {
|
|
171
|
+
const updatedAnnotations = [...config?.annotations]
|
|
172
|
+
updatedAnnotations[index].anchor.horizontal = e.target.checked
|
|
173
|
+
updateConfig({
|
|
174
|
+
...config,
|
|
175
|
+
annotations: updatedAnnotations
|
|
176
|
+
})
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
</label> */}
|
|
180
|
+
|
|
181
|
+
<label>
|
|
182
|
+
Opacity
|
|
183
|
+
<br />
|
|
184
|
+
<input
|
|
185
|
+
type='range'
|
|
186
|
+
onChange={e => {
|
|
187
|
+
const updatedAnnotations = [...config?.annotations]
|
|
188
|
+
updatedAnnotations[index].opacity = e.target.value
|
|
189
|
+
updateConfig({
|
|
190
|
+
...config,
|
|
191
|
+
annotations: updatedAnnotations
|
|
192
|
+
})
|
|
193
|
+
}}
|
|
194
|
+
value={config?.annotations?.[index]?.opacity || '100'}
|
|
195
|
+
/>
|
|
196
|
+
</label>
|
|
197
|
+
|
|
198
|
+
<label>
|
|
199
|
+
Edit Subject
|
|
200
|
+
<input
|
|
201
|
+
type='checkbox'
|
|
202
|
+
checked={config?.annotations[index]?.edit?.subject}
|
|
203
|
+
onClick={e => {
|
|
204
|
+
const updatedAnnotations = [...config?.annotations]
|
|
205
|
+
updatedAnnotations[index].edit.subject = e.target.checked
|
|
206
|
+
updateConfig({
|
|
207
|
+
...config,
|
|
208
|
+
annotations: updatedAnnotations
|
|
209
|
+
})
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
</label>
|
|
213
|
+
<label>
|
|
214
|
+
Edit Label
|
|
215
|
+
<input
|
|
216
|
+
type='checkbox'
|
|
217
|
+
checked={config?.annotations[index]?.edit?.label}
|
|
218
|
+
onClick={e => {
|
|
219
|
+
const updatedAnnotations = [...config?.annotations]
|
|
220
|
+
updatedAnnotations[index].edit.label = e.target.checked
|
|
221
|
+
updateConfig({
|
|
222
|
+
...config,
|
|
223
|
+
annotations: updatedAnnotations
|
|
224
|
+
})
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
</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
|
+
|
|
249
|
+
<label>
|
|
250
|
+
Connection Type:
|
|
251
|
+
<select
|
|
252
|
+
onChange={e => {
|
|
253
|
+
const updatedAnnotations = [...config?.annotations]
|
|
254
|
+
updatedAnnotations[index].connectionType = e.target.value
|
|
255
|
+
updateConfig({
|
|
256
|
+
...config,
|
|
257
|
+
annotations: updatedAnnotations
|
|
258
|
+
})
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{['curve', 'line', 'elbow', 'none'].map((side, index) => (
|
|
262
|
+
<option key={side} value={side}>
|
|
263
|
+
{side}
|
|
264
|
+
</option>
|
|
265
|
+
))}
|
|
266
|
+
</select>
|
|
267
|
+
</label>
|
|
268
|
+
|
|
269
|
+
{annotation.connectionType === 'curve' && (
|
|
270
|
+
<label>
|
|
271
|
+
Line Type:
|
|
272
|
+
<select
|
|
273
|
+
onChange={e => {
|
|
274
|
+
const updatedAnnotations = [...config?.annotations]
|
|
275
|
+
updatedAnnotations[index].lineType = e.target.value
|
|
276
|
+
updateConfig({
|
|
277
|
+
...config,
|
|
278
|
+
annotations: updatedAnnotations
|
|
279
|
+
})
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{Object.entries(approvedCurveTypes).map(([value, key]) => (
|
|
283
|
+
<option key={key} value={key}>
|
|
284
|
+
{value}
|
|
285
|
+
</option>
|
|
286
|
+
))}
|
|
287
|
+
</select>
|
|
288
|
+
</label>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* <label>
|
|
292
|
+
Connection Location:
|
|
293
|
+
<select
|
|
294
|
+
onChange={e => {
|
|
295
|
+
const updatedAnnotations = [...config?.annotations]
|
|
296
|
+
updatedAnnotations[index].connectionLocation = e.target.value
|
|
297
|
+
updateConfig({
|
|
298
|
+
...config,
|
|
299
|
+
annotations: updatedAnnotations
|
|
300
|
+
})
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
{['auto', 'left', 'top', 'bottom', 'right'].map((side, index) => (
|
|
304
|
+
<option key={side} value={side}>
|
|
305
|
+
{side}
|
|
306
|
+
</option>
|
|
307
|
+
))}
|
|
308
|
+
</select>
|
|
309
|
+
</label> */}
|
|
310
|
+
|
|
311
|
+
<label>
|
|
312
|
+
Marker
|
|
313
|
+
<select
|
|
314
|
+
onChange={e => {
|
|
315
|
+
const updatedAnnotations = [...config?.annotations]
|
|
316
|
+
updatedAnnotations[index].marker = e.target.value
|
|
317
|
+
updateConfig({
|
|
318
|
+
...config,
|
|
319
|
+
annotations: updatedAnnotations
|
|
320
|
+
})
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
{['circle', 'arrow'].map((column, columnIndex) => {
|
|
324
|
+
return <option>{column}</option>
|
|
325
|
+
})}
|
|
326
|
+
</select>
|
|
327
|
+
</label>
|
|
328
|
+
|
|
329
|
+
{/* <label>
|
|
330
|
+
Snap to Nearest Point
|
|
331
|
+
<input
|
|
332
|
+
type='checkbox'
|
|
333
|
+
checked={config?.annotations[index]?.snapToNearestPoint}
|
|
334
|
+
onClick={e => {
|
|
335
|
+
const updatedAnnotations = [...config?.annotations]
|
|
336
|
+
updatedAnnotations[index].snapToNearestPoint = e.target.checked
|
|
337
|
+
updateConfig({
|
|
338
|
+
...config,
|
|
339
|
+
annotations: updatedAnnotations
|
|
340
|
+
})
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
</label> */}
|
|
344
|
+
|
|
345
|
+
<Button className='warn btn-warn btn btn-remove delete' onClick={() => handleRemoveAnnotation(index)}>
|
|
346
|
+
Delete Annotation
|
|
347
|
+
</Button>
|
|
348
|
+
</div>
|
|
349
|
+
</Accordion.Section>
|
|
350
|
+
</Accordion>
|
|
351
|
+
))}
|
|
352
|
+
{config?.annotations?.length < 3 && <Button onClick={handleAddAnnotation}>Add Annotation</Button>}
|
|
353
|
+
</Accordion.Section>
|
|
354
|
+
</Accordion>
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export default PanelAnnotate
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useContext } from 'react'
|
|
2
2
|
import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
|
|
3
|
-
import ConfigContext from '
|
|
4
|
-
import { type MapContext } from '
|
|
3
|
+
import ConfigContext from '../../../../context'
|
|
4
|
+
import { type MapContext } from '../../../../types/MapContext'
|
|
5
5
|
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'
|
|
@@ -34,7 +34,6 @@ const Legend = forwardRef((props, ref) => {
|
|
|
34
34
|
} = useContext(ConfigContext)
|
|
35
35
|
|
|
36
36
|
const { legend } = state
|
|
37
|
-
const fontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? { fontSize: '11px' } : null
|
|
38
37
|
|
|
39
38
|
// Toggles if a legend is active and being applied to the map and data table.
|
|
40
39
|
const toggleLegendActive = (i, legendLabel) => {
|
|
@@ -96,7 +95,6 @@ const Legend = forwardRef((props, ref) => {
|
|
|
96
95
|
return (
|
|
97
96
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
|
|
98
97
|
<li
|
|
99
|
-
style={fontSize}
|
|
100
98
|
className={handleListItemClass().join(' ')}
|
|
101
99
|
key={idx}
|
|
102
100
|
title={`Legend item ${legendLabel} - Click to disable`}
|
|
@@ -181,11 +179,7 @@ const Legend = forwardRef((props, ref) => {
|
|
|
181
179
|
<aside id={skipId || 'legend'} className={legendClasses.aside.join(' ') || ''} role='region' aria-label='Legend' tabIndex={0} ref={ref}>
|
|
182
180
|
<section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
|
|
183
181
|
{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>
|
|
188
|
-
)}
|
|
182
|
+
{legend.dynamicDescription === false && legend.description && <p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>}
|
|
189
183
|
{legend.dynamicDescription === true &&
|
|
190
184
|
runtimeFilters.map((filter, idx) => {
|
|
191
185
|
const lookupStr = `${idx},${filter.values.indexOf(String(filter.active))}`
|
|
@@ -195,7 +189,7 @@ const Legend = forwardRef((props, ref) => {
|
|
|
195
189
|
|
|
196
190
|
if (desc.length > 0) {
|
|
197
191
|
return (
|
|
198
|
-
<p
|
|
192
|
+
<p key={`dynamic-description-${lookupStr}`} className={`dynamic-legend-description-${lookupStr}`}>
|
|
199
193
|
{desc}
|
|
200
194
|
</p>
|
|
201
195
|
)
|
|
@@ -229,7 +223,7 @@ const Legend = forwardRef((props, ref) => {
|
|
|
229
223
|
{cityStyleShapes[shape.toLowerCase()]}
|
|
230
224
|
</Group>
|
|
231
225
|
</svg>
|
|
232
|
-
<p
|
|
226
|
+
<p>{label}</p>
|
|
233
227
|
</div>
|
|
234
228
|
)
|
|
235
229
|
)}
|
|
@@ -7,15 +7,16 @@ import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
|
7
7
|
|
|
8
8
|
const TerritoryRectangle = ({ label, text, stroke, strokeWidth, textColor, hasPattern, territory, ...props }) => {
|
|
9
9
|
const { state, supportedTerritories } = useContext<MapContext>(ConfigContext)
|
|
10
|
+
const { territoryData, ...otherProps } = props
|
|
10
11
|
|
|
11
12
|
return (
|
|
12
|
-
<svg viewBox='0 0 45 28'>
|
|
13
|
-
<g {...
|
|
13
|
+
<svg viewBox='0 0 45 28' key={territory} className={territory}>
|
|
14
|
+
<g {...otherProps} strokeLinejoin='round' tabIndex={-1}>
|
|
14
15
|
<path
|
|
15
16
|
stroke={stroke}
|
|
16
17
|
strokeWidth={strokeWidth}
|
|
17
18
|
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'
|
|
18
|
-
{...
|
|
19
|
+
{...otherProps}
|
|
19
20
|
/>
|
|
20
21
|
<text textAnchor='middle' dominantBaseline='middle' x='50%' y='54%' fill={text} style={{ stroke: textColor, strokeWidth: 1 }} className='territory-text' paintOrder='stroke'>
|
|
21
22
|
{label}
|
|
@@ -10,6 +10,7 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
|
10
10
|
|
|
11
11
|
import useMapLayers from '../../../hooks/useMapLayers'
|
|
12
12
|
import ConfigContext from '../../../context'
|
|
13
|
+
import Annotation from '../../Annotation'
|
|
13
14
|
|
|
14
15
|
const getCountyTopoURL = year => {
|
|
15
16
|
return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
|
|
@@ -129,7 +130,8 @@ const CountyMap = props => {
|
|
|
129
130
|
state,
|
|
130
131
|
runtimeFilters,
|
|
131
132
|
tooltipId,
|
|
132
|
-
|
|
133
|
+
tooltipRef,
|
|
134
|
+
container
|
|
133
135
|
} = useContext(ConfigContext)
|
|
134
136
|
|
|
135
137
|
// CREATE STATE LINES
|
|
@@ -186,7 +188,6 @@ const CountyMap = props => {
|
|
|
186
188
|
|
|
187
189
|
const resetButton = useRef()
|
|
188
190
|
const canvasRef = useRef()
|
|
189
|
-
const tooltipRef = useRef()
|
|
190
191
|
|
|
191
192
|
// If runtimeData is not defined, show loader
|
|
192
193
|
if (!data || !isTopoReady(topoData, state, runtimeFilters)) {
|
|
@@ -238,8 +239,16 @@ const CountyMap = props => {
|
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
let focusIndex = -1;
|
|
243
|
+
for(let i = 0; i < topoData.mapData.length; i++){
|
|
244
|
+
if(topoData.mapData[i].id === clickedState.id){
|
|
245
|
+
focusIndex = i;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
241
250
|
// Redraw with focus on state
|
|
242
|
-
setFocus({ id: clickedState.id, center: geoCentroid(clickedState) })
|
|
251
|
+
setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState) })
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
if (state.general.type === 'us-geocode') {
|
|
@@ -266,17 +275,21 @@ const CountyMap = props => {
|
|
|
266
275
|
const canvasBounds = canvas.getBoundingClientRect()
|
|
267
276
|
const x = e.clientX - canvasBounds.left
|
|
268
277
|
const y = e.clientY - canvasBounds.top
|
|
278
|
+
const containerBounds = container?.getBoundingClientRect()
|
|
279
|
+
const tooltipX = e.clientX - (containerBounds?.left || 0)
|
|
280
|
+
const tooltipY = e.clientY - (containerBounds?.top || 0)
|
|
269
281
|
let pointCoordinates = topoData.projection.invert([x, y])
|
|
270
282
|
|
|
271
283
|
const currentTooltipIndex = parseInt(tooltipRef.current.getAttribute('data-index'))
|
|
272
284
|
const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
273
285
|
|
|
286
|
+
const context = canvas.getContext('2d')
|
|
287
|
+
const path = geoPath(topoData.projection, context)
|
|
288
|
+
|
|
274
289
|
// Handle standard county map hover
|
|
275
290
|
if (state.general.type !== 'us-geocode') {
|
|
276
291
|
//If no tooltip is shown, or if the current geo associated with the tooltip shown is no longer containing the mouse, then rerender the tooltip
|
|
277
292
|
if (isNaN(currentTooltipIndex) || !geoContains(topoData.mapData[currentTooltipIndex], pointCoordinates)) {
|
|
278
|
-
const context = canvas.getContext('2d')
|
|
279
|
-
const path = geoPath(topoData.projection, context)
|
|
280
293
|
if (!isNaN(currentTooltipIndex) && applyLegendToRow(data[topoData.mapData[currentTooltipIndex].id])) {
|
|
281
294
|
context.fillStyle = applyLegendToRow(data[topoData.mapData[currentTooltipIndex].id])[0]
|
|
282
295
|
context.strokeStyle = geoStrokeColor
|
|
@@ -323,8 +336,14 @@ const CountyMap = props => {
|
|
|
323
336
|
}
|
|
324
337
|
|
|
325
338
|
tooltipRef.current.style.display = 'block'
|
|
326
|
-
tooltipRef.current.style.top =
|
|
327
|
-
|
|
339
|
+
tooltipRef.current.style.top = tooltipY + 'px'
|
|
340
|
+
if(tooltipX > containerBounds.width / 2) {
|
|
341
|
+
tooltipRef.current.style.transform = 'translate(-100%, -50%)'
|
|
342
|
+
tooltipRef.current.style.left = (tooltipX - 5) + 'px'
|
|
343
|
+
} else {
|
|
344
|
+
tooltipRef.current.style.transform = 'translate(0, -50%)'
|
|
345
|
+
tooltipRef.current.style.left = (tooltipX + 5) + 'px'
|
|
346
|
+
}
|
|
328
347
|
tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
|
|
329
348
|
tooltipRef.current.setAttribute('data-index', countyIndex)
|
|
330
349
|
} else {
|
|
@@ -349,7 +368,7 @@ const CountyMap = props => {
|
|
|
349
368
|
let hoveredGeoIndex
|
|
350
369
|
for (let i = 0; i < runtimeKeys.length; i++) {
|
|
351
370
|
const pixelCoords = topoData.projection([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
|
|
352
|
-
if (state.visual.cityStyle === 'circle' && pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
|
|
371
|
+
if (state.visual.cityStyle === 'circle' && pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius && applyLegendToRow(data[runtimeKeys[i]])) {
|
|
353
372
|
hoveredGeo = data[runtimeKeys[i]]
|
|
354
373
|
hoveredGeoIndex = i
|
|
355
374
|
break
|
|
@@ -357,7 +376,7 @@ const CountyMap = props => {
|
|
|
357
376
|
|
|
358
377
|
if (state.visual.cityStyle === 'pin' && pixelCoords) {
|
|
359
378
|
const distance = Math.hypot(pixelCoords[0] - x, pixelCoords[1] - y)
|
|
360
|
-
if (distance < 15) {
|
|
379
|
+
if (distance < 15 && applyLegendToRow(data[runtimeKeys[i]])) {
|
|
361
380
|
hoveredGeo = data[runtimeKeys[i]]
|
|
362
381
|
hoveredGeoIndex = i
|
|
363
382
|
break
|
|
@@ -365,10 +384,16 @@ const CountyMap = props => {
|
|
|
365
384
|
}
|
|
366
385
|
}
|
|
367
386
|
|
|
368
|
-
if (hoveredGeo
|
|
387
|
+
if (hoveredGeo) {
|
|
369
388
|
tooltipRef.current.style.display = 'block'
|
|
370
|
-
tooltipRef.current.style.top =
|
|
371
|
-
|
|
389
|
+
tooltipRef.current.style.top = tooltipY + 'px'
|
|
390
|
+
if(tooltipX > containerBounds.width / 2) {
|
|
391
|
+
tooltipRef.current.style.transform = 'translate(-100%, -50%)'
|
|
392
|
+
tooltipRef.current.style.left = (tooltipX - 5) + 'px'
|
|
393
|
+
} else {
|
|
394
|
+
tooltipRef.current.style.transform = 'translate(0, -50%)'
|
|
395
|
+
tooltipRef.current.style.left = (tooltipX + 5) + 'px'
|
|
396
|
+
}
|
|
372
397
|
tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(hoveredGeo[state.columns.geo.name]), hoveredGeo)
|
|
373
398
|
tooltipRef.current.setAttribute('data-index', hoveredGeoIndex)
|
|
374
399
|
} else {
|
|
@@ -376,6 +401,14 @@ const CountyMap = props => {
|
|
|
376
401
|
tooltipRef.current.setAttribute('data-index', null)
|
|
377
402
|
}
|
|
378
403
|
}
|
|
404
|
+
|
|
405
|
+
if (focus.index !== -1) {
|
|
406
|
+
context.strokeStyle = 'black'
|
|
407
|
+
context.lineWidth = 1
|
|
408
|
+
context.beginPath()
|
|
409
|
+
path(topoData.mapData[focus.index])
|
|
410
|
+
context.stroke()
|
|
411
|
+
}
|
|
379
412
|
}
|
|
380
413
|
|
|
381
414
|
// Redraws canvas. Takes as parameters the fips id of a state to center on and the [lat,long] center of that state
|
|
@@ -411,7 +444,6 @@ const CountyMap = props => {
|
|
|
411
444
|
context.strokeStyle = geoStrokeColor
|
|
412
445
|
context.lineWidth = lineWidth
|
|
413
446
|
|
|
414
|
-
let focusIndex = -1
|
|
415
447
|
// Iterates through each state/county topo and renders it
|
|
416
448
|
topoData.mapData.forEach((geo, i) => {
|
|
417
449
|
// If invalid geo item, don't render
|
|
@@ -424,11 +456,6 @@ const CountyMap = props => {
|
|
|
424
456
|
// Gets numeric data associated with the topo data for this state/county
|
|
425
457
|
const geoData = data[geo.id]
|
|
426
458
|
|
|
427
|
-
// Marks that the focused state was found for the logic below
|
|
428
|
-
if (geo.id === focus.id) {
|
|
429
|
-
focusIndex = i
|
|
430
|
-
}
|
|
431
|
-
|
|
432
459
|
// Renders state/county
|
|
433
460
|
const legendValues = geoData !== undefined ? applyLegendToRow(geoData) : false
|
|
434
461
|
context.fillStyle = legendValues && state.general.type !== 'us-geocode' ? legendValues[0] : '#EEE'
|
|
@@ -439,11 +466,11 @@ const CountyMap = props => {
|
|
|
439
466
|
})
|
|
440
467
|
|
|
441
468
|
// If the focused state is found in the geo data, render it with a thicker outline
|
|
442
|
-
if (
|
|
469
|
+
if (focus.index !== -1) {
|
|
443
470
|
context.strokeStyle = 'black'
|
|
444
471
|
context.lineWidth = 2
|
|
445
472
|
context.beginPath()
|
|
446
|
-
path(topoData.mapData[
|
|
473
|
+
path(topoData.mapData[focus.index])
|
|
447
474
|
context.stroke()
|
|
448
475
|
}
|
|
449
476
|
|
|
@@ -481,6 +508,7 @@ const CountyMap = props => {
|
|
|
481
508
|
}
|
|
482
509
|
|
|
483
510
|
const drawCircle = (circle, context) => {
|
|
511
|
+
context.lineWidth = lineWidth
|
|
484
512
|
context.fillStyle = circle.color
|
|
485
513
|
context.beginPath()
|
|
486
514
|
context.arc(circle.x, circle.y, circle.geoRadius, 0, 2 * Math.PI)
|
|
@@ -524,7 +552,6 @@ const CountyMap = props => {
|
|
|
524
552
|
onClick={canvasClick}
|
|
525
553
|
className='county-map-canvas'
|
|
526
554
|
></canvas>
|
|
527
|
-
<div ref={tooltipRef} id={`tooltip__${tooltipId}`} className='tooltip' style={{ background: `rgba(255,255,255,${state.tooltips.opacity / 100})` }}></div>
|
|
528
555
|
<button className={`btn btn--reset`} onClick={onReset} ref={resetButton} tabIndex='0'>
|
|
529
556
|
Reset Zoom
|
|
530
557
|
</button>
|
|
@@ -10,6 +10,7 @@ import { Mercator } from '@visx/geo'
|
|
|
10
10
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
11
11
|
import topoJSON from '../data/us-regions-topo-2.json'
|
|
12
12
|
import ConfigContext from '../../../context'
|
|
13
|
+
import Annotation from '../../Annotation'
|
|
13
14
|
|
|
14
15
|
const { features: unitedStates } = feature(topoJSON, topoJSON.objects.regions)
|
|
15
16
|
|
|
@@ -252,6 +253,7 @@ const UsaRegionMap = props => {
|
|
|
252
253
|
<Mercator data={focusedStates} scale={620} translate={[1500, 735]}>
|
|
253
254
|
{({ features, projection }) => constructGeoJsx(features, projection)}
|
|
254
255
|
</Mercator>
|
|
256
|
+
{state.annotations.length > 0 && <Annotation.Draggable />}
|
|
255
257
|
</svg>
|
|
256
258
|
{territories.length > 0 && (
|
|
257
259
|
<section className='territories'>
|
|
@@ -10,6 +10,7 @@ import colorPalettes from '@cdc/core/data/colorPalettes'
|
|
|
10
10
|
import { geoAlbersUsaTerritories } from 'd3-composite-projections'
|
|
11
11
|
import CityList from '../../CityList'
|
|
12
12
|
import ConfigContext from '../../../context'
|
|
13
|
+
import Annotation from '../../Annotation'
|
|
13
14
|
|
|
14
15
|
// SVG ITEMS
|
|
15
16
|
const WIDTH = 880
|
|
@@ -296,6 +297,7 @@ const SingleStateMap = props => {
|
|
|
296
297
|
)
|
|
297
298
|
}}
|
|
298
299
|
</CustomProjection>
|
|
300
|
+
{state.annotations.length > 0 && <Annotation.Draggable />}
|
|
299
301
|
</svg>
|
|
300
302
|
)}
|
|
301
303
|
{!state.general.statePicked && 'No State Picked'}
|