返回 presentation-ai
InteractiveChartPreview.tsx
根目录 / src / components / presentation / edit-panel / sections / InteractiveChartPreview.tsx
1 "use client";
2
3 import { useLayoutEffect, useRef, useState } from "react";
4
5 import { ChartRenderer } from "@/components/notebook/presentation/editor/custom-elements/charts/ChartRenderer";
6 import { DEFAULT_CHART_DATA } from "@/components/notebook/presentation/editor/lib";
7
8 const BASE_WIDTH = 400;
9 const BASE_HEIGHT = 300;
10 const EMPTY_VARIANT_OPTIONS: Record<string, unknown> = {};
11
12 const sampleData = [
13 { label: "A", value: 400 },
14 { label: "B", value: 300 },
15 { label: "C", value: 500 },
16 { label: "D", value: 280 },
17 ];
18
19 const scatterData = [
20 { x: 10, y: 30, z: 15 },
21 { x: 25, y: 45, z: 22 },
22 { x: 35, y: 20, z: 18 },
23 { x: 45, y: 55, z: 28 },
24 ];
25
26 const ohlcData = [
27 { date: "Mon", open: 100, high: 105, low: 98, close: 103 },
28 { date: "Tue", open: 103, high: 108, low: 101, close: 107 },
29 { date: "Wed", open: 107, high: 112, low: 104, close: 106 },
30 ];
31
32 const boxPlotData = [
33 { category: "A", min: 10, q1: 25, median: 50, q3: 75, max: 95 },
34 { category: "B", min: 15, q1: 30, median: 55, q3: 80, max: 100 },
35 ];
36
37 const rangeData = [
38 { category: "A", low: 20, high: 80 },
39 { category: "B", low: 30, high: 70 },
40 { category: "C", low: 40, high: 90 },
41 ];
42
43 const flowData = [
44 { from: "A", to: "B", size: 10 },
45 { from: "A", to: "C", size: 5 },
46 { from: "B", to: "C", size: 15 },
47 ];
48
49 const heatmapData = [
50 { x: "A", y: "1", value: 10 },
51 { x: "B", y: "1", value: 20 },
52 { x: "A", y: "2", value: 30 },
53 { x: "B", y: "2", value: 15 },
54 ];
55
56 const histogramData = [
57 { label: "0-20", value: 4 },
58 { label: "20-40", value: 12 },
59 { label: "40-60", value: 20 },
60 { label: "60-80", value: 10 },
61 { label: "80-100", value: 3 },
62 ];
63
64 const waterfallData = [
65 { category: "Start", amount: 100 },
66 { category: "Add", amount: 50 },
67 { category: "Sub", amount: -30 },
68 { category: "Total", amount: 120 },
69 ];
70
71 function getSampleDataForChart(chartType: string): unknown {
72 switch (chartType) {
73 case "chart-scatter":
74 case "chart-bubble":
75 return scatterData;
76 case "chart-candlestick":
77 case "chart-ohlc":
78 return ohlcData;
79 case "chart-box-plot":
80 return boxPlotData;
81 case "chart-range-bar":
82 case "chart-range-area":
83 return rangeData;
84 case "chart-sankey":
85 case "chart-chord":
86 return flowData;
87 case "chart-treemap":
88 case "chart-sunburst":
89 return DEFAULT_CHART_DATA.hierarchy;
90 case "chart-heatmap":
91 return heatmapData;
92 case "chart-histogram":
93 return histogramData;
94 case "chart-waterfall":
95 return waterfallData;
96 case "chart-radial-gauge":
97 case "chart-linear-gauge":
98 return 75;
99 default:
100 return sampleData;
101 }
102 }
103
104 interface InteractiveChartPreviewProps {
105 chartType: string;
106 className?: string;
107 variantOptions?: Record<string, unknown>;
108 }
109
110 export function InteractiveChartPreview({
111 chartType,
112 className,
113 variantOptions = EMPTY_VARIANT_OPTIONS,
114 }: InteractiveChartPreviewProps) {
115 const containerRef = useRef<HTMLDivElement | null>(null);
116 const [scale, setScale] = useState(0.3);
117
118 const chartData = getSampleDataForChart(chartType);
119
120 const previewOptions: Record<string, unknown> = {
121 ...variantOptions,
122 showLegend: false,
123 showGrid: false,
124 showAxisLabels: false,
125 disableAnimation: true,
126 previewMode: true,
127 };
128
129 useLayoutEffect(() => {
130 if (!containerRef.current) return;
131
132 const updateScale = () => {
133 const rect = containerRef.current?.getBoundingClientRect();
134 const nextScale = rect && rect.width > 0 ? rect.width / BASE_WIDTH : 0.3;
135 setScale(nextScale);
136 };
137
138 updateScale();
139
140 const resizeObserver = new ResizeObserver(updateScale);
141 resizeObserver.observe(containerRef.current);
142
143 return () => resizeObserver.disconnect();
144 }, []);
145
146 return (
147 <div
148 ref={containerRef}
149 className={`pointer-events-none relative isolate select-none **:pointer-events-none **:select-none ${className ?? ""}`}
150 style={{
151 height: BASE_HEIGHT * scale,
152 overflow: "hidden",
153 position: "relative",
154 }}
155 >
156 <div
157 className="relative"
158 style={{
159 width: BASE_WIDTH,
160 height: BASE_HEIGHT,
161 transform: `scale(${scale})`,
162 transformOrigin: "top left",
163 }}
164 >
165 <ChartRenderer
166 chartType={chartType}
167 chartData={chartData}
168 chartOptions={previewOptions}
169 className="pointer-events-none h-full w-full border-0 p-1 shadow-none"
170 />
171 </div>
172 </div>
173 );
174 }
175
175 lines Plain Text