@cdc/map 4.25.10 → 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.
Files changed (88) 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 +27405 -25783
  7. package/examples/example-city-state.json +9 -1
  8. package/examples/multi-country-centering.json +45 -0
  9. package/examples/private/colors-2.json +221 -0
  10. package/examples/private/colors.json +221 -0
  11. package/index.html +2 -1
  12. package/package.json +4 -4
  13. package/src/CdcMapComponent.tsx +44 -20
  14. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  15. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  16. package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
  17. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  18. package/src/_stories/CdcMap.stories.tsx +22 -4
  19. package/src/_stories/_mock/column-wrap-test.json +265 -0
  20. package/src/_stories/_mock/multi-country-hide.json +78 -0
  21. package/src/_stories/_mock/multi-country.json +95 -0
  22. package/src/_stories/_mock/multi-state.json +887 -20403
  23. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  24. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  25. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  26. package/src/_stories/_mock/usa-state-gradient.json +2 -4
  27. package/src/components/BubbleList.tsx +1 -1
  28. package/src/components/EditorPanel/components/EditorPanel.tsx +630 -564
  29. package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
  30. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
  31. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
  32. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  33. package/src/components/Geo.tsx +20 -3
  34. package/src/components/Legend/components/Legend.tsx +34 -34
  35. package/src/components/Legend/components/index.scss +1 -1
  36. package/src/components/NavigationMenu.tsx +16 -13
  37. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  38. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  39. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  40. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  41. package/src/components/SmallMultiples/index.tsx +3 -0
  42. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +18 -3
  43. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  44. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  45. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
  46. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
  47. package/src/components/UsaMap/components/UsaMap.County.tsx +14 -2
  48. package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
  49. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +25 -5
  50. package/src/components/UsaMap/components/UsaMap.State.tsx +26 -3
  51. package/src/components/UsaMap/helpers/map.ts +2 -2
  52. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  53. package/src/components/WorldMap/WorldMap.tsx +81 -11
  54. package/src/data/initial-state.js +10 -0
  55. package/src/data/supported-geos.js +8 -76
  56. package/src/helpers/addUIDs.ts +13 -2
  57. package/src/helpers/applyColorToLegend.ts +25 -1
  58. package/src/helpers/constants.ts +1 -15
  59. package/src/helpers/displayGeoName.ts +19 -4
  60. package/src/helpers/generateRuntimeLegend.ts +0 -2
  61. package/src/helpers/getCountriesPicked.ts +103 -0
  62. package/src/helpers/getMapContainerClasses.ts +7 -0
  63. package/src/helpers/getPatternForRow.ts +2 -5
  64. package/src/helpers/index.ts +1 -9
  65. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  66. package/src/helpers/tests/titleCase.test.ts +76 -0
  67. package/src/helpers/titleCase.ts +13 -13
  68. package/src/helpers/urlDataHelpers.ts +1 -1
  69. package/src/hooks/useCountryZoom.tsx +241 -0
  70. package/src/hooks/useGeoClickHandler.ts +1 -1
  71. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  72. package/src/hooks/useResizeObserver.ts +5 -2
  73. package/src/hooks/useStateZoom.tsx +5 -2
  74. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  75. package/src/index.jsx +1 -0
  76. package/src/scss/editor-panel.scss +4 -440
  77. package/src/scss/main.scss +1 -1
  78. package/src/scss/map.scss +12 -15
  79. package/src/store/map.actions.ts +7 -7
  80. package/src/types/MapConfig.ts +30 -11
  81. package/src/types/MapContext.ts +6 -0
  82. package/src/types/runtimeLegend.ts +1 -1
  83. package/src/components/DataTable.tsx +0 -413
  84. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  85. package/src/hooks/useActiveElement.ts +0 -19
  86. package/src/scss/mixins.scss +0 -47
  87. package/src/types/Annotations.ts +0 -24
  88. /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={{
@@ -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,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
@@ -8,14 +8,31 @@ type GeoProps = {
8
8
  className?: string
9
9
  onMouseEnter?: () => void
10
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
11
19
  }
12
20
 
13
21
  const Geo: React.FC<GeoProps> = ({ path, styles, stroke, strokeWidth, ...props }) => {
14
- const { className, ...restProps } = props
15
- const geoClassName = String(props.additionalData?.name)?.toLowerCase()?.replaceAll(' ', '') || 'country'
22
+ const { className, 'data-country-code': dataCountryCode, ...restProps } = props
23
+ const geoClassName = String(props.additionalData?.name)?.toLowerCase()?.replace(/\s+/g, '') || 'country'
16
24
  return (
17
25
  <g className={`geo-group ${geoClassName}`} style={styles} {...restProps}>
18
- <path tabIndex={-1} className={`single-geo ${geoClassName}`} stroke={stroke} strokeWidth={strokeWidth} d={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
+ />
19
36
  </g>
20
37
  )
21
38
  }