Files
next-tjwater-drainage-frontend/features/agent/chart-data.ts
T

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);
}