@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.
Files changed (107) hide show
  1. package/.claude/agents/typescript-organizer.md +118 -0
  2. package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
  3. package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
  4. package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
  5. package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
  6. package/dist/cdcmap.js +58397 -55987
  7. package/examples/example-city-state.json +9 -1
  8. package/examples/multi-country-centering.json +45 -0
  9. package/examples/private/city_styles_variable.json +877 -0
  10. package/examples/private/colors-2.json +221 -0
  11. package/examples/private/colors.json +221 -0
  12. package/examples/private/map-filter-issue.json +2260 -0
  13. package/examples/private/map-legend.json +5303 -0
  14. package/index.html +27 -36
  15. package/package.json +6 -5
  16. package/src/CdcMapComponent.tsx +86 -26
  17. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  18. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  19. package/src/_stories/CdcMap.Editor.stories.tsx +3426 -0
  20. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  21. package/src/_stories/CdcMap.stories.tsx +116 -4
  22. package/src/_stories/_mock/column-wrap-test.json +265 -0
  23. package/src/_stories/_mock/multi-country-hide.json +78 -0
  24. package/src/_stories/_mock/multi-country.json +95 -0
  25. package/src/_stories/_mock/multi-state.json +887 -20403
  26. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  27. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  28. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  29. package/src/_stories/_mock/usa-state-gradient.json +3 -4
  30. package/src/components/BubbleList.tsx +1 -1
  31. package/src/components/CityList.tsx +24 -18
  32. package/src/components/EditorPanel/components/EditorPanel.tsx +2380 -2206
  33. package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
  34. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -19
  35. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
  36. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +351 -0
  37. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  38. package/src/components/Geo.tsx +20 -3
  39. package/src/components/Legend/components/Legend.tsx +58 -75
  40. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +1 -1
  41. package/src/components/Legend/components/index.scss +23 -6
  42. package/src/components/NavigationMenu.tsx +16 -13
  43. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  44. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  45. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  46. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  47. package/src/components/SmallMultiples/index.tsx +3 -0
  48. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +18 -3
  49. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  50. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  51. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +29 -9
  52. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +7 -0
  53. package/src/components/UsaMap/components/UsaMap.County.tsx +16 -4
  54. package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
  55. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +29 -12
  56. package/src/components/UsaMap/components/UsaMap.State.tsx +30 -5
  57. package/src/components/UsaMap/helpers/map.ts +2 -2
  58. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  59. package/src/components/WorldMap/WorldMap.tsx +81 -11
  60. package/src/data/initial-state.js +11 -0
  61. package/src/data/supported-geos.js +8 -76
  62. package/src/helpers/addUIDs.ts +13 -2
  63. package/src/helpers/applyColorToLegend.ts +25 -1
  64. package/src/helpers/applyLegendToRow.ts +5 -3
  65. package/src/helpers/constants.ts +3 -15
  66. package/src/helpers/displayGeoName.ts +22 -4
  67. package/src/helpers/generateRuntimeFilters.ts +1 -1
  68. package/src/helpers/generateRuntimeLegend.ts +1 -3
  69. package/src/helpers/generateRuntimeLegendHash.ts +1 -1
  70. package/src/helpers/getCountriesPicked.ts +103 -0
  71. package/src/helpers/getMapContainerClasses.ts +7 -0
  72. package/src/helpers/getPatternForRow.ts +2 -5
  73. package/src/helpers/index.ts +2 -4
  74. package/src/helpers/isLegendItemDisabled.ts +2 -2
  75. package/src/helpers/resetLegendToggles.ts +1 -0
  76. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  77. package/src/helpers/tests/hashObj.test.ts +1 -1
  78. package/src/helpers/tests/titleCase.test.ts +76 -0
  79. package/src/helpers/titleCase.ts +13 -13
  80. package/src/helpers/toggleLegendActive.ts +76 -8
  81. package/src/helpers/urlDataHelpers.ts +1 -1
  82. package/src/hooks/useCountryZoom.tsx +241 -0
  83. package/src/hooks/useGeoClickHandler.ts +1 -1
  84. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  85. package/src/hooks/useResizeObserver.ts +8 -2
  86. package/src/hooks/useStateZoom.tsx +7 -4
  87. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  88. package/src/index.jsx +1 -0
  89. package/src/scss/editor-panel.scss +4 -440
  90. package/src/scss/main.scss +1 -1
  91. package/src/scss/map.scss +12 -15
  92. package/src/store/map.actions.ts +7 -7
  93. package/src/test/CdcMap.test.jsx +1 -1
  94. package/src/types/MapConfig.ts +32 -11
  95. package/src/types/MapContext.ts +6 -0
  96. package/src/types/runtimeLegend.ts +2 -1
  97. package/LICENSE +0 -201
  98. package/src/components/DataTable.tsx +0 -413
  99. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  100. package/src/components/MapControls.tsx +0 -44
  101. package/src/helpers/getUniqueValues.ts +0 -19
  102. package/src/helpers/hashObj.ts +0 -25
  103. package/src/hooks/useActiveElement.ts +0 -19
  104. package/src/hooks/useLegendSeparators.ts +0 -26
  105. package/src/scss/mixins.scss +0 -47
  106. package/src/types/Annotations.ts +0 -24
  107. /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'>Shape Column</span>
140
- <select
174
+ <span className='edit-label column-heading'>Value</span>
175
+ <input
176
+ type='text'
141
177
  value={
142
- config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].shape
143
- ? config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].shape
144
- : 'select'
178
+ config.hexMap.shapeGroups[shapeGroupIndex].items[itemIndex].value || ''
179
+ }
180
+ onChange={e =>
181
+ handleItemUpdate('value', e.target.value, shapeGroupIndex, itemIndex)
145
182
  }
146
- onChange={e => {
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
- <label htmlFor={`pattern-dataKey--${patternIndex}`}>Data Key:</label>
200
- <select
201
- id={`pattern-dataKey--${patternIndex}`}
202
- value={pattern.dataKey !== '' ? pattern.dataKey : 'Select'}
203
- onChange={e => handlePatternFieldUpdate('dataKey', e.target.value, patternIndex)}
204
- >
205
- {/* TODO: sort these? */}
206
- {dataKeyOptions.map((d, index) => {
207
- return (
208
- <option value={d} key={index}>
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
- <label htmlFor={`pattern-type--${patternIndex}`}>Pattern Type:</label>
233
- <select
234
- id={`pattern-type--${patternIndex}`}
228
+ <Select
229
+ label='Pattern Type:'
235
230
  value={pattern?.pattern}
236
- onChange={e => handlePatternFieldUpdate('pattern', e.target.value, patternIndex)}
237
- >
238
- {patternTypes.map((patternName, index) => (
239
- <option value={patternName} key={index}>
240
- {patternName}
241
- </option>
242
- ))}
243
- </select>
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
- onChange={e => handlePatternFieldUpdate('size', e.target.value, patternIndex)}
249
- >
250
- {['small', 'medium', 'large'].map((size, index) => (
251
- <option value={size} key={index}>
252
- {size}
253
- </option>
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