Skip to content

Commit eb0d8ea

Browse files
committed
visual explain redesign
1 parent 5639fa7 commit eb0d8ea

16 files changed

+861
-152
lines changed

media/explain/borderAndIconDraw.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// @ts-nocheck
2+
import {
3+
getNodeTopLeftAbsolute,
4+
getTopLeftForBorder,
5+
getBorderWidthAndHeight,
6+
} from "./graphUtils.js";
7+
8+
export function deleteAllBorders() {
9+
document.querySelectorAll(".border").forEach((el) => el.remove());
10+
}
11+
12+
const iconMap = window.iconMap;
13+
14+
const getCodiconClass = (label) => {
15+
const className = iconMap[label];
16+
return className !== undefined ? `codicon-${className}` : "";
17+
};
18+
19+
function drawNodeIcon(fontSize, color, label) {
20+
const icon = document.createElement("i");
21+
const codiconClass = getCodiconClass(label);
22+
icon.className = `codicon ${codiconClass}`;
23+
Object.assign(icon.style, {
24+
fontSize: `${fontSize}px`,
25+
color,
26+
height: "fit-content",
27+
});
28+
return icon;
29+
}
30+
31+
export function drawBorderAndIconForEachExplainNode(
32+
cy,
33+
windowPadding
34+
) {
35+
const paddingX = 30;
36+
const paddingY = 10;
37+
const iconGap = 20;
38+
const borderRadius = 10;
39+
const iconColor = "#007acc";
40+
41+
let minIconSize = 50;
42+
cy.nodes().forEach((node) => {
43+
const nodeW = node.renderedWidth();
44+
const iconSize = nodeW / 2;
45+
minIconSize = Math.min(minIconSize, iconSize);
46+
});
47+
48+
49+
cy.nodes().forEach((node) => {
50+
const nodeW = node.renderedWidth();
51+
const nodeH = node.renderedHeight();
52+
53+
const nodeTopLeft = getNodeTopLeftAbsolute(node, cy);
54+
55+
const topLeft = getTopLeftForBorder(
56+
nodeTopLeft.x,
57+
nodeTopLeft.y,
58+
paddingX,
59+
paddingY,
60+
minIconSize,
61+
iconGap
62+
);
63+
const dimensions = getBorderWidthAndHeight(
64+
paddingX,
65+
nodeW,
66+
paddingY,
67+
nodeH,
68+
minIconSize,
69+
iconGap
70+
);
71+
72+
const border = document.createElement("div");
73+
border.className = "border";
74+
75+
Object.assign(border.style, {
76+
width: `${dimensions.width}px`,
77+
height: `${dimensions.height}px`,
78+
position: "absolute",
79+
top: `${topLeft.y}px`,
80+
left: `${topLeft.x}px`,
81+
display: "flex",
82+
justifyContent: "center",
83+
paddingTop: `${paddingY}px`,
84+
borderRadius: `${borderRadius}px`,
85+
borderColor: "transparent"
86+
});
87+
88+
border.appendChild(drawNodeIcon(minIconSize, iconColor, node.data().label));
89+
document.body.appendChild(border);
90+
});
91+
}

media/explain/cytoscape.min.js

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

media/explain/explain.css

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
.diagram-container {
2+
position: absolute;
3+
margin: 0;
4+
top: 0;
5+
left: 0;
6+
right: 0;
7+
bottom: 0;
8+
height: 100%;
9+
overflow: hidden;
10+
border: none;
11+
box-sizing: border-box;
12+
}
13+
14+
.hover-box {
15+
border-radius: 6px;
16+
padding: 6px 10px;
17+
font-size: 13px;
18+
font-family: sans-serif;
19+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
20+
border: 1px solid #444;
21+
white-space: nowrap;
22+
z-index: 1000;
23+
transition: opacity 0.2s ease;
24+
opacity: 0.95;
25+
position: absolute;
26+
background: #fff;
27+
border: 1px solid #aaa;
28+
padding: 4px 8px;
29+
color: black;
30+
}
31+
32+
.border {
33+
pointer-events: none;
34+
}
35+
36+
.warning-div {
37+
position: fixed;
38+
width: 100%;
39+
top: 50%;
40+
left: 50%;
41+
transform: translate(-50%, -50%);
42+
z-index: 9999;
43+
padding: 2rem;
44+
text-align: center;
45+
}
46+
47+
.warning-div-title {
48+
margin: 0;
49+
font-size: 1.5rem;
50+
color: #007acc;
51+
}

media/explain/explain.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// @ts-nocheck
2+
import {
3+
getTooltipPosition,
4+
showResizeWarning,
5+
isWindowTooSmall,
6+
isEnoughAreaWithoutBorders,
7+
} from "./graphUtils.js";
8+
import {
9+
deleteAllBorders,
10+
drawBorderAndIconForEachExplainNode,
11+
} from "./borderAndIconDraw.js";
12+
13+
const tooltips = window.tooltips;
14+
const vscode = window.acquireVsCodeApi();
15+
const GRAPH_PADDING = 50;
16+
17+
if (isWindowTooSmall(GRAPH_PADDING)) {
18+
showResizeWarning();
19+
} else {
20+
// Initialize Cytoscape
21+
const cy = cytoscape({
22+
container: document.getElementById("diagramContainer"),
23+
elements: window.data,
24+
userZoomingEnabled: false,
25+
style: [
26+
{
27+
selector: "node",
28+
style: {
29+
padding: "5px",
30+
width: "150px",
31+
shape: "roundrectangle",
32+
"background-color": "lightgray",
33+
color: "var(--vscode-list-activeSelectionForeground)",
34+
label: "data(label)",
35+
"text-wrap": "wrap",
36+
"text-max-width": "150px",
37+
"text-valign": "center",
38+
"text-halign": "center",
39+
"font-size": "14px",
40+
"line-height": "1.2",
41+
},
42+
},
43+
{
44+
selector: "edge",
45+
style: {
46+
width: 2,
47+
"line-color": "#5c96bc",
48+
"target-arrow-color": "#5c96bc",
49+
"target-arrow-shape": "triangle",
50+
"curve-style": "bezier",
51+
},
52+
},
53+
],
54+
55+
layout: {
56+
name: "grid",
57+
fit: true, // whether to fit the viewport to the graph
58+
padding: GRAPH_PADDING, // padding used on fit
59+
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
60+
avoidOverlapPadding: 10, // extra spacing around nodes when avoidOverlap: true
61+
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
62+
condense: false, // uses all available space on false, uses minimal space on true
63+
animate: false, // whether to transition the node positions
64+
},
65+
});
66+
67+
window.addEventListener("resize", () => {
68+
cy.resize();
69+
cy.fit(cy.nodes().boundingBox(), GRAPH_PADDING);
70+
cy.center();
71+
});
72+
73+
if (!isEnoughAreaWithoutBorders(cy, GRAPH_PADDING)) {
74+
cy.destroy();
75+
showResizeWarning();
76+
} else {
77+
// When clicked, we display the details for the node in the bottom tree view
78+
cy.on("tap", "node", function (evt) {
79+
const id = evt.target.id();
80+
vscode.postMessage({
81+
command: "selected",
82+
nodeId: id,
83+
});
84+
});
85+
86+
// === Tooltip Hover Handler ===
87+
cy.nodes().on("mouseover", (event) => {
88+
const node = event.target;
89+
90+
const hoverDiv = document.createElement("pre");
91+
const id = node.id();
92+
const tooltip = tooltips[id];
93+
hoverDiv.innerText = tooltip;
94+
hoverDiv.className = "hover-box";
95+
document.body.appendChild(hoverDiv);
96+
97+
function updatePosition() {
98+
const { left, top } = getTooltipPosition(
99+
node,
100+
cy.container(),
101+
hoverDiv
102+
);
103+
hoverDiv.style.left = `${left}px`;
104+
hoverDiv.style.top = `${top}px`;
105+
}
106+
107+
updatePosition();
108+
cy.on("pan zoom resize", updatePosition);
109+
node.on("position", updatePosition);
110+
111+
node.once("mouseout", () => {
112+
hoverDiv.remove();
113+
cy.off("pan zoom resize", updatePosition);
114+
node.off("position", updatePosition);
115+
});
116+
});
117+
118+
function redrawBorders() {
119+
deleteAllBorders();
120+
drawBorderAndIconForEachExplainNode(cy, GRAPH_PADDING);
121+
}
122+
123+
cy.on("pan zoom resize", redrawBorders);
124+
drawBorderAndIconForEachExplainNode(cy, GRAPH_PADDING);
125+
}
126+
}

media/explain/graphUtils.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
export const MIN_DISTANCE_TO_VIEWPORT = 4;
2+
export const TOOLTIP_OFFSET = 20;
3+
4+
// === Node Position Utilities ===
5+
6+
export function getNodeTopLeftAbsolute(node, cy) {
7+
const containerRect = cy.container().getBoundingClientRect();
8+
const pos = node.position();
9+
const zoom = cy.zoom();
10+
const pan = cy.pan();
11+
12+
const renderedX = pos.x * zoom + pan.x;
13+
const renderedY = pos.y * zoom + pan.y;
14+
15+
return {
16+
x: containerRect.left + renderedX - node.renderedWidth() / 2,
17+
y: containerRect.top + renderedY - node.renderedHeight() / 2,
18+
};
19+
}
20+
21+
// === Border Geometry Utilities ===
22+
23+
export function getTopLeftForBorder(x, y, padX, padY, iconH, iconGap) {
24+
return {
25+
x: x - padX - 2, // slight adjustment
26+
y: y - padY - iconH - iconGap,
27+
};
28+
}
29+
30+
export function getBorderWidthAndHeight(
31+
padX,
32+
nodeW,
33+
padY,
34+
nodeH,
35+
iconH,
36+
iconGap
37+
) {
38+
return {
39+
width: padX * 2 + nodeW,
40+
height: padY * 2 + iconH + iconGap + nodeH,
41+
};
42+
}
43+
44+
// === Tooltip Position Utility ===
45+
export function getTooltipPosition(node, container, tooltipBox) {
46+
const { x, y } = node.renderedPosition();
47+
const containerRect = container.getBoundingClientRect();
48+
const boxRect = tooltipBox.getBoundingClientRect();
49+
50+
let left = x + containerRect.left - boxRect.width / 2;
51+
let top =
52+
y -
53+
node.renderedOuterHeight() / 2 +
54+
containerRect.top -
55+
boxRect.height -
56+
TOOLTIP_OFFSET;
57+
58+
// Prevent overflow
59+
if (left < MIN_DISTANCE_TO_VIEWPORT) left = MIN_DISTANCE_TO_VIEWPORT;
60+
if (left + boxRect.width > window.innerWidth - MIN_DISTANCE_TO_VIEWPORT) {
61+
left = window.innerWidth - boxRect.width - MIN_DISTANCE_TO_VIEWPORT;
62+
}
63+
64+
if (top < MIN_DISTANCE_TO_VIEWPORT) {
65+
top = y + containerRect.top + node.renderedOuterHeight() / 2;
66+
}
67+
68+
return { left, top };
69+
}
70+
71+
export function showResizeWarning() {
72+
const warningDiv = document.createElement("div");
73+
warningDiv.className = "warning-div"
74+
75+
const h1 = document.createElement("h1");
76+
h1.className = "warning-div-title"
77+
h1.textContent = "Window is too small. Make your window larger and run again.";
78+
79+
warningDiv.appendChild(h1);
80+
document.body.appendChild(warningDiv);
81+
}
82+
83+
export function isEnoughAreaWithoutBorders(cy, windowPadding) {
84+
const windowWidth = window.innerWidth;
85+
const windowHeight = window.innerHeight;
86+
87+
let areaNeededForAllNodes = 0;
88+
let maxHeightNeededForNode = 60;
89+
let maxWidthNeededForNode = 100;
90+
91+
cy.nodes().forEach(node => {
92+
const nodeW = node.renderedWidth();
93+
const nodeH = node.renderedHeight();
94+
areaNeededForAllNodes += nodeW * nodeH
95+
maxHeightNeededForNode = Math.max(maxHeightNeededForNode, nodeH);
96+
maxWidthNeededForNode = Math.max(maxWidthNeededForNode, nodeW);
97+
});
98+
99+
const usableWidth = windowWidth - 2 * windowPadding;
100+
const usableHeight = windowHeight - 2 * windowPadding;
101+
102+
const maxCols = Math.floor(usableWidth / maxWidthNeededForNode);
103+
const maxRows = Math.floor(usableHeight / maxHeightNeededForNode);
104+
105+
const maxNodesThatCanFit = maxCols * maxRows;
106+
107+
return cy.nodes().length <= maxNodesThatCanFit;
108+
}
109+
110+
export function isWindowTooSmall(windowPadding){
111+
const extraRoom = 50
112+
return windowPadding * 2 >= window.innerWidth || windowPadding * 2 + extraRoom >= window.innerHeight
113+
}

0 commit comments

Comments
 (0)