/** * @file Chart Node - Data visualization node for charts and graphs */ import * as React from "react"; import type { NodeDefinition, NodeRendererProps, InspectorRenderProps, ExternalDataReference, } from "../../../../../types/NodeDefinition"; import { getTextColor } from "./colorUtils"; export type ChartData = { id: string; type: "bar" | "line" | "pie"; title: string; data: Array<{ label: string; value: number; color?: string }>; }; export const ChartRenderer = ({ node, isSelected, isDragging, externalData }: NodeRendererProps) => { const chartData = externalData as ChartData | undefined; const renderMiniChart = () => { if (!chartData?.data) { return null; } const maxValue = Math.max(...chartData.data.map((d) => d.value)); const chartHeight = 60; switch (chartData.type) { case "bar": return (
{chartData.data.map((item, index) => (
))}
); case "line": { const points = chartData.data .map((item, index) => { const x = (index / (chartData.data.length - 1)) * 100; const y = 100 - (item.value / maxValue) * 80; return `${x},${y}`; }) .join(" "); return ( {chartData.data.map((item, index) => { const x = (index / (chartData.data.length - 1)) * 100; const y = 100 - (item.value / maxValue) * 80; return ; })} ); } case "pie": { let currentAngle = 0; const radius = chartHeight / 2 - 5; const centerX = 50; const centerY = 50; const total = chartData.data.reduce((sum, item) => sum + item.value, 0); return ( {chartData.data.map((item, index) => { const angle = (item.value / total) * 360; const x1 = centerX + radius * Math.cos((currentAngle * Math.PI) / 180); const y1 = centerY + radius * Math.sin((currentAngle * Math.PI) / 180); const x2 = centerX + radius * Math.cos(((currentAngle + angle) * Math.PI) / 180); const y2 = centerY + radius * Math.sin(((currentAngle + angle) * Math.PI) / 180); const largeArcFlag = angle > 180 ? 1 : 0; const pathData = `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`; currentAngle += angle; return ( ); })} ); } default: return null; } }; const chartColor = "#10b981"; return (

{chartData?.title || "Chart"}

{chartData?.type?.toUpperCase() || "CHART"} • {chartData?.data?.length || 0} items
{renderMiniChart()}
{chartData?.data && (
Range: {Math.min(...chartData.data.map((d) => d.value))} - {Math.max(...chartData.data.map((d) => d.value))}
)}
); }; export const ChartInspectorRenderer = ({ externalData, onUpdateExternalData }: InspectorRenderProps) => { const chartData = externalData as ChartData | undefined; const [editedData, setEditedData] = React.useState({ id: chartData?.id || "", type: chartData?.type || "bar", title: chartData?.title || "New Chart", data: chartData?.data || [ { label: "A", value: 10, color: "#3b82f6" }, { label: "B", value: 20, color: "#10b981" }, { label: "C", value: 15, color: "#f59e0b" }, ], }); const addDataPoint = () => { setEditedData((prev) => ({ ...prev, data: [...prev.data, { label: `Item ${prev.data.length + 1}`, value: 5 }], })); }; const removeDataPoint = (index: number) => { setEditedData((prev) => ({ ...prev, data: prev.data.filter((_, i) => i !== index), })); }; const updateDataPoint = (index: number, updates: Partial) => { setEditedData((prev) => ({ ...prev, data: prev.data.map((item, i) => (i === index ? { ...item, ...updates } : item)), })); }; const handleSave = async () => { if (onUpdateExternalData) { await onUpdateExternalData(editedData); } }; return (

Chart Configuration

setEditedData({ ...editedData, title: e.target.value })} style={{ width: "100%", padding: "4px 8px" }} />
{editedData.data.map((item, index) => (
updateDataPoint(index, { label: e.target.value })} style={{ flex: 1, padding: "2px 4px", fontSize: "11px" }} placeholder="Label" /> updateDataPoint(index, { value: Number(e.target.value) })} style={{ width: "60px", padding: "2px 4px", fontSize: "11px" }} /> updateDataPoint(index, { color: e.target.value })} style={{ width: "30px", height: "24px", border: "none", cursor: "pointer" }} />
))}
); }; export const ChartNodeDefinition: NodeDefinition = { type: "chart", displayName: "Chart Visualization", description: "Data visualization node for charts and graphs", category: "Data", defaultData: { title: "Chart Node", }, defaultSize: { width: 200, height: 140 }, ports: [ { id: "data-input", type: "input", label: "Data", position: "left", }, { id: "chart-output", type: "output", label: "Visualization", position: "right", }, ], renderNode: ChartRenderer, renderInspector: ChartInspectorRenderer, loadExternalData: async (ref: ExternalDataReference) => { await new Promise((resolve) => setTimeout(resolve, 200)); return { id: ref.id, type: "bar", title: "Sample Chart", data: [ { label: "Jan", value: 10, color: "#3b82f6" }, { label: "Feb", value: 25, color: "#10b981" }, { label: "Mar", value: 18, color: "#f59e0b" }, { label: "Apr", value: 30, color: "#ef4444" }, ], }; }, updateExternalData: async (_ref: ExternalDataReference, data: unknown) => { await new Promise((resolve) => setTimeout(resolve, 200)); console.log("Updated chart data:", data); }, };