feat(studio): centralize chart styling and formatting

This commit is contained in:
Shine1i 2026-03-09 15:34:15 +01:00
commit 1ae8fb402b
5 changed files with 86 additions and 37 deletions

View file

@ -11,7 +11,15 @@ import { ChartAverageIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import type { ReactElement } from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import { formatMetric, formatStepTick, placeholderEvalData } from "./utils";
import {
CHART_CONTAINER_CLASS,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
formatAxisMetric,
formatMetric,
formatStepTick,
placeholderEvalData,
} from "./utils";
const evalLossConfig = {
loss: { label: "Eval Loss", color: "#ef4444" },
@ -33,22 +41,17 @@ export function EvalLossChartCard({
return (
<Card data-tour="studio-eval-loss" size="sm">
<CardHeader>
<CardTitle
className={`text-sm pl-1${data.length > 0 ? "" : " text-muted-foreground"}`}
>
<CardTitle className={`text-sm${data.length > 0 ? "" : " text-muted-foreground"}`}>
Eval Loss
</CardTitle>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<ChartContainer
config={evalLossConfig}
className="-ml-3 h-[220px] w-full"
>
<ChartContainer config={evalLossConfig} className={CHART_CONTAINER_CLASS}>
<LineChart
data={data}
accessibilityLayer={true}
margin={{ left: 0, right: 8 }}
margin={DEFAULT_CHART_MARGIN}
>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
@ -71,10 +74,11 @@ export function EvalLossChartCard({
allowDataOverflow={true}
tickLine={false}
axisLine={false}
tickMargin={4}
tickMargin={8}
tickCount={5}
fontSize={10}
width={80}
tickFormatter={(value) => formatMetric(Number(value))}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => formatAxisMetric(Number(value))}
/>
<ChartTooltip
content={
@ -106,12 +110,12 @@ export function EvalLossChartCard({
<div className="relative">
<ChartContainer
config={evalLossConfig}
className="-ml-3 h-[220px] w-full blur"
className={`${CHART_CONTAINER_CLASS} blur`}
>
<LineChart
data={placeholderEvalData}
accessibilityLayer={true}
margin={{ left: 0, right: 8 }}
margin={DEFAULT_CHART_MARGIN}
>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
@ -127,9 +131,10 @@ export function EvalLossChartCard({
<YAxis
tickLine={false}
axisLine={false}
tickMargin={4}
tickMargin={8}
tickCount={5}
fontSize={10}
width={40}
width={DEFAULT_Y_AXIS_WIDTH}
/>
<Line
type="monotone"

View file

@ -12,6 +12,10 @@ import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import type { ScaleMode } from "./types";
import {
CHART_SYNC_ID,
CHART_CONTAINER_CLASS,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
formatAxisMetric,
formatMetric,
formatStepTick,
fromLog1p,
@ -45,19 +49,16 @@ export function GradNormChartCard({
return (
<Card size="sm">
<CardHeader>
<CardTitle className="text-sm pl-1">Gradient Norm</CardTitle>
<CardTitle className="text-sm">Gradient Norm</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer
config={gradNormConfig}
className="-ml-3 h-[220px] w-full"
>
<ChartContainer config={gradNormConfig} className={CHART_CONTAINER_CLASS}>
<LineChart
data={data}
syncId={CHART_SYNC_ID}
syncMethod="value"
accessibilityLayer={true}
margin={{ left: 0, right: 8 }}
margin={DEFAULT_CHART_MARGIN}
>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
@ -80,16 +81,17 @@ export function GradNormChartCard({
allowDataOverflow={true}
tickLine={false}
axisLine={false}
tickMargin={4}
tickMargin={8}
tickCount={5}
fontSize={10}
width={80}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => {
const num = Number(value);
if (!Number.isFinite(num)) {
return "0";
}
const shown = scale === "log" ? fromLog1p(num) : num;
return formatMetric(shown);
return formatAxisMetric(shown);
}}
/>
<ChartTooltip

View file

@ -10,7 +10,14 @@ import type { ChartConfig } from "@/components/ui/chart";
import type { ReactElement } from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import type { ScaleMode } from "./types";
import { CHART_SYNC_ID, formatStepTick, fromLog1p } from "./utils";
import {
CHART_CONTAINER_CLASS,
CHART_SYNC_ID,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
formatStepTick,
fromLog1p,
} from "./utils";
const lrConfig = {
displayLr: { label: "LR", color: "#8b5cf6" },
@ -40,16 +47,16 @@ export function LearningRateChartCard({
return (
<Card size="sm">
<CardHeader>
<CardTitle className="text-sm pl-1">Learning Rate</CardTitle>
<CardTitle className="text-sm">Learning Rate</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={lrConfig} className="-ml-1.5 h-[220px] w-full">
<ChartContainer config={lrConfig} className={CHART_CONTAINER_CLASS}>
<LineChart
data={data}
syncId={CHART_SYNC_ID}
syncMethod="value"
accessibilityLayer={true}
margin={{ left: 0, right: 8 }}
margin={DEFAULT_CHART_MARGIN}
>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
@ -72,9 +79,10 @@ export function LearningRateChartCard({
allowDataOverflow={true}
tickLine={false}
axisLine={false}
tickMargin={4}
tickMargin={8}
tickCount={5}
fontSize={10}
width={52}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => {
const num = Number(value);
if (!Number.isFinite(num)) {

View file

@ -19,6 +19,10 @@ import {
import type { ScaleMode } from "./types";
import {
CHART_SYNC_ID,
CHART_CONTAINER_CLASS,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
formatAxisMetric,
formatMetric,
formatStepTick,
fromLog1p,
@ -65,16 +69,16 @@ export function TrainingLossChartCard({
return (
<Card data-tour="studio-training-loss" size="sm">
<CardHeader>
<CardTitle className="text-sm pl-1">Training Loss</CardTitle>
<CardTitle className="text-sm">Training Loss</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={lossConfig} className="-ml-3 h-[220px] w-full">
<ChartContainer config={lossConfig} className={CHART_CONTAINER_CLASS}>
<LineChart
data={data}
syncId={CHART_SYNC_ID}
syncMethod="value"
accessibilityLayer={true}
margin={{ left: 0, right: 8 }}
margin={DEFAULT_CHART_MARGIN}
>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
@ -97,16 +101,17 @@ export function TrainingLossChartCard({
allowDataOverflow={true}
tickLine={false}
axisLine={false}
tickMargin={4}
tickMargin={8}
tickCount={5}
fontSize={10}
width={80}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => {
const num = Number(value);
if (!Number.isFinite(num)) {
return "0";
}
const shown = scale === "log" ? fromLog1p(num) : num;
return formatMetric(shown);
return formatAxisMetric(shown);
}}
/>
<ChartTooltip

View file

@ -3,6 +3,9 @@ import type { LossHistoryItem, OutlierMode, SmoothedLossItem } from "./types";
export const CHART_SYNC_ID = "train-metrics-sync";
export const MAX_RENDER_POINTS = 800;
export const DEFAULT_VISIBLE_POINTS = 160;
export const CHART_CONTAINER_CLASS = "h-[220px] w-full";
export const DEFAULT_CHART_MARGIN = { top: 4, right: 8, bottom: 0, left: 4 };
export const DEFAULT_Y_AXIS_WIDTH = 41;
const TRAILING_ZEROES_RE = /\.?0+$/;
const NEGATIVE_ZERO_RE = /^-0$/;
@ -50,6 +53,32 @@ export function formatMetric(value: number): string {
.replace(NEGATIVE_ZERO_RE, "0");
}
export function formatAxisMetric(value: number): string {
if (!Number.isFinite(value)) {
return "0";
}
const abs = Math.abs(value);
let decimals = 4;
if (abs >= 1000) {
decimals = 0;
} else if (abs >= 100) {
decimals = 1;
} else if (abs >= 1) {
decimals = 3;
} else if (abs >= 0.01) {
decimals = 4;
} else {
decimals = 5;
}
return value
.toFixed(decimals)
.replace(TRAILING_ZEROES_RE, "")
.replace(NEGATIVE_ZERO_RE, "0");
}
export function formatStepTick(value: number): string {
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(1)}M`;