@casualoffice/sheets 0.17.0 → 0.19.0
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/dist/chrome.cjs +3339 -244
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.js +3298 -203
- package/dist/chrome.js.map +1 -1
- package/dist/embed/embed-runtime.js +203 -157
- package/dist/index.cjs +5971 -1572
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5961 -1548
- package/dist/index.js.map +1 -1
- package/dist/sheets.cjs +4937 -538
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.js +4945 -532
- package/dist/sheets.js.map +1 -1
- package/package.json +8 -7
- package/src/charts/ChartContextMenu.tsx +264 -0
- package/src/charts/ChartLayer.tsx +333 -0
- package/src/charts/ChartOverlay.tsx +293 -0
- package/src/charts/ChartsPanel.tsx +211 -0
- package/src/charts/FormatChartDialog.tsx +419 -0
- package/src/charts/InsertChartDialog.tsx +478 -0
- package/src/charts/build-option.ts +476 -0
- package/src/charts/charts-context.tsx +205 -0
- package/src/charts/echarts-init.ts +58 -0
- package/src/charts/hit-test.ts +130 -0
- package/src/charts/insert-chart.ts +106 -0
- package/src/charts/naming.ts +38 -0
- package/src/charts/render-to-png.ts +117 -0
- package/src/charts/resources.ts +108 -0
- package/src/charts/types.ts +239 -0
- package/src/charts/univer-dom.ts +102 -0
- package/src/chrome/CommentsPanel.tsx +427 -0
- package/src/chrome/ConditionalFormattingDialog.tsx +534 -0
- package/src/chrome/CustomSortDialog.tsx +357 -0
- package/src/chrome/DataValidationDialog.tsx +536 -0
- package/src/chrome/DeleteCellsDialog.tsx +183 -0
- package/src/chrome/GoalSeekDialog.tsx +370 -0
- package/src/chrome/HistoryPanel.tsx +319 -0
- package/src/chrome/InsertCellsDialog.tsx +185 -0
- package/src/chrome/InsertChartDialog.tsx +490 -0
- package/src/chrome/InsertFunctionDialog.tsx +493 -0
- package/src/chrome/InsertPivotDialog.tsx +488 -0
- package/src/chrome/InsertSparklineDialog.tsx +344 -0
- package/src/chrome/NameManagerDialog.tsx +378 -0
- package/src/chrome/PanelHost.tsx +55 -0
- package/src/chrome/PanelRail.tsx +90 -0
- package/src/chrome/PasteSpecialDialog.tsx +286 -0
- package/src/chrome/PivotFieldsPanel.tsx +1052 -0
- package/src/chrome/TablesPanel.tsx +301 -0
- package/src/chrome/dialog-context.tsx +24 -0
- package/src/chrome/panel-context.tsx +55 -0
- package/src/chrome/panel-registry.ts +48 -0
- package/src/sheets/CasualSheets.tsx +60 -34
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
import { Dialog } from '../chrome/Dialog';
|
|
19
|
+
import {
|
|
20
|
+
CHART_FAMILY_OF,
|
|
21
|
+
LEGEND_POSITIONS,
|
|
22
|
+
PALETTES,
|
|
23
|
+
PALETTE_LABELS,
|
|
24
|
+
mergeFormat,
|
|
25
|
+
type ChartFormat,
|
|
26
|
+
type ChartModel,
|
|
27
|
+
type ChartPalette,
|
|
28
|
+
} from './types';
|
|
29
|
+
|
|
30
|
+
type Props = {
|
|
31
|
+
model: ChartModel;
|
|
32
|
+
/** Optional list of series names — when present, the dialog renders
|
|
33
|
+
* a per-series colour-override picker below the palette section.
|
|
34
|
+
* Pulled by the caller from the chart's source range. */
|
|
35
|
+
seriesNames?: string[];
|
|
36
|
+
onCancel: () => void;
|
|
37
|
+
onConfirm: (next: ChartFormat) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Excel's "Format Chart Area" pane, compressed into a single dialog
|
|
42
|
+
* tailored to what we currently render. Covers:
|
|
43
|
+
*
|
|
44
|
+
* - Chart title (text + show/hide).
|
|
45
|
+
* - Legend position (Bottom / Top / Left / Right / None).
|
|
46
|
+
* - X-axis title text + Y-axis title text (axis families only).
|
|
47
|
+
* - Major gridlines on/off (axis families only).
|
|
48
|
+
* - Data labels on/off.
|
|
49
|
+
* - Colour palette (Office / Mono / Vivid / Pastel).
|
|
50
|
+
*
|
|
51
|
+
* The dialog confirms with a merged `ChartFormat`; ChartContextMenu
|
|
52
|
+
* applies it to `model.format` via `update()`.
|
|
53
|
+
*
|
|
54
|
+
* The chart title text is `model.title` (auto-named "Chart N" on
|
|
55
|
+
* insert). Editing it here also returns it so the caller can persist
|
|
56
|
+
* the rename together with the format change.
|
|
57
|
+
*/
|
|
58
|
+
export function FormatChartDialog({ model, seriesNames, onCancel, onConfirm }: Props) {
|
|
59
|
+
const merged = mergeFormat(model);
|
|
60
|
+
const family = CHART_FAMILY_OF[model.type];
|
|
61
|
+
const isAxisFamily = family !== 'pie';
|
|
62
|
+
// Combo (mix bar + line) and a secondary value axis only make sense
|
|
63
|
+
// on a vertical, absolute-scale chart: column / line / area. 100%-
|
|
64
|
+
// stacked variants pin every series to a shared 0–100% scale, and
|
|
65
|
+
// horizontal bars swap the axes so "secondary axis on the right"
|
|
66
|
+
// stops reading like Excel — mirror the gate in `build-option.ts`.
|
|
67
|
+
const is100 = model.type === 'column-stacked-100' || model.type === 'bar-stacked-100';
|
|
68
|
+
const isHorizontalBar = family === 'bar';
|
|
69
|
+
const supportsComboAndDualAxis =
|
|
70
|
+
isAxisFamily && family !== 'scatter' && !is100 && !isHorizontalBar;
|
|
71
|
+
const baseSeriesKind: 'bar' | 'line' = family === 'line' || family === 'area' ? 'line' : 'bar';
|
|
72
|
+
const [title, setTitle] = useState(model.title ?? '');
|
|
73
|
+
const [showTitle, setShowTitle] = useState(merged.showTitle);
|
|
74
|
+
const [legend, setLegend] = useState(merged.legend);
|
|
75
|
+
const [xAxisTitle, setXAxisTitle] = useState(merged.xAxisTitle ?? '');
|
|
76
|
+
const [yAxisTitle, setYAxisTitle] = useState(merged.yAxisTitle ?? '');
|
|
77
|
+
const [gridlines, setGridlines] = useState(merged.gridlines);
|
|
78
|
+
const [dataLabels, setDataLabels] = useState(merged.dataLabels);
|
|
79
|
+
const [palette, setPalette] = useState<ChartPalette>(merged.palette);
|
|
80
|
+
const [trendline, setTrendline] = useState(merged.trendline);
|
|
81
|
+
const [seriesColors, setSeriesColors] = useState<Record<string, string>>(
|
|
82
|
+
merged.seriesColors ?? {},
|
|
83
|
+
);
|
|
84
|
+
const [seriesTypes, setSeriesTypes] = useState<Record<string, 'bar' | 'line'>>(
|
|
85
|
+
merged.seriesTypes ?? {},
|
|
86
|
+
);
|
|
87
|
+
const [secondaryAxis, setSecondaryAxis] = useState<Record<string, boolean>>(
|
|
88
|
+
merged.secondaryAxis ?? {},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const confirm = () => {
|
|
92
|
+
// Strip empty / palette-matching overrides so the payload only
|
|
93
|
+
// carries explicit user picks.
|
|
94
|
+
const trimmedSeriesColors: Record<string, string> = {};
|
|
95
|
+
for (const [name, color] of Object.entries(seriesColors)) {
|
|
96
|
+
if (color && color.trim()) trimmedSeriesColors[name] = color;
|
|
97
|
+
}
|
|
98
|
+
// Only persist series-kind overrides that actually differ from the
|
|
99
|
+
// chart's base kind, and secondary-axis flags that are `true`, so
|
|
100
|
+
// the payload stays minimal (and toggling back to the default
|
|
101
|
+
// cleanly removes the override).
|
|
102
|
+
const trimmedSeriesTypes: Record<string, 'bar' | 'line'> = {};
|
|
103
|
+
if (supportsComboAndDualAxis) {
|
|
104
|
+
for (const [name, kind] of Object.entries(seriesTypes)) {
|
|
105
|
+
if (kind && kind !== baseSeriesKind) trimmedSeriesTypes[name] = kind;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const trimmedSecondaryAxis: Record<string, boolean> = {};
|
|
109
|
+
if (supportsComboAndDualAxis) {
|
|
110
|
+
for (const [name, on] of Object.entries(secondaryAxis)) {
|
|
111
|
+
if (on) trimmedSecondaryAxis[name] = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
onConfirm({
|
|
115
|
+
showTitle,
|
|
116
|
+
legend,
|
|
117
|
+
...(xAxisTitle.trim() ? { xAxisTitle: xAxisTitle.trim() } : { xAxisTitle: undefined }),
|
|
118
|
+
...(yAxisTitle.trim() ? { yAxisTitle: yAxisTitle.trim() } : { yAxisTitle: undefined }),
|
|
119
|
+
gridlines,
|
|
120
|
+
dataLabels,
|
|
121
|
+
palette,
|
|
122
|
+
trendline,
|
|
123
|
+
seriesColors: trimmedSeriesColors,
|
|
124
|
+
seriesTypes: trimmedSeriesTypes,
|
|
125
|
+
secondaryAxis: trimmedSecondaryAxis,
|
|
126
|
+
// Title text is part of the chart's identity (`ChartModel.title`)
|
|
127
|
+
// not its format. The caller reads the trimmed title via the
|
|
128
|
+
// form input below and applies it alongside the format patch.
|
|
129
|
+
});
|
|
130
|
+
// Side-channel the title back via a custom event so the caller
|
|
131
|
+
// doesn't need a separate prop just for one optional string.
|
|
132
|
+
const trimmed = title.trim();
|
|
133
|
+
if (trimmed !== (model.title ?? '')) {
|
|
134
|
+
const ce = new CustomEvent('casual-chart-title-changed', {
|
|
135
|
+
detail: { id: model.id, title: trimmed || undefined },
|
|
136
|
+
});
|
|
137
|
+
document.dispatchEvent(ce);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Dialog
|
|
143
|
+
title="Format chart"
|
|
144
|
+
onClose={onCancel}
|
|
145
|
+
data-testid="format-chart-dialog"
|
|
146
|
+
footer={
|
|
147
|
+
<>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
className="btn-secondary"
|
|
151
|
+
data-testid="format-chart-cancel"
|
|
152
|
+
onClick={onCancel}
|
|
153
|
+
>
|
|
154
|
+
Cancel
|
|
155
|
+
</button>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
className="btn-primary"
|
|
159
|
+
data-testid="format-chart-apply"
|
|
160
|
+
onClick={confirm}
|
|
161
|
+
>
|
|
162
|
+
Apply
|
|
163
|
+
</button>
|
|
164
|
+
</>
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
<div className="format-chart">
|
|
168
|
+
<Section legend="Title">
|
|
169
|
+
<div className="format-chart__row">
|
|
170
|
+
<label className="format-chart__checkbox">
|
|
171
|
+
<input
|
|
172
|
+
type="checkbox"
|
|
173
|
+
checked={showTitle}
|
|
174
|
+
data-testid="format-chart-show-title"
|
|
175
|
+
onChange={(e) => setShowTitle(e.target.checked)}
|
|
176
|
+
/>
|
|
177
|
+
<span>Show title</span>
|
|
178
|
+
</label>
|
|
179
|
+
</div>
|
|
180
|
+
<input
|
|
181
|
+
type="text"
|
|
182
|
+
className="format-chart__input"
|
|
183
|
+
data-testid="format-chart-title-input"
|
|
184
|
+
value={title}
|
|
185
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
186
|
+
placeholder="Chart 1"
|
|
187
|
+
disabled={!showTitle}
|
|
188
|
+
/>
|
|
189
|
+
</Section>
|
|
190
|
+
|
|
191
|
+
<Section legend="Legend">
|
|
192
|
+
<div className="format-chart__segment" role="radiogroup" aria-label="Legend position">
|
|
193
|
+
{LEGEND_POSITIONS.map((p) => (
|
|
194
|
+
<label
|
|
195
|
+
key={p.id}
|
|
196
|
+
className={`format-chart__seg-opt${legend === p.id ? ' format-chart__seg-opt--active' : ''}`}
|
|
197
|
+
data-testid={`format-chart-legend-${p.id}`}
|
|
198
|
+
>
|
|
199
|
+
<input
|
|
200
|
+
type="radio"
|
|
201
|
+
name="legend-pos"
|
|
202
|
+
value={p.id}
|
|
203
|
+
checked={legend === p.id}
|
|
204
|
+
onChange={() => setLegend(p.id)}
|
|
205
|
+
/>
|
|
206
|
+
<span>{p.label}</span>
|
|
207
|
+
</label>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</Section>
|
|
211
|
+
|
|
212
|
+
{isAxisFamily && (
|
|
213
|
+
<Section legend="Axes">
|
|
214
|
+
<div className="format-chart__axis-row">
|
|
215
|
+
<label className="format-chart__field">
|
|
216
|
+
<span className="format-chart__field-label">X-axis title</span>
|
|
217
|
+
<input
|
|
218
|
+
type="text"
|
|
219
|
+
className="format-chart__input"
|
|
220
|
+
data-testid="format-chart-x-axis-title"
|
|
221
|
+
value={xAxisTitle}
|
|
222
|
+
onChange={(e) => setXAxisTitle(e.target.value)}
|
|
223
|
+
placeholder="Auto"
|
|
224
|
+
/>
|
|
225
|
+
</label>
|
|
226
|
+
<label className="format-chart__field">
|
|
227
|
+
<span className="format-chart__field-label">Y-axis title</span>
|
|
228
|
+
<input
|
|
229
|
+
type="text"
|
|
230
|
+
className="format-chart__input"
|
|
231
|
+
data-testid="format-chart-y-axis-title"
|
|
232
|
+
value={yAxisTitle}
|
|
233
|
+
onChange={(e) => setYAxisTitle(e.target.value)}
|
|
234
|
+
placeholder="Auto"
|
|
235
|
+
/>
|
|
236
|
+
</label>
|
|
237
|
+
</div>
|
|
238
|
+
<label className="format-chart__checkbox">
|
|
239
|
+
<input
|
|
240
|
+
type="checkbox"
|
|
241
|
+
checked={gridlines}
|
|
242
|
+
data-testid="format-chart-gridlines"
|
|
243
|
+
onChange={(e) => setGridlines(e.target.checked)}
|
|
244
|
+
/>
|
|
245
|
+
<span>Show major gridlines</span>
|
|
246
|
+
</label>
|
|
247
|
+
</Section>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
<Section legend="Data labels">
|
|
251
|
+
<label className="format-chart__checkbox">
|
|
252
|
+
<input
|
|
253
|
+
type="checkbox"
|
|
254
|
+
checked={dataLabels}
|
|
255
|
+
data-testid="format-chart-data-labels"
|
|
256
|
+
onChange={(e) => setDataLabels(e.target.checked)}
|
|
257
|
+
/>
|
|
258
|
+
<span>Show values on each point</span>
|
|
259
|
+
</label>
|
|
260
|
+
</Section>
|
|
261
|
+
|
|
262
|
+
{/* Trendline only meaningful for axis-based charts. Pie /
|
|
263
|
+
doughnut have no time axis, so the toggle is hidden. */}
|
|
264
|
+
{isAxisFamily && (
|
|
265
|
+
<Section legend="Trendline">
|
|
266
|
+
<label className="format-chart__checkbox">
|
|
267
|
+
<input
|
|
268
|
+
type="checkbox"
|
|
269
|
+
checked={trendline}
|
|
270
|
+
data-testid="format-chart-trendline"
|
|
271
|
+
onChange={(e) => setTrendline(e.target.checked)}
|
|
272
|
+
/>
|
|
273
|
+
<span>Overlay linear-regression trendline on each series</span>
|
|
274
|
+
</label>
|
|
275
|
+
</Section>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* Combo + dual axis — per-series render-kind (bar/line) and a
|
|
279
|
+
secondary value axis. Only shown for column / line / area
|
|
280
|
+
charts where it's meaningful, and when the caller supplied
|
|
281
|
+
series names. */}
|
|
282
|
+
{supportsComboAndDualAxis && seriesNames && seriesNames.length > 0 && (
|
|
283
|
+
<Section legend="Series type & axis">
|
|
284
|
+
<p className="format-chart__hint">
|
|
285
|
+
Mix bars and lines, and plot a series against a secondary (right) axis.
|
|
286
|
+
</p>
|
|
287
|
+
<div className="format-chart__series-rows">
|
|
288
|
+
{seriesNames.map((name, idx) => {
|
|
289
|
+
const kind = seriesTypes[name] ?? baseSeriesKind;
|
|
290
|
+
const onSecondary = Boolean(secondaryAxis[name]);
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
key={name}
|
|
294
|
+
className="format-chart__series-row"
|
|
295
|
+
data-testid={`format-chart-combo-${idx}`}
|
|
296
|
+
>
|
|
297
|
+
<span className="format-chart__series-name">{name}</span>
|
|
298
|
+
<select
|
|
299
|
+
className="format-chart__series-kind"
|
|
300
|
+
data-testid={`format-chart-series-kind-${idx}`}
|
|
301
|
+
aria-label={`${name} chart type`}
|
|
302
|
+
value={kind}
|
|
303
|
+
onChange={(e) =>
|
|
304
|
+
setSeriesTypes({
|
|
305
|
+
...seriesTypes,
|
|
306
|
+
[name]: e.target.value as 'bar' | 'line',
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
>
|
|
310
|
+
<option value="bar">Bars</option>
|
|
311
|
+
<option value="line">Line</option>
|
|
312
|
+
</select>
|
|
313
|
+
<label className="format-chart__checkbox format-chart__series-secondary">
|
|
314
|
+
<input
|
|
315
|
+
type="checkbox"
|
|
316
|
+
data-testid={`format-chart-secondary-axis-${idx}`}
|
|
317
|
+
checked={onSecondary}
|
|
318
|
+
onChange={(e) =>
|
|
319
|
+
setSecondaryAxis({ ...secondaryAxis, [name]: e.target.checked })
|
|
320
|
+
}
|
|
321
|
+
/>
|
|
322
|
+
<span>Secondary axis</span>
|
|
323
|
+
</label>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
})}
|
|
327
|
+
</div>
|
|
328
|
+
</Section>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
<Section legend="Colors">
|
|
332
|
+
<div className="format-chart__palettes" role="radiogroup" aria-label="Color palette">
|
|
333
|
+
{(Object.keys(PALETTES) as ChartPalette[]).map((p) => (
|
|
334
|
+
<label
|
|
335
|
+
key={p}
|
|
336
|
+
className={`format-chart__palette${palette === p ? ' format-chart__palette--active' : ''}`}
|
|
337
|
+
data-testid={`format-chart-palette-${p}`}
|
|
338
|
+
title={PALETTE_LABELS[p]}
|
|
339
|
+
>
|
|
340
|
+
<input
|
|
341
|
+
type="radio"
|
|
342
|
+
name="palette"
|
|
343
|
+
value={p}
|
|
344
|
+
checked={palette === p}
|
|
345
|
+
onChange={() => setPalette(p)}
|
|
346
|
+
/>
|
|
347
|
+
<div className="format-chart__palette-swatches">
|
|
348
|
+
{PALETTES[p].slice(0, 5).map((c) => (
|
|
349
|
+
<span
|
|
350
|
+
key={c}
|
|
351
|
+
className="format-chart__palette-swatch"
|
|
352
|
+
style={{ backgroundColor: c }}
|
|
353
|
+
/>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
<span className="format-chart__palette-label">{PALETTE_LABELS[p]}</span>
|
|
357
|
+
</label>
|
|
358
|
+
))}
|
|
359
|
+
</div>
|
|
360
|
+
</Section>
|
|
361
|
+
|
|
362
|
+
{/* Per-series colour overrides — only rendered when the caller
|
|
363
|
+
supplied series names from the source range. Each row pairs
|
|
364
|
+
a colour swatch (native picker) with a Reset button that
|
|
365
|
+
removes the override and falls back to the palette. */}
|
|
366
|
+
{seriesNames && seriesNames.length > 0 && (
|
|
367
|
+
<Section legend="Series colors">
|
|
368
|
+
<div className="format-chart__series-rows">
|
|
369
|
+
{seriesNames.map((name, idx) => {
|
|
370
|
+
const override = seriesColors[name] ?? '';
|
|
371
|
+
const defaultColor = PALETTES[palette][idx % PALETTES[palette].length];
|
|
372
|
+
return (
|
|
373
|
+
<div
|
|
374
|
+
key={name}
|
|
375
|
+
className="format-chart__series-row"
|
|
376
|
+
data-testid={`format-chart-series-${idx}`}
|
|
377
|
+
>
|
|
378
|
+
<span className="format-chart__series-name">{name}</span>
|
|
379
|
+
<input
|
|
380
|
+
type="color"
|
|
381
|
+
className="format-chart__series-color"
|
|
382
|
+
data-testid={`format-chart-series-color-${idx}`}
|
|
383
|
+
value={override || defaultColor}
|
|
384
|
+
onChange={(e) => setSeriesColors({ ...seriesColors, [name]: e.target.value })}
|
|
385
|
+
/>
|
|
386
|
+
{override && (
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
className="format-chart__series-reset"
|
|
390
|
+
data-testid={`format-chart-series-reset-${idx}`}
|
|
391
|
+
title="Reset to palette default"
|
|
392
|
+
onClick={() => {
|
|
393
|
+
const next = { ...seriesColors };
|
|
394
|
+
delete next[name];
|
|
395
|
+
setSeriesColors(next);
|
|
396
|
+
}}
|
|
397
|
+
>
|
|
398
|
+
Reset
|
|
399
|
+
</button>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
})}
|
|
404
|
+
</div>
|
|
405
|
+
</Section>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
</Dialog>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function Section({ legend, children }: { legend: string; children: React.ReactNode }) {
|
|
413
|
+
return (
|
|
414
|
+
<fieldset className="format-chart__group">
|
|
415
|
+
<legend className="format-chart__legend">{legend}</legend>
|
|
416
|
+
{children}
|
|
417
|
+
</fieldset>
|
|
418
|
+
);
|
|
419
|
+
}
|