Skip to content

Feature/dove graph #422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
91 changes: 91 additions & 0 deletions media/explain/borderAndIconDraw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// @ts-nocheck
import {
getNodeTopLeftAbsolute,
getTopLeftForBorder,
getBorderWidthAndHeight,
} from "./graphUtils.js";

export function deleteAllBorders() {
document.querySelectorAll(".border").forEach((el) => el.remove());
}

const iconMap = window.iconMap;

const getCodiconClass = (label) => {
const className = iconMap[label];
return className !== undefined ? `codicon-${className}` : "";
};

function drawNodeIcon(fontSize, color, label) {
const icon = document.createElement("i");
const codiconClass = getCodiconClass(label);
icon.className = `codicon ${codiconClass}`;
Object.assign(icon.style, {
fontSize: `${fontSize}px`,
color,
height: "fit-content",
});
return icon;
}

export function drawBorderAndIconForEachExplainNode(
cy,
windowPadding
) {
const paddingX = 30;
const paddingY = 10;
const iconGap = 20;
const borderRadius = 10;
const iconColor = "#007acc";

let minIconSize = 50;
cy.nodes().forEach((node) => {
const nodeW = node.renderedWidth();
const iconSize = nodeW / 2;
minIconSize = Math.min(minIconSize, iconSize);
});


cy.nodes().forEach((node) => {
const nodeW = node.renderedWidth();
const nodeH = node.renderedHeight();

const nodeTopLeft = getNodeTopLeftAbsolute(node, cy);

const topLeft = getTopLeftForBorder(
nodeTopLeft.x,
nodeTopLeft.y,
paddingX,
paddingY,
minIconSize,
iconGap
);
const dimensions = getBorderWidthAndHeight(
paddingX,
nodeW,
paddingY,
nodeH,
minIconSize,
iconGap
);

const border = document.createElement("div");
border.className = "border";

Object.assign(border.style, {
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
position: "absolute",
top: `${topLeft.y}px`,
left: `${topLeft.x}px`,
display: "flex",
justifyContent: "center",
paddingTop: `${paddingY}px`,
borderRadius: `${borderRadius}px`,
borderColor: "transparent"
});

border.appendChild(drawNodeIcon(minIconSize, iconColor, node.data().label));
document.body.appendChild(border);
});
}
31 changes: 31 additions & 0 deletions media/explain/cytoscape.min.js

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions media/explain/explain.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.diagram-container {
position: absolute;
margin: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100%;
overflow: hidden;
border: none;
box-sizing: border-box;
}

.hover-box {
border-radius: 6px;
padding: 6px 10px;
font-size: 13px;
font-family: sans-serif;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid #444;
white-space: nowrap;
z-index: 1000;
transition: opacity 0.2s ease;
opacity: 0.95;
position: absolute;
background: #fff;
border: 1px solid #aaa;
padding: 4px 8px;
color: black;
}

.border {
pointer-events: none;
}

.warning-div {
position: fixed;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
padding: 2rem;
text-align: center;
}

.warning-div-title {
margin: 0;
font-size: 1.5rem;
color: #007acc;
}
132 changes: 132 additions & 0 deletions media/explain/explain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// @ts-nocheck
import {
getTooltipPosition,
showResizeWarning,
isWindowTooSmall,
isEnoughAreaWithoutBorders,
} from "./graphUtils.js";
import {
deleteAllBorders,
drawBorderAndIconForEachExplainNode,
} from "./borderAndIconDraw.js";

const tooltips = window.tooltips;
const vscode = window.acquireVsCodeApi();
const GRAPH_PADDING = 50;

if (isWindowTooSmall(GRAPH_PADDING)) {
showResizeWarning();
} else {
// Initialize Cytoscape
const cy = cytoscape({
container: document.getElementById("diagramContainer"),
elements: window.data,
userZoomingEnabled: false,
style: [
{
selector: "node",
style: {
padding: "5px",
width: "150px",
shape: "roundrectangle",
"background-color": "lightgray",
color: "black",
label: "data(label)",
"text-wrap": "wrap",
"text-max-width": "150px",
"text-valign": "center",
"text-halign": "center",
"font-size": "14px",
"line-height": "1.2",
},
},
{
selector: "node:selected",
style: {
"background-color": "deepskyblue",
},
},
{
selector: "edge",
style: {
width: 2,
"line-color": "#5c96bc",
"target-arrow-color": "#5c96bc",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
},
},
],

layout: {
name: "grid",
fit: true, // whether to fit the viewport to the graph
padding: GRAPH_PADDING, // padding used on fit
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
avoidOverlapPadding: 10, // extra spacing around nodes when avoidOverlap: true
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
condense: false, // uses all available space on false, uses minimal space on true
animate: false, // whether to transition the node positions
},
});

window.addEventListener("resize", () => {
cy.resize();
cy.fit(cy.nodes().boundingBox(), GRAPH_PADDING);
cy.center();
});

if (!isEnoughAreaWithoutBorders(cy, GRAPH_PADDING)) {
cy.destroy();
showResizeWarning();
} else {
// When clicked, we display the details for the node in the bottom tree view
cy.on("tap", "node", function (evt) {
const id = evt.target.id();
vscode.postMessage({
command: "selected",
nodeId: id,
});
});

// === Tooltip Hover Handler ===
cy.nodes().on("mouseover", (event) => {
const node = event.target;

const hoverDiv = document.createElement("pre");
const id = node.id();
const tooltip = tooltips[id];
hoverDiv.innerText = tooltip;
hoverDiv.className = "hover-box";
document.body.appendChild(hoverDiv);

function updatePosition() {
const { left, top } = getTooltipPosition(
node,
cy.container(),
hoverDiv
);
hoverDiv.style.left = `${left}px`;
hoverDiv.style.top = `${top}px`;
}

updatePosition();
cy.on("pan zoom resize", updatePosition);
node.on("position", updatePosition);

node.once("mouseout", () => {
hoverDiv.remove();
cy.off("pan zoom resize", updatePosition);
node.off("position", updatePosition);
});
});

function redrawBorders() {
deleteAllBorders();
drawBorderAndIconForEachExplainNode(cy, GRAPH_PADDING);
}

cy.on("pan zoom resize", redrawBorders);
drawBorderAndIconForEachExplainNode(cy, GRAPH_PADDING);
}
}
113 changes: 113 additions & 0 deletions media/explain/graphUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
export const MIN_DISTANCE_TO_VIEWPORT = 4;
export const TOOLTIP_OFFSET = 20;

// === Node Position Utilities ===

export function getNodeTopLeftAbsolute(node, cy) {
const containerRect = cy.container().getBoundingClientRect();
const pos = node.position();
const zoom = cy.zoom();
const pan = cy.pan();

const renderedX = pos.x * zoom + pan.x;
const renderedY = pos.y * zoom + pan.y;

return {
x: containerRect.left + renderedX - node.renderedWidth() / 2,
y: containerRect.top + renderedY - node.renderedHeight() / 2,
};
}

// === Border Geometry Utilities ===

export function getTopLeftForBorder(x, y, padX, padY, iconH, iconGap) {
return {
x: x - padX - 2, // slight adjustment
y: y - padY - iconH - iconGap,
};
}

export function getBorderWidthAndHeight(
padX,
nodeW,
padY,
nodeH,
iconH,
iconGap
) {
return {
width: padX * 2 + nodeW,
height: padY * 2 + iconH + iconGap + nodeH,
};
}

// === Tooltip Position Utility ===
export function getTooltipPosition(node, container, tooltipBox) {
const { x, y } = node.renderedPosition();
const containerRect = container.getBoundingClientRect();
const boxRect = tooltipBox.getBoundingClientRect();

let left = x + containerRect.left - boxRect.width / 2;
let top =
y -
node.renderedOuterHeight() / 2 +
containerRect.top -
boxRect.height -
TOOLTIP_OFFSET;

// Prevent overflow
if (left < MIN_DISTANCE_TO_VIEWPORT) left = MIN_DISTANCE_TO_VIEWPORT;
if (left + boxRect.width > window.innerWidth - MIN_DISTANCE_TO_VIEWPORT) {
left = window.innerWidth - boxRect.width - MIN_DISTANCE_TO_VIEWPORT;
}

if (top < MIN_DISTANCE_TO_VIEWPORT) {
top = y + containerRect.top + node.renderedOuterHeight() / 2;
}

return { left, top };
}

export function showResizeWarning() {
const warningDiv = document.createElement("div");
warningDiv.className = "warning-div"

const h1 = document.createElement("h1");
h1.className = "warning-div-title"
h1.textContent = "Window is too small. Make your window larger and run again.";

warningDiv.appendChild(h1);
document.body.appendChild(warningDiv);
}

export function isEnoughAreaWithoutBorders(cy, windowPadding) {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;

let areaNeededForAllNodes = 0;
let maxHeightNeededForNode = 60;
let maxWidthNeededForNode = 100;

cy.nodes().forEach(node => {
const nodeW = node.renderedWidth();
const nodeH = node.renderedHeight();
areaNeededForAllNodes += nodeW * nodeH
maxHeightNeededForNode = Math.max(maxHeightNeededForNode, nodeH);
maxWidthNeededForNode = Math.max(maxWidthNeededForNode, nodeW);
});

const usableWidth = windowWidth - 2 * windowPadding;
const usableHeight = windowHeight - 2 * windowPadding;

const maxCols = Math.floor(usableWidth / maxWidthNeededForNode);
const maxRows = Math.floor(usableHeight / maxHeightNeededForNode);

const maxNodesThatCanFit = maxCols * maxRows;

return cy.nodes().length <= maxNodesThatCanFit;
}

export function isWindowTooSmall(windowPadding){
const extraRoom = 50
return windowPadding * 2 >= window.innerWidth || windowPadding * 2 + extraRoom >= window.innerHeight
}
Loading