mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-15 15:41:05 +00:00
226
packages/web/components/common/charts/LineChartComponent.tsx
Normal file
226
packages/web/components/common/charts/LineChartComponent.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Box, HStack, useTheme } from '@chakra-ui/react';
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
type TooltipProps
|
||||
} from 'recharts';
|
||||
import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
import FillRowTabs from '../Tabs/FillRowTabs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
type LineConfig = {
|
||||
dataKey: string;
|
||||
name: string;
|
||||
color: string;
|
||||
gradient?: boolean;
|
||||
};
|
||||
|
||||
type TooltipItem = {
|
||||
label: string;
|
||||
dataKey: string;
|
||||
color: string;
|
||||
formatter?: (value: number) => string;
|
||||
customValue?: (data: Record<string, any>) => number;
|
||||
};
|
||||
|
||||
type LineChartComponentProps = {
|
||||
data: Record<string, any>[];
|
||||
title: string;
|
||||
HeaderLeftChildren?: React.ReactNode;
|
||||
lines: LineConfig[];
|
||||
tooltipItems?: TooltipItem[];
|
||||
|
||||
defaultDisplayMode?: 'incremental' | 'cumulative';
|
||||
enableIncremental?: boolean;
|
||||
enableCumulative?: boolean;
|
||||
enableTooltip?: boolean;
|
||||
startDateValue?: number;
|
||||
};
|
||||
|
||||
const CustomTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
tooltipItems
|
||||
}: TooltipProps<ValueType, NameType> & { tooltipItems?: TooltipItem[] }) => {
|
||||
const data = payload?.[0]?.payload;
|
||||
|
||||
if (!active || !data || !tooltipItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box bg="white" p={3} borderRadius="md" border="base" boxShadow="sm">
|
||||
<Box fontSize="sm" color="myGray.900" mb={2}>
|
||||
{data.xLabel || data.x}
|
||||
</Box>
|
||||
{tooltipItems.map((item, index) => {
|
||||
const value = item.customValue ? item.customValue(data) : data[item.dataKey];
|
||||
const displayValue = item.formatter ? item.formatter(value) : formatNumber(value);
|
||||
|
||||
return (
|
||||
<HStack key={index} fontSize="sm" _notLast={{ mb: 1 }}>
|
||||
<Box w={2} h={2} borderRadius="full" bg={item.color} />
|
||||
<Box>{item.label}</Box>
|
||||
<Box>{displayValue.toLocaleString()}</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const LineChartComponent = ({
|
||||
data,
|
||||
title,
|
||||
HeaderLeftChildren,
|
||||
lines,
|
||||
tooltipItems,
|
||||
defaultDisplayMode = 'incremental',
|
||||
enableIncremental = true,
|
||||
enableCumulative = true,
|
||||
startDateValue = 0
|
||||
}: LineChartComponentProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [displayMode, setDisplayMode] = useState<'incremental' | 'cumulative'>(defaultDisplayMode);
|
||||
|
||||
// Tab list constant
|
||||
const tabList = useMemo(
|
||||
() => [
|
||||
...(enableIncremental
|
||||
? [{ label: t('common:chart_mode_incremental'), value: 'incremental' as const }]
|
||||
: []),
|
||||
...(enableCumulative
|
||||
? [{ label: t('common:chart_mode_cumulative'), value: 'cumulative' as const }]
|
||||
: [])
|
||||
],
|
||||
[enableCumulative, enableIncremental, t]
|
||||
);
|
||||
|
||||
// Y-axis number formatter function
|
||||
const formatYAxisNumber = useCallback((value: number): string => {
|
||||
if (value >= 1000000) {
|
||||
return value / 1000000 + 'M';
|
||||
} else if (value >= 1000) {
|
||||
return value / 1000 + 'K';
|
||||
}
|
||||
return value.toString();
|
||||
}, []);
|
||||
|
||||
// Process data based on display mode
|
||||
const processedData = useMemo(() => {
|
||||
if (displayMode === 'incremental') {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Cumulative mode: accumulate values for each line's dataKey
|
||||
const cloneData = cloneDeep(data);
|
||||
|
||||
const dataKeys = lines.map((item) => item.dataKey);
|
||||
|
||||
return cloneData.map((item, index) => {
|
||||
if (index === 0) {
|
||||
item[dataKeys[0]] = startDateValue + item[dataKeys[0]];
|
||||
return item;
|
||||
}
|
||||
|
||||
dataKeys.forEach((key) => {
|
||||
if (typeof item[key] === 'number') {
|
||||
item[key] += cloneData[index - 1][key];
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
});
|
||||
}, [displayMode, data, lines, startDateValue]);
|
||||
|
||||
// Generate gradient definitions
|
||||
const gradientDefs = useMemo(
|
||||
() => (
|
||||
<defs>
|
||||
{lines.map((line) => (
|
||||
<linearGradient
|
||||
key={`gradient-${line.color}`}
|
||||
id={`gradient-${line.color}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor={line.color} stopOpacity={0.25} />
|
||||
<stop offset="100%" stopColor={line.color} stopOpacity={0.01} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
),
|
||||
[lines]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack mb={4} justifyContent={'space-between'} alignItems={'flex-start'}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'medium'}>
|
||||
{title}
|
||||
</Box>
|
||||
<HStack spacing={2}>
|
||||
{HeaderLeftChildren}
|
||||
{tabList.length > 1 && (
|
||||
<FillRowTabs<'incremental' | 'cumulative'>
|
||||
list={tabList}
|
||||
py={0.5}
|
||||
px={2}
|
||||
value={displayMode}
|
||||
onChange={setDisplayMode}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
<ResponsiveContainer width="100%" height={'100%'}>
|
||||
<AreaChart
|
||||
data={processedData}
|
||||
margin={{ top: 5, right: 30, left: 0, bottom: HeaderLeftChildren ? 20 : 15 }}
|
||||
>
|
||||
{gradientDefs}
|
||||
<XAxis
|
||||
dataKey="x"
|
||||
tickMargin={10}
|
||||
tick={{ fontSize: '12px', color: theme?.colors?.myGray['500'], fontWeight: '500' }}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickSize={0}
|
||||
tickMargin={10}
|
||||
tick={{ fontSize: '12px', color: theme?.colors?.myGray['500'], fontWeight: '500' }}
|
||||
interval="preserveStartEnd"
|
||||
tickFormatter={formatYAxisNumber}
|
||||
/>
|
||||
<CartesianGrid strokeDasharray="3 3" horizontal={true} vertical={false} />
|
||||
{tooltipItems && <Tooltip content={<CustomTooltip tooltipItems={tooltipItems} />} />}
|
||||
{lines.map((line, index) => (
|
||||
<Area
|
||||
key={line.dataKey}
|
||||
type="monotone"
|
||||
name={line.name}
|
||||
dataKey={line.dataKey}
|
||||
stroke={line.color}
|
||||
strokeWidth={2}
|
||||
fill={`url(#gradient-${line.color})`}
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LineChartComponent;
|
@@ -16,8 +16,6 @@
|
||||
"channel_status_enabled": "Enable",
|
||||
"channel_status_unknown": "unknown",
|
||||
"channel_type": "Protocol Type",
|
||||
"chart_mode_cumulative": "Cumulative",
|
||||
"chart_mode_incremental": "Incremental",
|
||||
"clear_model": "Clear the model",
|
||||
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
|
||||
"copy_model_id_success": "Copyed model id",
|
||||
|
@@ -108,6 +108,8 @@
|
||||
"button.extra_dataset_size_tip": "You are purchasing [Extra Knowledge Base Capacity]",
|
||||
"button.extra_points_tip": "You are purchasing [Extra AI Points]",
|
||||
"can_copy_content_tip": "It is not possible to copy automatically using the browser, please manually copy the following content",
|
||||
"chart_mode_cumulative": "Cumulative",
|
||||
"chart_mode_incremental": "Incremental",
|
||||
"choosable": "Choosable",
|
||||
"chose_condition": "Choose Condition",
|
||||
"chosen": "Chosen",
|
||||
@@ -264,7 +266,6 @@
|
||||
"core.app.have_saved": "Saved",
|
||||
"core.app.logs.Source And Time": "Source & Time",
|
||||
"core.app.more": "View More",
|
||||
"name": "name",
|
||||
"core.app.no_app": "No Apps Yet, Create One Now!",
|
||||
"core.app.not_saved": "Not Saved",
|
||||
"core.app.outLink.Can Drag": "Icon Can Be Dragged",
|
||||
@@ -732,7 +733,6 @@
|
||||
"core.workflow.inputType.switch": "Switch",
|
||||
"core.workflow.inputType.textInput": "Text Input box",
|
||||
"core.workflow.inputType.textarea": "Multi-line Input Box",
|
||||
"key": "key",
|
||||
"core.workflow.publish.OnRevert version": "Click to Revert to This Version",
|
||||
"core.workflow.publish.OnRevert version confirm": "Confirm to Revert to This Version? The configuration of the editing version will be saved, and a new release version will be created for the reverted version.",
|
||||
"core.workflow.publish.histories": "Release Records",
|
||||
@@ -741,7 +741,6 @@
|
||||
"core.workflow.template.Search": "Search",
|
||||
"core.workflow.tool.Handle": "Tool Connector",
|
||||
"core.workflow.tool.Select Tool": "Select Tool",
|
||||
"value": "Value",
|
||||
"core.workflow.variable": "Variable",
|
||||
"create": "Create",
|
||||
"create_failed": "Create failed",
|
||||
@@ -861,6 +860,7 @@
|
||||
"json_config": "JSON Configuration",
|
||||
"json_parse_error": "Possible JSON Error, Please Check Carefully",
|
||||
"just_now": "just",
|
||||
"key": "key",
|
||||
"key_repetition": "Key Repetition",
|
||||
"last_step": "Previous",
|
||||
"last_use_time": "Last Use Time",
|
||||
@@ -903,6 +903,7 @@
|
||||
"move.confirm": "Confirm move",
|
||||
"move_success": "Moved Successfully",
|
||||
"move_to": "Move to",
|
||||
"name": "name",
|
||||
"name_is_empty": "Name Cannot Be Empty",
|
||||
"navbar.Account": "Account",
|
||||
"navbar.Chat": "Chat",
|
||||
@@ -1300,6 +1301,7 @@
|
||||
"user.team.role.writer": "writable member",
|
||||
"user.type": "Type",
|
||||
"user_leaved": "Leaved",
|
||||
"value": "Value",
|
||||
"verification": "Verification",
|
||||
"workflow.template.communication": "Communication",
|
||||
"xx_search_result": "{{key}} Search Results",
|
||||
|
@@ -16,8 +16,6 @@
|
||||
"channel_status_enabled": "启用",
|
||||
"channel_status_unknown": "未知",
|
||||
"channel_type": "协议类型",
|
||||
"chart_mode_cumulative": "累积",
|
||||
"chart_mode_incremental": "分时",
|
||||
"clear_model": "清空模型",
|
||||
"confirm_delete_channel": "确认删除 【{{name}}】渠道?",
|
||||
"copy_model_id_success": "已复制模型id",
|
||||
|
@@ -108,6 +108,8 @@
|
||||
"button.extra_dataset_size_tip": "您正在购买【额外知识库容量】",
|
||||
"button.extra_points_tip": "您正在购买【额外 AI 积分】",
|
||||
"can_copy_content_tip": "无法使用浏览器自动复制,请手动复制下面内容",
|
||||
"chart_mode_cumulative": "累积",
|
||||
"chart_mode_incremental": "分时",
|
||||
"choosable": "可选",
|
||||
"chose_condition": "选择条件",
|
||||
"chosen": "已选",
|
||||
@@ -264,7 +266,6 @@
|
||||
"core.app.have_saved": "已保存",
|
||||
"core.app.logs.Source And Time": "来源 & 时间",
|
||||
"core.app.more": "查看更多",
|
||||
"name": "名称",
|
||||
"core.app.no_app": "还没有应用,快去创建一个吧!",
|
||||
"core.app.not_saved": "未保存",
|
||||
"core.app.outLink.Can Drag": "图标可拖拽",
|
||||
@@ -732,7 +733,6 @@
|
||||
"core.workflow.inputType.switch": "开关",
|
||||
"core.workflow.inputType.textInput": "文本输入框",
|
||||
"core.workflow.inputType.textarea": "多行输入框",
|
||||
"key": "键",
|
||||
"core.workflow.publish.OnRevert version": "点击回退到该版本",
|
||||
"core.workflow.publish.OnRevert version confirm": "确认回退至该版本?会为您保存编辑中版本的配置,并为回退版本创建一个新的发布版本。",
|
||||
"core.workflow.publish.histories": "发布记录",
|
||||
@@ -741,7 +741,6 @@
|
||||
"core.workflow.template.Search": "搜索",
|
||||
"core.workflow.tool.Handle": "工具连接器",
|
||||
"core.workflow.tool.Select Tool": "选择工具",
|
||||
"value": "值",
|
||||
"core.workflow.variable": "变量",
|
||||
"create": "去创建",
|
||||
"create_failed": "创建失败",
|
||||
@@ -861,6 +860,7 @@
|
||||
"json_config": "JSON 配置",
|
||||
"json_parse_error": "JSON 可能有误,请仔细检查",
|
||||
"just_now": "刚刚",
|
||||
"key": "键",
|
||||
"key_repetition": "key 重复",
|
||||
"last_step": "上一步",
|
||||
"last_use_time": "最后使用时间",
|
||||
@@ -903,6 +903,7 @@
|
||||
"move.confirm": "确认移动",
|
||||
"move_success": "移动成功",
|
||||
"move_to": "移动到",
|
||||
"name": "名称",
|
||||
"name_is_empty": "名称不能为空",
|
||||
"navbar.Account": "账号",
|
||||
"navbar.Chat": "聊天",
|
||||
@@ -1300,6 +1301,7 @@
|
||||
"user.team.role.writer": "可写成员",
|
||||
"user.type": "类型",
|
||||
"user_leaved": "已离开",
|
||||
"value": "值",
|
||||
"verification": "验证",
|
||||
"workflow.template.communication": "通信",
|
||||
"xx_search_result": "{{key}} 的搜索结果",
|
||||
|
@@ -16,8 +16,6 @@
|
||||
"channel_status_enabled": "啟用",
|
||||
"channel_status_unknown": "未知",
|
||||
"channel_type": "協議類型",
|
||||
"chart_mode_cumulative": "累積",
|
||||
"chart_mode_incremental": "分時",
|
||||
"clear_model": "清空模型",
|
||||
"confirm_delete_channel": "確認刪除【{{name}}】管道?",
|
||||
"copy_model_id_success": "已復制模型 id",
|
||||
|
@@ -108,6 +108,8 @@
|
||||
"button.extra_dataset_size_tip": "您正在購買【額外知識庫容量】",
|
||||
"button.extra_points_tip": "您正在購買【額外 AI 積分】",
|
||||
"can_copy_content_tip": "無法使用瀏覽器自動複製,請手動複製下面內容",
|
||||
"chart_mode_cumulative": "累積",
|
||||
"chart_mode_incremental": "分時",
|
||||
"choosable": "可選擇",
|
||||
"chose_condition": "選擇條件",
|
||||
"chosen": "已選擇",
|
||||
@@ -264,7 +266,6 @@
|
||||
"core.app.have_saved": "已儲存",
|
||||
"core.app.logs.Source And Time": "來源與時間",
|
||||
"core.app.more": "檢視更多",
|
||||
"name": "名稱",
|
||||
"core.app.no_app": "還沒有應用程式,快來建立一個吧!",
|
||||
"core.app.not_saved": "未儲存",
|
||||
"core.app.outLink.Can Drag": "圖示可拖曳",
|
||||
@@ -732,7 +733,6 @@
|
||||
"core.workflow.inputType.switch": "開關",
|
||||
"core.workflow.inputType.textInput": "文字輸入框",
|
||||
"core.workflow.inputType.textarea": "多行輸入框",
|
||||
"key": "鍵",
|
||||
"core.workflow.publish.OnRevert version": "點選回復至此版本",
|
||||
"core.workflow.publish.OnRevert version confirm": "確認回復至此版本?將為您儲存編輯中版本的設定,並為回復版本建立一個新的發布版本。",
|
||||
"core.workflow.publish.histories": "發布記錄",
|
||||
@@ -741,7 +741,6 @@
|
||||
"core.workflow.template.Search": "搜尋",
|
||||
"core.workflow.tool.Handle": "工具聯結器",
|
||||
"core.workflow.tool.Select Tool": "選擇工具",
|
||||
"value": "值",
|
||||
"core.workflow.variable": "變數",
|
||||
"create": "建立",
|
||||
"create_failed": "建立失敗",
|
||||
@@ -861,6 +860,7 @@
|
||||
"json_config": "JSON 設定",
|
||||
"json_parse_error": "可能有 JSON 錯誤,請仔細檢查",
|
||||
"just_now": "剛剛",
|
||||
"key": "鍵",
|
||||
"key_repetition": "鍵值重複",
|
||||
"last_step": "上一步",
|
||||
"last_use_time": "最後使用時間",
|
||||
@@ -903,6 +903,7 @@
|
||||
"move.confirm": "確認移動",
|
||||
"move_success": "移動成功",
|
||||
"move_to": "移動至",
|
||||
"name": "名稱",
|
||||
"name_is_empty": "名稱不能為空",
|
||||
"navbar.Account": "帳戶",
|
||||
"navbar.Chat": "對話",
|
||||
@@ -1300,6 +1301,7 @@
|
||||
"user.team.role.writer": "可寫入成員",
|
||||
"user.type": "類型",
|
||||
"user_leaved": "已離開",
|
||||
"value": "值",
|
||||
"verification": "驗證",
|
||||
"workflow.template.communication": "通訊",
|
||||
"xx_search_result": "{{key}} 的搜尋結果",
|
||||
|
@@ -21,6 +21,7 @@
|
||||
"ahooks": "^3.7.11",
|
||||
"date-fns": "2.30.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"recharts": "^2.15.0",
|
||||
"i18next": "23.16.8",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lexical": "0.12.6",
|
||||
|
Reference in New Issue
Block a user