| 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 |