184 lines
5.0 KiB
TypeScript
184 lines
5.0 KiB
TypeScript
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<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|