@cdc/map 4.25.10 → 4.26.1
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/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 +58397 -55987
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/city_styles_variable.json +877 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/map-filter-issue.json +2260 -0
- package/examples/private/map-legend.json +5303 -0
- package/index.html +27 -36
- package/package.json +6 -5
- package/src/CdcMapComponent.tsx +86 -26
- 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 +3426 -0
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.stories.tsx +116 -4
- package/src/_stories/_mock/column-wrap-test.json +265 -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/usa-state-gradient.json +3 -4
- package/src/components/BubbleList.tsx +1 -1
- package/src/components/CityList.tsx +24 -18
- package/src/components/EditorPanel/components/EditorPanel.tsx +2380 -2206
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -19
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +351 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/Geo.tsx +20 -3
- package/src/components/Legend/components/Legend.tsx +58 -75
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +1 -1
- package/src/components/Legend/components/index.scss +23 -6
- package/src/components/NavigationMenu.tsx +16 -13
- 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 +18 -3
- 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 +29 -9
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +7 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +16 -4
- package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +29 -12
- package/src/components/UsaMap/components/UsaMap.State.tsx +30 -5
- package/src/components/UsaMap/helpers/map.ts +2 -2
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +81 -11
- package/src/data/initial-state.js +11 -0
- package/src/data/supported-geos.js +8 -76
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +25 -1
- package/src/helpers/applyLegendToRow.ts +5 -3
- package/src/helpers/constants.ts +3 -15
- package/src/helpers/displayGeoName.ts +22 -4
- package/src/helpers/generateRuntimeFilters.ts +1 -1
- package/src/helpers/generateRuntimeLegend.ts +1 -3
- package/src/helpers/generateRuntimeLegendHash.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +2 -5
- package/src/helpers/index.ts +2 -4
- package/src/helpers/isLegendItemDisabled.ts +2 -2
- package/src/helpers/resetLegendToggles.ts +1 -0
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/hashObj.test.ts +1 -1
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- package/src/helpers/toggleLegendActive.ts +76 -8
- package/src/helpers/urlDataHelpers.ts +1 -1
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +1 -1
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +8 -2
- package/src/hooks/useStateZoom.tsx +7 -4
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/index.jsx +1 -0
- 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/test/CdcMap.test.jsx +1 -1
- package/src/types/MapConfig.ts +32 -11
- package/src/types/MapContext.ts +6 -0
- package/src/types/runtimeLegend.ts +2 -1
- package/LICENSE +0 -201
- package/src/components/DataTable.tsx +0 -413
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/components/MapControls.tsx +0 -44
- package/src/helpers/getUniqueValues.ts +0 -19
- package/src/helpers/hashObj.ts +0 -25
- package/src/hooks/useActiveElement.ts +0 -19
- package/src/hooks/useLegendSeparators.ts +0 -26
- 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
|
@@ -6,27 +6,20 @@ import {
|
|
|
6
6
|
AccordionItemPanel,
|
|
7
7
|
AccordionItemButton
|
|
8
8
|
} from 'react-accessible-accordion'
|
|
9
|
+
import { Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
9
10
|
import ConfigContext from '../../../context'
|
|
10
11
|
import _ from 'lodash'
|
|
11
12
|
import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
|
|
12
|
-
|
|
13
|
-
const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'Arrow Left', 'None']
|
|
14
|
-
|
|
15
|
-
// todo: Move duplicated operators to CORE
|
|
16
|
-
export const DATA_OPERATOR_LESS = '<'
|
|
17
|
-
export const DATA_OPERATOR_GREATER = '>'
|
|
18
|
-
export const DATA_OPERATOR_LESSEQUAL = '<='
|
|
19
|
-
export const DATA_OPERATOR_GREATEREQUAL = '>='
|
|
20
|
-
export const DATA_OPERATOR_EQUAL = '='
|
|
21
|
-
export const DATA_OPERATOR_NOTEQUAL = '≠'
|
|
22
|
-
export const DATA_OPERATORS = [
|
|
13
|
+
import {
|
|
23
14
|
DATA_OPERATOR_LESS,
|
|
24
15
|
DATA_OPERATOR_GREATER,
|
|
25
16
|
DATA_OPERATOR_LESSEQUAL,
|
|
26
17
|
DATA_OPERATOR_GREATEREQUAL,
|
|
27
18
|
DATA_OPERATOR_EQUAL,
|
|
28
19
|
DATA_OPERATOR_NOTEQUAL
|
|
29
|
-
|
|
20
|
+
} from '@cdc/core/helpers/constants'
|
|
21
|
+
|
|
22
|
+
const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'Arrow Left', 'None']
|
|
30
23
|
|
|
31
24
|
/**
|
|
32
25
|
* Notice: each shape Col has a legend title and description should the title/desc need to be different for different shapes.
|
|
@@ -135,91 +128,60 @@ const HexSettingShapeColumns = props => {
|
|
|
135
128
|
</AccordionItemHeading>
|
|
136
129
|
<AccordionItemPanel>
|
|
137
130
|
<>
|
|
131
|
+
<Select
|
|
132
|
+
label='Shape Column'
|
|
133
|
+
value={
|
|
134
|
+
config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].shape ||
|
|
135
|
+
'Arrow Up'
|
|
136
|
+
}
|
|
137
|
+
options={shapeOptions}
|
|
138
|
+
fieldName={`shape-${shapeGroupIndex}-${itemIndex}`}
|
|
139
|
+
updateField={(section, subsection, fieldName, value) => {
|
|
140
|
+
handleItemUpdate('shape', value, shapeGroupIndex, itemIndex)
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<Select
|
|
145
|
+
label='Column'
|
|
146
|
+
value={config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].key || ''}
|
|
147
|
+
options={columnsOptions.map(c => c.key)}
|
|
148
|
+
fieldName={`key-${shapeGroupIndex}-${itemIndex}`}
|
|
149
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
150
|
+
handleItemUpdate('key', value, shapeGroupIndex, itemIndex)
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
<Select
|
|
155
|
+
label='Operator'
|
|
156
|
+
value={
|
|
157
|
+
config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].operator || '='
|
|
158
|
+
}
|
|
159
|
+
options={[
|
|
160
|
+
DATA_OPERATOR_EQUAL,
|
|
161
|
+
DATA_OPERATOR_NOTEQUAL,
|
|
162
|
+
DATA_OPERATOR_LESS,
|
|
163
|
+
DATA_OPERATOR_GREATER,
|
|
164
|
+
DATA_OPERATOR_LESSEQUAL,
|
|
165
|
+
DATA_OPERATOR_GREATEREQUAL
|
|
166
|
+
]}
|
|
167
|
+
fieldName={`operator-${shapeGroupIndex}-${itemIndex}`}
|
|
168
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
169
|
+
handleItemUpdate('operator', value, shapeGroupIndex, itemIndex)
|
|
170
|
+
}
|
|
171
|
+
/>
|
|
172
|
+
|
|
138
173
|
<label>
|
|
139
|
-
<span className='edit-label column-heading'>
|
|
140
|
-
<
|
|
174
|
+
<span className='edit-label column-heading'>Value</span>
|
|
175
|
+
<input
|
|
176
|
+
type='text'
|
|
141
177
|
value={
|
|
142
|
-
config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].value || ''
|
|
179
|
+
}
|
|
180
|
+
onChange={e =>
|
|
181
|
+
handleItemUpdate('value', e.target.value, shapeGroupIndex, itemIndex)
|
|
145
182
|
}
|
|
146
|
-
|
|
147
|
-
handleItemUpdate('shape', e.target.value, shapeGroupIndex, itemIndex)
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
{shapeOptions.map(shape => (
|
|
151
|
-
<option value={shape}>{shape}</option>
|
|
152
|
-
))}
|
|
153
|
-
</select>
|
|
183
|
+
/>
|
|
154
184
|
</label>
|
|
155
|
-
|
|
156
|
-
<div className='cove-input-group'>
|
|
157
|
-
<label className=''>
|
|
158
|
-
<span className='edit-label cove-input__label'>Column Conditional</span>
|
|
159
|
-
</label>
|
|
160
|
-
<div className='cove-accordion__panel-row cove-accordion__small-inputs'>
|
|
161
|
-
<div className='cove-accordion__panel-col cove-input'>
|
|
162
|
-
<select
|
|
163
|
-
value={
|
|
164
|
-
config.hexMap.shapeGroups[shapeGroupIndex].key === ''
|
|
165
|
-
? 'Select'
|
|
166
|
-
: config.hexMap.shapeGroups[shapeGroupIndex].key
|
|
167
|
-
}
|
|
168
|
-
className='cove-input'
|
|
169
|
-
onChange={e =>
|
|
170
|
-
handleItemUpdate('key', e.target.value, shapeGroupIndex, itemIndex)
|
|
171
|
-
}
|
|
172
|
-
>
|
|
173
|
-
{columnsOptions}
|
|
174
|
-
</select>
|
|
175
|
-
</div>
|
|
176
|
-
<div className='cove-accordion__panel-col cove-input'>
|
|
177
|
-
<select
|
|
178
|
-
value={
|
|
179
|
-
config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].operator ||
|
|
180
|
-
'-SELECT-'
|
|
181
|
-
}
|
|
182
|
-
initial='Select'
|
|
183
|
-
className='cove-input'
|
|
184
|
-
onChange={e =>
|
|
185
|
-
handleItemUpdate('operator', e.target.value, shapeGroupIndex, itemIndex)
|
|
186
|
-
}
|
|
187
|
-
>
|
|
188
|
-
{[DATA_OPERATOR_EQUAL].map(option => {
|
|
189
|
-
return <option value={option}>{option}</option>
|
|
190
|
-
})}
|
|
191
|
-
{[DATA_OPERATOR_NOTEQUAL].map(option => {
|
|
192
|
-
return <option value={option}>{option}</option>
|
|
193
|
-
})}
|
|
194
|
-
{[DATA_OPERATOR_LESS].map(option => {
|
|
195
|
-
return <option value={option}>{option}</option>
|
|
196
|
-
})}
|
|
197
|
-
{[DATA_OPERATOR_GREATER].map(option => {
|
|
198
|
-
return <option value={option}>{option}</option>
|
|
199
|
-
})}
|
|
200
|
-
{[DATA_OPERATOR_LESSEQUAL].map(option => {
|
|
201
|
-
return <option value={option}>{option}</option>
|
|
202
|
-
})}
|
|
203
|
-
{[DATA_OPERATOR_GREATEREQUAL].map(option => {
|
|
204
|
-
return <option value={option}>{option}</option>
|
|
205
|
-
})}
|
|
206
|
-
</select>
|
|
207
|
-
</div>
|
|
208
|
-
<div className='cove-accordion__panel-col cove-input'>
|
|
209
|
-
<input
|
|
210
|
-
type='text'
|
|
211
|
-
value={
|
|
212
|
-
config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].value || ''
|
|
213
|
-
}
|
|
214
|
-
className='cove-input'
|
|
215
|
-
style={{ height: '100%' }}
|
|
216
|
-
onChange={e =>
|
|
217
|
-
handleItemUpdate('value', e.target.value, shapeGroupIndex, itemIndex)
|
|
218
|
-
}
|
|
219
|
-
/>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
185
|
<button
|
|
224
186
|
className='cove-button cove-button--warn'
|
|
225
187
|
style={{
|
|
@@ -13,25 +13,6 @@ import { setConfig } from 'dompurify'
|
|
|
13
13
|
|
|
14
14
|
const PanelAnnotate: React.FC = props => {
|
|
15
15
|
const { config, setConfig, dimensions, isDraggingAnnotation } = useContext<MapContext>(ConfigContext)
|
|
16
|
-
const getColumns = (filter = true) => {
|
|
17
|
-
const columns = {}
|
|
18
|
-
config.data.forEach(row => {
|
|
19
|
-
Object.keys(row).forEach(columnName => (columns[columnName] = true))
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
if (filter) {
|
|
23
|
-
Object.keys(columns).forEach(key => {
|
|
24
|
-
if (
|
|
25
|
-
(config.series && config.series.filter(series => series.dataKey === key).length > 0) ||
|
|
26
|
-
(config.confidenceKeys && Object.keys(config.confidenceKeys).includes(key))
|
|
27
|
-
) {
|
|
28
|
-
delete columns[key]
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return Object.keys(columns)
|
|
34
|
-
}
|
|
35
16
|
|
|
36
17
|
const handleAnnotationUpdate = (value, property, index) => {
|
|
37
18
|
const annotations = [...config?.annotations]
|
|
@@ -12,6 +12,7 @@ import { type MapContext } from '../../../../types/MapContext'
|
|
|
12
12
|
import Button from '@cdc/core/components/elements/Button'
|
|
13
13
|
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
14
14
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
15
|
+
import { Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
15
16
|
import './Panel.PatternSettings-style.css'
|
|
16
17
|
import Alert from '@cdc/core/components/Alert'
|
|
17
18
|
import _ from 'lodash'
|
|
@@ -196,21 +197,16 @@ const PatternSettings = ({ name }: PanelProps) => {
|
|
|
196
197
|
message='Error: <a href="https://webaim.org/resources/contrastchecker/" target="_blank"> Review Color Contrast</a>'
|
|
197
198
|
/>
|
|
198
199
|
)}{' '}
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{d}
|
|
210
|
-
</option>
|
|
211
|
-
)
|
|
212
|
-
})}
|
|
213
|
-
</select>
|
|
200
|
+
<Select
|
|
201
|
+
label='Data Key:'
|
|
202
|
+
value={pattern.dataKey}
|
|
203
|
+
options={dataKeyOptions.filter(d => d !== 'Select')}
|
|
204
|
+
initial='Select'
|
|
205
|
+
fieldName={`pattern-dataKey--${patternIndex}`}
|
|
206
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
207
|
+
handlePatternFieldUpdate('dataKey', value, patternIndex)
|
|
208
|
+
}
|
|
209
|
+
/>
|
|
214
210
|
<label htmlFor={`pattern-dataValue--${patternIndex}`}>
|
|
215
211
|
Data Value:
|
|
216
212
|
<input
|
|
@@ -229,30 +225,24 @@ const PatternSettings = ({ name }: PanelProps) => {
|
|
|
229
225
|
value={pattern.label === '' ? '' : pattern.label}
|
|
230
226
|
/>
|
|
231
227
|
</label>
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
id={`pattern-type--${patternIndex}`}
|
|
228
|
+
<Select
|
|
229
|
+
label='Pattern Type:'
|
|
235
230
|
value={pattern?.pattern}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
<label htmlFor={`pattern-size--${patternIndex}`}>Pattern Size:</label>
|
|
245
|
-
<select
|
|
246
|
-
id={`pattern-size--${patternIndex}`}
|
|
231
|
+
options={patternTypes}
|
|
232
|
+
fieldName={`pattern-type--${patternIndex}`}
|
|
233
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
234
|
+
handlePatternFieldUpdate('pattern', value, patternIndex)
|
|
235
|
+
}
|
|
236
|
+
/>
|
|
237
|
+
<Select
|
|
238
|
+
label='Pattern Size:'
|
|
247
239
|
value={pattern?.size}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
))}
|
|
255
|
-
</select>
|
|
240
|
+
options={['small', 'medium', 'large']}
|
|
241
|
+
fieldName={`pattern-size--${patternIndex}`}
|
|
242
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
243
|
+
handlePatternFieldUpdate('size', value, patternIndex)
|
|
244
|
+
}
|
|
245
|
+
/>
|
|
256
246
|
<div className='pattern-input__color'>
|
|
257
247
|
<label htmlFor='patternColor'>
|
|
258
248
|
Pattern Color
|
|
@@ -0,0 +1,351 @@
|
|
|
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
|
+
import { useDataColumns } from '@cdc/core/hooks/useDataColumns'
|
|
15
|
+
|
|
16
|
+
// contexts
|
|
17
|
+
import ConfigContext from '../../../../context'
|
|
18
|
+
import { MapContext } from '../../../../types/MapContext'
|
|
19
|
+
import { getTileKeys } from '../../../../helpers/smallMultiplesHelpers'
|
|
20
|
+
|
|
21
|
+
interface PanelSmallMultiplesProps {
|
|
22
|
+
name?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PanelSmallMultiples: FC<PanelSmallMultiplesProps> = props => {
|
|
26
|
+
const { config, setConfig } = useContext<MapContext>(ConfigContext)
|
|
27
|
+
const { general } = config
|
|
28
|
+
|
|
29
|
+
// Extract column names from data with memoization (replaces getColumns)
|
|
30
|
+
// Filter out geo and primary columns
|
|
31
|
+
const columns = useDataColumns(config.data, {
|
|
32
|
+
excludeColumns: [config.columns?.geo?.name, config.columns?.primary?.name].filter(Boolean)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const updateField = (section, subsection, fieldName, value) => {
|
|
36
|
+
const newConfig = { ...config }
|
|
37
|
+
|
|
38
|
+
if (subsection) {
|
|
39
|
+
newConfig[section] = {
|
|
40
|
+
...newConfig[section],
|
|
41
|
+
[subsection]: {
|
|
42
|
+
...newConfig[section]?.[subsection],
|
|
43
|
+
[fieldName]: value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
newConfig[section] = {
|
|
48
|
+
...newConfig[section],
|
|
49
|
+
[fieldName]: value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setConfig(newConfig)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleColumnChange = (section, subsection, fieldName, value) => {
|
|
57
|
+
const newConfig = { ...config }
|
|
58
|
+
|
|
59
|
+
// Set the column value
|
|
60
|
+
newConfig.smallMultiples = {
|
|
61
|
+
...newConfig.smallMultiples,
|
|
62
|
+
tileColumn: value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Automatically set mode based on whether a column is selected
|
|
66
|
+
if (value) {
|
|
67
|
+
newConfig.smallMultiples.mode = 'by-column'
|
|
68
|
+
} else {
|
|
69
|
+
newConfig.smallMultiples.mode = undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setConfig(newConfig)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Small multiples only supported for us, single-state, and us-region map types
|
|
76
|
+
if (!['us', 'single-state', 'us-region'].includes(general.geoType)) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<AccordionItem>
|
|
82
|
+
<AccordionItemHeading>
|
|
83
|
+
<AccordionItemButton>Small Multiples</AccordionItemButton>
|
|
84
|
+
</AccordionItemHeading>
|
|
85
|
+
<AccordionItemPanel>
|
|
86
|
+
<Select
|
|
87
|
+
value={config.smallMultiples?.tileColumn || ''}
|
|
88
|
+
fieldName='tileColumn'
|
|
89
|
+
section='smallMultiples'
|
|
90
|
+
label='Tile By Column'
|
|
91
|
+
initial='Select Column'
|
|
92
|
+
updateField={handleColumnChange}
|
|
93
|
+
options={columns}
|
|
94
|
+
tooltip={
|
|
95
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
96
|
+
<Tooltip.Target>
|
|
97
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
98
|
+
</Tooltip.Target>
|
|
99
|
+
<Tooltip.Content>
|
|
100
|
+
<p>
|
|
101
|
+
Select the column whose unique values will create separate map tiles. Choosing a column will enable
|
|
102
|
+
small multiples mode.
|
|
103
|
+
</p>
|
|
104
|
+
</Tooltip.Content>
|
|
105
|
+
</Tooltip>
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
{config.smallMultiples?.tileColumn && (
|
|
110
|
+
<>
|
|
111
|
+
<TextField
|
|
112
|
+
type='number'
|
|
113
|
+
value={config.smallMultiples?.tilesPerRowDesktop}
|
|
114
|
+
section='smallMultiples'
|
|
115
|
+
fieldName='tilesPerRowDesktop'
|
|
116
|
+
label='Tiles Per Row (Desktop)'
|
|
117
|
+
updateField={updateField}
|
|
118
|
+
min={1}
|
|
119
|
+
max={3}
|
|
120
|
+
tooltip={
|
|
121
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
122
|
+
<Tooltip.Target>
|
|
123
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
124
|
+
</Tooltip.Target>
|
|
125
|
+
<Tooltip.Content>
|
|
126
|
+
<p>
|
|
127
|
+
Number of map tiles to display per row on desktop screens. Mobile will always show 1 tile per row.
|
|
128
|
+
</p>
|
|
129
|
+
</Tooltip.Content>
|
|
130
|
+
</Tooltip>
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
{/* Tile Ordering */}
|
|
135
|
+
{(() => {
|
|
136
|
+
const availableTiles = getTileKeys(config, config.data).map(String)
|
|
137
|
+
if (availableTiles.length === 0) return null
|
|
138
|
+
|
|
139
|
+
const tileOrderOptions = [
|
|
140
|
+
{
|
|
141
|
+
label: 'Ascending By Title',
|
|
142
|
+
value: 'asc'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
label: 'Descending By Title',
|
|
146
|
+
value: 'desc'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
label: 'Custom',
|
|
150
|
+
value: 'custom'
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
const currentOrderType = config.smallMultiples?.tileOrderType || 'asc'
|
|
155
|
+
|
|
156
|
+
const handleOrderTypeChange = orderType => {
|
|
157
|
+
const newConfig = {
|
|
158
|
+
...config,
|
|
159
|
+
smallMultiples: {
|
|
160
|
+
...config.smallMultiples,
|
|
161
|
+
tileOrderType: orderType
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If switching to custom, initialize with current tile order
|
|
166
|
+
if (orderType === 'custom' && !config.smallMultiples?.tileOrder?.length) {
|
|
167
|
+
newConfig.smallMultiples.tileOrder = [...availableTiles]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setConfig(newConfig)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const handleCustomTileOrderChange = (sourceIndex, destinationIndex) => {
|
|
174
|
+
if (destinationIndex === null) return
|
|
175
|
+
|
|
176
|
+
const currentOrder = config.smallMultiples?.tileOrder || [...availableTiles]
|
|
177
|
+
const newOrder = [...currentOrder]
|
|
178
|
+
const [removed] = newOrder.splice(sourceIndex, 1)
|
|
179
|
+
newOrder.splice(destinationIndex, 0, removed)
|
|
180
|
+
|
|
181
|
+
setConfig({
|
|
182
|
+
...config,
|
|
183
|
+
smallMultiples: {
|
|
184
|
+
...config.smallMultiples,
|
|
185
|
+
tileOrder: newOrder,
|
|
186
|
+
tileOrderType: 'custom'
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
<Select
|
|
194
|
+
value={currentOrderType}
|
|
195
|
+
options={tileOrderOptions}
|
|
196
|
+
label='Tile Order'
|
|
197
|
+
fieldName='tileOrderType'
|
|
198
|
+
section='smallMultiples'
|
|
199
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
200
|
+
handleOrderTypeChange(value)
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
|
|
204
|
+
{currentOrderType === 'custom' && (
|
|
205
|
+
<DragDropContext
|
|
206
|
+
onDragEnd={({ source, destination }) =>
|
|
207
|
+
handleCustomTileOrderChange(source.index, destination?.index)
|
|
208
|
+
}
|
|
209
|
+
>
|
|
210
|
+
<Droppable droppableId='tile_order'>
|
|
211
|
+
{provided => (
|
|
212
|
+
<ul
|
|
213
|
+
{...provided.droppableProps}
|
|
214
|
+
className='sort-list'
|
|
215
|
+
ref={provided.innerRef}
|
|
216
|
+
style={{ marginTop: '1em' }}
|
|
217
|
+
>
|
|
218
|
+
{(config.smallMultiples?.tileOrder || availableTiles).map((tileKey, index) => (
|
|
219
|
+
<Draggable key={tileKey} draggableId={`tile-${tileKey}`} index={index}>
|
|
220
|
+
{(provided, snapshot) => (
|
|
221
|
+
<li>
|
|
222
|
+
<div
|
|
223
|
+
className={snapshot.isDragging ? 'currently-dragging' : ''}
|
|
224
|
+
style={provided.draggableProps.style}
|
|
225
|
+
ref={provided.innerRef}
|
|
226
|
+
{...provided.draggableProps}
|
|
227
|
+
{...provided.dragHandleProps}
|
|
228
|
+
>
|
|
229
|
+
{tileKey}
|
|
230
|
+
</div>
|
|
231
|
+
</li>
|
|
232
|
+
)}
|
|
233
|
+
</Draggable>
|
|
234
|
+
))}
|
|
235
|
+
{provided.placeholder}
|
|
236
|
+
</ul>
|
|
237
|
+
)}
|
|
238
|
+
</Droppable>
|
|
239
|
+
</DragDropContext>
|
|
240
|
+
)}
|
|
241
|
+
</>
|
|
242
|
+
)
|
|
243
|
+
})()}
|
|
244
|
+
|
|
245
|
+
{/* Custom Tile Titles */}
|
|
246
|
+
<div>
|
|
247
|
+
<label style={{ marginTop: '1.5rem', marginBottom: '0.5rem' }}>Custom Tile Titles</label>
|
|
248
|
+
|
|
249
|
+
{(() => {
|
|
250
|
+
const availableTiles = getTileKeys(config, config.data).map(String)
|
|
251
|
+
if (availableTiles.length === 0) return null
|
|
252
|
+
|
|
253
|
+
const handleTitleChange = (tileKey, customTitle) => {
|
|
254
|
+
const newTitles = { ...config.smallMultiples?.tileTitles }
|
|
255
|
+
if (customTitle.trim() === '' || customTitle === tileKey) {
|
|
256
|
+
delete newTitles[tileKey] // Remove entry if empty or same as key
|
|
257
|
+
} else {
|
|
258
|
+
newTitles[tileKey] = customTitle
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setConfig({
|
|
262
|
+
...config,
|
|
263
|
+
smallMultiples: {
|
|
264
|
+
...config.smallMultiples,
|
|
265
|
+
tileTitles: newTitles
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className='tile-titles-editor' style={{ maxWidth: '100%', overflow: 'hidden' }}>
|
|
272
|
+
{availableTiles.map(tileKey => {
|
|
273
|
+
const customTitle = config.smallMultiples?.tileTitles?.[tileKey] || ''
|
|
274
|
+
return (
|
|
275
|
+
<div
|
|
276
|
+
key={tileKey}
|
|
277
|
+
className='tile-title-row'
|
|
278
|
+
style={{
|
|
279
|
+
display: 'flex',
|
|
280
|
+
alignItems: 'center',
|
|
281
|
+
marginBottom: '0.75rem',
|
|
282
|
+
maxWidth: '100%'
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
<label
|
|
286
|
+
style={{
|
|
287
|
+
minWidth: '80px',
|
|
288
|
+
maxWidth: '120px',
|
|
289
|
+
marginRight: '0.75rem',
|
|
290
|
+
fontWeight: 'normal',
|
|
291
|
+
fontSize: '13px',
|
|
292
|
+
overflow: 'hidden',
|
|
293
|
+
textOverflow: 'ellipsis',
|
|
294
|
+
whiteSpace: 'nowrap',
|
|
295
|
+
flexShrink: 0
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{tileKey}:
|
|
299
|
+
</label>
|
|
300
|
+
<input
|
|
301
|
+
type='text'
|
|
302
|
+
value={customTitle}
|
|
303
|
+
placeholder={tileKey}
|
|
304
|
+
onChange={event => handleTitleChange(tileKey, event.target.value)}
|
|
305
|
+
style={{
|
|
306
|
+
flex: 1,
|
|
307
|
+
minWidth: 0,
|
|
308
|
+
maxWidth: '200px',
|
|
309
|
+
fontSize: '13px',
|
|
310
|
+
padding: '4px 8px',
|
|
311
|
+
height: '30px',
|
|
312
|
+
border: '1px solid #ccc',
|
|
313
|
+
borderRadius: '3px'
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
)
|
|
318
|
+
})}
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
})()}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<CheckBox
|
|
325
|
+
value={config.smallMultiples?.synchronizedTooltips}
|
|
326
|
+
fieldName='synchronizedTooltips'
|
|
327
|
+
section='smallMultiples'
|
|
328
|
+
label='Synchronized Tooltips'
|
|
329
|
+
updateField={updateField}
|
|
330
|
+
tooltip={
|
|
331
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
332
|
+
<Tooltip.Target>
|
|
333
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
334
|
+
</Tooltip.Target>
|
|
335
|
+
<Tooltip.Content>
|
|
336
|
+
<p>
|
|
337
|
+
When checked, hovering over a geography in one map will show synchronized tooltips for that same
|
|
338
|
+
geography on all other maps at the same position.
|
|
339
|
+
</p>
|
|
340
|
+
</Tooltip.Content>
|
|
341
|
+
</Tooltip>
|
|
342
|
+
}
|
|
343
|
+
/>
|
|
344
|
+
</>
|
|
345
|
+
)}
|
|
346
|
+
</AccordionItemPanel>
|
|
347
|
+
</AccordionItem>
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
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
|