Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { Meta, StoryObj } from '@storybook/react';
import {
MetricTrendPanel,
type MetricTrendPoint,
} from '@studio/components/charts/MetricTrendPanel';

/** Deterministic wobbly upward trend so the story renders the same every run. */
const makePoints = (start: number, step: number, count = 30): MetricTrendPoint[] =>
Array.from({ length: count }, (_, i) => ({
label: `Day ${i + 1}`,
value: Number((start + i * step + (i % 3 === 0 ? -step : step) * 0.8).toFixed(1)),
}));

const meta: Meta<typeof MetricTrendPanel> = {
title: 'Charts/MetricTrendPanel',
component: MetricTrendPanel,
args: {
title: 'Primary use cases',
description:
'Continuously evaluate every merge to main against the full Support-Bench v3 benchmark.',
comparisonLabel: 'vs. 7 days ago',
series: [
{ id: 'solved', label: 'Solved', value: 78.4, delta: 3.3, points: makePoints(62, 0.55) },
{
id: 'helpfulness',
label: 'Helpfulness',
value: 84.1,
delta: 1.2,
points: makePoints(74, 0.35),
},
{ id: 'tool-use', label: 'Tool Use', value: 69.2, delta: -2.4, points: makePoints(78, -0.3) },
],
},
decorators: [
(Story) => (
<div className="max-w-4xl">
<Story />
</div>
),
],
};

export default meta;

type Story = StoryObj<typeof MetricTrendPanel>;

export const Default: Story = {
args: { onViewClick: () => {} },
};

export const SingleSeries: Story = {
args: {
series: [
{ id: 'solved', label: 'Solved', value: 78.4, delta: 3.3, points: makePoints(62, 0.55) },
],
},
};

export const NegativeDelta: Story = {
args: { selectedSeriesId: 'tool-use' },
};

export const Loading: Story = {
args: { isPending: true },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
MetricTrendPanel,
type MetricTrendSeries,
} from '@studio/components/charts/MetricTrendPanel';
import { fireEvent, render, screen } from '@studio/tests/util/render';

const series: MetricTrendSeries[] = [
{
id: 'solved',
label: 'Solved',
value: 78.4,
delta: 3.3,
points: [
{ label: 'Day 1', value: 70 },
{ label: 'Day 2', value: 78.4 },
],
},
{
id: 'tool-use',
label: 'Tool Use',
value: 69.2,
delta: -2.4,
points: [
{ label: 'Day 1', value: 71 },
{ label: 'Day 2', value: 69.2 },
],
},
];

describe('MetricTrendPanel', () => {
it('renders the first series value and delta by default', () => {
render(
<MetricTrendPanel
title="Primary use cases"
series={series}
comparisonLabel="vs. 7 days ago"
/>
);

expect(screen.getByText('78.4%')).toBeInTheDocument();
expect(screen.getByText('+3.3')).toBeInTheDocument();
expect(screen.getByText('vs. 7 days ago')).toBeInTheDocument();
});

it('switches series when a pill is clicked', () => {
render(<MetricTrendPanel title="Primary use cases" series={series} />);

fireEvent.click(screen.getByRole('button', { name: 'Tool Use' }));

expect(screen.getByText('69.2%')).toBeInTheDocument();
expect(screen.getByText('−2.4')).toBeInTheDocument();
});

it('stays on the controlled series and reports the change', () => {
const onSeriesChange = vi.fn();
render(
<MetricTrendPanel
title="Primary use cases"
series={series}
selectedSeriesId="solved"
onSeriesChange={onSeriesChange}
/>
);

fireEvent.click(screen.getByRole('button', { name: 'Tool Use' }));

expect(onSeriesChange).toHaveBeenCalledWith('tool-use');
expect(screen.getByText('78.4%')).toBeInTheDocument();
});

it('calls onViewClick from the header action', () => {
const onViewClick = vi.fn();
render(
<MetricTrendPanel title="Primary use cases" series={series} onViewClick={onViewClick} />
);

fireEvent.click(screen.getByRole('button', { name: 'View' }));

expect(onViewClick).toHaveBeenCalledTimes(1);
});
});
215 changes: 215 additions & 0 deletions web/packages/studio/src/components/charts/MetricTrendPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
Button,
Flex,
PanelContent,
PanelHeader,
PanelRoot,
Stack,
Tag,
Text,
} from '@nvidia/foundations-react-core';
import { useNvColorMode } from '@studio/components/DagCanvas/useNvColorMode';
import { StackedSkeleton } from '@studio/components/StackedSkeleton';
import { Triangle } from 'lucide-react';
import { FC, useId, useMemo, useState } from 'react';
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';

export interface MetricTrendPoint {
/** X-axis label for the point, shown in the tooltip. */
label: string;
value: number;
}

export interface MetricTrendSeries {
id: string;
/** Pill label, e.g. "Solved". */
label: string;
value: number;
/** Change over the compared period. Positive renders up, negative down. */
delta?: number;
points: MetricTrendPoint[];
}

interface Props {
title: string;
description?: string;
series: MetricTrendSeries[];
/** Qualifies the delta, e.g. "vs. 7 days ago". */
comparisonLabel?: string;
/** Controls the active pill. Leave undefined to let the panel manage it. */
selectedSeriesId?: string;
onSeriesChange?: (seriesId: string) => void;
onViewClick?: () => void;
viewLabel?: string;
formatValue?: (value: number) => string;
formatDelta?: (delta: number) => string;
chartHeight?: number;
isPending?: boolean;
}

const DEFAULT_CHART_HEIGHT = 160;

/**
* The area fill sits on a near-black surface in dark mode, so the light-mode opacities
* wash out to almost nothing. Carry more of the accent color, and keep a floor at the
* bottom of the gradient so the fill stays readable all the way down.
*/
const AREA_GRADIENT = {
dark: { top: 0.65, mid: 0.3, bottom: 0.08 },
light: { top: 0.3, mid: 0.12, bottom: 0 },
} as const;

const formatPercent = (value: number): string => `${value.toFixed(1)}%`;

const formatSignedDelta = (delta: number): string =>
`${delta > 0 ? '+' : delta < 0 ? '−' : ''}${Math.abs(delta).toFixed(1)}`;

export const MetricTrendPanel: FC<Props> = ({
title,
description,
series,
comparisonLabel,
selectedSeriesId,
onSeriesChange,
onViewClick,
viewLabel = 'View',
formatValue = formatPercent,
formatDelta = formatSignedDelta,
chartHeight = DEFAULT_CHART_HEIGHT,
isPending = false,
}) => {
// useId() emits colons, which are not valid inside an SVG `url(#...)` reference.
const gradientId = `metric-trend-${useId().replace(/:/g, '')}`;
const [internalSeriesId, setInternalSeriesId] = useState<string | undefined>(series[0]?.id);

const activeId = selectedSeriesId ?? internalSeriesId;
const active = useMemo(
() => series.find((s) => s.id === activeId) ?? series[0],
[series, activeId]
);

const selectSeries = (seriesId: string) => {
if (selectedSeriesId === undefined) {
setInternalSeriesId(seriesId);
}
onSeriesChange?.(seriesId);
};

const delta = active?.delta;
const isNegative = delta !== undefined && delta < 0;
const deltaColor = isNegative ? 'red' : 'green';
const lineColor = isNegative ? 'var(--text-color-accent-red)' : 'var(--text-color-brand)';
const colorMode = useNvColorMode();
const gradient = colorMode === 'dark' ? AREA_GRADIENT.dark : AREA_GRADIENT.light;

return (
<PanelRoot elevation="mid">
<PanelHeader className="items-start">
<Stack gap="density-xs" className="min-w-0 flex-1">
<Text kind="label/bold/xl">{title}</Text>
{description && (
<Text kind="body/regular/md" className="text-secondary">
{description}
</Text>
)}
</Stack>
{onViewClick && (
<Button kind="tertiary" size="small" className="shrink-0" onClick={onViewClick}>
{viewLabel}
</Button>
)}
</PanelHeader>

<PanelContent>
<Stack gap="density-lg">
<Text kind="display/lg">{active ? formatValue(active.value) : '—'}</Text>

<Flex align="center" gap="density-lg" wrap="wrap">
{delta !== undefined && (
<Flex align="center" gap="density-sm">
<Tag readOnly color={deltaColor} density="compact">
<Triangle
size={12}
className={`fill-current ${isNegative ? 'rotate-180' : ''}`}
aria-hidden
/>
{formatDelta(delta)}
</Tag>
Comment on lines +101 to +140

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Render zero delta as neutral.

When delta === 0, the panel shows a green upward triangle. This reports no change as an increase. Use a neutral tag and omit the directional icon for zero.

Add a zero-delta test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/packages/studio/src/components/charts/MetricTrendPanel/index.tsx` around
lines 101 - 140, Update the delta styling and rendering in MetricTrendPanel so
delta === 0 uses the neutral tag color and omits the Triangle, while preserving
the existing red/downward behavior for negative values and green/upward behavior
for positive values. Add a test covering zero delta and asserting the neutral
presentation without a directional icon.

{comparisonLabel && (
<Text kind="body/regular/md" className="text-secondary">
{comparisonLabel}
</Text>
)}
</Flex>
)}

{series.length > 1 && (
<Flex align="center" gap="density-sm" wrap="wrap" role="group" aria-label={title}>
{series.map((s) => {
const isActive = s.id === active?.id;
return (
<Tag
key={s.id}
color={isActive ? 'green' : 'gray'}
kind={isActive ? 'solid' : 'outline'}
selected={isActive}
aria-pressed={isActive}
onClick={() => selectSeries(s.id)}
>
{s.label}
</Tag>
);
})}
</Flex>
)}
</Flex>
</Stack>
</PanelContent>

<div className="-mx-density-2xl -mb-density-2xl overflow-hidden rounded-b-density-xl">
{isPending || !active ? (
<StackedSkeleton count={1} height={chartHeight} className="w-full" />
) : (
Comment on lines +172 to +175

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not show loading for empty data.

When series is empty and isPending is false, !active renders StackedSkeleton indefinitely. Render an empty state, or require a non-empty series contract.

Add a test for series={[]} with isPending={false}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/packages/studio/src/components/charts/MetricTrendPanel/index.tsx` around
lines 172 - 175, Update the loading condition in MetricTrendPanel so an empty
series with isPending=false does not render StackedSkeleton indefinitely; render
the established empty state instead, or enforce a non-empty series contract. Add
a test covering series={[]} with isPending={false} and verify the loading
skeleton is not shown.

<ResponsiveContainer width="100%" height={chartHeight}>
<AreaChart data={active.points} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={lineColor} stopOpacity={gradient.top} />
<stop offset="55%" stopColor={lineColor} stopOpacity={gradient.mid} />
<stop offset="100%" stopColor={lineColor} stopOpacity={gradient.bottom} />
</linearGradient>
</defs>
<XAxis dataKey="label" hide />
<YAxis domain={['dataMin', 'dataMax']} hide />
<Tooltip
cursor={{ stroke: 'var(--border-color-base)', strokeWidth: 1 }}
formatter={(value: number) => [formatValue(value), active.label]}
contentStyle={{
fontSize: 12,
backgroundColor: 'var(--background-color-component-tooltip)',
borderColor: 'var(--border-color-base)',
color: 'var(--text-color-base)',
}}
labelStyle={{ color: 'var(--text-color-base)' }}
itemStyle={{ color: 'var(--text-color-base)' }}
/>
<Area
type="linear"
dataKey="value"
name={active.label}
stroke={lineColor}
strokeWidth={2}
fill={`url(#${gradientId})`}
dot={active.points.length <= 2}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
</PanelRoot>
);
};
Loading