Files
TJWaterFrontend_Refine/src/components/chat/ChatInlineChart.tsx
T

179 lines
4.4 KiB
TypeScript

"use client";
import React, { useMemo } from "react";
import ReactECharts from "echarts-for-react";
import * as echarts from "echarts";
import { Box, Paper, Typography, alpha, useTheme } from "@mui/material";
/* ------------------------------------------------------------------ */
/* Inline chart rendered inside a chat message bubble. */
/* Accepts structured data produced by the AI tool_call. */
/* ------------------------------------------------------------------ */
export interface ChatChartSeries {
name: string;
data: number[];
type?: "line" | "bar";
}
export interface ChatInlineChartProps {
title?: string;
chart_type?: "line" | "bar" | "pie";
x_data?: string[];
series?: ChatChartSeries[];
y_axis_name?: string;
x_axis_name?: string;
}
const COLORS = [
"#5470c6",
"#91cc75",
"#fac858",
"#ee6666",
"#73c0de",
"#3ba272",
"#fc8452",
"#9a60b4",
"#ea7ccc",
];
export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
title,
chart_type: chartType = "line",
x_data: xData,
series = [],
y_axis_name: yAxisName,
x_axis_name: xAxisName,
}) => {
const theme = useTheme();
const option = useMemo(() => {
if (!series.length) return null;
/* ---------- Pie chart ---------- */
if (chartType === "pie") {
const pieData =
series[0]?.data.map((value, i) => ({
name: xData?.[i] ?? `${i}`,
value,
})) ?? [];
return {
tooltip: { trigger: "item" },
legend: { top: "bottom", textStyle: { fontSize: 11 } },
series: [
{
type: "pie",
radius: ["30%", "60%"],
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
label: { fontSize: 11 },
},
],
color: COLORS,
};
}
/* ---------- Line / Bar chart ---------- */
return {
tooltip: { trigger: "axis", confine: true },
legend: { top: "top", textStyle: { fontSize: 11 } },
grid: {
left: "5%",
right: "5%",
bottom: "12%",
top: title ? "18%" : "14%",
containLabel: true,
},
xAxis: {
type: "category" as const,
boundaryGap: chartType === "bar",
data: xData ?? [],
axisLabel: {
fontSize: 10,
rotate: xData && xData.length > 10 ? 30 : 0,
},
name: xAxisName,
},
yAxis: {
type: "value" as const,
scale: true,
axisLabel: { fontSize: 10 },
name: yAxisName,
},
dataZoom:
xData && xData.length > 20
? [{ type: "inside", start: 0, end: 100 }]
: undefined,
series: series.map((s, i) => {
const color = COLORS[i % COLORS.length];
return {
name: s.name,
type: (s.type ?? chartType) as string,
data: s.data,
symbol: chartType === "line" ? "none" : undefined,
smooth: chartType === "line",
itemStyle: { color },
...(chartType === "line"
? {
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: alpha(color, 0.3) },
{ offset: 1, color: alpha(color, 0.05) },
]),
opacity: 0.3,
},
}
: {}),
};
}),
color: COLORS,
};
}, [chartType, xData, series, title, yAxisName, xAxisName]);
if (!option) {
return (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
</Typography>
);
}
return (
<Paper
elevation={0}
sx={{
mt: 1.5,
mb: 1,
borderRadius: 3,
border: `1px solid ${alpha(theme.palette.divider, 0.15)}`,
bgcolor: alpha("#fff", 0.92),
overflow: "hidden",
}}
>
{title && (
<Typography
variant="subtitle2"
sx={{ px: 2, pt: 1.5, fontWeight: 600, color: "text.primary" }}
>
{title}
</Typography>
)}
<Box sx={{ px: 1, pb: 1 }}>
<ReactECharts
option={option}
style={{ height: 240, width: "100%" }}
notMerge
lazyUpdate
/>
</Box>
</Paper>
);
};