作者 202304001

echart图表

import chatStyles from "@/app/components/chat.module.scss";
import homeStyles from "@/app/components/home.module.scss";
import styles from "./bgRemoval.module.scss";
import { WindowContent } from "@/app/components/home";
import { useMobileScreen } from "@/app/utils";
... ... @@ -12,14 +13,17 @@ import { getClientConfig } from "@/app/config/client";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useAppConfig } from "@/app/store";
import { BgSiderBar } from "./bg-siderBar";
import { message } from "antd";
import { Button, Flex, Upload, message } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd";
import ReturnIcon from "@/app/icons/return.svg";
import MinIcon from "@/app/icons/min.svg";
import MaxIcon from "@/app/icons/max.svg";
import SDIcon from "@/app/icons/sd.svg";
import { ChartComponent } from "../chart";
import LoadingIcon from "@/app/icons/three-dots.svg";
import BotIcon from "@/app/icons/bot.svg";
import CloseIcon from "@/app/icons/close.svg";
export function BgRemoval() {
const isMobileScreen = useMobileScreen();
... ... @@ -117,34 +121,48 @@ export function BgRemoval() {
</div>
</div>
<div className={chatStyles["chat-body"]} ref={scrollRef}>
{/* <Flex vertical justify='center' align="center" gap="middle" className={styles['panelFlex']}>
{isLoading ? (
<div className={clsx("no-dark", styles["loading-content"])}>
<BotIcon />
<LoadingIcon/>
</div>
) : previewUrl ? (
<div className={styles['preview']}>
<img src={previewUrl} alt="Preview" className={styles['previewImage']} />
<IconButton icon={<CloseIcon />} bordered onClick={closePic} className={styles['icon']} />
</div>
) : (
<Upload
onChange={onChange}
beforeUpload={(file) => {
setFileData(file);
return false;
}}
showUploadList={false}
accept="image/*"
>
<Button icon={<UploadOutlined />} size='large'>
上传图片
</Button>
</Upload>
)}
</Flex> */}
<ChartComponent />
<Flex
vertical
justify="center"
align="center"
gap="middle"
className={styles["panelFlex"]}
>
{isLoading ? (
<div className={clsx("no-dark", styles["loading-content"])}>
<BotIcon />
<LoadingIcon />
</div>
) : previewUrl ? (
<div className={styles["preview"]}>
<img
src={previewUrl}
alt="Preview"
className={styles["previewImage"]}
/>
<IconButton
icon={<CloseIcon />}
bordered
onClick={closePic}
className={styles["icon"]}
/>
</div>
) : (
<Upload
onChange={onChange}
beforeUpload={(file) => {
setFileData(file);
return false;
}}
showUploadList={false}
accept="image/*"
>
<Button icon={<UploadOutlined />} size="large">
上传图片
</Button>
</Upload>
)}
</Flex>
</div>
</div>
</WindowContent>
... ...
import { Card } from "antd";
import * as echarts from "echarts/core";
import { GridComponent, GridComponentOption } from "echarts/components";
import { BarChart, BarSeriesOption } from "echarts/charts";
import {
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent,
} from "echarts/components";
import {
BarChart,
PieChart,
LineChart,
BarSeriesOption,
PieSeriesOption,
LineSeriesOption,
} from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import { useRef, useState } from "react";
const tabList = [
{
key: "bar",
label: "柱状图",
},
{
key: "pie",
label: "饼图",
},
{
key: "line",
label: "折线图",
},
];
import { useRef, useEffect, useState } from "react";
import { ChartComponentProps, ChartData } from "./getChartData";
// 注册必要的组件
echarts.use([
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent,
BarChart,
PieChart,
LineChart,
CanvasRenderer,
]);
echarts.use([GridComponent, BarChart, CanvasRenderer]);
type EChartsOption = echarts.ComposeOption<
GridComponentOption | BarSeriesOption
BarSeriesOption | PieSeriesOption | LineSeriesOption
>;
var option: EChartsOption;
const tabList = [
{ key: "bar", label: "柱状图" },
{ key: "pie", label: "饼图" },
{ key: "line", label: "折线图" },
];
option = {
xAxis: {
type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
},
yAxis: {
type: "value",
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: "bar",
},
],
};
export function ChartComponent() {
const [activeTabKey, setActiveTabKey] = useState<string>("bar");
const line = useRef<HTMLCanvasElement>(null);
const bar = useRef<HTMLCanvasElement>(null);
const pie = useRef<HTMLCanvasElement>(null);
const contentListNoTitle: Record<string, React.ReactNode> = {
bar: <canvas ref={bar}></canvas>,
pie: <canvas ref={pie}></canvas>,
line: <canvas ref={line}></canvas>,
// 图表配置生成器
const getOption = (type: string, data: ChartData): EChartsOption => {
const commonOption = {
title: { text: `${tabList.find((t) => t.key === type)?.label}` },
tooltip: { trigger: "item" },
};
// var myChart = echarts.init(bar);
// 提取数值数组
const values = data.values.map((item) => item.value);
switch (type) {
case "bar":
return {
...commonOption,
xAxis: { type: "category", data: data.categories },
yAxis: { type: "value" },
series: [{ data: values, type: "bar" }],
};
case "pie":
return {
...commonOption,
series: [
{
type: "pie",
data: data.categories.map((name, i) => ({
name,
value: values[i],
})),
radius: "50%",
},
],
};
case "line":
return {
...commonOption,
xAxis: { type: "category", data: data.categories },
yAxis: { type: "value" },
series: [
{
data: values,
type: "line",
smooth: true,
areaStyle: {},
},
],
};
default:
return {};
}
};
export function ChartComponent({
data = { categories: [], values: [] },
}: ChartComponentProps) {
const [activeTabKey, setActiveTabKey] = useState("bar");
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
useEffect(() => {
if (!chartRef.current) return;
// 数据校验
if (
!data?.categories?.length ||
!data?.values?.length ||
data.values.some((item) => typeof item.value !== "number")
) {
console.warn("Invalid chart data");
return;
}
// 销毁旧实例
if (chartInstance.current) {
chartInstance.current.dispose();
}
// 初始化新图表
chartInstance.current = echarts.init(chartRef.current);
chartInstance.current.setOption(getOption(activeTabKey, data));
// 窗口resize监听
const resizeHandler = () => chartInstance.current?.resize();
window.addEventListener("resize", resizeHandler);
// 清理函数
return () => {
window.removeEventListener("resize", resizeHandler);
chartInstance.current?.dispose();
};
}, [activeTabKey, data]);
return (
<>
<Card
style={{ width: "100%" }}
tabList={tabList}
activeTabKey={activeTabKey}
onTabChange={setActiveTabKey}
tabProps={{ size: "middle" }}
>
{contentListNoTitle[activeTabKey]}
</Card>
</>
<Card
style={{ width: "100%", minHeight: 400 }}
tabList={tabList}
activeTabKey={activeTabKey}
onTabChange={setActiveTabKey}
tabProps={{ size: "middle" }}
>
<div
ref={chartRef}
style={{
width: "100%",
height: 400,
minHeight: 400,
}}
/>
</Card>
);
}
... ...
export interface ValueItem {
value: number;
itemStyle?: {
color: string;
};
}
export interface ChartData {
categories: string[];
values: ValueItem[];
}
export interface ChartComponentProps {
data: {
categories: string[];
values: ValueItem[];
};
}
export function extractDataFromText(text: string): ChartComponentProps | null {
const startMarkers = ["```javascript", "```json"];
let dataStartIndex = -1;
let markerLength = 0;
// 查找有效的数据块起始标记
for (const marker of startMarkers) {
const index = text.indexOf(marker);
if (index !== -1) {
dataStartIndex = index;
markerLength = marker.length;
break;
}
}
let parsedData: any = null;
// 如果找到有效数据块
if (dataStartIndex !== -1) {
// 查找数据块结束标记
const dataEndIndex = text.indexOf("```", dataStartIndex + markerLength);
if (dataEndIndex !== -1) {
// 提取并清理数据块内容
const dataBlock = text
.slice(dataStartIndex + markerLength, dataEndIndex)
.trim()
.replace(/'/g, '"') // 转换单引号为双引号
.replace(/(\w+)(?=\s*:)/g, '"$1"'); // 为键添加双引号
try {
// 尝试安全解析数据块内容
parsedData = JSON.parse(dataBlock);
} catch (error) {
return null;
}
}
}
// 如果没有找到数据块,尝试将整个文本解析为 ChartData
if (!parsedData) {
try {
const potentialData: any = JSON.parse(text);
// 验证解析后的数据是否符合 ChartData 结构
if (
potentialData &&
Array.isArray(potentialData.categories) &&
potentialData.categories.every((c: any) => typeof c === "string") &&
Array.isArray(potentialData.values) &&
potentialData.values.every(
(v: any) =>
typeof v?.value === "number" &&
v?.itemStyle &&
typeof v.itemStyle?.color === "string",
)
) {
parsedData = potentialData;
}
} catch (error) {
return null; // 如果解析失败,返回 null
}
}
// 如果成功解析了数据并且符合 ChartData 类型,返回
if (
parsedData &&
Array.isArray(parsedData.categories) &&
Array.isArray(parsedData.values) &&
parsedData.values.every(
(v: any) =>
typeof v?.value === "number" &&
v?.itemStyle &&
typeof v.itemStyle?.color === "string",
)
) {
return { data: parsedData as ChartData };
}
return null; // 如果未满足条件,则返回 null
}
... ...
... ... @@ -25,6 +25,9 @@ import { IconButton } from "./button";
import { useAppConfig } from "../store/config";
import clsx from "clsx";
import { ChartComponentProps, extractDataFromText } from "./chart/getChartData";
import { ChartComponent } from "./chart";
export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null);
const [hasError, setHasError] = useState(false);
... ... @@ -79,6 +82,7 @@ export function PreCode(props: { children: any }) {
const { height } = useWindowSize();
const chatStore = useChatStore();
const session = chatStore.currentSession();
const [chartData, setChartData] = useState<ChartComponentProps | null>(null);
const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return;
... ... @@ -102,10 +106,20 @@ export function PreCode(props: { children: any }) {
const config = useAppConfig();
const enableArtifacts =
session.mask?.enableArtifacts !== false && config.enableArtifacts;
//Wrap the paragraph for plain-text
useEffect(() => {
if (ref.current) {
const textContent = ref.current.innerText;
if (textContent) {
console.log("有textContent");
console.log(textContent);
const data = extractDataFromText(textContent);
console.log(data);
if (data) {
console.log("data");
setChartData(data);
}
}
const codeElements = ref.current.querySelectorAll(
"code",
) as NodeListOf<HTMLElement>;
... ... @@ -156,6 +170,7 @@ export function PreCode(props: { children: any }) {
/>
</FullScreen>
)}
{chartData && <ChartComponent data={chartData.data} />}
<pre ref={ref}>
<span
className="copy-code-button"
... ... @@ -271,7 +286,6 @@ function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => {
return tryWrapHtmlCode(escapeBrackets(props.content));
}, [props.content]);
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
... ...