@cdc/chart 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 (85) hide show
  1. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  2. package/dist/cdcchart.js +36258 -34658
  3. package/examples/feature/__data__/planet-example-data.json +1 -1
  4. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  5. package/examples/private/DEV-11825.json +573 -0
  6. package/examples/private/na.json +913 -0
  7. package/examples/private/test-data.csv +28 -0
  8. package/index.html +2 -121
  9. package/package.json +4 -4
  10. package/src/CdcChart.tsx +8 -11
  11. package/src/CdcChartComponent.tsx +256 -87
  12. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  13. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  14. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  15. package/src/_stories/Chart.Patterns.stories.tsx +2 -1
  16. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  17. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  18. package/src/_stories/ChartAnnotation.stories.tsx +6 -3
  19. package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
  20. package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
  21. package/src/_stories/ChartEditor.stories.tsx +1 -2
  22. package/src/_stories/_mock/combo.json +451 -0
  23. package/src/_stories/_mock/editor-test-configs.json +376 -0
  24. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  25. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  26. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  27. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  28. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  29. package/src/_stories/_mock/pie_config.json +257 -62
  30. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  31. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  32. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  33. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  35. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  36. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
  37. package/src/components/AreaChart/index.tsx +1 -2
  38. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -4
  39. package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
  40. package/src/components/BoxPlot/helpers/index.ts +3 -3
  41. package/src/components/Brush/BrushChart.tsx +1 -1
  42. package/src/components/EditorPanel/EditorPanel.tsx +199 -190
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +102 -55
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
  47. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
  48. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
  49. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  50. package/src/components/EditorPanel/editor-panel.scss +0 -20
  51. package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
  52. package/src/components/Forecasting/Forecasting.tsx +139 -21
  53. package/src/components/Legend/Legend.Component.tsx +16 -9
  54. package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
  55. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  56. package/src/components/LineChart/LineChartProps.ts +0 -3
  57. package/src/components/LineChart/helpers.ts +1 -1
  58. package/src/components/LineChart/index.tsx +36 -13
  59. package/src/components/LinearChart.tsx +75 -80
  60. package/src/components/Regions/components/Regions.tsx +3 -24
  61. package/src/components/Sankey/types/index.ts +1 -1
  62. package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
  63. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  64. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  65. package/src/components/SmallMultiples/index.ts +2 -0
  66. package/src/data/initial-state.js +13 -1
  67. package/src/helpers/buildForecastPaletteOptions.ts +0 -38
  68. package/src/helpers/getColorScale.ts +10 -0
  69. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
  70. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  71. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  72. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  73. package/src/hooks/useScales.ts +88 -34
  74. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  75. package/src/hooks/useTooltip.tsx +60 -15
  76. package/src/scss/main.scss +1 -80
  77. package/src/store/chart.actions.ts +2 -0
  78. package/src/store/chart.reducer.ts +4 -0
  79. package/src/types/ChartConfig.ts +24 -6
  80. package/src/types/ChartContext.ts +3 -0
  81. package/src/_stories/_mock/pie_data.json +0 -218
  82. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  83. package/src/helpers/sort.ts +0 -7
  84. package/src/hooks/useActiveElement.js +0 -19
  85. package/src/hooks/useChartClasses.js +0 -41
@@ -2,7 +2,6 @@ import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../../../ConfigContext'
3
3
 
4
4
  // Core
5
- import InputSelect from '@cdc/core/components/inputs/InputSelect'
6
5
  import Check from '@cdc/core/assets/icon-check.svg'
7
6
  import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
8
7
  import { colorPalettesChartV1, colorPalettesChartV2, sequentialPalettes } from '@cdc/core/data/colorPalettes'
@@ -98,12 +97,12 @@ const SeriesDropdownLineType = props => {
98
97
  })
99
98
 
100
99
  return (
101
- <InputSelect
100
+ <Select
102
101
  initial='Select an option'
103
102
  value={series.lineType ? series.lineType : 'curveLinear'}
104
103
  label='Series Line Type'
105
- onChange={event => {
106
- changeLineType(index, event.target.value)
104
+ updateField={(_section, _subsection, _fieldName, value) => {
105
+ changeLineType(index, value)
107
106
  }}
108
107
  options={options}
109
108
  />
@@ -118,35 +117,35 @@ const SeriesDropdownSeriesType = props => {
118
117
 
119
118
  const getOptions = () => {
120
119
  if (config.visualizationType === 'Combo') {
121
- return {
122
- Bar: 'Bar',
123
- Line: 'Line',
124
- 'dashed-sm': 'Small Dashed',
125
- 'dashed-md': 'Medium Dashed',
126
- 'dashed-lg': 'Large Dashed',
127
- 'Area Chart': 'Area Chart',
128
- Forecasting: 'Forecasting'
129
- }
120
+ return [
121
+ { value: 'Bar', label: 'Bar' },
122
+ { value: 'Line', label: 'Line' },
123
+ { value: 'dashed-sm', label: 'Small Dashed' },
124
+ { value: 'dashed-md', label: 'Medium Dashed' },
125
+ { value: 'dashed-lg', label: 'Large Dashed' },
126
+ { value: 'Area Chart', label: 'Area Chart' },
127
+ { value: 'Forecasting', label: 'Forecasting' }
128
+ ]
130
129
  }
131
130
  if (config.visualizationType === 'Line' || config.visualizationType === 'Bump Chart') {
132
- return {
133
- Line: 'Line',
134
- 'dashed-sm': 'Small Dashed',
135
- 'dashed-md': 'Medium Dashed',
136
- 'dashed-lg': 'Large Dashed'
137
- }
131
+ return [
132
+ { value: 'Line', label: 'Line' },
133
+ { value: 'dashed-sm', label: 'Small Dashed' },
134
+ { value: 'dashed-md', label: 'Medium Dashed' },
135
+ { value: 'dashed-lg', label: 'Large Dashed' }
136
+ ]
138
137
  }
139
138
  }
140
139
 
141
140
  // Allowable changes
142
141
  if (!['Line', 'Combo', 'Bump Chart'].includes(config.visualizationType)) return
143
142
  return (
144
- <InputSelect
143
+ <Select
145
144
  initial='Select an option'
146
145
  value={series.type}
147
146
  label='Series Type'
148
- onChange={event => {
149
- updateSeries(index, event.target.value, 'type')
147
+ updateField={(_section, _subsection, _fieldName, value) => {
148
+ updateSeries(index, value, 'type')
150
149
  }}
151
150
  options={getOptions()}
152
151
  />
@@ -162,13 +161,13 @@ const SeriesDropdownForecastingStage = props => {
162
161
  // Only combo charts are allowed to have different options
163
162
 
164
163
  return (
165
- <InputSelect
164
+ <Select
166
165
  initial='Select an option'
167
166
  value={series.stageColumn}
168
167
  label='Add Forecasting Stages'
169
- onChange={e => {
168
+ updateField={(_section, _subsection, _fieldName, value) => {
170
169
  let stageObjects = []
171
- let tempGroups = new Set(rawData?.map(item => item[e.target.value])) // [estimate, forecast, etc.]
170
+ let tempGroups = new Set(rawData?.map(item => item[value])) // [estimate, forecast, etc.]
172
171
  tempGroups = Array.from(tempGroups) // convert set to array
173
172
 
174
173
  tempGroups = tempGroups.filter(group => group !== undefined) // removes undefined
@@ -176,7 +175,7 @@ const SeriesDropdownForecastingStage = props => {
176
175
  tempGroups.forEach(group => stageObjects.push({ key: group }))
177
176
 
178
177
  const copyOfSeries = [...config.series] // copy the entire series array
179
- copyOfSeries[index] = { ...copyOfSeries[index], stages: stageObjects, stageColumn: e.target.value }
178
+ copyOfSeries[index] = { ...copyOfSeries[index], stages: stageObjects, stageColumn: value }
180
179
 
181
180
  updateConfig({
182
181
  ...config,
@@ -200,19 +199,19 @@ const SeriesDropdownForecastingColumn = props => {
200
199
  if (!series.stageColumn) return
201
200
 
202
201
  let tempGroups = new Set(rawData.map(item => item[series.stageColumn])) // [estimate, forecast, etc.]
203
- tempGroups = Array.from(tempGroups) // convert set to array
202
+ let tempGroupsArray = Array.from(tempGroups) // convert set to array
204
203
 
205
- tempGroups = tempGroups.filter(group => group !== undefined) // removes undefined
204
+ tempGroupsArray = tempGroupsArray.filter(group => group !== undefined) // removes undefined
206
205
 
207
206
  return (
208
- <InputSelect
207
+ <Select
209
208
  initial='Select an option'
210
209
  value={series.stageItem}
211
210
  label='Forecasting Item Column'
212
- onChange={event => {
213
- updateSeries(index, event.target.value, 'stageItem')
211
+ updateField={(_section, _subsection, _fieldName, value) => {
212
+ updateSeries(index, value, 'stageItem')
214
213
  }}
215
- options={tempGroups}
214
+ options={tempGroupsArray}
216
215
  />
217
216
  )
218
217
  }
@@ -229,17 +228,17 @@ const SeriesDropdownAxisPosition = props => {
229
228
  return
230
229
  }
231
230
  return (
232
- <InputSelect
231
+ <Select
233
232
  initial='Select an option'
234
233
  value={series.axis ? series.axis : 'Left'}
235
234
  label='Series Axis'
236
- onChange={event => {
237
- updateSeries(index, event.target.value, 'axis')
238
- }}
239
- options={{
240
- ['Left']: 'Left',
241
- ['Right']: 'Right'
235
+ updateField={(_section, _subsection, _fieldName, value) => {
236
+ updateSeries(index, value, 'axis')
242
237
  }}
238
+ options={[
239
+ { value: 'Left', label: 'Left' },
240
+ { value: 'Right', label: 'Right' }
241
+ ]}
243
242
  />
244
243
  )
245
244
  }
@@ -267,17 +266,23 @@ const SeriesDropdownForecastColor = props => {
267
266
 
268
267
  // For dropdown options, only show version-specific palettes
269
268
  const processedPalettes = updatePaletteNames(forecastPalettes)
270
- const paletteOptions = buildForecastPaletteOptions(processedPalettes, paletteVersion)
269
+ const paletteOptionsObject = buildForecastPaletteOptions(processedPalettes, paletteVersion)
270
+
271
+ // Convert object to array format for Select component
272
+ const paletteOptions = Object.entries(paletteOptionsObject).map(([value, label]) => ({
273
+ value,
274
+ label
275
+ }))
271
276
 
272
277
  return series?.stages?.map((stage, stageIndex) => (
273
- <InputSelect
278
+ <Select
274
279
  key={`${stage}--${stageIndex}`}
275
280
  initial='Select an option'
276
281
  value={config.series?.[index].stages?.[stageIndex].color || 'Select'}
277
282
  label={`${stage.key} Series Color`}
278
- onChange={event => {
283
+ updateField={(_section, _subsection, _fieldName, value) => {
279
284
  if (handleForecastPaletteSelection) {
280
- handleForecastPaletteSelection(event.target.value, index, stageIndex)
285
+ handleForecastPaletteSelection(value, index, stageIndex)
281
286
  }
282
287
  }}
283
288
  options={paletteOptions}
@@ -325,7 +330,7 @@ const SeriesDropdownConfidenceInterval = props => {
325
330
  </>
326
331
  </AccordionItemHeading>
327
332
  <AccordionItemPanel>
328
- <InputSelect
333
+ <Select
329
334
  initial='Select an option'
330
335
  value={
331
336
  config.series[index].confidenceIntervals[ciIndex].low
@@ -333,9 +338,9 @@ const SeriesDropdownConfidenceInterval = props => {
333
338
  : 'Select'
334
339
  }
335
340
  label='Low Confidence Interval'
336
- onChange={e => {
341
+ updateField={(_section, _subsection, _fieldName, value) => {
337
342
  const copiedConfidenceArray = [...config.series[index].confidenceIntervals]
338
- copiedConfidenceArray[ciIndex].low = e.target.value
343
+ copiedConfidenceArray[ciIndex].low = value
339
344
  const copyOfSeries = [...config.series] // copy the entire series array
340
345
  copyOfSeries[index] = { ...copyOfSeries[index], confidenceIntervals: copiedConfidenceArray }
341
346
  updateConfig({
@@ -345,7 +350,7 @@ const SeriesDropdownConfidenceInterval = props => {
345
350
  }}
346
351
  options={getColumns()}
347
352
  />
348
- <InputSelect
353
+ <Select
349
354
  initial='Select an option'
350
355
  value={
351
356
  config.series[index].confidenceIntervals[ciIndex].high
@@ -353,9 +358,9 @@ const SeriesDropdownConfidenceInterval = props => {
353
358
  : 'Select'
354
359
  }
355
360
  label='High Confidence Interval'
356
- onChange={e => {
361
+ updateField={(_section, _subsection, _fieldName, value) => {
357
362
  const copiedConfidenceArray = [...config.series[index].confidenceIntervals]
358
- copiedConfidenceArray[ciIndex].high = e.target.value
363
+ copiedConfidenceArray[ciIndex].high = value
359
364
  const copyOfSeries = [...config.series] // copy the entire series array
360
365
  copyOfSeries[index] = { ...copyOfSeries[index], confidenceIntervals: copiedConfidenceArray }
361
366
  updateConfig({
@@ -0,0 +1,422 @@
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 { ChartContext } from './../../../../types/ChartContext.js'
17
+ import { useEditorPermissions } from '../../useEditorPermissions.js'
18
+ import { useEditorPanelContext } from '../../EditorPanelContext.js'
19
+ import ConfigContext from '../../../../ConfigContext.js'
20
+ import { PanelProps } from '../PanelProps'
21
+ import { getTileKeys } from '../../../../helpers/smallMultiplesHelpers'
22
+
23
+ const PanelSmallMultiples: FC<PanelProps> = props => {
24
+ const { config, rawData, updateConfig } = useContext<ChartContext>(ConfigContext)
25
+ const { updateField } = useEditorPanelContext()
26
+ const { visSupportsSmallMultiples } = useEditorPermissions()
27
+
28
+ const getColumns = (filter = true) => {
29
+ let columns = {}
30
+ rawData?.forEach(row => {
31
+ Object.keys(row).forEach(columnName => (columns[columnName] = true))
32
+ })
33
+
34
+ if (filter) {
35
+ const { lower, upper } = config.confidenceKeys || {}
36
+ Object.keys(columns).forEach(key => {
37
+ if (
38
+ (config.series && config.series.filter(series => series.dataKey === key).length > 0) ||
39
+ (config.confidenceKeys &&
40
+ Object.keys(config.confidenceKeys).includes(key) &&
41
+ ((lower && upper) || lower || upper) &&
42
+ key !== lower &&
43
+ key !== upper)
44
+ ) {
45
+ delete columns[key]
46
+ }
47
+ })
48
+ }
49
+
50
+ return Object.keys(columns)
51
+ }
52
+
53
+ return (
54
+ <>
55
+ {visSupportsSmallMultiples() && (
56
+ <AccordionItem>
57
+ <AccordionItemHeading>
58
+ <AccordionItemButton>Small Multiples</AccordionItemButton>
59
+ </AccordionItemHeading>
60
+ <AccordionItemPanel>
61
+ <Select
62
+ value={config.smallMultiples?.mode || ''}
63
+ fieldName='mode'
64
+ section='smallMultiples'
65
+ label='Tile Mode'
66
+ initial='Select Mode'
67
+ updateField={updateField}
68
+ options={[
69
+ { label: 'By data series', value: 'by-series' },
70
+ { label: 'By column values', value: 'by-column' }
71
+ ]}
72
+ tooltip={
73
+ <Tooltip style={{ textTransform: 'none' }}>
74
+ <Tooltip.Target>
75
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
76
+ </Tooltip.Target>
77
+ <Tooltip.Content>
78
+ <p>
79
+ Choose how to create multiple charts. "By Data Series" creates a tile for each configured data
80
+ series. "By Column Values" creates a tile for each unique value in the selected column.
81
+ </p>
82
+ </Tooltip.Content>
83
+ </Tooltip>
84
+ }
85
+ />
86
+ {config.smallMultiples?.mode === 'by-column' && (
87
+ <Select
88
+ value={config.smallMultiples?.tileColumn || ''}
89
+ fieldName='tileColumn'
90
+ section='smallMultiples'
91
+ label='Tile By Column'
92
+ initial='Select Column'
93
+ updateField={updateField}
94
+ options={getColumns()}
95
+ />
96
+ )}
97
+
98
+ {config.smallMultiples?.mode && (
99
+ <>
100
+ <TextField
101
+ type='number'
102
+ value={config.smallMultiples?.tilesPerRowDesktop}
103
+ section='smallMultiples'
104
+ fieldName='tilesPerRowDesktop'
105
+ label='Tiles Per Row (Desktop)'
106
+ updateField={updateField}
107
+ min={1}
108
+ max={3}
109
+ tooltip={
110
+ <Tooltip style={{ textTransform: 'none' }}>
111
+ <Tooltip.Target>
112
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
113
+ </Tooltip.Target>
114
+ <Tooltip.Content>
115
+ <p>
116
+ Number of chart tiles to display per row on desktop screens. Mobile will always show 1 tile
117
+ per row.
118
+ </p>
119
+ </Tooltip.Content>
120
+ </Tooltip>
121
+ }
122
+ />
123
+
124
+ {/* Tile Ordering */}
125
+ {(() => {
126
+ const availableTiles = getTileKeys(config, rawData)
127
+ if (availableTiles.length === 0) return null
128
+
129
+ const tileOrderOptions = [
130
+ {
131
+ label: 'Ascending By Title',
132
+ value: 'asc'
133
+ },
134
+ {
135
+ label: 'Descending By Title',
136
+ value: 'desc'
137
+ },
138
+ {
139
+ label: 'Custom',
140
+ value: 'custom'
141
+ }
142
+ ]
143
+
144
+ const currentOrderType = config.smallMultiples?.tileOrderType || 'asc'
145
+
146
+ const handleOrderTypeChange = orderType => {
147
+ const newConfig = {
148
+ ...config,
149
+ smallMultiples: {
150
+ ...config.smallMultiples,
151
+ tileOrderType: orderType
152
+ }
153
+ }
154
+
155
+ // If switching to custom, initialize with current tile order
156
+ if (orderType === 'custom' && !config.smallMultiples?.tileOrder?.length) {
157
+ newConfig.smallMultiples.tileOrder = [...availableTiles]
158
+ }
159
+
160
+ updateConfig(newConfig)
161
+ }
162
+
163
+ const handleCustomTileOrderChange = (sourceIndex, destinationIndex) => {
164
+ if (destinationIndex === null) return
165
+
166
+ const currentOrder = config.smallMultiples?.tileOrder || [...availableTiles]
167
+ const newOrder = [...currentOrder]
168
+ const [removed] = newOrder.splice(sourceIndex, 1)
169
+ newOrder.splice(destinationIndex, 0, removed)
170
+
171
+ updateConfig({
172
+ ...config,
173
+ smallMultiples: {
174
+ ...config.smallMultiples,
175
+ tileOrder: newOrder,
176
+ tileOrderType: 'custom'
177
+ }
178
+ })
179
+ }
180
+
181
+ return (
182
+ <>
183
+ <Select
184
+ value={currentOrderType}
185
+ options={tileOrderOptions}
186
+ label='Tile Order'
187
+ updateField={(_section, _subsection, _fieldName, value) => {
188
+ handleOrderTypeChange(value)
189
+ }}
190
+ />
191
+
192
+ {currentOrderType === 'custom' && (
193
+ <DragDropContext
194
+ onDragEnd={({ source, destination }) =>
195
+ handleCustomTileOrderChange(source.index, destination?.index)
196
+ }
197
+ >
198
+ <Droppable droppableId='tile_order'>
199
+ {provided => (
200
+ <ul
201
+ {...provided.droppableProps}
202
+ className='sort-list'
203
+ ref={provided.innerRef}
204
+ style={{ marginTop: '1em' }}
205
+ >
206
+ {(config.smallMultiples?.tileOrder || availableTiles).map((tileKey, index) => (
207
+ <Draggable key={tileKey} draggableId={`tile-${tileKey}`} index={index}>
208
+ {(provided, snapshot) => (
209
+ <li>
210
+ <div
211
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
212
+ style={provided.draggableProps.style}
213
+ ref={provided.innerRef}
214
+ {...provided.draggableProps}
215
+ {...provided.dragHandleProps}
216
+ >
217
+ {tileKey}
218
+ </div>
219
+ </li>
220
+ )}
221
+ </Draggable>
222
+ ))}
223
+ {provided.placeholder}
224
+ </ul>
225
+ )}
226
+ </Droppable>
227
+ </DragDropContext>
228
+ )}
229
+ </>
230
+ )
231
+ })()}
232
+
233
+ {/* Color Mode */}
234
+ <Select
235
+ value={config.smallMultiples?.colorMode || 'different'}
236
+ options={[
237
+ {
238
+ label: 'Same Color',
239
+ value: 'same'
240
+ },
241
+ {
242
+ label: 'Different Colors',
243
+ value: 'different'
244
+ }
245
+ ]}
246
+ label='Color Mode'
247
+ updateField={(_section, _subsection, _fieldName, value) => {
248
+ updateConfig({
249
+ ...config,
250
+ smallMultiples: {
251
+ ...config.smallMultiples,
252
+ colorMode: value
253
+ }
254
+ })
255
+ }}
256
+ tooltip={
257
+ <Tooltip style={{ textTransform: 'none' }}>
258
+ <Tooltip.Target>
259
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
260
+ </Tooltip.Target>
261
+ <Tooltip.Content>
262
+ <p>
263
+ When "Different Colors" is selected, each tile will use the next color in the configured color
264
+ palette.
265
+ </p>
266
+ </Tooltip.Content>
267
+ </Tooltip>
268
+ }
269
+ />
270
+
271
+ {/* Custom Tile Titles - only show for by-column mode */}
272
+ {config.smallMultiples?.mode === 'by-column' && (
273
+ <div>
274
+ <label style={{ marginTop: '1.5rem', marginBottom: '0.5rem' }}>Custom Tile Titles</label>
275
+
276
+ {(() => {
277
+ const availableTiles = getTileKeys(config, rawData)
278
+ if (availableTiles.length === 0) return null
279
+
280
+ const handleTitleChange = (tileKey, customTitle) => {
281
+ const newTitles = { ...config.smallMultiples?.tileTitles }
282
+ if (customTitle.trim() === '' || customTitle === tileKey) {
283
+ delete newTitles[tileKey] // Remove entry if empty or same as key
284
+ } else {
285
+ newTitles[tileKey] = customTitle
286
+ }
287
+
288
+ updateConfig({
289
+ ...config,
290
+ smallMultiples: {
291
+ ...config.smallMultiples,
292
+ tileTitles: newTitles
293
+ }
294
+ })
295
+ }
296
+
297
+ return (
298
+ <div className='tile-titles-editor' style={{ maxWidth: '100%', overflow: 'hidden' }}>
299
+ {availableTiles.map(tileKey => {
300
+ const customTitle = config.smallMultiples?.tileTitles?.[tileKey] || ''
301
+ return (
302
+ <div
303
+ key={tileKey}
304
+ className='tile-title-row'
305
+ style={{
306
+ display: 'flex',
307
+ alignItems: 'center',
308
+ marginBottom: '0.75rem',
309
+ maxWidth: '100%'
310
+ }}
311
+ >
312
+ <label
313
+ style={{
314
+ minWidth: '80px',
315
+ maxWidth: '120px',
316
+ marginRight: '0.75rem',
317
+ fontWeight: 'normal',
318
+ fontSize: '13px',
319
+ overflow: 'hidden',
320
+ textOverflow: 'ellipsis',
321
+ whiteSpace: 'nowrap',
322
+ flexShrink: 0
323
+ }}
324
+ >
325
+ {tileKey}:
326
+ </label>
327
+ <input
328
+ type='text'
329
+ value={customTitle}
330
+ placeholder={tileKey}
331
+ onChange={event => handleTitleChange(tileKey, event.target.value)}
332
+ style={{
333
+ flex: 1,
334
+ minWidth: 0,
335
+ maxWidth: '200px',
336
+ fontSize: '13px',
337
+ padding: '4px 8px',
338
+ height: '30px',
339
+ border: '1px solid #ccc',
340
+ borderRadius: '3px'
341
+ }}
342
+ />
343
+ </div>
344
+ )
345
+ })}
346
+ </div>
347
+ )
348
+ })()}
349
+ </div>
350
+ )}
351
+
352
+ <CheckBox
353
+ value={config.smallMultiples?.independentYAxis}
354
+ section='smallMultiples'
355
+ fieldName='independentYAxis'
356
+ label='Independent Y-Axis Scales'
357
+ updateField={updateField}
358
+ tooltip={
359
+ <Tooltip style={{ textTransform: 'none' }}>
360
+ <Tooltip.Target>
361
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
362
+ </Tooltip.Target>
363
+ <Tooltip.Content>
364
+ <p>
365
+ When checked, the y-axis scale for each tile will be calculated separately. The chart's y-axis
366
+ min/max will override this setting if they are configured.
367
+ </p>
368
+ </Tooltip.Content>
369
+ </Tooltip>
370
+ }
371
+ />
372
+
373
+ <CheckBox
374
+ value={config.smallMultiples?.synchronizedTooltips}
375
+ fieldName='synchronizedTooltips'
376
+ section='smallMultiples'
377
+ label='Synchronized Tooltips'
378
+ updateField={updateField}
379
+ tooltip={
380
+ <Tooltip style={{ textTransform: 'none' }}>
381
+ <Tooltip.Target>
382
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
383
+ </Tooltip.Target>
384
+ <Tooltip.Content>
385
+ <p>
386
+ When checked, hovering over one chart will show synchronized tooltips on all other charts at
387
+ the same data point.
388
+ </p>
389
+ </Tooltip.Content>
390
+ </Tooltip>
391
+ }
392
+ />
393
+
394
+ {config.visualizationType === 'Line' && (
395
+ <CheckBox
396
+ value={config.smallMultiples?.showAreaUnderLine}
397
+ fieldName='showAreaUnderLine'
398
+ section='smallMultiples'
399
+ label='Shade Area Under Lines'
400
+ updateField={updateField}
401
+ tooltip={
402
+ <Tooltip style={{ textTransform: 'none' }}>
403
+ <Tooltip.Target>
404
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
405
+ </Tooltip.Target>
406
+ <Tooltip.Content>
407
+ <p>When checked, each tile chart will display a shaded area underneath the line.</p>
408
+ </Tooltip.Content>
409
+ </Tooltip>
410
+ }
411
+ />
412
+ )}
413
+ </>
414
+ )}
415
+ </AccordionItemPanel>
416
+ </AccordionItem>
417
+ )}
418
+ </>
419
+ )
420
+ }
421
+
422
+ export default PanelSmallMultiples