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
2 changes: 1 addition & 1 deletion .github/workflows/github-action-test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
build-test:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
strategy:
# Run all versions to the end, even if one of them fails
fail-fast: false
Expand Down
64 changes: 53 additions & 11 deletions src/programflow-visualization/web/graph-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const MARGIN_Y = 24;
*/
const MARGIN_X = 8;

/** Room above the nodes for the "Frames" / "Objects" captions. */
/** Room above the nodes for the "Variables" / "Objects" captions. */
const BAND_HEIGHT = 28;

/**
Expand Down Expand Up @@ -189,6 +189,12 @@ export function renderGraph(
const edgesIntoNode = new Map<string, SVGElement[]>();
const edgesOutOfNode = new Map<string, SVGElement[]>();
const edgesBySourcePort = new Map<string, SVGElement[]>();
// The node at the far end of each edge's path, keyed by the path itself (never the
// badge): lets a hover on one end light up the node the arrow actually reaches, see
// `otherEndNodes` below.
const edgeEndpoints = new Map<SVGElement, { source: string; target: string }>();
// Filled in as nodes are built below; `paint` looks a peer node's element up here.
const nodeElementsById = new Map<string, HTMLElement>();
const track = (
index: Map<string, SVGElement[]>,
key: string | undefined,
Expand Down Expand Up @@ -243,9 +249,14 @@ export function renderGraph(
parts.push(badge);
}

track(edgesOutOfNode, portToNode.get(edge.sources[0]), parts);
track(edgesIntoNode, portToNode.get(edge.targets[0]), parts);
const sourceNode = portToNode.get(edge.sources[0]);
const targetNode = portToNode.get(edge.targets[0]);
track(edgesOutOfNode, sourceNode, parts);
track(edgesIntoNode, targetNode, parts);
track(edgesBySourcePort, edge.sources[0], parts);
if (sourceNode && targetNode) {
edgeEndpoints.set(path, { source: sourceNode, target: targetNode });
}
}
container.append(svg);

Expand All @@ -259,10 +270,35 @@ export function renderGraph(
/** `row` washes the row itself; both `box` kinds outline the whole box. */
kind: "row" | "box-incoming" | "box-all";
edges: SVGElement[];
/** The node(s) at the far end of `edges` -- what the arrow(s) actually reach. */
peerNodeIds: Set<string>;
};

let active: Hover | undefined;

/**
* The node at the other end of each of `edges` from `ownNodeId`, so that pointing at
* a variable or attribute also picks out the object its arrow reaches (and pointing
* at a box that is pointed to picks out whoever points at it). Self-loops -- an
* object referencing itself -- contribute nothing, since that end is already
* `ownNodeId`.
*/
const otherEndNodes = (edges: SVGElement[], ownNodeId: string): Set<string> => {
const peers = new Set<string>();
for (const edge of edges) {
const ends = edgeEndpoints.get(edge);
if (!ends) {
continue;
}
if (ends.source === ownNodeId && ends.target !== ownNodeId) {
peers.add(ends.target);
} else if (ends.target === ownNodeId && ends.source !== ownNodeId) {
peers.add(ends.source);
}
}
return peers;
};

/**
* The edges leaving one row. Two of them when the row is a dict entry whose key is a
* reference: that row owns a second port, and both arrows leave the row being pointed
Expand Down Expand Up @@ -301,14 +337,16 @@ export function renderGraph(
if (row) {
const leaving = edgesLeavingRow(row);
return leaving.length > 0
? { element: row, kind: "row", edges: leaving }
: { element: node, kind: "box-incoming", edges: incoming };
? { element: row, kind: "row", edges: leaving, peerNodeIds: otherEndNodes(leaving, nodeId) }
: {
element: node,
kind: "box-incoming",
edges: incoming,
peerNodeIds: otherEndNodes(incoming, nodeId),
};
}
return {
element: node,
kind: "box-all",
edges: [...incoming, ...(edgesOutOfNode.get(nodeId) ?? [])],
};
const edges = [...incoming, ...(edgesOutOfNode.get(nodeId) ?? [])];
return { element: node, kind: "box-all", edges, peerNodeIds: otherEndNodes(edges, nodeId) };
};

const paint = (on: boolean) => {
Expand All @@ -325,6 +363,9 @@ export function renderGraph(
} else {
active.element.classList.toggle("elk-node-active", on);
}
for (const peerId of active.peerNodeIds) {
nodeElementsById.get(peerId)?.classList.toggle("elk-node-active", on);
}
for (const path of active.edges) {
path.classList.toggle("elk-edge-active", on);
}
Expand Down Expand Up @@ -353,6 +394,7 @@ export function renderGraph(
}
const element = renderNode(model);
element.dataset.nodeId = child.id;
nodeElementsById.set(child.id, element);
element.style.left = `${(child.x ?? 0) + offsetX}px`;
element.style.top = `${(child.y ?? 0) + offsetY}px`;
element.style.width = `${child.width ?? 0}px`;
Expand Down Expand Up @@ -400,7 +442,7 @@ export function renderGraph(
// Column captions, derived from where the nodes actually ended up: there are no
// container nodes to hang them off (plan 4).
if (framesRight > Number.NEGATIVE_INFINITY) {
container.append(band("Frames", "elk-band-frames", MARGIN_X, framesRight));
container.append(band("Variables", "elk-band-frames", MARGIN_X, framesRight));
}
if (objectsLeft < Number.POSITIVE_INFINITY) {
container.append(
Expand Down
6 changes: 6 additions & 0 deletions src/programflow-visualization/web/node-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export function renderNode(model: NodeModel): HTMLElement {
header.setAttribute("aria-expanded", String(!model.collapsed));
header.append(div("elk-caret", model.collapsed ? "\u25B8" : "\u25BE"));
}
if (model.kind === "current-frame") {
// The frame currently executing, marked the way a debugger marks the active
// stack frame, since the border-left accent alone reads as just another colour
// (plan 6.4).
header.append(div("elk-current-frame-marker", "\u25B6"));
}
header.append(div("elk-node-title", model.header));
node.append(header);

Expand Down
25 changes: 23 additions & 2 deletions src/programflow-visualization/web/webview.css
Original file line number Diff line number Diff line change
Expand Up @@ -357,17 +357,38 @@ body {
background-image: linear-gradient(var(--wypp-frame-bg), var(--wypp-frame-bg));
}

.elk-frame .elk-node-header,
.elk-current-frame .elk-node-header {
.elk-frame .elk-node-header {
background-color: var(--wypp-frame-header-bg);
}

/* The current frame's header gets its own, stronger tint -- rather than sharing the
wash every frame gets -- so the stack's one active frame reads as different at a
glance, not just differently outlined (plan 6.4). */
.elk-current-frame .elk-node-header {
background-color: color-mix(
in srgb,
var(--wypp-accent-current-frame) 32%,
var(--wypp-frame-header-bg)
);
}

.elk-frame {
border-left-color: var(--wypp-accent-frame);
}

.elk-current-frame {
border-left-color: var(--wypp-accent-current-frame);
/* Wider than the 4px every other node gets, on top of the colour difference: the
stack has exactly one current frame, and it is worth a second cue. */
border-left-width: 6px;
}

/* The small triangle in the current frame's header, echoing the arrow a debugger
points at the active stack frame with. */
.elk-current-frame-marker {
flex: 0 0 auto;
color: var(--wypp-accent-current-frame);
font-size: 9px;
}

/* One accent per heap type, so kind is readable at a glance and at small zoom
Expand Down
Loading