export type SafeChartSpec = { title?: string; chartType: "line" | "bar"; xAxisName?: string; yAxisName?: string; }; export type SafeChartSeries = { name: string; data: number[]; type?: "line" | "bar"; }; export type SafeChartData = { xData: string[]; series: SafeChartSeries[]; }; type RawChartPoint = number | string | [unknown, unknown] | RawChartPointObject; type RawChartPointObject = { 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 function parseChartSpec(value: unknown): SafeChartSpec { if (!isRecord(value)) { return { chartType: "line" }; } return { title: stringValue(value.title), chartType: value.chart_type === "bar" || value.chartType === "bar" ? "bar" : "line", xAxisName: stringValue(value.x_axis_name ?? value.xAxisName), yAxisName: stringValue(value.y_axis_name ?? value.yAxisName) }; } export function normalizeSafeChartData(value: unknown, maxPoints = 24): SafeChartData | null { if (!isRecord(value)) { return null; } const xData = normalizeXData(value.x_data ?? value.xData); const rawSeriesItems = normalizeRawSeriesItems(value.series); if (!rawSeriesItems.length) { return null; } const normalizedSeries = rawSeriesItems .map((rawItem, seriesIndex): SafeChartSeries | null => { const item = isRecord(rawItem) && !isRawChartPoint(rawItem) ? rawItem : ({ data: rawItem } satisfies RawChartSeries); const rawData = item.data ?? item.points ?? item.values; if (!Array.isArray(rawData)) { return null; } const labelsFromPoints: string[] = []; const data = rawData .slice(0, maxPoints) .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((item): item is number => item !== null); if (!data.length) { return null; } if (!xData.length && labelsFromPoints.length) { xData.push(...labelsFromPoints); } return { name: stringValue(item.name) ?? `系列 ${seriesIndex + 1}`, data, type: item.type === "line" || item.type === "bar" ? item.type : undefined }; }) .filter((item): item is SafeChartSeries => Boolean(item)); const clippedXData = xData.slice(0, maxPoints); const clippedSeries = normalizedSeries.map((series) => ({ ...series, data: series.data.slice(0, clippedXData.length) })); return clippedXData.length > 0 && clippedSeries.length > 0 ? { xData: clippedXData, series: clippedSeries } : null; } export function 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 (isRecord(point)) { 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; } function normalizeXData(rawXData: unknown): string[] { return Array.isArray(rawXData) ? rawXData.map((item) => String(item ?? "")).filter(Boolean) : []; } function normalizeRawSeriesItems(rawSeries: unknown): unknown[] { if (!Array.isArray(rawSeries)) { return isRecord(rawSeries) ? [rawSeries] : []; } return rawSeries.length > 0 && rawSeries.every(isRawChartPoint) ? [{ name: "数据", data: rawSeries }] : rawSeries; } function isRawChartPoint(item: unknown): boolean { if (toFiniteNumber(item) !== null) { return true; } if (Array.isArray(item)) { return item.length >= 2 && toFiniteNumber(item[1]) !== null; } if (isRecord(item)) { return item.data === undefined && item.points === undefined && item.values === undefined && toFiniteNumber(item.value ?? item.y) !== null; } return false; } function 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; } function stringValue(value: unknown) { return typeof value === "string" && value.trim() ? value.trim() : undefined; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }