@arronqzy/react-view 1.0.22 → 1.0.24

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.
@@ -0,0 +1,799 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import {
4
+ Checkbox,
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ Input,
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ Textarea,
15
+ } from "@arronqzy/ui";
16
+ import type { PanelChartConfig, PanelElement } from "../../types";
17
+ import { buildChartOption, getChartLabelsDisplayText, getChartValuesDisplayText } from "../../utils/chartOptionBuilder";
18
+ import { ConfigHintIcon } from "../ConfigHintIcon";
19
+ import {
20
+ type ConfigSectionHelpers,
21
+ type UpdateElement,
22
+ mergeOptionPatch,
23
+ patchChart,
24
+ } from "./helpers";
25
+
26
+ export function PanelConfigChartSection({
27
+ element,
28
+ helpers,
29
+ updateElement,
30
+ }: {
31
+ element: PanelElement;
32
+ helpers: ConfigSectionHelpers;
33
+ updateElement: UpdateElement;
34
+ }) {
35
+ const { t } = useI18n();
36
+ const {
37
+ renderSection,
38
+ renderFieldGroup,
39
+ renderColorField,
40
+ hasSearch,
41
+ isSectionExpanded,
42
+ setSectionExpanded,
43
+ optionCheckboxClass,
44
+ optionInputClass,
45
+ optionSelectTriggerClass,
46
+ renderOptionLabel,
47
+ renderFormatterLabel,
48
+ } = helpers;
49
+ const optionLabelTextClass = "truncate whitespace-nowrap text-[11px] leading-none";
50
+ const selectedElement = element;
51
+ const [isAdvancedOptionMode, setIsAdvancedOptionMode] = useState(false);
52
+ const [optionJsonText, setOptionJsonText] = useState("{}");
53
+ const [optionJsonError, setOptionJsonError] = useState<string | null>(null);
54
+
55
+ const selectedChartType = (selectedElement.materialType ?? "") as
56
+ | "bar"
57
+ | "line"
58
+ | "pie"
59
+ | "area"
60
+ | "scatter"
61
+ | "radar"
62
+ | "gauge"
63
+ | "funnel"
64
+ | "";
65
+
66
+ const updateSelectedChart = useCallback(
67
+ (patch: Partial<PanelChartConfig>) => {
68
+ updateElement(selectedElement.id, {
69
+ chart: patchChart(selectedElement.chart, patch),
70
+ });
71
+ },
72
+ [selectedElement, updateElement]
73
+ );
74
+
75
+ const updateSelectedOptionForm = useCallback(
76
+ (patch: Record<string, unknown>) => {
77
+ updateSelectedChart({
78
+ option: mergeOptionPatch(
79
+ (selectedElement.chart?.option as Record<string, unknown> | undefined) ?? {},
80
+ patch
81
+ ),
82
+ });
83
+ },
84
+ [selectedElement, updateSelectedChart]
85
+ );
86
+
87
+ useEffect(() => {
88
+ setOptionJsonText(JSON.stringify(buildChartOption(selectedElement), null, 2));
89
+ setOptionJsonError(null);
90
+ }, [selectedElement]);
91
+
92
+ return (
93
+ <>
94
+ {renderSection(
95
+ "chartBasic",
96
+ t("panel.config.sectionChartBasic"),
97
+ <>
98
+ {renderFieldGroup(
99
+ t("panel.config.groupBasicDisplay"),
100
+ <>
101
+ <label className="block space-y-1.5" data-config-field="chart.title">
102
+ <div>{t("panel.config.title")}</div>
103
+ <Input
104
+ value={selectedElement.chart?.title ?? ""}
105
+ onChange={(e) => updateSelectedChart({ title: e.target.value })}
106
+ className="h-7"
107
+ />
108
+ </label>
109
+
110
+ {renderColorField(
111
+ t("panel.config.primaryColor"),
112
+ selectedElement.chart?.color ?? "#3b82f6",
113
+ (next) => updateSelectedChart({ color: next || "#3b82f6" })
114
+ )}
115
+ </>
116
+ )}
117
+ {renderFieldGroup(
118
+ t("panel.config.groupData"),
119
+ <>
120
+ <label className="block space-y-1">
121
+ <div>{t("panel.config.labelsCsv")}</div>
122
+ <Input
123
+ value={getChartLabelsDisplayText(selectedElement.chart)}
124
+ onChange={(e) =>
125
+ updateSelectedChart({
126
+ labelsText: e.target.value,
127
+ })
128
+ }
129
+ className="h-7"
130
+ />
131
+ </label>
132
+
133
+ <label className="block space-y-1">
134
+ <div>{t("panel.config.valuesCsv")}</div>
135
+ <Input
136
+ value={getChartValuesDisplayText(selectedElement.chart)}
137
+ onChange={(e) =>
138
+ updateSelectedChart({
139
+ valuesText: e.target.value,
140
+ })
141
+ }
142
+ className="h-7"
143
+ />
144
+ </label>
145
+ </>
146
+ )}
147
+
148
+ {selectedChartType === "bar" ||
149
+ selectedChartType === "line" ||
150
+ selectedChartType === "area" ||
151
+ selectedChartType === "pie" ? (
152
+ renderFieldGroup(
153
+ t("panel.config.groupSeries"),
154
+ <>
155
+ {selectedChartType === "bar" ? (
156
+ <label className="block space-y-1">
157
+ <div>{t("panel.config.barWidthPx")}</div>
158
+ <Input
159
+ type="number"
160
+ min={1}
161
+ placeholder={t("panel.config.barWidthAuto")}
162
+ value={selectedElement.chart?.barWidth ?? ""}
163
+ onChange={(e) => {
164
+ const raw = e.target.value.trim();
165
+ updateSelectedChart({
166
+ barWidth: raw
167
+ ? Math.max(1, Number(raw) || 1)
168
+ : undefined,
169
+ });
170
+ }}
171
+ className="h-7"
172
+ />
173
+ </label>
174
+ ) : null}
175
+
176
+ {selectedChartType === "line" || selectedChartType === "area" ? (
177
+ <label className="flex items-center gap-2">
178
+ <Checkbox
179
+ checked={selectedElement.chart?.smooth ?? true}
180
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
181
+ onCheckedChange={(checked) =>
182
+ updateSelectedChart({ smooth: checked === true })
183
+ }
184
+ />
185
+ <span>{t("panel.config.smooth")}</span>
186
+ </label>
187
+ ) : null}
188
+
189
+ {selectedChartType === "pie" ? (
190
+ <div className="grid grid-cols-2 gap-2">
191
+ <label className="block space-y-1">
192
+ <div>{t("panel.config.pieInnerRadiusPct")}</div>
193
+ <Input
194
+ type="number"
195
+ min={0}
196
+ max={99}
197
+ value={selectedElement.chart?.pieInnerRadius ?? 30}
198
+ onChange={(e) =>
199
+ updateSelectedChart({
200
+ pieInnerRadius: Math.max(
201
+ 0,
202
+ Math.min(99, Number(e.target.value) || 0)
203
+ ),
204
+ })
205
+ }
206
+ className="h-7"
207
+ />
208
+ </label>
209
+ <label className="block space-y-1">
210
+ <div>{t("panel.config.pieOuterRadiusPct")}</div>
211
+ <Input
212
+ type="number"
213
+ min={1}
214
+ max={100}
215
+ value={selectedElement.chart?.pieOuterRadius ?? 65}
216
+ onChange={(e) =>
217
+ updateSelectedChart({
218
+ pieOuterRadius: Math.max(
219
+ 1,
220
+ Math.min(100, Number(e.target.value) || 1)
221
+ ),
222
+ })
223
+ }
224
+ className="h-7"
225
+ />
226
+ </label>
227
+ </div>
228
+ ) : null}
229
+ </>
230
+ )
231
+ ) : null}
232
+ {renderFieldGroup(
233
+ t("panel.config.groupChartDisplayMore"),
234
+ <>
235
+ <label className="flex items-center gap-2">
236
+ <Checkbox
237
+ checked={selectedElement.chart?.colorMode === "gradient"}
238
+ onCheckedChange={(checked) =>
239
+ updateSelectedChart({ colorMode: checked ? "gradient" : "solid" })
240
+ }
241
+ />
242
+ <span>{t("panel.config.usePrimaryGradient")}</span>
243
+ </label>
244
+ {selectedElement.chart?.colorMode === "gradient" ? (
245
+ <div className="grid grid-cols-2 gap-2">
246
+ {renderColorField(
247
+ t("panel.config.gradientFrom"),
248
+ selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6",
249
+ (next) => updateSelectedChart({ gradientFrom: next || "#3b82f6" })
250
+ )}
251
+ {renderColorField(
252
+ t("panel.config.gradientTo"),
253
+ selectedElement.chart?.gradientTo ?? "#22d3ee",
254
+ (next) => updateSelectedChart({ gradientTo: next || "#22d3ee" })
255
+ )}
256
+ <label className="block space-y-1 col-span-2">
257
+ <div>{t("panel.config.gradientDirection")}</div>
258
+ <Select
259
+ value={selectedElement.chart?.gradientDirection ?? "to-right"}
260
+ onValueChange={(value) =>
261
+ updateSelectedChart({
262
+ gradientDirection: value as
263
+ | "to-right"
264
+ | "to-bottom"
265
+ | "to-bottom-right"
266
+ | "to-top-right",
267
+ })
268
+ }
269
+ >
270
+ <SelectTrigger className="h-7">
271
+ <SelectValue placeholder={t("panel.config.selectGradientDirection")} />
272
+ </SelectTrigger>
273
+ <SelectContent>
274
+ <SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
275
+ <SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
276
+ <SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
277
+ <SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
278
+ </SelectContent>
279
+ </Select>
280
+ </label>
281
+ <div className="col-span-2 space-y-1">
282
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
283
+ <div
284
+ className="h-6 rounded border border-border/60"
285
+ style={{
286
+ backgroundImage: `linear-gradient(${
287
+ (selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom"
288
+ ? "to bottom"
289
+ : (selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
290
+ ? "to bottom right"
291
+ : (selectedElement.chart?.gradientDirection ?? "to-right") === "to-top-right"
292
+ ? "to top right"
293
+ : "to right"
294
+ }, ${selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6"}, ${selectedElement.chart?.gradientTo ?? "#22d3ee"})`,
295
+ }}
296
+ />
297
+ </div>
298
+ </div>
299
+ ) : null}
300
+ <label className="block space-y-1.5">
301
+ <div>{t("panel.config.renderMode")}</div>
302
+ <Select
303
+ value={selectedElement.chart?.renderMode ?? "canvas"}
304
+ onValueChange={(value) =>
305
+ updateSelectedChart({ renderMode: value as "canvas" | "svg" })
306
+ }
307
+ >
308
+ <SelectTrigger className="h-7">
309
+ <SelectValue placeholder={t("panel.config.selectRenderMode")} />
310
+ </SelectTrigger>
311
+ <SelectContent>
312
+ <SelectItem value="canvas">Canvas</SelectItem>
313
+ <SelectItem value="svg">SVG</SelectItem>
314
+ </SelectContent>
315
+ </Select>
316
+ </label>
317
+ </>,
318
+ undefined,
319
+ { groupKey: "chartDisplayMore", defaultOpen: false }
320
+ )}
321
+ {renderFieldGroup(
322
+ t("panel.config.groupTooltip"),
323
+ <>
324
+ <div className="grid grid-cols-2 gap-2">
325
+ <label className="flex items-center gap-2">
326
+ <Checkbox
327
+ checked={selectedElement.chart?.tooltipShow ?? true}
328
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
329
+ onCheckedChange={(checked) =>
330
+ updateSelectedChart({ tooltipShow: checked === true })
331
+ }
332
+ />
333
+ <span>{t("panel.config.showTooltip")}</span>
334
+ </label>
335
+ <label className="block space-y-1">
336
+ <div>{t("panel.config.tooltipTrigger")}</div>
337
+ <Select
338
+ value={selectedElement.chart?.tooltipTrigger ?? "axis"}
339
+ onValueChange={(value) =>
340
+ updateSelectedChart({ tooltipTrigger: value as "axis" | "item" })
341
+ }
342
+ >
343
+ <SelectTrigger className="h-7">
344
+ <SelectValue placeholder={t("panel.config.selectTooltipTrigger")} />
345
+ </SelectTrigger>
346
+ <SelectContent>
347
+ <SelectItem value="axis">axis</SelectItem>
348
+ <SelectItem value="item">item</SelectItem>
349
+ </SelectContent>
350
+ </Select>
351
+ </label>
352
+ </div>
353
+ <div className="grid grid-cols-2 gap-2">
354
+ {renderColorField(
355
+ t("panel.config.tooltipBgColor"),
356
+ selectedElement.chart?.tooltipBackgroundColor ?? "#0f172a",
357
+ (next) => updateSelectedChart({ tooltipBackgroundColor: next || "#0f172a" })
358
+ )}
359
+ {renderColorField(
360
+ t("panel.config.tooltipTextColor"),
361
+ selectedElement.chart?.tooltipTextColor ?? "#f8fafc",
362
+ (next) => updateSelectedChart({ tooltipTextColor: next || "#f8fafc" })
363
+ )}
364
+ </div>
365
+ <label className="block space-y-1">
366
+ {renderFormatterLabel(t("panel.config.tooltipFormatter"))}
367
+ <Input
368
+ value={selectedElement.chart?.tooltipFormatter ?? ""}
369
+ onChange={(e) =>
370
+ updateSelectedChart({ tooltipFormatter: e.target.value || undefined })
371
+ }
372
+ placeholder={t("panel.config.tooltipFormatterPlaceholder")}
373
+ className="h-7"
374
+ />
375
+ </label>
376
+ </>,
377
+ undefined,
378
+ { groupKey: "chartTooltip", defaultOpen: false }
379
+ )}
380
+ {selectedChartType === "bar" ||
381
+ selectedChartType === "line" ||
382
+ selectedChartType === "area" ||
383
+ selectedChartType === "scatter" ? (
384
+ renderFieldGroup(
385
+ t("panel.config.groupAxes"),
386
+ <>
387
+ <div className="grid grid-cols-2 gap-2">
388
+ <label className="block">
389
+ <div className="mb-1">{t("panel.config.xAxisName")}</div>
390
+ <Input
391
+ value={selectedElement.chart?.xAxisName ?? ""}
392
+ onChange={(e) => updateSelectedChart({ xAxisName: e.target.value })}
393
+ placeholder={t("panel.config.xAxisNamePlaceholder")}
394
+ className="h-7"
395
+ />
396
+ </label>
397
+ <label className="block">
398
+ <div className="mb-1">{t("panel.config.yAxisName")}</div>
399
+ <Input
400
+ value={selectedElement.chart?.yAxisName ?? ""}
401
+ onChange={(e) => updateSelectedChart({ yAxisName: e.target.value })}
402
+ placeholder={t("panel.config.yAxisNamePlaceholder")}
403
+ className="h-7"
404
+ />
405
+ </label>
406
+ </div>
407
+ <div className="grid grid-cols-2 gap-2">
408
+ <label className="flex items-center gap-2">
409
+ <Checkbox
410
+ checked={selectedElement.chart?.xAxisTickShow ?? true}
411
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
412
+ onCheckedChange={(checked) =>
413
+ updateSelectedChart({ xAxisTickShow: checked === true })
414
+ }
415
+ />
416
+ <span>{t("panel.config.xAxisTick")}</span>
417
+ </label>
418
+ <label className="flex items-center gap-2">
419
+ <Checkbox
420
+ checked={selectedElement.chart?.yAxisTickShow ?? true}
421
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
422
+ onCheckedChange={(checked) =>
423
+ updateSelectedChart({ yAxisTickShow: checked === true })
424
+ }
425
+ />
426
+ <span>{t("panel.config.yAxisTick")}</span>
427
+ </label>
428
+ </div>
429
+ <div className="grid grid-cols-2 gap-2">
430
+ {renderColorField(
431
+ t("panel.config.xAxisTickColor"),
432
+ selectedElement.chart?.xAxisTickColor ?? "#94a3b8",
433
+ (next) => updateSelectedChart({ xAxisTickColor: next || "#94a3b8" })
434
+ )}
435
+ {renderColorField(
436
+ t("panel.config.yAxisTickColor"),
437
+ selectedElement.chart?.yAxisTickColor ?? "#94a3b8",
438
+ (next) => updateSelectedChart({ yAxisTickColor: next || "#94a3b8" })
439
+ )}
440
+ </div>
441
+ <div className="grid grid-cols-2 gap-2">
442
+ <label className="flex items-center gap-2">
443
+ <Checkbox
444
+ checked={selectedElement.chart?.xAxisSplitLineShow ?? false}
445
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
446
+ onCheckedChange={(checked) =>
447
+ updateSelectedChart({ xAxisSplitLineShow: checked === true })
448
+ }
449
+ />
450
+ <span>{t("panel.config.xAxisSplitLine")}</span>
451
+ </label>
452
+ <label className="flex items-center gap-2">
453
+ <Checkbox
454
+ checked={selectedElement.chart?.yAxisSplitLineShow ?? true}
455
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
456
+ onCheckedChange={(checked) =>
457
+ updateSelectedChart({ yAxisSplitLineShow: checked === true })
458
+ }
459
+ />
460
+ <span>{t("panel.config.yAxisSplitLine")}</span>
461
+ </label>
462
+ </div>
463
+ <div className="grid grid-cols-2 gap-2">
464
+ {renderColorField(
465
+ t("panel.config.xAxisSplitLineColor"),
466
+ selectedElement.chart?.xAxisSplitLineColor ?? "#e2e8f0",
467
+ (next) => updateSelectedChart({ xAxisSplitLineColor: next || "#e2e8f0" })
468
+ )}
469
+ {renderColorField(
470
+ t("panel.config.yAxisSplitLineColor"),
471
+ selectedElement.chart?.yAxisSplitLineColor ?? "#e2e8f0",
472
+ (next) => updateSelectedChart({ yAxisSplitLineColor: next || "#e2e8f0" })
473
+ )}
474
+ </div>
475
+ <div className="grid grid-cols-2 gap-2">
476
+ {renderColorField(
477
+ t("panel.config.xAxisLabelColor"),
478
+ selectedElement.chart?.xAxisLabelColor ?? "#64748b",
479
+ (next) => updateSelectedChart({ xAxisLabelColor: next || "#64748b" })
480
+ )}
481
+ {renderColorField(
482
+ t("panel.config.yAxisLabelColor"),
483
+ selectedElement.chart?.yAxisLabelColor ?? "#64748b",
484
+ (next) => updateSelectedChart({ yAxisLabelColor: next || "#64748b" })
485
+ )}
486
+ </div>
487
+ <div className="grid grid-cols-2 gap-2">
488
+ <label className="block">
489
+ <div className="mb-1">{t("panel.config.xAxisLabelFontSize")}</div>
490
+ <Input
491
+ type="number"
492
+ min={8}
493
+ max={48}
494
+ value={selectedElement.chart?.xAxisLabelFontSize ?? 10}
495
+ onChange={(e) =>
496
+ updateSelectedChart({
497
+ xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
498
+ })
499
+ }
500
+ className="h-7"
501
+ />
502
+ </label>
503
+ <label className="block">
504
+ <div className="mb-1">{t("panel.config.yAxisLabelFontSize")}</div>
505
+ <Input
506
+ type="number"
507
+ min={8}
508
+ max={48}
509
+ value={selectedElement.chart?.yAxisLabelFontSize ?? 10}
510
+ onChange={(e) =>
511
+ updateSelectedChart({
512
+ yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
513
+ })
514
+ }
515
+ className="h-7"
516
+ />
517
+ </label>
518
+ </div>
519
+ <div className="grid grid-cols-2 gap-2">
520
+ <label className="flex items-center gap-2">
521
+ <Checkbox
522
+ checked={selectedElement.chart?.xAxisLabelAutoEllipsis ?? false}
523
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
524
+ onCheckedChange={(checked) =>
525
+ updateSelectedChart({ xAxisLabelAutoEllipsis: checked === true })
526
+ }
527
+ />
528
+ <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
529
+ </label>
530
+ <label className="flex items-center gap-2">
531
+ <Checkbox
532
+ checked={selectedElement.chart?.yAxisLabelAutoEllipsis ?? false}
533
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
534
+ onCheckedChange={(checked) =>
535
+ updateSelectedChart({ yAxisLabelAutoEllipsis: checked === true })
536
+ }
537
+ />
538
+ <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
539
+ </label>
540
+ </div>
541
+ </>,
542
+ undefined,
543
+ { groupKey: "chartAxes", defaultOpen: false }
544
+ )
545
+ ) : null}
546
+
547
+ </>,
548
+ true,
549
+ [
550
+ t("panel.config.searchKwChart"),
551
+ "title",
552
+ "tooltip",
553
+ t("panel.config.searchKwXAxis"),
554
+ t("panel.config.searchKwYAxis"),
555
+ "axis",
556
+ "label",
557
+ t("panel.config.searchKwTick"),
558
+ t("panel.config.searchKwSplit"),
559
+ "render",
560
+ "svg",
561
+ "canvas",
562
+ ]
563
+ )}
564
+
565
+ {renderSection(
566
+ "chartAdvanced",
567
+ t("panel.config.sectionChartAdvanced"),
568
+ <>
569
+ {renderFieldGroup(
570
+ t("panel.config.groupCommonForm"),
571
+ <div className="grid grid-cols-1 gap-2">
572
+ <div className="rounded-lg border border-border/60 bg-background/70 p-3">
573
+ <Collapsible
574
+ open={isSectionExpanded("chartAdvancedLayout", false)}
575
+ onOpenChange={(open) => setSectionExpanded("chartAdvancedLayout", open)}
576
+ >
577
+ <div className="mb-2 flex items-center gap-1.5">
578
+ <CollapsibleTrigger asChild>
579
+ <button
580
+ type="button"
581
+ className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
582
+ aria-label={isSectionExpanded("chartAdvancedLayout", false) ? t("panel.config.collapseLayoutCoord") : t("panel.config.expandLayoutCoord")}
583
+ >
584
+ {isSectionExpanded("chartAdvancedLayout", false) ? "▾" : "▸"}
585
+ </button>
586
+ </CollapsibleTrigger>
587
+ <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupLayoutAndCoord")}</div>
588
+ </div>
589
+ <CollapsibleContent>
590
+ <div className="grid grid-cols-2 gap-2">
591
+ <label className="block space-y-1.5">
592
+ <div className={optionLabelTextClass}>{t("panel.config.gridLeftForm")}</div>
593
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.left ?? 28)} onChange={(e) => updateSelectedOptionForm({ grid: { left: Number(e.target.value) || 0 } })} />
594
+ </label>
595
+ <label className="block space-y-1.5">
596
+ <div className={optionLabelTextClass}>{t("panel.config.gridRightForm")}</div>
597
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.right ?? 10)} onChange={(e) => updateSelectedOptionForm({ grid: { right: Number(e.target.value) || 0 } })} />
598
+ </label>
599
+ <label className="block space-y-1.5">
600
+ <div className={optionLabelTextClass}>{t("panel.config.gridTopForm")}</div>
601
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.top ?? 30)} onChange={(e) => updateSelectedOptionForm({ grid: { top: Number(e.target.value) || 0 } })} />
602
+ </label>
603
+ <label className="block space-y-1.5">
604
+ <div className={optionLabelTextClass}>{t("panel.config.gridBottomForm")}</div>
605
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.bottom ?? 20)} onChange={(e) => updateSelectedOptionForm({ grid: { bottom: Number(e.target.value) || 0 } })} />
606
+ </label>
607
+ <label className="block space-y-1.5">
608
+ <div className={optionLabelTextClass}>{t("panel.config.legendPositionForm")}</div>
609
+ <Select value={String((selectedElement.chart?.option as any)?.legend?.top ?? "top")} onValueChange={(value) => updateSelectedOptionForm({ legend: { top: value } })}>
610
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendPosition")} /></SelectTrigger>
611
+ <SelectContent>
612
+ <SelectItem value="top">{t("panel.config.legendTop")}</SelectItem>
613
+ <SelectItem value="bottom">{t("panel.config.legendBottom")}</SelectItem>
614
+ <SelectItem value="left">{t("panel.config.legendLeft")}</SelectItem>
615
+ <SelectItem value="right">{t("panel.config.legendRight")}</SelectItem>
616
+ </SelectContent>
617
+ </Select>
618
+ </label>
619
+ <label className="block space-y-1.5">
620
+ <div className={optionLabelTextClass}>{t("panel.config.legendOrientForm")}</div>
621
+ <Select value={String((selectedElement.chart?.option as any)?.legend?.orient ?? "horizontal")} onValueChange={(value) => updateSelectedOptionForm({ legend: { orient: value } })}>
622
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendOrient")} /></SelectTrigger>
623
+ <SelectContent>
624
+ <SelectItem value="horizontal">{t("panel.config.legendHorizontal")}</SelectItem>
625
+ <SelectItem value="vertical">{t("panel.config.legendVertical")}</SelectItem>
626
+ </SelectContent>
627
+ </Select>
628
+ </label>
629
+ <label className="block space-y-1.5">
630
+ {renderOptionLabel(t("panel.config.xAxisMin"), "xAxis.min", t("panel.config.xAxisMinHint"))}
631
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { min: Number(e.target.value) || 0 } })} />
632
+ </label>
633
+ <label className="block space-y-1.5">
634
+ {renderOptionLabel(t("panel.config.xAxisMax"), "xAxis.max", t("panel.config.xAxisMaxHint"))}
635
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ xAxis: { max: Number(e.target.value) || 0 } })} />
636
+ </label>
637
+ <label className="block space-y-1.5">
638
+ {renderOptionLabel(t("panel.config.yAxisMin"), "yAxis.min", t("panel.config.yAxisMinHint"))}
639
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { min: Number(e.target.value) || 0 } })} />
640
+ </label>
641
+ <label className="block space-y-1.5">
642
+ {renderOptionLabel(t("panel.config.yAxisMax"), "yAxis.max", t("panel.config.yAxisMaxHint"))}
643
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ yAxis: { max: Number(e.target.value) || 0 } })} />
644
+ </label>
645
+ <label className="block space-y-1.5">
646
+ {renderOptionLabel(t("panel.config.xAxisLabelRotate"), "xAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
647
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
648
+ </label>
649
+ <label className="block space-y-1.5">
650
+ {renderOptionLabel(t("panel.config.yAxisLabelRotate"), "yAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
651
+ <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
652
+ </label>
653
+ </div>
654
+ </CollapsibleContent>
655
+ </Collapsible>
656
+ </div>
657
+ <div className="rounded-lg border border-border/60 bg-background/70 p-3">
658
+ <div className="space-y-2">
659
+ <Collapsible
660
+ open={isSectionExpanded("chartAdvancedHighFreq", true)}
661
+ onOpenChange={(open) => setSectionExpanded("chartAdvancedHighFreq", open)}
662
+ >
663
+ <div className="mb-2 flex items-center gap-1.5">
664
+ <CollapsibleTrigger asChild>
665
+ <button
666
+ type="button"
667
+ className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
668
+ aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? t("panel.config.collapseHighFreq") : t("panel.config.expandHighFreq")}
669
+ >
670
+ {isSectionExpanded("chartAdvancedHighFreq", true) ? "▾" : "▸"}
671
+ </button>
672
+ </CollapsibleTrigger>
673
+ <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupHighFreq")}</div>
674
+ </div>
675
+ <CollapsibleContent>
676
+ <div className="grid grid-cols-1 gap-2">
677
+ <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
678
+ <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.legend?.show ?? true)} onCheckedChange={(checked) => updateSelectedOptionForm({ legend: { show: checked === true } })} />
679
+ {renderOptionLabel(t("panel.config.showLegend"), "legend.show", t("panel.config.showLegendHint"))}
680
+ </label>
681
+ <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
682
+ <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.grid?.containLabel ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ grid: { containLabel: checked === true } })} />
683
+ {renderOptionLabel(t("panel.config.gridContainLabel"), "grid.containLabel", t("panel.config.gridContainLabelHint"))}
684
+ </label>
685
+ <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
686
+ <Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }] : prev.filter((z: any) => z?.type !== "inside"); updateSelectedOptionForm({ dataZoom: next }); }} />
687
+ {renderOptionLabel(t("panel.config.zoomInside"), "dataZoom[type=inside]", t("panel.config.zoomInsideHint"))}
688
+ </label>
689
+ <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
690
+ <Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }] : prev.filter((z: any) => z?.type !== "slider"); updateSelectedOptionForm({ dataZoom: next }); }} />
691
+ {renderOptionLabel(t("panel.config.zoomSlider"), "dataZoom[type=slider]", t("panel.config.zoomSliderHint"))}
692
+ </label>
693
+ <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
694
+ <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.animation ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ animation: checked === true })} />
695
+ {renderOptionLabel(t("panel.config.enableAnimation"), "animation", t("panel.config.enableAnimationHint"))}
696
+ </label>
697
+ <label className="block space-y-1.5">
698
+ {renderOptionLabel(t("panel.config.animationDuration"), "animationDuration", t("panel.config.animationDurationHint"))}
699
+ <Input type="number" min={0} className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.animationDuration ?? 300)} onChange={(e) => updateSelectedOptionForm({ animationDuration: Math.max(0, Number(e.target.value) || 0) })} />
700
+ </label>
701
+ </div>
702
+ </CollapsibleContent>
703
+ </Collapsible>
704
+ <Collapsible
705
+ open={isSectionExpanded("chartAdvancedAxisPointer", false)}
706
+ onOpenChange={(open) => setSectionExpanded("chartAdvancedAxisPointer", open)}
707
+ >
708
+ <div className="mb-2 flex items-center gap-1.5">
709
+ <CollapsibleTrigger asChild>
710
+ <button
711
+ type="button"
712
+ className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
713
+ aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? t("panel.config.collapseAxisPointer") : t("panel.config.expandAxisPointer")}
714
+ >
715
+ {isSectionExpanded("chartAdvancedAxisPointer", false) ? "▾" : "▸"}
716
+ </button>
717
+ </CollapsibleTrigger>
718
+ <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupAxisPointer")}</div>
719
+ </div>
720
+ <CollapsibleContent>
721
+ <div className="grid grid-cols-1 gap-2">
722
+ <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
723
+ <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.show ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { show: checked === true } })} />
724
+ {renderOptionLabel(t("panel.config.showAxisPointer"), "axisPointer.show", t("panel.config.showAxisPointerHint"))}
725
+ </label>
726
+ <label className="block space-y-1.5 rounded-md bg-muted/30 px-2 py-1.5">
727
+ {renderOptionLabel(t("panel.config.axisPointerType"), "axisPointer.type", t("panel.config.axisPointerTypeHint"))}
728
+ <Select value={String((selectedElement.chart?.option as any)?.axisPointer?.type ?? "line")} onValueChange={(value) => updateSelectedOptionForm({ axisPointer: { type: value } })}>
729
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectAxisPointerType")} /></SelectTrigger>
730
+ <SelectContent>
731
+ <SelectItem value="line">{t("panel.config.axisPointerLine")}</SelectItem>
732
+ <SelectItem value="shadow">{t("panel.config.axisPointerShadow")}</SelectItem>
733
+ <SelectItem value="cross">{t("panel.config.axisPointerCross")}</SelectItem>
734
+ </SelectContent>
735
+ </Select>
736
+ </label>
737
+ <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
738
+ <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.snap ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { snap: checked === true } })} />
739
+ {renderOptionLabel(t("panel.config.axisPointerSnap"), "axisPointer.snap", t("panel.config.axisPointerSnapHint"))}
740
+ </label>
741
+ </div>
742
+ </CollapsibleContent>
743
+ </Collapsible>
744
+ </div>
745
+ </div>
746
+ </div>
747
+ )}
748
+ {renderFieldGroup(
749
+ t("panel.config.groupJsonAdvanced"),
750
+ <>
751
+ <label className="flex items-center gap-2 rounded-md bg-background/70 px-2 py-1.5">
752
+ <Checkbox
753
+ checked={isAdvancedOptionMode}
754
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
755
+ onCheckedChange={(checked) => setIsAdvancedOptionMode(checked === true)}
756
+ />
757
+ <span>{t("panel.config.enableAdvancedJson")}</span>
758
+ <ConfigHintIcon label={t("panel.config.advancedJsonHintLabel")}>
759
+ {t("panel.config.advancedJsonHint")}
760
+ </ConfigHintIcon>
761
+ </label>
762
+ {isAdvancedOptionMode ? (
763
+ <>
764
+ <Textarea
765
+ value={optionJsonText}
766
+ onChange={(e) => {
767
+ const next = e.target.value;
768
+ setOptionJsonText(next);
769
+ try {
770
+ const parsed = JSON.parse(next) as Record<string, unknown>;
771
+ updateSelectedChart({ option: parsed });
772
+ setOptionJsonError(null);
773
+ } catch {
774
+ setOptionJsonError(t("panel.config.jsonInvalid"));
775
+ }
776
+ }}
777
+ spellCheck={false}
778
+ className="h-44 font-mono text-[11px]"
779
+ />
780
+ {optionJsonError ? (
781
+ <div className="rounded border border-destructive/40 bg-destructive/10 px-2 py-1.5 text-[11px] text-destructive">
782
+ {optionJsonError}
783
+ </div>
784
+ ) : (
785
+ <div className="rounded border border-emerald-500/30 bg-emerald-500/10 px-2 py-1.5 text-[11px] text-emerald-700 dark:text-emerald-300">
786
+ {t("panel.config.jsonValidApplied")}
787
+ </div>
788
+ )}
789
+ </>
790
+ ) : null}
791
+ </>
792
+ )}
793
+ </>,
794
+ false,
795
+ ["json", "option", t("panel.config.searchKwAdvanced"), "echarts"]
796
+ )}
797
+ </>
798
+ );
799
+ }