返回 presentation-ai
ChartControls.tsx
根目录 / src / components / presentation / floating-toolbar / ChartControls.tsx
1 "use client";
2
3 import { Settings, Table2 } from "lucide-react";
4 import * as React from "react";
5
6 import { ChartDataEditorDialog } from "@/components/notebook/presentation/editor/custom-elements/chart-data-editor-dialog";
7 import {
8 BAR_CHART_ELEMENT,
9 BOX_PLOT_CHART_ELEMENT,
10 CONE_FUNNEL_CHART_ELEMENT,
11 FUNNEL_CHART_ELEMENT,
12 LINEAR_GAUGE_ELEMENT,
13 PYRAMID_CHART_ELEMENT,
14 RADIAL_GAUGE_ELEMENT,
15 RANGE_BAR_CHART_ELEMENT,
16 } from "@/components/notebook/presentation/editor/lib";
17 import {
18 DropdownMenu,
19 DropdownMenuContent,
20 DropdownMenuRadioItem,
21 DropdownMenuTrigger,
22 } from "@/components/plate/ui/dropdown-menu";
23 import {
24 ToolbarButton,
25 ToolbarGroup,
26 ToolbarMenuGroup,
27 } from "@/components/plate/ui/toolbar";
28 import { Input } from "@/components/ui/input";
29 import { useToolbarContext } from "./ToolbarContext";
30
31 const orientationSupportedCharts: string[] = [
32 BAR_CHART_ELEMENT,
33 RANGE_BAR_CHART_ELEMENT,
34 FUNNEL_CHART_ELEMENT,
35 CONE_FUNNEL_CHART_ELEMENT,
36 PYRAMID_CHART_ELEMENT,
37 LINEAR_GAUGE_ELEMENT,
38 BOX_PLOT_CHART_ELEMENT,
39 ];
40
41 export function ChartControls() {
42 const [chartDataEditorOpen, setChartDataEditorOpen] = React.useState(false);
43 const {
44 element,
45 elementType,
46 isCurrentElementChart,
47 isComposedChart,
48 selectedOption,
49 chartData,
50 chartDataType,
51 seriesChartTypes,
52 handleNodePropertyUpdate,
53 handleChartDataUpdate,
54 handleSeriesChartTypesUpdate,
55 handleOpenChartEditor,
56 } = useToolbarContext();
57
58 if (!isCurrentElementChart) {
59 return null;
60 }
61
62 // Check if this is a gauge chart
63 const isGaugeChart =
64 elementType === RADIAL_GAUGE_ELEMENT ||
65 elementType === LINEAR_GAUGE_ELEMENT;
66
67 // Extract gauge value from data
68 const getGaugeValue = (): number => {
69 if (typeof chartData === "number") return chartData;
70 if (
71 typeof chartData === "object" &&
72 chartData !== null &&
73 "value" in chartData
74 ) {
75 return (chartData as { value: number }).value;
76 }
77 if (Array.isArray(chartData) && chartData.length > 0) {
78 const firstItem = chartData[0] as Record<string, unknown>;
79 const numericKey = Object.keys(firstItem).find(
80 (key) => typeof firstItem[key] === "number",
81 );
82 if (numericKey) return firstItem[numericKey] as number;
83 }
84 return 50;
85 };
86
87 const handleGaugeValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
88 const newValue = parseFloat(e.target.value);
89 if (!Number.isNaN(newValue)) {
90 const clampedValue = Math.min(100, Math.max(0, newValue));
91 handleChartDataUpdate([{ value: clampedValue }]);
92 }
93 };
94
95 const variant = (element as { variant?: string })?.variant ?? "default";
96 const scatterShape =
97 (element as { scatterShape?: string })?.scatterShape ?? "circle";
98
99 // New property extractions
100 const orientation =
101 (element as { orientation?: string })?.orientation ?? "vertical";
102 const interpolation =
103 (element as { interpolation?: string })?.interpolation ?? "smooth";
104 const needleEnabled =
105 (element as { needle?: { enabled?: boolean } })?.needle?.enabled ?? false;
106 const barEnabled =
107 (element as { bar?: { enabled?: boolean } })?.bar?.enabled ?? true;
108
109 const supportsVariant =
110 elementType === "chart-bar" ||
111 elementType === "chart-area" ||
112 elementType === "chart-pie" ||
113 elementType === "chart-radar";
114
115 const supportsCurveType =
116 elementType === "chart-line" || elementType === "chart-area";
117
118 const supportsScatterShape = elementType === "chart-scatter";
119
120 // Charts that support orientation (vertical/horizontal)
121
122 const supportsChartOrientation =
123 orientationSupportedCharts.includes(elementType);
124
125 // Radial gauge specific
126 const isRadialGauge = elementType === RADIAL_GAUGE_ELEMENT;
127
128 return (
129 <>
130 {/* Gauge Chart: Inline Value Input */}
131 {isGaugeChart ? (
132 <div className="flex items-center gap-1.5 border-r px-1 pr-2">
133 <Input
134 type="number"
135 value={getGaugeValue()}
136 onChange={handleGaugeValueChange}
137 min={0}
138 max={100}
139 className="h-7 w-16 [appearance:textfield] text-center text-sm [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
140 />
141 </div>
142 ) : (
143 /* Other Charts: Edit Data Button */
144 <ToolbarGroup>
145 <ToolbarButton
146 onClick={() => setChartDataEditorOpen(true)}
147 tooltip="Edit Chart Data"
148 size="sm"
149 className="gap-1"
150 >
151 <Table2 className="size-4" />
152 <span>Edit Data</span>
153 </ToolbarButton>
154 </ToolbarGroup>
155 )}
156
157 {/* Chart Variant Dropdown */}
158 {supportsVariant && (
159 <ToolbarGroup>
160 <DropdownMenu modal={false}>
161 <DropdownMenuTrigger asChild>
162 <ToolbarButton tooltip="Chart Variant" size="sm" isDropdown>
163 <span className="capitalize">{variant}</span>
164 </ToolbarButton>
165 </DropdownMenuTrigger>
166 <DropdownMenuContent
167 className="ignore-click-outside/toolbar min-w-0"
168 align="start"
169 >
170 <ToolbarMenuGroup
171 value={variant}
172 onValueChange={(v) => handleNodePropertyUpdate("variant", v)}
173 label="Variant"
174 >
175 <DropdownMenuRadioItem value="default">
176 Default
177 </DropdownMenuRadioItem>
178 {elementType === "chart-bar" && (
179 <DropdownMenuRadioItem value="stacked">
180 Stacked
181 </DropdownMenuRadioItem>
182 )}
183 {elementType === "chart-area" && (
184 <DropdownMenuRadioItem value="stacked">
185 Stacked
186 </DropdownMenuRadioItem>
187 )}
188 {elementType === "chart-pie" && (
189 <DropdownMenuRadioItem value="donut">
190 Donut
191 </DropdownMenuRadioItem>
192 )}
193 {elementType === "chart-radar" && (
194 <DropdownMenuRadioItem value="outline">
195 Outline
196 </DropdownMenuRadioItem>
197 )}
198 </ToolbarMenuGroup>
199 </DropdownMenuContent>
200 </DropdownMenu>
201 </ToolbarGroup>
202 )}
203
204 {/* Chart Interpolation Dropdown (Line/Area) */}
205 {supportsCurveType && (
206 <ToolbarGroup>
207 <DropdownMenu modal={false}>
208 <DropdownMenuTrigger asChild>
209 <ToolbarButton tooltip="Interpolation" size="sm" isDropdown>
210 <span className="capitalize">{interpolation}</span>
211 </ToolbarButton>
212 </DropdownMenuTrigger>
213 <DropdownMenuContent
214 className="ignore-click-outside/toolbar min-w-0"
215 align="start"
216 >
217 <ToolbarMenuGroup
218 value={interpolation}
219 onValueChange={(v) =>
220 handleNodePropertyUpdate("interpolation", v)
221 }
222 label="Interpolation"
223 >
224 <DropdownMenuRadioItem value="linear">
225 Linear
226 </DropdownMenuRadioItem>
227 <DropdownMenuRadioItem value="smooth">
228 Smooth
229 </DropdownMenuRadioItem>
230 <DropdownMenuRadioItem value="step">Step</DropdownMenuRadioItem>
231 <DropdownMenuRadioItem value="step-start">
232 Step Start
233 </DropdownMenuRadioItem>
234 <DropdownMenuRadioItem value="step-end">
235 Step End
236 </DropdownMenuRadioItem>
237 </ToolbarMenuGroup>
238 </DropdownMenuContent>
239 </DropdownMenu>
240 </ToolbarGroup>
241 )}
242
243 {/* Scatter Shape Dropdown */}
244 {supportsScatterShape && (
245 <ToolbarGroup>
246 <DropdownMenu modal={false}>
247 <DropdownMenuTrigger asChild>
248 <ToolbarButton tooltip="Point Shape" size="sm" isDropdown>
249 <span className="capitalize">{scatterShape}</span>
250 </ToolbarButton>
251 </DropdownMenuTrigger>
252 <DropdownMenuContent
253 className="ignore-click-outside/toolbar min-w-0"
254 align="start"
255 >
256 <ToolbarMenuGroup
257 value={scatterShape}
258 onValueChange={(s) =>
259 handleNodePropertyUpdate("scatterShape", s)
260 }
261 label="Shape"
262 >
263 <DropdownMenuRadioItem value="circle">
264 Circle
265 </DropdownMenuRadioItem>
266 <DropdownMenuRadioItem value="cross">
267 Cross
268 </DropdownMenuRadioItem>
269 <DropdownMenuRadioItem value="diamond">
270 Diamond
271 </DropdownMenuRadioItem>
272 <DropdownMenuRadioItem value="heart">
273 Heart
274 </DropdownMenuRadioItem>
275 <DropdownMenuRadioItem value="pin">Pin</DropdownMenuRadioItem>
276 <DropdownMenuRadioItem value="plus">Plus</DropdownMenuRadioItem>
277 <DropdownMenuRadioItem value="square">
278 Square
279 </DropdownMenuRadioItem>
280 <DropdownMenuRadioItem value="star">Star</DropdownMenuRadioItem>
281 <DropdownMenuRadioItem value="triangle">
282 Triangle
283 </DropdownMenuRadioItem>
284 </ToolbarMenuGroup>
285 </DropdownMenuContent>
286 </DropdownMenu>
287 </ToolbarGroup>
288 )}
289
290 {/* Chart Orientation Dropdown */}
291 {supportsChartOrientation && (
292 <ToolbarGroup>
293 <DropdownMenu modal={false}>
294 <DropdownMenuTrigger asChild>
295 <ToolbarButton tooltip="Orientation" size="sm" isDropdown>
296 <span className="capitalize">{orientation}</span>
297 </ToolbarButton>
298 </DropdownMenuTrigger>
299 <DropdownMenuContent
300 className="ignore-click-outside/toolbar min-w-0"
301 align="start"
302 >
303 <ToolbarMenuGroup
304 value={orientation}
305 onValueChange={(v) =>
306 handleNodePropertyUpdate("orientation", v)
307 }
308 label="Orientation"
309 >
310 <DropdownMenuRadioItem value="vertical">
311 Vertical
312 </DropdownMenuRadioItem>
313 <DropdownMenuRadioItem value="horizontal">
314 Horizontal
315 </DropdownMenuRadioItem>
316 </ToolbarMenuGroup>
317 </DropdownMenuContent>
318 </DropdownMenu>
319 </ToolbarGroup>
320 )}
321
322 {/* Radial Gauge Needle/Bar Toggles */}
323 {isRadialGauge && (
324 <>
325 <ToolbarGroup>
326 <ToolbarButton
327 onClick={() =>
328 handleNodePropertyUpdate("needle", { enabled: !needleEnabled })
329 }
330 tooltip={needleEnabled ? "Hide Needle" : "Show Needle"}
331 size="sm"
332 pressed={needleEnabled}
333 >
334 Needle
335 </ToolbarButton>
336 </ToolbarGroup>
337 <ToolbarGroup>
338 <ToolbarButton
339 onClick={() =>
340 handleNodePropertyUpdate("bar", { enabled: !barEnabled })
341 }
342 tooltip={barEnabled ? "Hide Bar" : "Show Bar"}
343 size="sm"
344 pressed={barEnabled}
345 >
346 Bar
347 </ToolbarButton>
348 </ToolbarGroup>
349 </>
350 )}
351
352 {/* Edit Chart Button - Opens sidebar for all chart settings */}
353 <ToolbarGroup>
354 <ToolbarButton
355 onClick={handleOpenChartEditor}
356 tooltip="Edit Chart"
357 size="sm"
358 className="gap-1"
359 >
360 <Settings className="size-4" />
361 <span>Edit Chart</span>
362 </ToolbarButton>
363 </ToolbarGroup>
364
365 {/* Chart Data Editor Dialog */}
366 <ChartDataEditorDialog
367 open={chartDataEditorOpen}
368 onOpenChange={setChartDataEditorOpen}
369 data={chartData || []}
370 onDataChange={handleChartDataUpdate}
371 chartType={chartDataType}
372 title={`Edit ${selectedOption} Data`}
373 isComposedChart={isComposedChart}
374 seriesChartTypes={seriesChartTypes}
375 onSeriesChartTypesChange={handleSeriesChartTypesUpdate}
376 previewChartType={elementType}
377 chartOptions={(element ?? {}) as Record<string, unknown>}
378 />
379 </>
380 );
381 }
382
382 lines Plain Text