feat: latency heatmap overlay on process diagram (#94)
Some checks failed
Some checks failed
Add latencyHeatmap prop to ProcessDiagram that colors nodes green→yellow→red based on their relative contribution to route latency (pctOfRoute). Shows avg duration label on each node. Threaded through CompoundNode for nested EIP patterns. Heatmap is active only when no execution overlay is present. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||
import type { NodeConfig } from './types';
|
||||
import type { NodeConfig, LatencyHeatmapEntry } from './types';
|
||||
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||
import { colorForType, iconForType, type IconElement } from './node-colors';
|
||||
import { ConfigBadge } from './ConfigBadge';
|
||||
@@ -16,12 +16,27 @@ interface DiagramNodeProps {
|
||||
config?: NodeConfig;
|
||||
executionState?: NodeExecutionState;
|
||||
overlayActive?: boolean;
|
||||
heatmapEntry?: LatencyHeatmapEntry;
|
||||
onClick: () => void;
|
||||
onDoubleClick?: () => void;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
}
|
||||
|
||||
/** Interpolate green (120°) → yellow (60°) → red (0°) based on pctOfRoute */
|
||||
function heatmapColor(pct: number): string {
|
||||
const clamped = Math.max(0, Math.min(100, pct));
|
||||
// 0% → hue 120 (green), 50% → hue 60 (yellow), 100% → hue 0 (red)
|
||||
const hue = 120 - (clamped / 100) * 120;
|
||||
return `hsl(${Math.round(hue)}, 70%, 92%)`;
|
||||
}
|
||||
|
||||
function heatmapBorderColor(pct: number): string {
|
||||
const clamped = Math.max(0, Math.min(100, pct));
|
||||
const hue = 120 - (clamped / 100) * 120;
|
||||
return `hsl(${Math.round(hue)}, 60%, 50%)`;
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
@@ -29,7 +44,7 @@ function formatDuration(ms: number): string {
|
||||
|
||||
export function DiagramNode({
|
||||
node, isHovered, isSelected, config,
|
||||
executionState, overlayActive,
|
||||
executionState, overlayActive, heatmapEntry,
|
||||
onClick, onDoubleClick, onMouseEnter, onMouseLeave,
|
||||
}: DiagramNodeProps) {
|
||||
const x = node.x ?? 0;
|
||||
@@ -49,7 +64,7 @@ export function DiagramNode({
|
||||
const isFailed = executionState?.status === 'FAILED';
|
||||
const isSkipped = overlayActive && !executionState;
|
||||
|
||||
// Colors based on execution state
|
||||
// Colors based on execution state (heatmap takes priority when no execution overlay)
|
||||
let cardFill = isHovered ? '#F5F0EA' : 'white';
|
||||
let borderStroke = isHovered || isSelected ? color : '#E4DFD8';
|
||||
let borderWidth = isHovered || isSelected ? 1.5 : 1;
|
||||
@@ -67,6 +82,11 @@ export function DiagramNode({
|
||||
borderWidth = 2;
|
||||
topBarColor = '#C0392B';
|
||||
labelColor = '#C0392B';
|
||||
} else if (heatmapEntry && !overlayActive) {
|
||||
cardFill = heatmapColor(heatmapEntry.pctOfRoute);
|
||||
borderStroke = heatmapBorderColor(heatmapEntry.pctOfRoute);
|
||||
borderWidth = 1.5;
|
||||
topBarColor = heatmapBorderColor(heatmapEntry.pctOfRoute);
|
||||
}
|
||||
|
||||
const statusColor = isCompleted ? '#3D7C47' : isFailed ? '#C0392B' : undefined;
|
||||
@@ -195,6 +215,20 @@ export function DiagramNode({
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Heatmap: avg duration label at bottom-right */}
|
||||
{heatmapEntry && !overlayActive && !executionState && (
|
||||
<text
|
||||
x={w - 6}
|
||||
y={h - 4}
|
||||
textAnchor="end"
|
||||
fill={heatmapBorderColor(heatmapEntry.pctOfRoute)}
|
||||
fontSize={9}
|
||||
fontWeight={600}
|
||||
>
|
||||
{formatDuration(heatmapEntry.avgDurationMs)}
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Sub-route failure: drill-down arrow at bottom-left */}
|
||||
{isFailed && executionState?.subRouteFailed && (
|
||||
<g transform={`translate(4, ${h - 14})`}>
|
||||
|
||||
Reference in New Issue
Block a user