From ab4c9fdb8fc09a3ee0e4578569c4c1f4417bb722 Mon Sep 17 00:00:00 2001
From: CalciumIon <1808837298@qq.com>
Date: Thu, 12 Dec 2024 14:56:16 +0800
Subject: [PATCH 1/3] feat: Enhance color mapping and chart rendering in Detail
component
- Added base and extended color palettes for improved model color mapping.
- Introduced a new `modelToColor` function to dynamically assign colors based on model names.
- Updated the Detail component to utilize the new color mapping for pie and line charts.
- Refactored chart data handling to support dynamic color assignment and improved data visualization.
- Cleaned up unused state variables and optimized data loading logic for better performance.
---
web/src/helpers/render.js | 63 +++++-
web/src/pages/Detail/index.js | 350 +++++++++++++++++-----------------
2 files changed, 239 insertions(+), 174 deletions(-)
diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js
index ef537eedf..345849b28 100644
--- a/web/src/helpers/render.js
+++ b/web/src/helpers/render.js
@@ -268,6 +268,44 @@ const colors = [
'yellow',
];
+// 基础10色色板 (N ≤ 10)
+const baseColors = [
+ '#1664FF', // 主色
+ '#1AC6FF',
+ '#FF8A00',
+ '#3CC780',
+ '#7442D4',
+ '#FFC400',
+ '#304D77',
+ '#B48DEB',
+ '#009488',
+ '#FF7DDA'
+];
+
+// 扩展20色色板 (10 < N ≤ 20)
+const extendedColors = [
+ '#1664FF',
+ '#B2CFFF',
+ '#1AC6FF',
+ '#94EFFF',
+ '#FF8A00',
+ '#FFCE7A',
+ '#3CC780',
+ '#B9EDCD',
+ '#7442D4',
+ '#DDC5FA',
+ '#FFC400',
+ '#FAE878',
+ '#304D77',
+ '#8B959E',
+ '#B48DEB',
+ '#EFE3FF',
+ '#009488',
+ '#59BAA8',
+ '#FF7DDA',
+ '#FFCFEE'
+];
+
export const modelColorMap = {
'dall-e': 'rgb(147,112,219)', // 深紫色
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
@@ -312,14 +350,33 @@ export const modelColorMap = {
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
};
+export function modelToColor(modelName) {
+ // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
+ if (modelColorMap[modelName]) {
+ return modelColorMap[modelName];
+ }
+
+ // 2. 生成一个稳定的数字作为索引
+ let hash = 0;
+ for (let i = 0; i < modelName.length; i++) {
+ hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
+ hash = hash & hash; // Convert to 32-bit integer
+ }
+ hash = Math.abs(hash);
+
+ // 3. 根据模型名称长度选择不同的色板
+ const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
+
+ // 4. 使用hash值选择颜色
+ const index = hash % colorPalette.length;
+ return colorPalette[index];
+}
+
export function stringToColor(str) {
let sum = 0;
- // 对字符串中的每个字符进行操作
for (let i = 0; i < str.length; i++) {
- // 将字符的ASCII值加到sum中
sum += str.charCodeAt(i);
}
- // 使用模运算得到个位数
let i = sum % colors.length;
return colors[i];
}
diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js
index f334dd3f6..a00b3453d 100644
--- a/web/src/pages/Detail/index.js
+++ b/web/src/pages/Detail/index.js
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
-import VChart from '@visactor/vchart';
+import { VChart } from "@visactor/react-vchart";
import {
API,
isAdmin,
@@ -17,6 +17,7 @@ import {
renderQuota,
renderQuotaNumberWithDigit,
stringToColor,
+ modelToColor,
} from '../../helpers/render';
const Detail = (props) => {
@@ -40,8 +41,6 @@ const Detail = (props) => {
inputs;
const isAdminUser = isAdmin();
const initialized = useRef(false);
- const [modelDataChart, setModelDataChart] = useState(null);
- const [modelDataPieChart, setModelDataPieChart] = useState(null);
const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
@@ -49,23 +48,68 @@ const Detail = (props) => {
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
localStorage.getItem('data_export_default_time') || 'hour',
);
-
- const handleInputChange = (value, name) => {
- if (name === 'data_export_default_time') {
- setDataExportDefaultTime(value);
- return;
- }
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- };
-
- const spec_line = {
- type: 'bar',
- data: [
- {
- id: 'barData',
- values: [],
+ const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
+ const [lineData, setLineData] = useState([]);
+ const [spec_pie, setSpecPie] = useState({
+ type: 'pie',
+ data: [{
+ id: 'id0',
+ values: pieData
+ }],
+ outerRadius: 0.8,
+ innerRadius: 0.5,
+ padAngle: 0.6,
+ valueField: 'value',
+ categoryField: 'type',
+ pie: {
+ style: {
+ cornerRadius: 10,
},
- ],
+ state: {
+ hover: {
+ outerRadius: 0.85,
+ stroke: '#000',
+ lineWidth: 1,
+ },
+ selected: {
+ outerRadius: 0.85,
+ stroke: '#000',
+ lineWidth: 1,
+ },
+ },
+ },
+ title: {
+ visible: true,
+ text: '模型调用次数占比',
+ subtext: `总计:${renderNumber(times)}`,
+ },
+ legends: {
+ visible: true,
+ orient: 'left',
+ },
+ label: {
+ visible: true,
+ },
+ tooltip: {
+ mark: {
+ content: [
+ {
+ key: (datum) => datum['type'],
+ value: (datum) => renderNumber(datum['value']),
+ },
+ ],
+ },
+ },
+ color: {
+ specified: modelColorMap,
+ },
+ });
+ const [spec_line, setSpecLine] = useState({
+ type: 'bar',
+ data: [{
+ id: 'barData',
+ values: lineData
+ }],
xField: 'Time',
yField: 'Usage',
seriesField: 'Model',
@@ -77,7 +121,7 @@ const Detail = (props) => {
title: {
visible: true,
text: '模型消耗分布',
- subtext: '0',
+ subtext: `总计:${renderQuota(consumeQuota, 2)}`,
},
bar: {
// The state style of bar
@@ -129,196 +173,160 @@ const Detail = (props) => {
color: {
specified: modelColorMap,
},
- };
+ });
- const spec_pie = {
- type: 'pie',
- data: [
- {
- id: 'id0',
- values: [{ type: 'null', value: '0' }],
- },
- ],
- outerRadius: 0.8,
- innerRadius: 0.5,
- padAngle: 0.6,
- valueField: 'value',
- categoryField: 'type',
- pie: {
- style: {
- cornerRadius: 10,
- },
- state: {
- hover: {
- outerRadius: 0.85,
- stroke: '#000',
- lineWidth: 1,
- },
- selected: {
- outerRadius: 0.85,
- stroke: '#000',
- lineWidth: 1,
- },
- },
- },
- title: {
- visible: true,
- text: '模型调用次数占比',
- },
- legends: {
- visible: true,
- orient: 'left',
- },
- label: {
- visible: true,
- },
- tooltip: {
- mark: {
- content: [
- {
- key: (datum) => datum['type'],
- value: (datum) => renderNumber(datum['value']),
- },
- ],
- },
- },
- color: {
- specified: modelColorMap,
- },
+ // 添加一个新的状态来存储模型-颜色映射
+ const [modelColors, setModelColors] = useState({});
+
+ const handleInputChange = (value, name) => {
+ if (name === 'data_export_default_time') {
+ setDataExportDefaultTime(value);
+ return;
+ }
+ setInputs((inputs) => ({ ...inputs, [name]: value }));
};
- const loadQuotaData = async (lineChart, pieChart) => {
+ const loadQuotaData = async () => {
setLoading(true);
-
- let url = '';
- let localStartTimestamp = Date.parse(start_timestamp) / 1000;
- let localEndTimestamp = Date.parse(end_timestamp) / 1000;
- if (isAdminUser) {
- url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
- } else {
- url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
- }
- const res = await API.get(url);
- const { success, message, data } = res.data;
- if (success) {
- setQuotaData(data);
- if (data.length === 0) {
- data.push({
- count: 0,
- model_name: '无数据',
- quota: 0,
- created_at: now.getTime() / 1000,
- });
+ try {
+ let url = '';
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
+ if (isAdminUser) {
+ url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
+ } else {
+ url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
- // 根据dataExportDefaultTime重制时间粒度
- let timeGranularity = 3600;
- if (dataExportDefaultTime === 'day') {
- timeGranularity = 86400;
- } else if (dataExportDefaultTime === 'week') {
- timeGranularity = 604800;
+ const res = await API.get(url);
+ const { success, message, data } = res.data;
+ if (success) {
+ setQuotaData(data);
+ if (data.length === 0) {
+ data.push({
+ count: 0,
+ model_name: '无数据',
+ quota: 0,
+ created_at: now.getTime() / 1000,
+ });
+ }
+ // 根据dataExportDefaultTime重制时间粒度
+ let timeGranularity = 3600;
+ if (dataExportDefaultTime === 'day') {
+ timeGranularity = 86400;
+ } else if (dataExportDefaultTime === 'week') {
+ timeGranularity = 604800;
+ }
+ // sort created_at
+ data.sort((a, b) => a.created_at - b.created_at);
+ data.forEach((item) => {
+ item['created_at'] =
+ Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
+ });
+ updateChartData(data);
+ } else {
+ showError(message);
}
- // sort created_at
- data.sort((a, b) => a.created_at - b.created_at);
- data.forEach((item) => {
- item['created_at'] =
- Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
- });
- updateChart(lineChart, pieChart, data);
- } else {
- showError(message);
+ } finally {
+ setLoading(false);
}
- setLoading(false);
};
const refresh = async () => {
- await loadQuotaData(modelDataChart, modelDataPieChart);
+ await loadQuotaData();
};
const initChart = async () => {
- let lineChart = modelDataChart;
- if (!modelDataChart) {
- lineChart = new VChart(spec_line, { dom: 'model_data' });
- setModelDataChart(lineChart);
- lineChart.renderAsync();
- }
- let pieChart = modelDataPieChart;
- if (!modelDataPieChart) {
- pieChart = new VChart(spec_pie, { dom: 'model_pie' });
- setModelDataPieChart(pieChart);
- pieChart.renderAsync();
- }
- console.log('init vchart');
- await loadQuotaData(lineChart, pieChart);
+ await loadQuotaData();
};
- const updateChart = (lineChart, pieChart, data) => {
- if (isAdminUser) {
- // 将所有用户合并
- }
- let pieData = [];
- let lineData = [];
- let consumeQuota = 0;
- let times = 0;
+ const updateChartData = (data) => {
+ let newPieData = [];
+ let newLineData = [];
+ let totalQuota = 0;
+ let totalTimes = 0;
+ let uniqueModels = new Set();
+
+ // 首先收集所有唯一的模型名称
+ data.forEach(item => uniqueModels.add(item.model_name));
+
+ // 为每个唯一的模型生成或获取颜色
+ const newModelColors = {};
+ Array.from(uniqueModels).forEach((modelName) => {
+ // 优先使用 modelColorMap 中的颜色,然后是已存在的颜色,最后使用新的颜色生成函数
+ newModelColors[modelName] = modelColorMap[modelName] ||
+ modelColors[modelName] ||
+ modelToColor(modelName); // 使用新的颜色生成函数替代 stringToColor
+ });
+ setModelColors(newModelColors);
+
for (let i = 0; i < data.length; i++) {
const item = data[i];
- consumeQuota += item.quota;
- times += item.count;
+ totalQuota += item.quota;
+ totalTimes += item.count;
// 合并model_name
- let pieItem = pieData.find((it) => it.type === item.model_name);
+ let pieItem = newPieData.find((it) => it.type === item.model_name);
if (pieItem) {
pieItem.value += item.count;
} else {
- pieData.push({
+ newPieData.push({
type: item.model_name,
value: item.count,
});
}
- // 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
- // 转换日期格式
+ // 合并created_at和model_name 为 lineData
let createTime = timestamp2string1(
item.created_at,
dataExportDefaultTime,
);
- let lineItem = lineData.find(
+ let lineItem = newLineData.find(
(it) => it.Time === createTime && it.Model === item.model_name,
);
if (lineItem) {
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
} else {
- lineData.push({
+ newLineData.push({
Time: createTime,
Model: item.model_name,
Usage: parseFloat(getQuotaWithUnit(item.quota)),
});
}
}
- setConsumeQuota(consumeQuota);
- setTimes(times);
// sort by count
- pieData.sort((a, b) => b.value - a.value);
- spec_pie.title.subtext = `总计:${renderNumber(times)}`;
- spec_pie.data[0].values = pieData;
+ newPieData.sort((a, b) => b.value - a.value);
- spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
- spec_line.data[0].values = lineData;
- pieChart.updateSpec(spec_pie);
- lineChart.updateSpec(spec_line);
+ // 更新图表配置和数据
+ setSpecPie(prev => ({
+ ...prev,
+ data: [{ id: 'id0', values: newPieData }],
+ title: {
+ ...prev.title,
+ subtext: `总计:${renderNumber(totalTimes)}`
+ },
+ color: {
+ specified: newModelColors
+ }
+ }));
- // pieChart.updateData('id0', pieData);
- // lineChart.updateData('barData', lineData);
- pieChart.reLayout();
- lineChart.reLayout();
+ setSpecLine(prev => ({
+ ...prev,
+ data: [{ id: 'barData', values: newLineData }],
+ title: {
+ ...prev.title,
+ subtext: `总计:${renderQuota(totalQuota, 2)}`
+ },
+ color: {
+ specified: newModelColors
+ }
+ }));
+
+ setPieData(newPieData);
+ setLineData(newLineData);
+ setConsumeQuota(totalQuota);
+ setTimes(totalTimes);
};
useEffect(() => {
- // setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
- // if (dataExportDefaultTime === 'day') {
- // // 设置开始时间为7天前
- // let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
- // inputs.start_timestamp = st;
- // formRef.current.formApi.setValue('start_timestamp', st);
- // }
if (!initialized.current) {
initVChartSemiTheme({
isWatchingThemeSwitch: true,
@@ -405,16 +413,16 @@ const Detail = (props) => {