Files
Mujoco_WASM/web_platform/src/tuning/ScalarChart.tsx
T
chenlin deead17a9a
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
feat(training): release V0.8 自调参 Agent
2026-09-02 13:49:34 +08:00

82 lines
2.7 KiB
TypeScript

import { useEffect, useMemo, useRef } from 'react';
import uPlot from 'uplot';
import type { ScalarSeries } from '../training/types';
const COLORS = ['#38d39f', '#60a5fa', '#f59e0b', '#f472b6', '#a78bfa', '#fb7185'];
function smooth(values: (number | null)[], factor: number): (number | null)[] {
if (factor <= 0) return values;
let previous: number | undefined;
return values.map((value) => {
if (value === null) return null;
previous = previous === undefined ? value : factor * previous + (1 - factor) * value;
return previous;
});
}
export function ScalarChart({ series, smoothing }: { series: ScalarSeries[]; smoothing: number }) {
const host = useRef<HTMLDivElement>(null);
const prepared = useMemo(() => {
const steps = Array.from(
new Set(series.flatMap((item) => item.points.map((point) => point.step))),
).sort((a, b) => a - b);
const columns: uPlot.AlignedData = [steps];
for (const item of series) {
const byStep = new Map(item.points.map((point) => [point.step, point.value]));
columns.push(
smooth(
steps.map((step) => byStep.get(step) ?? null),
smoothing,
),
);
}
return columns;
}, [series, smoothing]);
useEffect(() => {
if (!host.current || series.length === 0 || prepared[0].length === 0) return;
const element = host.current;
const chart = new uPlot(
{
width: Math.max(320, element.clientWidth),
height: 360,
title: '训练与评估 Scalars',
cursor: { drag: { x: true, y: true, setScale: true } },
scales: { x: { time: false } },
axes: [
{ stroke: '#8fa0b5', grid: { stroke: '#213044' } },
{ stroke: '#8fa0b5', grid: { stroke: '#213044' } },
],
series: [
{ label: 'Step' },
...series.map((item, index) => ({
label: item.tag,
stroke: COLORS[index % COLORS.length],
width: 2,
spanGaps: true,
})),
],
},
prepared,
element,
);
const observer = new ResizeObserver((entries) => {
const width = entries[0]?.contentRect.width;
if (width) chart.setSize({ width: Math.max(320, Math.floor(width)), height: 360 });
});
observer.observe(element);
return () => {
observer.disconnect();
chart.destroy();
};
}, [prepared, series]);
if (series.length === 0)
return (
<div className="grid h-[360px] place-items-center rounded-lg border border-border bg-app text-xs text-text-tertiary">
当前 trial 尚无 scalar 数据
</div>
);
return <div ref={host} className="min-w-0 overflow-hidden rounded-lg bg-app p-2" />;
}