Skip to content
Merged
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
1 change: 1 addition & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ optimization_results/
optimizations/
filestorage/
.agents/skills/kaizen-ui/
storybook-static

# SDK output is generated on `pnpm install` (see packages/sdk/generateAll.ts).
packages/sdk/generated/
1 change: 1 addition & 0 deletions web/packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"react-dom": "catalog:",
"react-oidc-context": "catalog:",
"react-router": "catalog:",
"recharts": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
ANNOTATION_COLOR,
ANNOTATION_TEXT_COLOR,
} from '@nemo/common/src/components/ComparisonLineChart/consts';
import type { FC } from 'react';

interface Props {
label: string;
description?: string;
color?: string;
pointsUp: boolean;
/** Which side of the arrow the text sits on, so callouts near the right edge stay in frame. */
labelSide: 'left' | 'right';
/** Injected by recharts when this is passed as a `<ReferenceLine label>`. */
viewBox?: { x?: number; y?: number; width?: number; height?: number };
}

const ARROW_HALF_WIDTH = 5;
const ARROW_LENGTH = 9;
const TEXT_OFFSET = 12;

/**
* Draws the arrowhead and callout text for a `ComparisonAnnotation`. The dashed shaft is the
* `ReferenceLine` itself; this only renders what recharts has no primitive for.
*/
export const ComparisonAnnotationLabel: FC<Props> = ({
label,
description,
color = ANNOTATION_COLOR,
pointsUp,
labelSide,
viewBox,
}) => {
const { x = 0, y = 0, height = 0 } = viewBox ?? {};
const tipY = pointsUp ? y : y + height;
const baseY = pointsUp ? tipY + ARROW_LENGTH : tipY - ARROW_LENGTH;
const onLeft = labelSide === 'left';
const textX = onLeft ? x - TEXT_OFFSET : x + TEXT_OFFSET;
const textAnchor = onLeft ? 'end' : 'start';
const textY = y + height / 2;

return (
<g>
<polygon
points={`${x},${tipY} ${x - ARROW_HALF_WIDTH},${baseY} ${x + ARROW_HALF_WIDTH},${baseY}`}
fill={color}
/>
<text
x={textX}
y={textY}
textAnchor={textAnchor}
fill={ANNOTATION_TEXT_COLOR}
fontSize={20}
fontWeight={700}
>
{label}
</text>
{description && (
<text
x={textX}
y={textY + 16}
textAnchor={textAnchor}
fill={ANNOTATION_TEXT_COLOR}
fontSize={11}
>
{description}
</text>
)}
</g>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Flex, Text } from '@nvidia/foundations-react-core';
import type { FC, ReactNode } from 'react';

interface Props {
title?: ReactNode;
legend?: ReactNode;
}

/** Title on the left, legend on the right; renders when either is present. */
export const ComparisonChartHeader: FC<Props> = ({ title, legend }) =>
title || legend ? (
<Flex justify={title ? 'between' : 'end'} align="center" gap="density-md" className="pb-2">
{title && <Text kind="label/bold/lg">{title}</Text>}
{legend}
</Flex>
) : null;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Flex, Text } from '@nvidia/foundations-react-core';
import classNames from 'classnames';
import type { FC } from 'react';

export interface ComparisonLegendItem {
id: string;
label: string;
color: string;
dashed?: boolean;
hidden?: boolean;
}

interface Props {
items: ComparisonLegendItem[];
interactive?: boolean;
justify?: 'center' | 'end';
onToggle?: (id: string) => void;
onHover?: (id: string | null) => void;
}

const DOT_SIZE = 10;
const DOT_RADIUS = 4;

export const ComparisonLegend: FC<Props> = ({
items,
interactive = true,
justify = 'end',
onToggle,
onHover,
}) => (
<Flex wrap="wrap" gap="density-md" justify={justify} align="center">
{items.map((item) => (
<button
key={item.id}
type="button"
disabled={!interactive}
aria-pressed={!item.hidden}
className={classNames(
'flex items-center gap-1.5 rounded px-1 py-0.5',
interactive && 'cursor-pointer hover:bg-component-hover',
item.hidden && 'opacity-40'
)}
onClick={() => onToggle?.(item.id)}
onMouseEnter={() => onHover?.(item.id)}
onMouseLeave={() => onHover?.(null)}
onFocus={() => onHover?.(item.id)}
onBlur={() => onHover?.(null)}
>
<svg width={DOT_SIZE} height={DOT_SIZE} aria-hidden focusable="false">
{/* Dashed series get a hollow dot so the legend still distinguishes them. */}
<circle
cx={DOT_SIZE / 2}
cy={DOT_SIZE / 2}
r={item.dashed ? DOT_RADIUS - 0.75 : DOT_RADIUS}
fill={item.dashed ? 'none' : item.color}
stroke={item.dashed ? item.color : 'none'}
strokeWidth={item.dashed ? 1.5 : 0}
/>
</svg>
<Text kind="label/regular/md" className="text-placeholder">
{item.label}
</Text>
</button>
))}
</Flex>
);
Loading
Loading