Skip to content
Closed
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
27 changes: 24 additions & 3 deletions ui/packages/@quent/components/src/dag/DAGChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ import type { DAGData } from '../services/query-plan/types';
import { QueryPlanNode, type QueryPlanNodeData } from '../query-plan/QueryPlanNode';
import { DAGLegend } from './DAGLegend';
import { parseCustomStatistics } from '../lib/queryBundle.utils';
import { continuousColor, getOperationTypeColor, buildOperatorColorMap } from '@quent/utils';
import { inferFieldFormatter } from '@quent/utils';
import {
continuousColor,
getOperationTypeColor,
buildOperatorColorMap,
inferFieldFormatter,
} from '@quent/utils';
import type { QuantitySpec } from '@quent/utils';

// Edge geometry constants
const EDGE_STROKE_WIDTH_DEFAULT = 1.5;
Expand Down Expand Up @@ -317,6 +322,21 @@ const FlowLayout = ({
[data.nodes]
);

// Build a stat-key → QuantitySpec map for quantity-aware legend formatting.
const statQuantitySpecs = useMemo((): Record<string, QuantitySpec> => {
if (!data.quantitySpecs) return {};
const result: Record<string, QuantitySpec> = {};
for (const node of data.nodes) {
for (const stat of parseCustomStatistics(node.metadata?.rawNode)) {
if (stat.quantity && !(stat.key in result)) {
const spec = data.quantitySpecs[stat.quantity];
if (spec) result[stat.key] = spec;
}
}
}
return result;
}, [data.nodes, data.quantitySpecs]);

// Convert DAGData to ReactFlow format
const convertToReactFlow = useCallback(() => {
// Determine which nodes have incoming/outgoing edges
Expand All @@ -338,6 +358,7 @@ const FlowLayout = ({
isDark,
baseColor: operatorColorMap.get(node.type.toLowerCase()),
flowBarVisible,
quantitySpecs: data.quantitySpecs,
},
style: {
width: NODE_LAYOUT_WIDTH,
Expand Down Expand Up @@ -455,7 +476,7 @@ const FlowLayout = ({
defaultEdgeOptions={{ type: 'smoothstep' }}
>
<Background />
<DAGLegend isDark={isDark} />
<DAGLegend isDark={isDark} statQuantitySpecs={statQuantitySpecs} />
<MiniMap
pannable
zoomable
Expand Down
29 changes: 25 additions & 4 deletions ui/packages/@quent/components/src/dag/DAGLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
getLegendGradientStops,
type PaletteTheme,
} from '@quent/utils';
import { inferFieldFormatter } from '@quent/utils';
import { inferFieldFormatter, formatQuantity } from '@quent/utils';
import type { QuantitySpec } from '@quent/utils';
import { DataFlowTierLegend } from './DataFlowTierLegend';
import type { NodeColoring, EdgeColoring } from '../services/query-plan/types';
import type { ContinuousPaletteName } from '@quent/utils';
Expand All @@ -33,10 +34,20 @@ interface ContinuousLegendProps {
max: number;
palette: ContinuousPaletteName;
isDark: boolean;
quantitySpec?: QuantitySpec;
}

const ContinuousLegend = ({ field, min, max, palette, isDark }: ContinuousLegendProps) => {
const fmt = inferFieldFormatter(field);
const ContinuousLegend = ({
field,
min,
max,
palette,
isDark,
quantitySpec,
}: ContinuousLegendProps) => {
const fmt = quantitySpec
? (v: number) => formatQuantity(v, quantitySpec, 'Occupancy')
: inferFieldFormatter(field);
return (
<div className="flex flex-col gap-1">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide">
Expand Down Expand Up @@ -135,11 +146,13 @@ function NodeLegendContent({
field,
palette,
isDark,
statQuantitySpecs,
}: {
coloring: NodeColoring;
field: string | null;
palette: ContinuousPaletteName;
isDark: boolean;
statQuantitySpecs?: Record<string, QuantitySpec>;
}) {
if (!coloring || !field) return null;
if (coloring.type === 'continuous') {
Expand All @@ -150,6 +163,7 @@ function NodeLegendContent({
max={coloring.max}
palette={palette}
isDark={isDark}
quantitySpec={statQuantitySpecs?.[field]}
/>
);
}
Expand All @@ -161,11 +175,13 @@ function EdgeLegendContent({
field,
palette,
isDark,
statQuantitySpecs,
}: {
coloring: EdgeColoring;
field: string | null;
palette: ContinuousPaletteName;
isDark: boolean;
statQuantitySpecs?: Record<string, QuantitySpec>;
}) {
if (!coloring || !field) return null;
if (coloring.type === 'continuous') {
Expand All @@ -176,6 +192,7 @@ function EdgeLegendContent({
max={coloring.max}
palette={palette}
isDark={isDark}
quantitySpec={statQuantitySpecs?.[field]}
/>
);
}
Expand All @@ -185,10 +202,12 @@ function EdgeLegendContent({
interface DAGLegendProps {
/** Whether dark mode is active. Passed explicitly to decouple from ThemeContext. */
isDark: boolean;
/** Pre-resolved stat-key → QuantitySpec map for quantity-aware legend formatting. */
statQuantitySpecs?: Record<string, QuantitySpec>;
}

/** Panel overlay showing node/edge coloring legends within the ReactFlow canvas. */
export const DAGLegend = ({ isDark }: DAGLegendProps) => {
export const DAGLegend = ({ isDark, statQuantitySpecs }: DAGLegendProps) => {
const nodeColoring = useNodeColoringValue();
const edgeColoring = useEdgeColoring();
const [nodePalette] = useNodeColorPalette();
Expand Down Expand Up @@ -247,13 +266,15 @@ export const DAGLegend = ({ isDark }: DAGLegendProps) => {
field={nodeField}
palette={nodePalette}
isDark={isDark}
statQuantitySpecs={statQuantitySpecs}
/>
{hasNode && hasEdge && <div className="border-t border-border" />}
<EdgeLegendContent
coloring={edgeColoring}
field={edgeField}
palette={edgePalette}
isDark={isDark}
statQuantitySpecs={statQuantitySpecs}
/>
{(hasNode || hasEdge) && hasDataFlow && <div className="border-t border-border" />}
{hasDataFlow && (
Expand Down
17 changes: 13 additions & 4 deletions ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ import {
} from '@quent/hooks';
import { DataText } from '../ui/data-text';
import { thinScrollbarClass } from '../ui/thin-scroll';
import { inferFieldFormatter } from '@quent/utils';
import { formatStatWithQuantity } from '@quent/utils';
import type { QuantitySpec } from '@quent/utils';
import { DataFlowMatrix } from './DataFlowMatrix';

export const DAGNodeInfoPanel = ({ isDark = false }: { isDark?: boolean }) => {
export const DAGNodeInfoPanel = ({
isDark = false,
quantitySpecs,
}: {
isDark?: boolean;
quantitySpecs?: Record<string, QuantitySpec>;
}) => {
const selectedNodeData = useSelectedNodeData();
const dataFlowEnabled = useDataFlowEnabled();
const dataFlowMeta = useDataFlowMeta();
Expand Down Expand Up @@ -78,7 +85,7 @@ export const DAGNodeInfoPanel = ({ isDark = false }: { isDark?: boolean }) => {
{selectedNodeData.nodeId}
</DataText>
</div>
{selectedNodeData.statistics?.map(({ key, value }) => (
{selectedNodeData.statistics?.map(({ key, value, quantity }) => (
<div key={key} className="text-xs">
{Array.isArray(value) ? (
<div className="flex items-center justify-between gap-0.5">
Expand All @@ -95,7 +102,9 @@ export const DAGNodeInfoPanel = ({ isDark = false }: { isDark?: boolean }) => {
<div className="flex items-center justify-between">
<DataText className="capitalize">{key.replace(/_/g, ' ')}:</DataText>
<DataText className="text-muted-foreground ml-1">
{typeof value === 'number' ? inferFieldFormatter(key)(value) : String(value)}
{typeof value === 'number'
? formatStatWithQuantity(value, key, quantity, quantitySpecs)
: String(value)}
</DataText>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export type OperatorActiveSpanEntry = {
/** Plan ID this operator belongs to. */
planId: string;
/** Pre-computed custom statistics for the operator popup. */
statistics: Array<{ key: string; value: StatValue }>;
statistics: Array<{ key: string; value: StatValue; quantity?: string }>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function DataCell({ row, stat }: DataCellProps<PivotedRow>) {
onMouseEnter: () => interaction.setHoveredStat(derived.buildHoveredStatInfo(stat)),
onMouseLeave: () => interaction.setHoveredStat(null),
};
const quantitySpec = display.statQuantitySpecs?.[stat];
if (!display.isAggregating) {
const val = row.values.get(stat) ?? null;
return (
Expand All @@ -188,7 +189,7 @@ function DataCell({ row, stat }: DataCellProps<PivotedRow>) {
style={{ backgroundColor: bg, boxShadow: cellHighlight }}
{...statCellProps}
>
{formatStatValue(val, stat)}
{formatStatValue(val, stat, quantitySpec)}
</td>
);
}
Expand All @@ -211,7 +212,7 @@ function DataCell({ row, stat }: DataCellProps<PivotedRow>) {
style={{ backgroundColor: bg, boxShadow: cellHighlight }}
{...statCellProps}
>
{formatNumericStat(displayVal, stat)}
{formatNumericStat(displayVal, stat, quantitySpec)}
</td>
);
}
Expand Down Expand Up @@ -246,6 +247,8 @@ interface PivotedStatTableProps<TRow> {
/** Optional controlled sort state, forwarded to the underlying GroupedDataTable. */
sorting?: SortingState;
onSortingChange?: OnChangeFn<SortingState>;
/** Per-stat QuantitySpec for quantity-aware formatting, keyed by stat name. */
statQuantitySpecs?: Record<string, import('@quent/utils').QuantitySpec>;
}

export function PivotedStatTable<TRow>({
Expand All @@ -266,6 +269,7 @@ export function PivotedStatTable<TRow>({
onReorderStat,
sorting,
onSortingChange,
statQuantitySpecs,
}: PivotedStatTableProps<TRow>) {
const [nodePalette] = useNodeColorPalette();
const rowRefs = useRef<Map<string, HTMLTableRowElement>>(new Map());
Expand Down Expand Up @@ -515,8 +519,9 @@ export function PivotedStatTable<TRow>({
aggMode,
colorPalette: nodePalette,
darkMode: isDark,
statQuantitySpecs,
}),
[isAggregating, aggMode, nodePalette, isDark]
[isAggregating, aggMode, nodePalette, isDark, statQuantitySpecs]
);
const dndContextValue = useMemo(
() => ({
Expand Down
4 changes: 3 additions & 1 deletion ui/packages/@quent/components/src/pivot-table/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { StatValue, ContinuousPaletteName } from '@quent/utils';
import type { StatValue, ContinuousPaletteName, QuantitySpec } from '@quent/utils';
import type { AggMode, HoveredStatInfo } from '@quent/hooks';

// Re-exports of pivot-table-related types that originate in @quent/hooks but
Expand Down Expand Up @@ -86,6 +86,8 @@ export interface PivotTableDisplayConfig {
aggMode: AggMode;
colorPalette: ContinuousPaletteName;
darkMode: boolean;
/** Per-stat QuantitySpec for quantity-aware formatting, keyed by stat name. */
statQuantitySpecs?: Record<string, QuantitySpec>;
}

// --- PivotedStatTable types ---
Expand Down
19 changes: 14 additions & 5 deletions ui/packages/@quent/components/src/pivot-table/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { inferFieldFormatter } from '@quent/utils';
import type { StatValue, ContinuousPaletteName } from '@quent/utils';
import { inferFieldFormatter, formatQuantity } from '@quent/utils';
import type { StatValue, ContinuousPaletteName, QuantitySpec } from '@quent/utils';
import { continuousColor } from '@quent/utils';
import type {
StatGroupExpandedRow,
Expand All @@ -23,8 +23,13 @@ export interface GroupIndexDef {
getLabel: (row: StatGroupExpandedRow) => string;
}

export function formatNumericStat(n: number | null, statName: string): string {
export function formatNumericStat(
n: number | null,
statName: string,
quantitySpec?: QuantitySpec
): string {
if (n === null) return '-';
if (quantitySpec) return formatQuantity(n, quantitySpec, 'Occupancy');
return inferFieldFormatter(statName)(n);
}

Expand All @@ -41,9 +46,13 @@ export function itemHasId(items: Iterable<string>, target: ReadonlySet<string>):
return false;
}

export function formatStatValue(value: StatValue, statName: string): string {
export function formatStatValue(
value: StatValue,
statName: string,
quantitySpec?: QuantitySpec
): string {
if (value === null || value === undefined) return '-';
if (typeof value === 'number') return formatNumericStat(value, statName);
if (typeof value === 'number') return formatNumericStat(value, statName, quantitySpec);
if (typeof value === 'boolean') return value ? 'true' : 'false';
if (Array.isArray(value)) return value.join(', ');
return String(value);
Expand Down
16 changes: 11 additions & 5 deletions ui/packages/@quent/components/src/query-plan/QueryPlanNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
useSetHighlightedNodeIds,
} from '@quent/hooks';
import { parseCustomStatistics } from '../lib/queryBundle.utils';
import { inferFieldFormatter } from '@quent/utils';
import { formatStatWithQuantity } from '@quent/utils';
import { DataText } from '../ui/data-text';
import { NodeFlowBar } from './NodeFlowBar';

Expand All @@ -53,6 +53,8 @@ export interface QueryPlanNodeData extends Record<string, unknown> {
* relayouts exactly once.
*/
flowBarVisible?: boolean;
/** Quantity specs from the QueryBundle, forwarded for quantity-aware stat formatting. */
quantitySpecs?: { [key: string]: import('@quent/utils').QuantitySpec | undefined };
}

const nodeVariants = cva(
Expand Down Expand Up @@ -118,14 +120,18 @@ export const QueryPlanNode = memo(({ data }: { data: QueryPlanNodeData }) => {
return data.label;
}, [nodeLabelField, data]);

const colorFieldValue = colorField
? (statistics.find(s => s.key === colorField)?.value ?? null)
: null;
const colorFieldStat = colorField ? statistics.find(s => s.key === colorField) : null;
const colorFieldValue = colorFieldStat?.value ?? null;
const formattedColorFieldValue =
colorFieldValue === null
? null
: typeof colorFieldValue === 'number'
? inferFieldFormatter(colorField!)(colorFieldValue)
? formatStatWithQuantity(
colorFieldValue,
colorField!,
colorFieldStat?.quantity,
data.quantitySpecs
)
: String(colorFieldValue);

const baseColor = data.baseColor ?? getOperationTypeColor(data.operationType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface DAGData {
nodes: import('@quent/utils').DAGNode[];
edges: import('@quent/utils').DAGEdge[];
queryData: QueryPlanDataItem[];
quantitySpecs?: { [key in string]?: import('@quent/utils').QuantitySpec };
}

export interface QueryPlanNodeData extends Record<string, unknown> {
Expand Down
2 changes: 1 addition & 1 deletion ui/packages/@quent/hooks/src/atoms/dagControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface InspectedNodeData {
nodeId: string;
label: string;
operationType: string;
statistics: Array<{ key: string; value: StatValue }>;
statistics: Array<{ key: string; value: StatValue; quantity?: string }>;
}

/** Data for the currently selected/pinned node (persists in the panel after click) */
Expand Down
Loading