feat(web-platform): release V0.8.3 常规界面优化
web-platform-release / Build and publish release (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
web-platform-release / Build and publish release (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { RotateCcw, ZoomIn, ZoomOut } from 'lucide-react';
|
||||
import uPlot from 'uplot';
|
||||
import type { ScalarSeries } from '../training/types';
|
||||
|
||||
@@ -14,8 +15,19 @@ function smooth(values: (number | null)[], factor: number): (number | null)[] {
|
||||
});
|
||||
}
|
||||
|
||||
export function ScalarChart({ series, smoothing }: { series: ScalarSeries[]; smoothing: number }) {
|
||||
export function ScalarChart({
|
||||
series,
|
||||
smoothing,
|
||||
title = '训练与评估 Scalars',
|
||||
}: {
|
||||
series: ScalarSeries[];
|
||||
smoothing: number;
|
||||
title?: string;
|
||||
}) {
|
||||
const host = useRef<HTMLDivElement>(null);
|
||||
const chartRef = useRef<uPlot | null>(null);
|
||||
const trackZoom = useRef(false);
|
||||
const zoomRanges = useRef<Partial<Record<'x' | 'y', { min: number; max: number }>>>({});
|
||||
const prepared = useMemo(() => {
|
||||
const steps = Array.from(
|
||||
new Set(series.flatMap((item) => item.points.map((point) => point.step))),
|
||||
@@ -36,13 +48,24 @@ export function ScalarChart({ series, smoothing }: { series: ScalarSeries[]; smo
|
||||
useEffect(() => {
|
||||
if (!host.current || series.length === 0 || prepared[0].length === 0) return;
|
||||
const element = host.current;
|
||||
const width = Math.max(280, Math.floor(element.getBoundingClientRect().width));
|
||||
trackZoom.current = false;
|
||||
const chart = new uPlot(
|
||||
{
|
||||
width: Math.max(320, element.clientWidth),
|
||||
height: 360,
|
||||
title: '训练与评估 Scalars',
|
||||
width,
|
||||
height: 280,
|
||||
cursor: { drag: { x: true, y: true, setScale: true } },
|
||||
scales: { x: { time: false } },
|
||||
hooks: {
|
||||
setScale: [
|
||||
(instance, key) => {
|
||||
if (!trackZoom.current || (key !== 'x' && key !== 'y')) return;
|
||||
const scale = instance.scales[key];
|
||||
if (typeof scale.min === 'number' && typeof scale.max === 'number')
|
||||
zoomRanges.current[key] = { min: scale.min, max: scale.max };
|
||||
},
|
||||
],
|
||||
},
|
||||
axes: [
|
||||
{ stroke: '#8fa0b5', grid: { stroke: '#213044' } },
|
||||
{ stroke: '#8fa0b5', grid: { stroke: '#213044' } },
|
||||
@@ -60,22 +83,123 @@ export function ScalarChart({ series, smoothing }: { series: ScalarSeries[]; smo
|
||||
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 });
|
||||
chartRef.current = chart;
|
||||
trackZoom.current = true;
|
||||
for (const key of ['x', 'y'] as const) {
|
||||
const range = zoomRanges.current[key];
|
||||
if (range) chart.setScale(key, range);
|
||||
}
|
||||
const wheelZoom = (event: WheelEvent) => {
|
||||
if (!event.deltaY) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const bounds = chart.over.getBoundingClientRect();
|
||||
if (!bounds.width || !bounds.height) return;
|
||||
const xRatio = Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width));
|
||||
const yRatio = Math.min(1, Math.max(0, (event.clientY - bounds.top) / bounds.height));
|
||||
const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? window.innerHeight : 1;
|
||||
const factor = Math.min(2, Math.max(0.5, Math.exp(event.deltaY * unit * 0.002)));
|
||||
for (const key of ['x', 'y'] as const) {
|
||||
const scale = chart.scales[key];
|
||||
if (typeof scale.min !== 'number' || typeof scale.max !== 'number') continue;
|
||||
const ratio = key === 'x' ? xRatio : 1 - yRatio;
|
||||
const anchor = scale.min + (scale.max - scale.min) * ratio;
|
||||
chart.setScale(key, {
|
||||
min: anchor - (anchor - scale.min) * factor,
|
||||
max: anchor + (scale.max - anchor) * factor,
|
||||
});
|
||||
}
|
||||
};
|
||||
chart.over.addEventListener('wheel', wheelZoom, { passive: false });
|
||||
let frame = 0;
|
||||
let lastWidth = width;
|
||||
const observer = new ResizeObserver(() => {
|
||||
window.cancelAnimationFrame(frame);
|
||||
frame = window.requestAnimationFrame(() => {
|
||||
const nextWidth = Math.max(280, Math.floor(element.getBoundingClientRect().width));
|
||||
if (nextWidth !== lastWidth) {
|
||||
lastWidth = nextWidth;
|
||||
chart.setSize({ width: nextWidth, height: 280 });
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(element);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
chart.over.removeEventListener('wheel', wheelZoom);
|
||||
window.cancelAnimationFrame(frame);
|
||||
trackZoom.current = false;
|
||||
chartRef.current = null;
|
||||
chart.destroy();
|
||||
};
|
||||
}, [prepared, series]);
|
||||
|
||||
const zoom = (factor: number) => {
|
||||
const chart = chartRef.current;
|
||||
if (!chart) return;
|
||||
for (const key of ['x', 'y']) {
|
||||
const scale = chart.scales[key];
|
||||
if (typeof scale.min !== 'number' || typeof scale.max !== 'number') continue;
|
||||
const center = (scale.min + scale.max) / 2;
|
||||
const radius = ((scale.max - scale.min) * factor) / 2 || 1;
|
||||
chart.setScale(key, { min: center - radius, max: center + radius });
|
||||
}
|
||||
};
|
||||
const resetZoom = () => {
|
||||
const chart = chartRef.current;
|
||||
if (!chart) return;
|
||||
zoomRanges.current = {};
|
||||
trackZoom.current = false;
|
||||
chart.setData(chart.data, true);
|
||||
trackZoom.current = true;
|
||||
};
|
||||
|
||||
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">
|
||||
<div className="grid h-[280px] 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" />;
|
||||
return (
|
||||
<section className="min-w-0 overflow-hidden rounded-lg border border-border bg-app">
|
||||
<header className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
|
||||
<h3 className="min-w-0 truncate text-[10px] font-semibold" title={title}>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${title} 放大`}
|
||||
title="放大"
|
||||
className="rounded p-1 text-text-tertiary hover:bg-element-hover hover:text-text-primary"
|
||||
onClick={() => zoom(0.7)}
|
||||
>
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${title} 缩小`}
|
||||
title="缩小"
|
||||
className="rounded p-1 text-text-tertiary hover:bg-element-hover hover:text-text-primary"
|
||||
onClick={() => zoom(1.4)}
|
||||
>
|
||||
<ZoomOut className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${title} 重置缩放`}
|
||||
title="重置缩放"
|
||||
className="rounded p-1 text-text-tertiary hover:bg-element-hover hover:text-text-primary"
|
||||
onClick={resetZoom}
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div ref={host} className="min-w-0 w-full overflow-hidden" />
|
||||
<p className="border-t border-border px-3 py-1.5 text-[9px] text-text-tertiary">
|
||||
图表区域滚轮以指针位置缩放;也可拖拽框选,或使用右上角按钮缩放和复位。
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user