fix(chat): normalize chart tool data
This commit is contained in:
@@ -16,11 +16,25 @@ export interface ChatChartSeries {
|
||||
type?: "line" | "bar";
|
||||
}
|
||||
|
||||
type RawChartPoint =
|
||||
| number
|
||||
| string
|
||||
| [unknown, unknown]
|
||||
| { x?: unknown; y?: unknown; time?: unknown; timestamp?: unknown; label?: unknown; name?: unknown; value?: unknown };
|
||||
|
||||
type RawChartSeries = {
|
||||
name?: unknown;
|
||||
data?: unknown;
|
||||
points?: unknown;
|
||||
values?: unknown;
|
||||
type?: unknown;
|
||||
};
|
||||
|
||||
export interface ChatInlineChartProps {
|
||||
title?: string;
|
||||
chart_type?: "line" | "bar" | "pie";
|
||||
x_data?: string[];
|
||||
series?: ChatChartSeries[];
|
||||
x_data?: unknown;
|
||||
series?: unknown;
|
||||
y_axis_name?: string;
|
||||
x_axis_name?: string;
|
||||
}
|
||||
@@ -37,23 +51,148 @@ const COLORS = [
|
||||
"#ea7ccc",
|
||||
];
|
||||
|
||||
const toFiniteNumber = (value: unknown): number | null => {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const pointToLabelValue = (
|
||||
point: RawChartPoint,
|
||||
fallbackLabel: string,
|
||||
): { label: string; value: number } | null => {
|
||||
const directValue = toFiniteNumber(point);
|
||||
if (directValue !== null) {
|
||||
return { label: fallbackLabel, value: directValue };
|
||||
}
|
||||
|
||||
if (Array.isArray(point)) {
|
||||
const value = toFiniteNumber(point[1]);
|
||||
if (value === null) return null;
|
||||
return { label: String(point[0] ?? fallbackLabel), value };
|
||||
}
|
||||
|
||||
if (point && typeof point === "object") {
|
||||
const value = toFiniteNumber(point.value ?? point.y);
|
||||
if (value === null) return null;
|
||||
const label =
|
||||
point.x ?? point.time ?? point.timestamp ?? point.label ?? point.name ?? fallbackLabel;
|
||||
return { label: String(label), value };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const normalizeXData = (rawXData: unknown): string[] =>
|
||||
Array.isArray(rawXData)
|
||||
? rawXData.map((item) => String(item ?? "")).filter((item) => item.length > 0)
|
||||
: [];
|
||||
|
||||
const normalizeSeriesType = (type: unknown): "line" | "bar" | undefined =>
|
||||
type === "line" || type === "bar" ? type : undefined;
|
||||
|
||||
const isRawChartPoint = (item: unknown): boolean => {
|
||||
if (toFiniteNumber(item) !== null) return true;
|
||||
if (Array.isArray(item)) return item.length >= 2 && toFiniteNumber(item[1]) !== null;
|
||||
if (item && typeof item === "object") {
|
||||
const rawItem = item as RawChartSeries & RawChartPoint;
|
||||
return (
|
||||
rawItem.data === undefined &&
|
||||
rawItem.points === undefined &&
|
||||
rawItem.values === undefined &&
|
||||
toFiniteNumber(rawItem.value ?? rawItem.y) !== null
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const normalizeRawSeriesItems = (rawSeries: unknown): unknown[] => {
|
||||
if (!Array.isArray(rawSeries)) {
|
||||
return rawSeries && typeof rawSeries === "object" ? [rawSeries] : [];
|
||||
}
|
||||
|
||||
return rawSeries.length > 0 && rawSeries.every(isRawChartPoint)
|
||||
? [{ name: "数据", data: rawSeries }]
|
||||
: rawSeries;
|
||||
};
|
||||
|
||||
export const normalizeChartData = (
|
||||
rawXData: unknown,
|
||||
rawSeries: unknown,
|
||||
): { xData: string[]; series: ChatChartSeries[] } => {
|
||||
const xData = normalizeXData(rawXData);
|
||||
const rawSeriesItems = normalizeRawSeriesItems(rawSeries);
|
||||
if (!rawSeriesItems.length) {
|
||||
return { xData, series: [] };
|
||||
}
|
||||
|
||||
const normalizedSeries = rawSeriesItems
|
||||
.map((rawItem, seriesIndex): ChatChartSeries | null => {
|
||||
const item =
|
||||
rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)
|
||||
? (rawItem as RawChartSeries)
|
||||
: ({ data: rawItem } satisfies RawChartSeries);
|
||||
const rawData = item.data ?? item.points ?? item.values;
|
||||
if (!Array.isArray(rawData)) return null;
|
||||
|
||||
const labelsFromPoints: string[] = [];
|
||||
const data = rawData
|
||||
.map((point, index) => {
|
||||
const parsed = pointToLabelValue(
|
||||
point as RawChartPoint,
|
||||
xData[index] ?? `${index + 1}`,
|
||||
);
|
||||
if (!parsed) return null;
|
||||
labelsFromPoints[index] = parsed.label;
|
||||
return parsed.value;
|
||||
})
|
||||
.filter((value): value is number => value !== null);
|
||||
|
||||
if (!data.length) return null;
|
||||
if (!xData.length && labelsFromPoints.length) {
|
||||
xData.push(...labelsFromPoints);
|
||||
}
|
||||
|
||||
return {
|
||||
name:
|
||||
typeof item.name === "string" && item.name.trim()
|
||||
? item.name
|
||||
: `系列 ${seriesIndex + 1}`,
|
||||
data,
|
||||
type: normalizeSeriesType(item.type),
|
||||
};
|
||||
})
|
||||
.filter((item): item is ChatChartSeries => Boolean(item));
|
||||
|
||||
return { xData, series: normalizedSeries };
|
||||
};
|
||||
|
||||
export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
|
||||
title,
|
||||
chart_type: chartType = "line",
|
||||
x_data: xData,
|
||||
series = [],
|
||||
x_data,
|
||||
series,
|
||||
y_axis_name: yAxisName,
|
||||
x_axis_name: xAxisName,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { xData, series: chartSeries } = useMemo(
|
||||
() => normalizeChartData(x_data, series),
|
||||
[x_data, series],
|
||||
);
|
||||
|
||||
const option = useMemo(() => {
|
||||
if (!series.length) return null;
|
||||
if (!chartSeries.length) return null;
|
||||
|
||||
/* ---------- Pie chart ---------- */
|
||||
if (chartType === "pie") {
|
||||
const pieData =
|
||||
series[0]?.data.map((value, i) => ({
|
||||
chartSeries[0]?.data.map((value, i) => ({
|
||||
name: xData?.[i] ?? `${i}`,
|
||||
value,
|
||||
})) ?? [];
|
||||
@@ -111,7 +250,7 @@ export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
|
||||
xData && xData.length > 20
|
||||
? [{ type: "inside", start: 0, end: 100 }]
|
||||
: undefined,
|
||||
series: series.map((s, i) => {
|
||||
series: chartSeries.map((s, i) => {
|
||||
const color = COLORS[i % COLORS.length];
|
||||
return {
|
||||
name: s.name,
|
||||
@@ -135,7 +274,7 @@ export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
|
||||
}),
|
||||
color: COLORS,
|
||||
};
|
||||
}, [chartType, xData, series, title, yAxisName, xAxisName]);
|
||||
}, [chartType, xData, chartSeries, title, yAxisName, xAxisName]);
|
||||
|
||||
if (!option) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user