Skip to content

Commit efa3996

Browse files
committed
Add in tooltips
1 parent 83b542d commit efa3996

File tree

3 files changed

+167
-32
lines changed

3 files changed

+167
-32
lines changed

src/views/cytoscape/index.ts

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import { ViewColumn, window } from "vscode";
2-
import * as vscode from 'vscode';
2+
import * as vscode from "vscode";
33
import { ContextProvider } from "../../contextProvider";
44
import { icons } from "../results/explain/icons";
5+
import { ExplainNode } from "../results/explain/nodes";
56

6-
export type Styles = {[key: string]: string};
7+
export type Styles = { [key: string]: string };
78

89
export interface Element {
9-
data: {id: string, label: string},
10-
style: Styles,
11-
classes: string
10+
data: { id: string; label: string };
11+
style: Styles;
12+
classes: string;
1213
}
1314

1415
export interface Edge {
15-
data: {id: string, source: string, target: string}
16+
data: { id: string; source: string; target: string };
1617
}
1718

18-
interface NewNode {
19-
label: string,
20-
styles?: Styles,
21-
parent?: string,
22-
data?: any;
19+
interface NewNode {
20+
label: string;
21+
styles?: Styles;
22+
parent?: string;
23+
data?: ExplainNode;
2324
}
2425

2526
const randomId = () => Math.random().toString(36).substring(7);
2627

2728
export class CytoscapeGraph {
2829
private elementData = new Map<string, any>();
30+
private tooltips = {};
2931
private elements: Element[] = [];
3032
private edges: Edge[] = [];
3133

@@ -36,49 +38,109 @@ export class CytoscapeGraph {
3638

3739
if (node.data) {
3840
this.elementData.set(id, node.data);
41+
const tooltip = node.data.tooltipProps
42+
.map((prop) => `${prop.title}: ${prop.value}`)
43+
.join("\n");
44+
this.tooltips[id] = tooltip;
3945
}
4046

4147
this.elements.push({
42-
data: {id, label: node.label},
48+
data: { id, label: node.label },
4349
style: node.styles || {},
44-
classes: "l1"
50+
classes: "l1",
4551
});
4652

4753
if (node.parent) {
4854
this.edges.push({
49-
data: {id: randomId(), source: node.parent, target: id}
55+
data: { id: randomId(), source: node.parent, target: id },
5056
});
5157
}
5258

5359
return id;
5460
}
5561

5662
createView(title: string, onNodeSelected: (data: unknown) => void): any {
57-
const webview = window.createWebviewPanel(`c`, title, {viewColumn: ViewColumn.One}, {enableScripts: true, retainContextWhenHidden: true});
63+
const webview = window.createWebviewPanel(
64+
`c`,
65+
title,
66+
{ viewColumn: ViewColumn.One },
67+
{ enableScripts: true, retainContextWhenHidden: true }
68+
);
5869
webview.webview.html = this.getHtml(webview.webview);
5970

60-
webview.webview.onDidReceiveMessage((message) => {
61-
if (message.command === 'selected') {
62-
const data = this.elementData.get(message.nodeId);
63-
onNodeSelected(data);
64-
}
65-
}, undefined, []);
71+
webview.webview.onDidReceiveMessage(
72+
(message) => {
73+
if (message.command === "selected") {
74+
const data = this.elementData.get(message.nodeId);
75+
onNodeSelected(data);
76+
}
77+
},
78+
undefined,
79+
[]
80+
);
6681

6782
return webview;
6883
}
6984

7085
private getHtml(webview: vscode.Webview): string {
71-
const data = JSON.stringify([...this.elements, ...this.edges])
86+
const data = JSON.stringify([...this.elements, ...this.edges]);
7287
const iconMap = JSON.stringify(icons);
73-
const context = ContextProvider.getContext()
74-
const cssUri = webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri,'src', 'views', 'cytoscape', 'media', 'explain.css'))
75-
const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri,'src', 'views', 'cytoscape', 'media', 'codicons', 'dist', 'codicon.css'))
76-
const cytoscapeUri = webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri,'src', 'views', 'cytoscape', 'media', 'cytoscape.min.js'))
77-
const cytoscapeHtmlLabelUri = webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri,'src', 'views', 'cytoscape', 'media', 'cytoscape-node-html-label.min.js'))
78-
const explainUri = webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri,'src', 'views', 'cytoscape', 'media', 'explain.js'))
79-
80-
81-
return /*html*/`
88+
const tooltips = JSON.stringify(this.tooltips);
89+
const context = ContextProvider.getContext();
90+
const cssUri = webview.asWebviewUri(
91+
vscode.Uri.joinPath(
92+
context.extensionUri,
93+
"src",
94+
"views",
95+
"cytoscape",
96+
"media",
97+
"explain.css"
98+
)
99+
);
100+
const codiconsUri = webview.asWebviewUri(
101+
vscode.Uri.joinPath(
102+
context.extensionUri,
103+
"src",
104+
"views",
105+
"cytoscape",
106+
"media",
107+
"codicons",
108+
"dist",
109+
"codicon.css"
110+
)
111+
);
112+
const cytoscapeUri = webview.asWebviewUri(
113+
vscode.Uri.joinPath(
114+
context.extensionUri,
115+
"src",
116+
"views",
117+
"cytoscape",
118+
"media",
119+
"cytoscape.min.js"
120+
)
121+
);
122+
const cytoscapeHtmlLabelUri = webview.asWebviewUri(
123+
vscode.Uri.joinPath(
124+
context.extensionUri,
125+
"src",
126+
"views",
127+
"cytoscape",
128+
"media",
129+
"cytoscape-node-html-label.min.js"
130+
)
131+
);
132+
const explainUri = webview.asWebviewUri(
133+
vscode.Uri.joinPath(
134+
context.extensionUri,
135+
"src",
136+
"views",
137+
"cytoscape",
138+
"media",
139+
"explain.js"
140+
)
141+
);
142+
143+
return /*html*/ `
82144
<!DOCTYPE html>
83145
<html lang="en">
84146
@@ -93,6 +155,7 @@ export class CytoscapeGraph {
93155
<script>
94156
window.data = ${data};
95157
window.iconMap = ${iconMap}
158+
window.tooltips = ${tooltips}
96159
</script>
97160
</head>
98161
<body>
@@ -101,4 +164,4 @@ export class CytoscapeGraph {
101164
</html>
102165
`;
103166
}
104-
}
167+
}

src/views/cytoscape/media/explain.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,22 @@
1313
height: 100%;
1414
border: none;
1515
margin: 0;
16+
}
17+
18+
.hover-box {
19+
border-radius: 6px;
20+
padding: 6px 10px;
21+
font-size: 13px;
22+
font-family: sans-serif;
23+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
24+
border: 1px solid #444;
25+
white-space: nowrap;
26+
z-index: 1000;
27+
transition: opacity 0.2s ease;
28+
opacity: 0.95;
29+
position: absolute;
30+
background: #fff;
31+
border: 1px solid #aaa;
32+
padding: 4px 8px;
33+
color: black;
1634
}

src/views/cytoscape/media/explain.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// @ts-ignore
44
const iconMap = window.iconMap;
55
// @ts-ignore
6+
const tooltips = window.tooltips;
7+
// @ts-ignore
68
const vscode = window.acquireVsCodeApi();
79
// @ts-ignore
810
const cy = cytoscape({
@@ -72,3 +74,55 @@ cy.nodeHtmlLabel([
7274
},
7375
},
7476
]);
77+
78+
cy.nodes().on("mouseover", (event) => {
79+
const node = event.target;
80+
const id = node.id();
81+
const tooltip = tooltips[id];
82+
const hoverDiv = document.createElement("pre");
83+
hoverDiv.innerText = tooltip;
84+
hoverDiv.className = "hover-box";
85+
document.body.appendChild(hoverDiv);
86+
87+
function updatePosition() {
88+
const { x, y } = node.renderedPosition(); // center of node
89+
const containerRect = cy.container().getBoundingClientRect(); // Cytoscape canvas
90+
const boxRect = hoverDiv.getBoundingClientRect(); // Tooltip box
91+
const boxWidth = boxRect.width;
92+
const boxHeight = boxRect.height;
93+
94+
const nodeTopY = y - node.renderedOuterHeight() / 2;
95+
const offset = 20;
96+
97+
let top = nodeTopY + containerRect.top - boxHeight - offset;
98+
let left = x + containerRect.left - boxWidth / 2;
99+
100+
// Constrain to visible area
101+
const viewportWidth = window.innerWidth;
102+
const viewportHeight = window.innerHeight;
103+
104+
// Keep inside horizontal bounds
105+
if (left < 4) left = 4;
106+
if (left + boxWidth > viewportWidth - 4) {
107+
left = viewportWidth - boxWidth - 4;
108+
}
109+
110+
// If tooltip would be cut off vertically, show it *below* the node instead
111+
if (top < 4) {
112+
top = y + containerRect.top + node.renderedOuterHeight() / 2;
113+
}
114+
115+
hoverDiv.style.left = `${left}px`;
116+
hoverDiv.style.top = `${top}px`;
117+
}
118+
119+
updatePosition(); // initial position
120+
cy.on("pan zoom resize", updatePosition);
121+
node.on("position", updatePosition);
122+
123+
node.once("mouseout", () => {
124+
hoverDiv.remove();
125+
cy.off("pan zoom resize", updatePosition);
126+
node.off("position", updatePosition);
127+
});
128+
});

0 commit comments

Comments
 (0)