@cdc/map 4.25.8 → 4.25.11
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/.claude/agents/typescript-organizer.md +118 -0
- package/.claude/settings.local.json +30 -0
- package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
- package/dist/cdcmap.js +56991 -53706
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/d.json +345 -0
- package/examples/private/g.json +1 -0
- package/examples/private/h.json +105911 -0
- package/examples/private/measles-data.json +378 -0
- package/examples/private/measles.json +211 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/state-with-pattern.json +883 -0
- package/index.html +36 -34
- package/package.json +26 -5
- package/src/CdcMap.tsx +23 -8
- package/src/CdcMapComponent.tsx +238 -308
- package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
- package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
- package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
- package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
- package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
- package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +37 -9
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/column-wrap-test.json +265 -0
- package/src/_stories/_mock/equal-number.json +1109 -0
- package/src/_stories/_mock/multi-country-hide.json +78 -0
- package/src/_stories/_mock/multi-country.json +95 -0
- package/src/_stories/_mock/multi-state.json +887 -20403
- package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
- package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
- package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
- package/src/_stories/_mock/us-bubble-cities.json +306 -0
- package/src/_stories/_mock/usa-state-gradient.json +2 -4
- package/src/components/BubbleList.tsx +17 -13
- package/src/components/CityList.tsx +85 -107
- package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/Geo.tsx +22 -3
- package/src/components/Legend/components/Legend.tsx +76 -40
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/Legend/components/index.scss +1 -1
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/NavigationMenu.tsx +27 -15
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
- package/src/components/SmallMultiples/index.tsx +3 -0
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
- package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
- package/src/components/UsaMap/helpers/map.ts +4 -4
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +193 -35
- package/src/components/ZoomControls.tsx +6 -9
- package/src/context/LegendMemoContext.tsx +30 -0
- package/src/context.ts +1 -40
- package/src/data/initial-state.js +153 -130
- package/src/data/supported-geos.js +25 -78
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +140 -20
- package/src/helpers/applyLegendToRow.ts +10 -6
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -14
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +18 -3
- package/src/helpers/generateRuntimeLegend.ts +44 -10
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +33 -0
- package/src/helpers/getStatesPicked.ts +8 -5
- package/src/helpers/index.ts +3 -3
- package/src/helpers/isLegendItemDisabled.ts +16 -0
- package/src/helpers/mapObserverHelpers.ts +40 -0
- package/src/helpers/resetLegendToggles.ts +3 -2
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- package/src/helpers/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +36 -2
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +5 -2
- package/src/hooks/useStateZoom.tsx +30 -8
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +1 -2
- package/src/scss/editor-panel.scss +4 -440
- package/src/scss/main.scss +1 -1
- package/src/scss/map.scss +12 -15
- package/src/store/map.actions.ts +7 -7
- package/src/store/map.reducer.ts +17 -6
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +46 -18
- package/src/types/MapContext.ts +6 -7
- package/src/types/runtimeLegend.ts +17 -1
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- package/src/components/DataTable.tsx +0 -385
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/coreStyles_map.scss +0 -3
- package/src/helpers/colorDistributions.ts +0 -12
- package/src/helpers/generateColorsArray.ts +0 -14
- package/src/helpers/tests/generateColorsArray.test.ts +0 -18
- package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
- package/src/hooks/useActiveElement.ts +0 -19
- package/src/scss/mixins.scss +0 -47
- package/src/types/Annotations.ts +0 -24
- /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { useContext, FC } from 'react'
|
|
2
|
+
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
|
|
3
|
+
import {
|
|
4
|
+
AccordionItem,
|
|
5
|
+
AccordionItemHeading,
|
|
6
|
+
AccordionItemPanel,
|
|
7
|
+
AccordionItemButton
|
|
8
|
+
} from 'react-accessible-accordion'
|
|
9
|
+
|
|
10
|
+
// core
|
|
11
|
+
import { TextField, Select, CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
|
|
12
|
+
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
13
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
14
|
+
|
|
15
|
+
// contexts
|
|
16
|
+
import ConfigContext from '../../../../context'
|
|
17
|
+
import { MapContext } from '../../../../types/MapContext'
|
|
18
|
+
import { getTileKeys } from '../../../../helpers/smallMultiplesHelpers'
|
|
19
|
+
|
|
20
|
+
interface PanelSmallMultiplesProps {
|
|
21
|
+
name?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PanelSmallMultiples: FC<PanelSmallMultiplesProps> = props => {
|
|
25
|
+
const { config, setConfig } = useContext<MapContext>(ConfigContext)
|
|
26
|
+
const { general } = config
|
|
27
|
+
|
|
28
|
+
const getColumns = () => {
|
|
29
|
+
let columns = {}
|
|
30
|
+
config.data?.forEach(row => {
|
|
31
|
+
Object.keys(row).forEach(columnName => (columns[columnName] = true))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Filter out geo and primary columns
|
|
35
|
+
if (config.columns?.geo?.name) {
|
|
36
|
+
delete columns[config.columns.geo.name]
|
|
37
|
+
}
|
|
38
|
+
if (config.columns?.primary?.name) {
|
|
39
|
+
delete columns[config.columns.primary.name]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Object.keys(columns)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const updateField = (section, subsection, fieldName, value) => {
|
|
46
|
+
const newConfig = { ...config }
|
|
47
|
+
|
|
48
|
+
if (subsection) {
|
|
49
|
+
newConfig[section] = {
|
|
50
|
+
...newConfig[section],
|
|
51
|
+
[subsection]: {
|
|
52
|
+
...newConfig[section]?.[subsection],
|
|
53
|
+
[fieldName]: value
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
newConfig[section] = {
|
|
58
|
+
...newConfig[section],
|
|
59
|
+
[fieldName]: value
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setConfig(newConfig)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handleColumnChange = (section, subsection, fieldName, value) => {
|
|
67
|
+
const newConfig = { ...config }
|
|
68
|
+
|
|
69
|
+
// Set the column value
|
|
70
|
+
newConfig.smallMultiples = {
|
|
71
|
+
...newConfig.smallMultiples,
|
|
72
|
+
tileColumn: value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Automatically set mode based on whether a column is selected
|
|
76
|
+
if (value) {
|
|
77
|
+
newConfig.smallMultiples.mode = 'by-column'
|
|
78
|
+
} else {
|
|
79
|
+
newConfig.smallMultiples.mode = undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setConfig(newConfig)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<AccordionItem>
|
|
87
|
+
<AccordionItemHeading>
|
|
88
|
+
<AccordionItemButton>Small Multiples</AccordionItemButton>
|
|
89
|
+
</AccordionItemHeading>
|
|
90
|
+
<AccordionItemPanel>
|
|
91
|
+
<Select
|
|
92
|
+
value={config.smallMultiples?.tileColumn || ''}
|
|
93
|
+
fieldName='tileColumn'
|
|
94
|
+
section='smallMultiples'
|
|
95
|
+
label='Tile By Column'
|
|
96
|
+
initial='Select Column'
|
|
97
|
+
updateField={handleColumnChange}
|
|
98
|
+
options={getColumns()}
|
|
99
|
+
tooltip={
|
|
100
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
101
|
+
<Tooltip.Target>
|
|
102
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
103
|
+
</Tooltip.Target>
|
|
104
|
+
<Tooltip.Content>
|
|
105
|
+
<p>
|
|
106
|
+
Select the column whose unique values will create separate map tiles. Choosing a column will enable
|
|
107
|
+
small multiples mode.
|
|
108
|
+
</p>
|
|
109
|
+
</Tooltip.Content>
|
|
110
|
+
</Tooltip>
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
{config.smallMultiples?.tileColumn && (
|
|
115
|
+
<>
|
|
116
|
+
<TextField
|
|
117
|
+
type='number'
|
|
118
|
+
value={config.smallMultiples?.tilesPerRowDesktop}
|
|
119
|
+
section='smallMultiples'
|
|
120
|
+
fieldName='tilesPerRowDesktop'
|
|
121
|
+
label='Tiles Per Row (Desktop)'
|
|
122
|
+
updateField={updateField}
|
|
123
|
+
min={1}
|
|
124
|
+
max={3}
|
|
125
|
+
tooltip={
|
|
126
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
127
|
+
<Tooltip.Target>
|
|
128
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
129
|
+
</Tooltip.Target>
|
|
130
|
+
<Tooltip.Content>
|
|
131
|
+
<p>
|
|
132
|
+
Number of map tiles to display per row on desktop screens. Mobile will always show 1 tile per row.
|
|
133
|
+
</p>
|
|
134
|
+
</Tooltip.Content>
|
|
135
|
+
</Tooltip>
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
{/* Tile Ordering */}
|
|
140
|
+
{(() => {
|
|
141
|
+
const availableTiles = getTileKeys(config, config.data).map(String)
|
|
142
|
+
if (availableTiles.length === 0) return null
|
|
143
|
+
|
|
144
|
+
const tileOrderOptions = [
|
|
145
|
+
{
|
|
146
|
+
label: 'Ascending By Title',
|
|
147
|
+
value: 'asc'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
label: 'Descending By Title',
|
|
151
|
+
value: 'desc'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
label: 'Custom',
|
|
155
|
+
value: 'custom'
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
const currentOrderType = config.smallMultiples?.tileOrderType || 'asc'
|
|
160
|
+
|
|
161
|
+
const handleOrderTypeChange = orderType => {
|
|
162
|
+
const newConfig = {
|
|
163
|
+
...config,
|
|
164
|
+
smallMultiples: {
|
|
165
|
+
...config.smallMultiples,
|
|
166
|
+
tileOrderType: orderType
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// If switching to custom, initialize with current tile order
|
|
171
|
+
if (orderType === 'custom' && !config.smallMultiples?.tileOrder?.length) {
|
|
172
|
+
newConfig.smallMultiples.tileOrder = [...availableTiles]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setConfig(newConfig)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleCustomTileOrderChange = (sourceIndex, destinationIndex) => {
|
|
179
|
+
if (destinationIndex === null) return
|
|
180
|
+
|
|
181
|
+
const currentOrder = config.smallMultiples?.tileOrder || [...availableTiles]
|
|
182
|
+
const newOrder = [...currentOrder]
|
|
183
|
+
const [removed] = newOrder.splice(sourceIndex, 1)
|
|
184
|
+
newOrder.splice(destinationIndex, 0, removed)
|
|
185
|
+
|
|
186
|
+
setConfig({
|
|
187
|
+
...config,
|
|
188
|
+
smallMultiples: {
|
|
189
|
+
...config.smallMultiples,
|
|
190
|
+
tileOrder: newOrder,
|
|
191
|
+
tileOrderType: 'custom'
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
<Select
|
|
199
|
+
value={currentOrderType}
|
|
200
|
+
options={tileOrderOptions}
|
|
201
|
+
label='Tile Order'
|
|
202
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
203
|
+
handleOrderTypeChange(value)
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
|
|
207
|
+
{currentOrderType === 'custom' && (
|
|
208
|
+
<DragDropContext
|
|
209
|
+
onDragEnd={({ source, destination }) =>
|
|
210
|
+
handleCustomTileOrderChange(source.index, destination?.index)
|
|
211
|
+
}
|
|
212
|
+
>
|
|
213
|
+
<Droppable droppableId='tile_order'>
|
|
214
|
+
{provided => (
|
|
215
|
+
<ul
|
|
216
|
+
{...provided.droppableProps}
|
|
217
|
+
className='sort-list'
|
|
218
|
+
ref={provided.innerRef}
|
|
219
|
+
style={{ marginTop: '1em' }}
|
|
220
|
+
>
|
|
221
|
+
{(config.smallMultiples?.tileOrder || availableTiles).map((tileKey, index) => (
|
|
222
|
+
<Draggable key={tileKey} draggableId={`tile-${tileKey}`} index={index}>
|
|
223
|
+
{(provided, snapshot) => (
|
|
224
|
+
<li>
|
|
225
|
+
<div
|
|
226
|
+
className={snapshot.isDragging ? 'currently-dragging' : ''}
|
|
227
|
+
style={provided.draggableProps.style}
|
|
228
|
+
ref={provided.innerRef}
|
|
229
|
+
{...provided.draggableProps}
|
|
230
|
+
{...provided.dragHandleProps}
|
|
231
|
+
>
|
|
232
|
+
{tileKey}
|
|
233
|
+
</div>
|
|
234
|
+
</li>
|
|
235
|
+
)}
|
|
236
|
+
</Draggable>
|
|
237
|
+
))}
|
|
238
|
+
{provided.placeholder}
|
|
239
|
+
</ul>
|
|
240
|
+
)}
|
|
241
|
+
</Droppable>
|
|
242
|
+
</DragDropContext>
|
|
243
|
+
)}
|
|
244
|
+
</>
|
|
245
|
+
)
|
|
246
|
+
})()}
|
|
247
|
+
|
|
248
|
+
{/* Custom Tile Titles */}
|
|
249
|
+
<div>
|
|
250
|
+
<label style={{ marginTop: '1.5rem', marginBottom: '0.5rem' }}>Custom Tile Titles</label>
|
|
251
|
+
|
|
252
|
+
{(() => {
|
|
253
|
+
const availableTiles = getTileKeys(config, config.data).map(String)
|
|
254
|
+
if (availableTiles.length === 0) return null
|
|
255
|
+
|
|
256
|
+
const handleTitleChange = (tileKey, customTitle) => {
|
|
257
|
+
const newTitles = { ...config.smallMultiples?.tileTitles }
|
|
258
|
+
if (customTitle.trim() === '' || customTitle === tileKey) {
|
|
259
|
+
delete newTitles[tileKey] // Remove entry if empty or same as key
|
|
260
|
+
} else {
|
|
261
|
+
newTitles[tileKey] = customTitle
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setConfig({
|
|
265
|
+
...config,
|
|
266
|
+
smallMultiples: {
|
|
267
|
+
...config.smallMultiples,
|
|
268
|
+
tileTitles: newTitles
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div className='tile-titles-editor' style={{ maxWidth: '100%', overflow: 'hidden' }}>
|
|
275
|
+
{availableTiles.map(tileKey => {
|
|
276
|
+
const customTitle = config.smallMultiples?.tileTitles?.[tileKey] || ''
|
|
277
|
+
return (
|
|
278
|
+
<div
|
|
279
|
+
key={tileKey}
|
|
280
|
+
className='tile-title-row'
|
|
281
|
+
style={{
|
|
282
|
+
display: 'flex',
|
|
283
|
+
alignItems: 'center',
|
|
284
|
+
marginBottom: '0.75rem',
|
|
285
|
+
maxWidth: '100%'
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<label
|
|
289
|
+
style={{
|
|
290
|
+
minWidth: '80px',
|
|
291
|
+
maxWidth: '120px',
|
|
292
|
+
marginRight: '0.75rem',
|
|
293
|
+
fontWeight: 'normal',
|
|
294
|
+
fontSize: '13px',
|
|
295
|
+
overflow: 'hidden',
|
|
296
|
+
textOverflow: 'ellipsis',
|
|
297
|
+
whiteSpace: 'nowrap',
|
|
298
|
+
flexShrink: 0
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
{tileKey}:
|
|
302
|
+
</label>
|
|
303
|
+
<input
|
|
304
|
+
type='text'
|
|
305
|
+
value={customTitle}
|
|
306
|
+
placeholder={tileKey}
|
|
307
|
+
onChange={event => handleTitleChange(tileKey, event.target.value)}
|
|
308
|
+
style={{
|
|
309
|
+
flex: 1,
|
|
310
|
+
minWidth: 0,
|
|
311
|
+
maxWidth: '200px',
|
|
312
|
+
fontSize: '13px',
|
|
313
|
+
padding: '4px 8px',
|
|
314
|
+
height: '30px',
|
|
315
|
+
border: '1px solid #ccc',
|
|
316
|
+
borderRadius: '3px'
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
})}
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
})()}
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<CheckBox
|
|
328
|
+
value={config.smallMultiples?.synchronizedTooltips}
|
|
329
|
+
fieldName='synchronizedTooltips'
|
|
330
|
+
section='smallMultiples'
|
|
331
|
+
label='Synchronized Tooltips'
|
|
332
|
+
updateField={updateField}
|
|
333
|
+
tooltip={
|
|
334
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
335
|
+
<Tooltip.Target>
|
|
336
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
337
|
+
</Tooltip.Target>
|
|
338
|
+
<Tooltip.Content>
|
|
339
|
+
<p>
|
|
340
|
+
When checked, hovering over a geography in one map will show synchronized tooltips for that same
|
|
341
|
+
geography on all other maps at the same position.
|
|
342
|
+
</p>
|
|
343
|
+
</Tooltip.Content>
|
|
344
|
+
</Tooltip>
|
|
345
|
+
}
|
|
346
|
+
/>
|
|
347
|
+
</>
|
|
348
|
+
)}
|
|
349
|
+
</AccordionItemPanel>
|
|
350
|
+
</AccordionItem>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export default PanelSmallMultiples
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import Annotate from './Panel.Annotate'
|
|
3
3
|
import PatternSettings from './Panel.PatternSettings'
|
|
4
|
+
import SmallMultiples from './Panel.SmallMultiples'
|
|
4
5
|
|
|
5
6
|
const Panels = {
|
|
6
7
|
Annotate,
|
|
7
|
-
PatternSettings
|
|
8
|
+
PatternSettings,
|
|
9
|
+
SmallMultiples
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export default Panels
|
package/src/components/Geo.tsx
CHANGED
|
@@ -6,14 +6,33 @@ type GeoProps = {
|
|
|
6
6
|
strokeWidth?: number
|
|
7
7
|
path?: string
|
|
8
8
|
className?: string
|
|
9
|
+
onMouseEnter?: () => void
|
|
10
|
+
onClick?: () => void
|
|
11
|
+
'data-country-code'?: string
|
|
12
|
+
'data-tooltip-id'?: string
|
|
13
|
+
'data-tooltip-html'?: string
|
|
14
|
+
additionalData?: any
|
|
15
|
+
geoData?: any
|
|
16
|
+
additionaldata?: string
|
|
17
|
+
geodata?: string
|
|
18
|
+
tabIndex?: number
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
const Geo: React.FC<GeoProps> = ({ path, styles, stroke, strokeWidth, ...props }) => {
|
|
12
|
-
const { className, ...restProps } = props
|
|
13
|
-
const geoClassName = String(props.additionalData?.name)?.toLowerCase()?.
|
|
22
|
+
const { className, 'data-country-code': dataCountryCode, ...restProps } = props
|
|
23
|
+
const geoClassName = String(props.additionalData?.name)?.toLowerCase()?.replace(/\s+/g, '') || 'country'
|
|
14
24
|
return (
|
|
15
25
|
<g className={`geo-group ${geoClassName}`} style={styles} {...restProps}>
|
|
16
|
-
<path
|
|
26
|
+
<path
|
|
27
|
+
tabIndex={-1}
|
|
28
|
+
className={`single-geo ${geoClassName} ${className || ''}`}
|
|
29
|
+
stroke={stroke}
|
|
30
|
+
strokeWidth={strokeWidth}
|
|
31
|
+
strokeLinejoin='round'
|
|
32
|
+
strokeLinecap='round'
|
|
33
|
+
d={path}
|
|
34
|
+
data-country-code={dataCountryCode}
|
|
35
|
+
/>
|
|
17
36
|
</g>
|
|
18
37
|
)
|
|
19
38
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//TODO: Move legends to core
|
|
2
|
-
import { forwardRef, useContext } from 'react'
|
|
2
|
+
import { forwardRef, useContext, useMemo } from 'react'
|
|
3
3
|
import parse from 'html-react-parser'
|
|
4
|
+
import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
|
|
4
5
|
|
|
5
6
|
//types
|
|
6
7
|
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
@@ -20,11 +21,12 @@ import './index.scss'
|
|
|
20
21
|
import { type ViewPort } from '@cdc/core/types/ViewPort'
|
|
21
22
|
import { isBelowBreakpoint, isMobileFontViewport } from '@cdc/core/helpers/viewports'
|
|
22
23
|
import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
|
|
23
|
-
import { toggleLegendActive } from '
|
|
24
|
+
import { toggleLegendActive } from '../../../helpers/toggleLegendActive'
|
|
24
25
|
import { resetLegendToggles } from '../../../helpers'
|
|
25
26
|
import { MapContext } from '../../../types/MapContext'
|
|
26
27
|
import LegendGroup from './LegendGroup/Legend.Group'
|
|
27
28
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
29
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
28
30
|
|
|
29
31
|
const LEGEND_PADDING = 30
|
|
30
32
|
|
|
@@ -45,8 +47,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
45
47
|
dimensions,
|
|
46
48
|
mapId,
|
|
47
49
|
runtimeFilters,
|
|
48
|
-
runtimeLegend
|
|
49
|
-
setRuntimeLegend
|
|
50
|
+
runtimeLegend
|
|
50
51
|
} = useContext<MapContext>(ConfigContext)
|
|
51
52
|
|
|
52
53
|
const dispatch = useContext(MapDispatchContext)
|
|
@@ -61,7 +62,10 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
61
62
|
|
|
62
63
|
const getFormattedLegendItems = () => {
|
|
63
64
|
try {
|
|
64
|
-
if (!runtimeLegend.items)
|
|
65
|
+
if (!runtimeLegend.items) {
|
|
66
|
+
console.warn('No runtime legend data available')
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
65
69
|
return runtimeLegend.items.map((entry, idx) => {
|
|
66
70
|
const entryMax = displayDataAsText(entry.max, 'primary', config)
|
|
67
71
|
|
|
@@ -120,34 +124,36 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
120
124
|
return classes.join(' ')
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
const setAccessibleStatus = (message: string) => {
|
|
124
|
-
dispatch({ type: 'SET_ACCESSIBLE_STATUS', payload: message })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
127
|
return (
|
|
128
128
|
<li
|
|
129
129
|
className={handleListItemClass()}
|
|
130
130
|
key={idx}
|
|
131
131
|
title={`Legend item ${item.label} - Click to disable`}
|
|
132
132
|
onClick={() => {
|
|
133
|
-
toggleLegendActive(idx, item.label, runtimeLegend,
|
|
134
|
-
publishAnalyticsEvent(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
'
|
|
139
|
-
|
|
133
|
+
toggleLegendActive(idx, item.label, runtimeLegend, dispatch)
|
|
134
|
+
publishAnalyticsEvent({
|
|
135
|
+
vizType: config.type,
|
|
136
|
+
vizSubType: getVizSubType(config),
|
|
137
|
+
eventType: `map_legend_item_toggled`,
|
|
138
|
+
eventAction: 'click',
|
|
139
|
+
eventLabel: `${interactionLabel}`,
|
|
140
|
+
vizTitle: getVizTitle(config),
|
|
141
|
+
specifics: `mode: isolate, label: ${item.label}`
|
|
142
|
+
})
|
|
140
143
|
}}
|
|
141
144
|
onKeyDown={e => {
|
|
142
145
|
if (e.key === 'Enter') {
|
|
143
146
|
e.preventDefault()
|
|
144
|
-
toggleLegendActive(idx, item.label, runtimeLegend,
|
|
145
|
-
publishAnalyticsEvent(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
'
|
|
150
|
-
|
|
147
|
+
toggleLegendActive(idx, item.label, runtimeLegend, dispatch)
|
|
148
|
+
publishAnalyticsEvent({
|
|
149
|
+
vizType: config.type,
|
|
150
|
+
vizSubType: getVizSubType(config),
|
|
151
|
+
eventType: `map_legend_item_toggled`,
|
|
152
|
+
eventAction: 'keydown',
|
|
153
|
+
eventLabel: `${interactionLabel}`,
|
|
154
|
+
vizTitle: getVizTitle(config),
|
|
155
|
+
specifics: `mode: isolate, label: ${item.label}`
|
|
156
|
+
})
|
|
151
157
|
}
|
|
152
158
|
}}
|
|
153
159
|
tabIndex={0}
|
|
@@ -165,9 +171,9 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
165
171
|
const { pattern, dataKey, size } = patternData
|
|
166
172
|
let defaultPatternColor = 'black'
|
|
167
173
|
const sizes = {
|
|
168
|
-
small:
|
|
169
|
-
medium:
|
|
170
|
-
large:
|
|
174
|
+
small: 8,
|
|
175
|
+
medium: 10,
|
|
176
|
+
large: 12
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
const legendSize = 16
|
|
@@ -240,8 +246,15 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
240
246
|
if (e) {
|
|
241
247
|
e.preventDefault()
|
|
242
248
|
}
|
|
243
|
-
publishAnalyticsEvent(
|
|
244
|
-
|
|
249
|
+
publishAnalyticsEvent({
|
|
250
|
+
vizType: config.type,
|
|
251
|
+
vizSubType: getVizSubType(config),
|
|
252
|
+
eventType: 'map_legend_reset',
|
|
253
|
+
eventAction: 'click',
|
|
254
|
+
eventLabel: interactionLabel,
|
|
255
|
+
vizTitle: getVizTitle(config)
|
|
256
|
+
})
|
|
257
|
+
resetLegendToggles(runtimeLegend, dispatch)
|
|
245
258
|
dispatch({
|
|
246
259
|
type: 'SET_ACCESSIBLE_STATUS',
|
|
247
260
|
payload: 'Legend has been reset, please reference the data table to see updated values.'
|
|
@@ -261,14 +274,17 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
261
274
|
/>
|
|
262
275
|
)
|
|
263
276
|
|
|
264
|
-
const cityStyleShapes =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
277
|
+
const cityStyleShapes = useMemo(
|
|
278
|
+
() => ({
|
|
279
|
+
pin: pin,
|
|
280
|
+
circle: <GlyphCircle color='#000' size={150} />,
|
|
281
|
+
square: <GlyphSquare color='#000' size={150} />,
|
|
282
|
+
diamond: <GlyphDiamond color='#000' size={150} />,
|
|
283
|
+
star: <GlyphStar color='#000' size={150} />,
|
|
284
|
+
triangle: <GlyphTriangle color='#000' size={150} />
|
|
285
|
+
}),
|
|
286
|
+
[pin]
|
|
287
|
+
)
|
|
272
288
|
|
|
273
289
|
const shouldRenderLegendList = legendListItems.length > 0 && ['Select Option', ''].includes(config.legend.groupBy)
|
|
274
290
|
|
|
@@ -286,9 +302,29 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
286
302
|
<section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
|
|
287
303
|
{(legend.title || legend.description || legend.dynamicDescription) && (
|
|
288
304
|
<div className='mb-3'>
|
|
289
|
-
{legend.title &&
|
|
305
|
+
{legend.title && (
|
|
306
|
+
<h3 className={legendClasses.title.join(' ') || ''}>
|
|
307
|
+
{parse(
|
|
308
|
+
config.enableMarkupVariables && config.markupVariables?.length > 0
|
|
309
|
+
? processMarkupVariables(legend.title, config.data || [], config.markupVariables, {
|
|
310
|
+
isEditor: false,
|
|
311
|
+
filters: config.filters || []
|
|
312
|
+
}).processedContent
|
|
313
|
+
: legend.title
|
|
314
|
+
)}
|
|
315
|
+
</h3>
|
|
316
|
+
)}
|
|
290
317
|
{legend.dynamicDescription === false && legend.description && (
|
|
291
|
-
<p className={legendClasses.description.join(' ') || ''}>
|
|
318
|
+
<p className={legendClasses.description.join(' ') || ''}>
|
|
319
|
+
{parse(
|
|
320
|
+
config.enableMarkupVariables && config.markupVariables?.length > 0
|
|
321
|
+
? processMarkupVariables(legend.description, config.data || [], config.markupVariables, {
|
|
322
|
+
isEditor: false,
|
|
323
|
+
filters: config.filters || []
|
|
324
|
+
}).processedContent
|
|
325
|
+
: legend.description
|
|
326
|
+
)}
|
|
327
|
+
</p>
|
|
292
328
|
)}
|
|
293
329
|
{legend.dynamicDescription === true &&
|
|
294
330
|
runtimeFilters.map((filter, idx) => {
|
|
@@ -351,9 +387,9 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
351
387
|
)}
|
|
352
388
|
|
|
353
389
|
{config.visual.additionalCityStyles.map(
|
|
354
|
-
({ shape, label }) =>
|
|
390
|
+
({ shape, label }, index) =>
|
|
355
391
|
label && (
|
|
356
|
-
<div>
|
|
392
|
+
<div key={`additional-city-style-${index}-${shape}`}>
|
|
357
393
|
<svg>
|
|
358
394
|
<Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
|
|
359
395
|
{cityStyleShapes[shape.toLowerCase()]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useContext, useMemo } from 'react'
|
|
2
2
|
import './legend.group.css'
|
|
3
3
|
import LegendShape from '@cdc/core/components/LegendShape'
|
|
4
|
-
import { toggleLegendActive } from '
|
|
4
|
+
import { toggleLegendActive } from '../../../../helpers/toggleLegendActive'
|
|
5
5
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
6
6
|
import ConfigContext, { MapDispatchContext } from '../../../../context'
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@ interface GroupedData {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const LegendGroup = ({ legendItems }) => {
|
|
20
|
-
const { runtimeLegend,
|
|
20
|
+
const { runtimeLegend, config } = useContext(ConfigContext)
|
|
21
21
|
const dispatch = useContext(MapDispatchContext)
|
|
22
22
|
const groupLegendItems = (items: LegendItem[], data: object[], groupByKey: string): GroupedData => {
|
|
23
23
|
if (!groupByKey || !data || !items) return {}
|
|
@@ -59,10 +59,13 @@ const LegendGroup = ({ legendItems }) => {
|
|
|
59
59
|
const wasDisabled = runtimeLegend.items.find(i => i.value === item.label)?.disabled
|
|
60
60
|
const delta = wasDisabled ? -1 : 1
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
dispatch({
|
|
63
|
+
type: 'SET_RUNTIME_LEGEND',
|
|
64
|
+
payload: {
|
|
65
|
+
...runtimeLegend,
|
|
66
|
+
items: newItems,
|
|
67
|
+
disabledAmt: (runtimeLegend.disabledAmt ?? 0) + delta
|
|
68
|
+
}
|
|
66
69
|
})
|
|
67
70
|
const message = `${wasDisabled ? 'Enabled' : 'Disabled'} legend item ${
|
|
68
71
|
item.label
|
|
@@ -109,7 +112,7 @@ const LegendGroup = ({ legendItems }) => {
|
|
|
109
112
|
onKeyDown={e => {
|
|
110
113
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
111
114
|
e.preventDefault()
|
|
112
|
-
toggleLegendActive(index, item.label, runtimeLegend,
|
|
115
|
+
toggleLegendActive(index, item.label, runtimeLegend, dispatch)
|
|
113
116
|
}
|
|
114
117
|
}}
|
|
115
118
|
>
|