DAG Visualization Styling Reference

This document captures the exact styling specifications for the DAG (Directed Acyclic Graph) visualization, derived from the prototype HTML at docs/penpot-exports/html/audittrail-prototype.html. It serves as the single source of truth for any future DAG-related styling work.


Prototype Reference

The prototype renders the DAG using D3.js + dagre layout directly in SVG. The production implementation uses React Flow (@xyflow/react) + dagre (@dagrejs/dagre) with custom React node components. Both must produce visually identical output.


Edge Styling

Curve Type

PropertyPrototype (D3)Production (React Flow)
CurveCubic bezier: M x1,y1 C x1,midY x2,midY x2,y2type: "default" (bezier)
Stroke width2strokeWidth: 2
Stroke opacity0.4 (normal), 0.7 (highlighted), 0.05 (dimmed)strokeOpacity: 0.4 / 0.7 / 0.05
Stroke colorstatusColors[targetNode.status]EDGE_STATUS_COLOR[targetStatus]

Important: The prototype uses cubic bezier curves, NOT stepped/angular paths. React Flow's type: "smoothstep" produces right-angle paths with rounded corners — this is visually wrong. Always use type: "default" which renders true bezier curves matching the prototype.

Arrow Markers

PropertyPrototype (D3)Production (React Flow)
ShapeOpen V-shape (unfilled, two lines)MarkerType.Arrow (open V)
Size4px wide, 6px tallwidth: 10, height: 10
StrokeSame color as edge, stroke-width: 1.5, stroke-opacity: 0.5strokeWidth: 1.5, color matches edge

Important: Use MarkerType.Arrow (open/unfilled), NOT MarkerType.ArrowClosed (filled triangle). The prototype draws an open V-shape chevron, not a solid triangle.

Edge Color by Target Node Status

ok / success / complete  ->  #22c55e  (green)
error                    ->  #f87171  (red)
warning / running        ->  #eab308  (amber)
default                  ->  #444444  (dark gray)

Highlighting Behavior

  • Node click: Walk ancestors + descendants, set connected edges to strokeOpacity: 0.7, non-connected to 0.05
  • Focus errors: Walk error node ancestors, set connected edges to strokeOpacity: 0.8 with stroke: #f87171, non-connected to 0.03
  • Reset: All edges return to strokeOpacity: 0.4 with original status color

Node Styling

Shared Structure (all 4 node types)

+---+--------------------------------------+
|   | [BADGE]  Node Name              [*]  |   <- * = status dot
| A |  12ms $0.002                         |   <- A = left accent bar
| C |  gpt-4o                              |   <- model (LLM only)
+---+--------------------------------------+
PropertyValue
Width200px
HeightAuto (content-driven), dagre uses 72px estimate
Border radius8px
Background#1a1a2e
Border2px solid <statusColor> at 0.8 opacity
Left borderSet to 0 width (accent bar covers it)
Left accent bar4px wide, full height, type color, border-radius: 8px 0 0 8px
Content padding10px 14px 10px 18px

Node Border Opacity

The prototype sets stroke-opacity: 0.8 on node borders. Production must use rgba equivalents:

ok / success / complete  ->  rgba(34, 197, 94, 0.8)
error                    ->  rgba(248, 113, 113, 0.8)
warning / running        ->  rgba(234, 179, 8, 0.8)
default                  ->  rgba(99, 99, 102, 0.8)

When selected, the border switches to the type color at full opacity with a glow shadow.

Type Badge

PropertyValue
Padding1px 6px (production) / 2px 6px (prototype)
Border radius4px
Font size10px (production) / 9px (prototype)
Font weight500 (production) / 700 (prototype)
Text transformuppercase
Font familyInter, sans-serif
BackgroundType color at 0.2 opacity
Text colorType color

Type Colors

LLM       ->  #60a5fa  (blue)
Tool      ->  #c084fc  (purple)
Agent     ->  #2dd4bf  (teal)
Internal  ->  #636366  (gray)

Status Dot

PropertyValue
Size8px diameter (production) / 6px (prototype node), 8px (prototype detail)
PositionRight side of badge row, margin-left: auto
ShapeCircle (border-radius: 50%)
ColorStatus color (same as border, full opacity)

Node Name

PropertyValue
Font size13px
Font weight400
Color#fafafa
Overflowtext-overflow: ellipsis, white-space: nowrap
Max charsTruncate at 22 chars in prototype

Subtitle (Duration / Cost / Model)

PropertyValue
Font size11px (duration/cost), 10px (model)
Color#a1a1a1 (duration/cost), #636366 (model)
Font familyInter, sans-serif

React Flow Handles (connection points)

PropertyValue
Size8px x 8px
BackgroundType color
Border2px solid #1a1a2e (matches node background)
PositionTop (target), Bottom (source)

Canvas

PropertyValue
Background color#0a0a0a
Background patternDots, gap: 20, size: 1, color: #222
Min zoom0.1 (prototype: 0.2)
Max zoom2 (prototype: 3)
Color modedark
Fit view padding0.15

MiniMap

PropertyValue
Positionbottom-right
Background#111
Border1px solid #333
Border radius8px
Mask colorrgba(0, 0, 0, 0.6)
Node colorsSame as type colors (LLM=#60a5fa, Tool=#c084fc, Agent=#2dd4bf, Internal=#636366)

Tooltip (on hover)

Matches prototype .d3-tooltip class exactly.

PropertyValue
Background#1c1c1e
Border1px solid #333333
Border radius8px
Padding10px 14px
Font size12px
Text color#fafafa
Max width280px (production) / 320px (prototype)
Box shadow0 8px 24px rgba(0,0,0,0.5)
Z-index9999
Pointer eventsnone

Tooltip Content

  • Title: font-weight: 600, font-size: 13px, margin-bottom: 6px
  • Row labels: color: #a1a1a1
  • Row values: font-family: monospace, font-size: 11px, color: #a855f7 (purple)
  • Type value color: Matches type color (LLM=#60a5fa, Tool=#c084fc, Agent=#2dd4bf, Internal=#636366)
  • Status value color: Matches status color

Dagre Layout Configuration

PropertyValue
DirectionTB (top to bottom)
Node separation40px
Rank separation60px
Margin X20px (production) / 40px (prototype)
Margin Y20px (production) / 40px (prototype)
Node width estimate200px (matches node width)
Node height estimate72px

Controls Panel

PropertyValue
Positionabsolute top-3 right-3 z-10
Containerrounded-lg border border-[#333] bg-[#171717]/90 p-1 backdrop-blur-sm
ButtonsDAG label, Fit View, Focus Errors, Collapse All
Error buttontext-[#f87171] hover:text-[#f87171] hover:bg-[#f87171]/10

Bug History

Edge curves rendering as angular steps (fixed 2026-03-30)

Symptom: DAG edges appeared as right-angle stepped paths instead of smooth curves.

Root cause: React Flow edge type was set to "smoothstep" which renders orthogonal paths with rounded corners. The prototype uses cubic bezier curves (C command in SVG path data).

Fix: Changed type: "smoothstep" to type: "default" in apiEdgeToFlowEdge() in dag-viewer.tsx. React Flow's "default" edge type renders true bezier curves matching the prototype.

Edge arrows rendering as filled triangles (fixed 2026-03-30)

Symptom: Arrow markers at edge endpoints were solid filled triangles instead of open V-shapes.

Root cause: MarkerType.ArrowClosed renders a filled triangle. The prototype draws an open chevron using two lines.

Fix: Changed MarkerType.ArrowClosed to MarkerType.Arrow with strokeWidth: 1.5.

Node borders at full opacity (fixed 2026-03-30)

Symptom: Node borders appeared slightly heavier/brighter than prototype.

Root cause: Border used raw hex colors at full opacity. The prototype applies stroke-opacity: 0.8 to all node borders.

Fix: Added statusBorder lookup map with rgba values at 0.8 alpha to all 4 node components (llm-node, tool-node, agent-node, internal-node).


Files

FilePurpose
apps/web/components/dag/dag-viewer.tsxMain DAG component, edge conversion, layout, highlighting
apps/web/components/dag/nodes/llm-node.tsxLLM node rendering
apps/web/components/dag/nodes/tool-node.tsxTool node rendering
apps/web/components/dag/nodes/agent-node.tsxAgent node rendering
apps/web/components/dag/nodes/internal-node.tsxInternal node rendering
apps/web/components/dag/dag-controls.tsxDAG toolbar (Fit View, Focus Errors, etc.)
apps/web/hooks/use-dag-data.tsAPI hook + TypeScript interfaces for DAG data
apps/api/src/audittrail/dag.pyBackend DAG builder (node/edge generation from spans)
docs/penpot-exports/html/audittrail-prototype.htmlPrototype source of truth (lines 2155-2268)