返回 presentation-ai
echart-wrapper.tsx
1 "use client";
2
3 import { type EChartsOption } from "echarts";
4 import {
5 BarChart,
6 BoxplotChart,
7 CandlestickChart,
8 CustomChart,
9 FunnelChart,
10 GaugeChart,
11 GraphChart,
12 HeatmapChart,
13 LineChart,
14 PieChart,
15 RadarChart,
16 SankeyChart,
17 ScatterChart,
18 SunburstChart,
19 TreemapChart,
20 } from "echarts/charts";
21 import {
22 GraphicComponent,
23 GridComponent,
24 LegendComponent,
25 PolarComponent,
26 RadarComponent,
27 TitleComponent,
28 TooltipComponent,
29 VisualMapComponent,
30 } from "echarts/components";
31 import * as echarts from "echarts/core";
32 import { type EChartsType } from "echarts/core";
33 import { CanvasRenderer, SVGRenderer } from "echarts/renderers";
34 import { useCallback, useEffect, useRef, type CSSProperties } from "react";
35 import { useInView } from "react-intersection-observer";
36
37 import { cn } from "@/lib/utils";
38
39 export type PresentationEChartsOption = EChartsOption;
40 type EChartsSeriesItem = Record<string, unknown>;
41
42 echarts.use([
43 BarChart,
44 BoxplotChart,
45 CandlestickChart,
46 CustomChart,
47 FunnelChart,
48 GaugeChart,
49 GraphChart,
50 HeatmapChart,
51 LineChart,
52 PieChart,
53 RadarChart,
54 SankeyChart,
55 ScatterChart,
56 SunburstChart,
57 TreemapChart,
58 GraphicComponent,
59 GridComponent,
60 LegendComponent,
61 PolarComponent,
62 RadarComponent,
63 TitleComponent,
64 TooltipComponent,
65 VisualMapComponent,
66 CanvasRenderer,
67 SVGRenderer,
68 ]);
69
70 export interface EChartWrapperProps {
71 options: PresentationEChartsOption;
72 className?: string;
73 style?: CSSProperties;
74 renderer?: "svg" | "canvas";
75 previewMode?: boolean;
76 animationReplayDelayMs?: number;
77 isPresenting?: boolean;
78 }
79
80 function withoutEChartsAnimation(
81 options: PresentationEChartsOption,
82 ): PresentationEChartsOption {
83 const series = options.series;
84 const disableSeriesAnimation = (seriesItem: unknown): unknown => {
85 if (
86 typeof seriesItem !== "object" ||
87 seriesItem === null ||
88 Array.isArray(seriesItem)
89 ) {
90 return seriesItem;
91 }
92
93 return {
94 ...(seriesItem as EChartsSeriesItem),
95 animation: false,
96 animationDuration: 0,
97 animationDurationUpdate: 0,
98 };
99 };
100
101 return {
102 ...options,
103 animation: false,
104 animationDuration: 0,
105 animationDurationUpdate: 0,
106 series: Array.isArray(series)
107 ? (series.map(
108 disableSeriesAnimation,
109 ) as PresentationEChartsOption["series"])
110 : (disableSeriesAnimation(series) as PresentationEChartsOption["series"]),
111 };
112 }
113
114 export function EChartWrapper({
115 options,
116 className,
117 style,
118 renderer = "svg",
119 previewMode = false,
120 animationReplayDelayMs = 0,
121 isPresenting = false,
122 }: EChartWrapperProps) {
123 const rootRef = useRef<HTMLDivElement | null>(null);
124 const chartRef = useRef<EChartsType | null>(null);
125 const { inView, ref: inViewRef } = useInView({
126 threshold: 0.6,
127 });
128 const setRootRef = useCallback(
129 (node: HTMLDivElement | null) => {
130 rootRef.current = node;
131 inViewRef(node);
132 },
133 [inViewRef],
134 );
135
136 useEffect(() => {
137 const root = rootRef.current;
138 if (!root) return;
139
140 const chart =
141 chartRef.current ??
142 echarts.init(root, null, {
143 renderer,
144 });
145 chartRef.current = chart;
146
147 return () => {
148 chart.dispose();
149 chartRef.current = null;
150 };
151 }, [renderer]);
152
153 useEffect(() => {
154 const chart = chartRef.current;
155 if (!chart) return;
156 const visiblePresentAnimation = isPresenting && inView;
157
158 chart.setOption(
159 visiblePresentAnimation ? options : withoutEChartsAnimation(options),
160 {
161 notMerge: true,
162 lazyUpdate: false,
163 },
164 );
165 }, [inView, isPresenting, options]);
166
167 useEffect(() => {
168 if (!isPresenting || !inView || options.animation !== true) return;
169
170 const chart = chartRef.current;
171 if (!chart) return;
172
173 let timeoutId: number | null = null;
174 let firstFrameId: number | null = null;
175 let secondFrameId: number | null = null;
176
177 const replayAnimation = () => {
178 const currentChart = chartRef.current;
179 const root = rootRef.current;
180 if (!currentChart || !root) return;
181
182 currentChart.resize();
183 currentChart.clear();
184 currentChart.setOption(options, {
185 notMerge: true,
186 lazyUpdate: false,
187 });
188 };
189
190 timeoutId = window.setTimeout(() => {
191 firstFrameId = window.requestAnimationFrame(() => {
192 secondFrameId = window.requestAnimationFrame(replayAnimation);
193 });
194 }, animationReplayDelayMs);
195
196 return () => {
197 if (timeoutId !== null) {
198 window.clearTimeout(timeoutId);
199 }
200 if (firstFrameId !== null) {
201 window.cancelAnimationFrame(firstFrameId);
202 }
203 if (secondFrameId !== null) {
204 window.cancelAnimationFrame(secondFrameId);
205 }
206 };
207 }, [animationReplayDelayMs, inView, isPresenting, options]);
208
209 useEffect(() => {
210 const root = rootRef.current;
211 const chart = chartRef.current;
212 if (!root || !chart) return;
213
214 const observer = new ResizeObserver(() => {
215 chart.resize();
216 });
217 observer.observe(root);
218 chart.resize();
219
220 return () => {
221 observer.disconnect();
222 };
223 }, []);
224
225 return (
226 <div
227 className={cn("h-full min-h-[inherit] w-full", className)}
228 data-echart="true"
229 data-echart-renderer={renderer}
230 data-echart-preview={previewMode ? "true" : "false"}
231 data-ppt-ignore="true"
232 style={{
233 backgroundColor: "var(--presentation-background)",
234 color: "var(--presentation-text)",
235 ...style,
236 }}
237 >
238 <div ref={setRootRef} className="h-full min-h-[inherit] w-full" />
239 </div>
240 );
241 }
242
243 export const CHART_COLORS = [
244 "#2563eb",
245 "#16a34a",
246 "#ea580c",
247 "#8b5cf6",
248 "#ec4899",
249 "#14b8a6",
250 "#f59e0b",
251 "#ef4444",
252 "#06b6d4",
253 "#84cc16",
254 ];
255
256 export function getChartColor(index: number): string {
257 return CHART_COLORS[index % CHART_COLORS.length] ?? CHART_COLORS[0]!;
258 }
259
260 export function getChartColors(): string[] {
261 return CHART_COLORS;
262 }
263
264
264 lines Plain Text