// @ts-ignore
import * as d3 from "https://cdn.skypack.dev/d3@7";
/**
* Instance of the WebView API.
* @type {any}
*/
// @ts-ignore
const vscode = acquireVsCodeApi();
/**
* JSON string with visualizaton data.
* @type {any}
*/
var graph_data;
/**
* Mode of analysis.
* @type {string}
*/
var mode;
/**
* List of nodes removed from the visualization.
* @type {any[]}
*/
var removedNodes = [];
/**
* List of links removed from the visualization.
* @type {{ source: { id: any; }; target: { id: any; }; }[]}
*/
var removedLinks = [];
/**
* Width of the panel.
* @type {number}
*/
var width;
/**
* Height of the panel.
* @type {number}
*/
var height;
/**
* SVG element with the visualization.
* @type {d3.Selection<SVGGElement, any, HTMLElement, any>}
*/
var svg;
/**
* List of rectangles representing nodes in the graph.
* @type {d3.Selection<SVGRectElement, any, SVGGElement, any>}
*/
var graph_nodes;
/**
* List of text elements with node names.
* @type {d3.Selection<SVGTextElement, any, SVGGElement, any>}
*/
var graph_package_names;
/**
* List of lines representing links in the graph.
* @type {d3.Selection<SVGLineElement, any, SVGGElement, any>}
*/
var graph_links;
/**
* Instance of the simulation.
* @type {any}
*/
var simulation;
/**
* Dictionary storing which node is connected to which node.
* @type {{}}
*/
var linkMatrix = {};
/**
* Colors for 10 most used licenses.
*/
const LICENSE_COLORS = ["red", "green", "blue", "yellow", "orange", "purple", "lime", "wheat", "violet", "olive"];
/**
* Base color of nodes.
* @type {string}
*/
const BASE_NODE_COLOR = "cyan";
/**
* Base color of links.
* @type {string}
*/
const BASE_LINK_COLOR = "white";
/**
* Base color of affected nodes.
* @type {string}
*/
const BASE_AFFECTED_NODE_COLOR = "red";
/**
* Base color of selected nodes.
* @type {string}
*/
const BASE_SELECTED_NODE_COLOR = "yellow";
/**
* Base color of requested nodes.
* @type {string}
*/
const BASE_REQUESTED_NODE_COLOR = "red";
/**
* Base color of requested nodes.
* @type {string}
*/
const BASE_FOUND_NODE_COLOR = "lime";
/**
* Base color of used-by nodes.
* @type {string}
*/
const BASE_USED_BY_NODE_COLOR = "blue";
/**
* Base node name color.
* @type {string}
*/
const BASE_NODE_NAME_COLOR = "red";
/**
* List of all used licenses.
* @type {{ name: string; count: number}[]}
*/
var used_licenses = [];
/**
* Height of the node.
* @type {number}
*/
const node_height = 30;
/**
* Width of the node.
* @type {number}
*/
var node_width;
/**
* Set dimensions of the graph.
*/
function setDimensions() {
width = screen.width;
height = screen.height;
}
/**
* Load data from HTML elements. If license analysis mode is used get all used licenses and send legend to WebView.
*/
function initData() {
var graphElement = document.getElementById("graph");
var modeElement = document.getElementById("mode");
var graphJson;
// load JSON with graph data
if (graphElement !== null) {
// @ts-ignore
graphJson = graphElement.value;
graph_data = JSON.parse(graphJson);
}
else {
graphJson = null;
graph_data = null;
}
// load selected mode
mode = "default";
if (modeElement !== null) {
// @ts-ignore
mode = modeElement.value;
}
// load used licenses
if (mode === "licenses") {
graph_data.nodes.forEach((/** @type {any} */ d) => {
var lic = used_licenses.find((lic) => lic.name == d.license);
if (lic !== undefined) {
// @ts-ignore
lic.count = lic.count + 1;
}
else {
used_licenses.push({ name: d.license, count: 1 });
}
});
used_licenses = used_licenses.filter((lic) => lic.name != "" && lic.name != "none");
used_licenses.sort((a, b) => (a.count < b.count) ? 1 : -1);
/**
* @type {{ license: string; color: string;}[]}
*/
var legend = [];
var count = Math.min(LICENSE_COLORS.length, used_licenses.length);
for (var i = 0; i < count; i++) {
legend.push({ license: used_licenses[i].name, color: LICENSE_COLORS[i] });
}
vscode.postMessage({
command: "show-legend-v",
legend: legend
});
}
}
/**
* Update nodes in the visualization.
*/
function nodesUpdate() {
graph_nodes = svg
.selectAll("rect")
.data(graph_data.nodes)
.enter()
.append("rect")
.attr('width', 70)
.attr('height', 30)
.attr('stroke-width', 3)
.style("stroke", BASE_NODE_COLOR)
.on('click', function (/** @type {any} */ event, /** @type {any} */ node) {
selectNode(node);
});
if (mode === "licenses") {
setLicensesColors();
}
labelsUpdate();
// get length of the visualized text and use it as the with of the node rectangle
var maxTextWidth = d3.max(graph_package_names.nodes(), (/** @type {{ getComputedTextLength: () => any; }} */ n) => n.getComputedTextLength());
if (maxTextWidth === undefined) {
maxTextWidth = 20;
}
maxTextWidth = maxTextWidth + 10;
graph_nodes.attr('width', maxTextWidth);
node_width = maxTextWidth;
}
/**
* Update node names in the visualization.
*/
function labelsUpdate() {
graph_package_names = svg.selectAll("text")
.data(graph_data.nodes)
.enter()
.append("text");
graph_package_names.style("fill", BASE_NODE_NAME_COLOR)
.attr("width", "70")
.attr("height", "30")
.style("font-size", "8px")
.text(function (/** @type {{ name: any; }} */ d) { return d.name; });
}
/**
* Update links in the visualization.
*/
function linksUpdate() {
graph_links = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph_data.links)
.enter().append("line")
.attr("stroke", BASE_LINK_COLOR)
.attr("marker-end", "url(#arrow)");
}
/**
* Create arrows for lines.
*/
function arrowInit() {
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 50)
.attr("refY", 0)
.attr("markerWidth", 13)
.attr("markerHeight", 13)
.attr("fill", BASE_LINK_COLOR)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
}
/**
* Select node in the visualization if default or licenses analysis is used.
* @param {any} node
* @param {boolean} licenses
*/
function selectNodeNormalConnections(node, licenses) {
/**
* @type {{ name: string; recipe: string; is_removed: number; }[]}
*/
var usedByNodes = [];
/**
* @type {{ name: string; recipe: string; is_removed: number; }[]}
*/
var requestedNodes = [];
if (!licenses) {
setSelectedColors(node.id);
}
else {
setLicensesColors();
}
// get requested nodes and nodes that request the selected node from the nodes in the visualization
graph_data.nodes.forEach(function (/** @type {any} */ d) {
// @ts-ignore
if (linkMatrix[node.id + "," + d.id] === 1) {
requestedNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 0 });
}
// @ts-ignore
else if (linkMatrix[d.id + "," + node.id] === 1) {
usedByNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 0 });
}
});
// get requested nodes and nodes that request the selected node from the removed nodes
removedNodes.forEach(function (/** @type {any} */ d) {
// @ts-ignore
if (linkMatrix[node.id + "," + d.id] === 1) {
requestedNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 1 });
}
// @ts-ignore
else if (linkMatrix[d.id + "," + node.id] === 1) {
usedByNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 1 });
}
});
vscode.postMessage({
command: "select-node-v",
name: node.name,
id: node.id,
recipe: node.recipe,
used_by: usedByNodes,
requested: requestedNodes,
affected: []
});
}
/**
* @param {any} node
*/
function selectNodeAffectedConnections(node) {
/**
* @type {{ name: string; recipe: string; is_removed: number; }[]}
*/
var usedByNodes = [];
/**
* @type {{ name: string; recipe: string; is_removed: number; }[]}
*/
var requestedNodes = [];
/**
* @type {{ name: string; recipe: string; is_removed: number; }[]}
*/
var affected_nodes = [];
// get requested nodes and nodes that request the selected node from the nodes in the visualization
// get all nodes that directly or indirectly depend on the selected node
graph_data.nodes.forEach(function (/** @type {any} */ d) {
// @ts-ignore
if (linkMatrix[node.id + "," + d.id] === 1) {
requestedNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 0 });
}
// @ts-ignore
else if (linkMatrix[d.id + "," + node.id] === 1 && d.id !== node.id) {
usedByNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 0 });
addAffectedNodes(affected_nodes, d, node, 0);
}
});
// get requested nodes and nodes that request the selected node from the removed nodes
// get all nodes that directly or indirectly depend on the selected node
removedNodes.forEach(function (/** @type {any} */ d) {
// @ts-ignore
if (linkMatrix[node.id + "," + d.id] === 1) {
requestedNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 1 });
}
// @ts-ignore
else if (linkMatrix[d.id + "," + node.id] === 1 && d.id !== node.id) {
usedByNodes.push({ "name": d.name, "recipe": d.recipe, "is_removed": 1 });
addAffectedNodes(affected_nodes, d, node, 1);
}
});
setAffectedColors(node.id, affected_nodes);
vscode.postMessage({
command: "select-node-v",
name: node.name,
id: node.id,
recipe: node.recipe,
used_by: usedByNodes,
requested: requestedNodes,
affected: affected_nodes
});
}
/**
* Store all nodes that depend on the specified node (called recursively).
* @param {{name: string;recipe: string;is_removed: number;}[]} affected_nodes List of nodes
* that directly or indirectly depend on the starting node.
* @param {any} node Current node.
* @param {any} starting_node Starting node.
* @param {number} is_removed Stores if noded is removed from visualization.
*/
function addAffectedNodes(affected_nodes, node, starting_node, is_removed) {
if (affected_nodes.find((n) => n.name === node.name)) {
return;
}
affected_nodes.push({ "name": node.name, "recipe": node.recipe, "is_removed": is_removed });
// get affected nodes from the JSON with graph data
graph_data.nodes.forEach(function (/** @type {any} */ d) {
// @ts-ignore
if (linkMatrix[d.id + "," + node.id] === 1 && d.id !== starting_node.id) {
if (affected_nodes.find((n) => n.name === d.name)) {
return;
}
addAffectedNodes(affected_nodes, d, starting_node, 0);
}
});
// get affected nodes from the list of removed nodes
removedNodes.forEach(function (/** @type {any} */ d) {
// @ts-ignore
if (linkMatrix[d.id + "," + node.id] === 1) {
if (affected_nodes.find((n) => n.name === d.name) && d.id !== starting_node.id) {
return;
}
addAffectedNodes(affected_nodes, d, starting_node, 1);
}
});
}
/**
* Select node from the graph.
* @param {any} node Selected node.
*/
function selectNode(node) {
if (mode === "affected_nodes") {
selectNodeAffectedConnections(node);
}
else if (mode === "licenses") {
selectNodeNormalConnections(node, true);
}
else {
selectNodeNormalConnections(node, false);
}
}
/**
* Set colors of all nodes.
* If the node is requested by the node with a given ID set the color to BASE_REQUESTED_NODE_COLOR.
* If the node is depends on the node with a given ID set the color to BASE_USED_BY_COLOR.
* If the node equals the node with a given ID set color to BASE_SELECTED_NODE_COLOR.
* @param {number} id ID of the selected node.
*/
function setSelectedColors(id) {
graph_nodes.style("stroke", BASE_NODE_COLOR);
graph_nodes.style("stroke", function (/** @type {any} */ d) {
// @ts-ignore
if (linkMatrix[id + "," + d.id] === 1) {
return BASE_REQUESTED_NODE_COLOR;
}
// @ts-ignore
else if (linkMatrix[d.id + "," + id] === 1) {
return BASE_USED_BY_NODE_COLOR;
}
else if (d.id === id) {
return BASE_SELECTED_NODE_COLOR;
}
return BASE_NODE_COLOR;
});
}
/**
* Set BASE_AFFECTED_NODE_COLOR color to nodes directly or indirectly dependent on the selected node.
* Set color to BASE_SELECTED_NODE_COLOR if node equals the selected node with given ID.
* @param {number} id ID of the selected node.
* @param {{name: string;recipe: string;is_removed: number;}[]} affected_nodes List of nodes
* that directly or indirectly depend on the starting node.
*/
function setAffectedColors(id, affected_nodes) {
graph_nodes.style("stroke", BASE_NODE_COLOR);
graph_nodes.style("stroke", function (/** @type {any} */ d) {
// @ts-ignore
if (affected_nodes.find((node) => node.name == d.name)) {
return BASE_AFFECTED_NODE_COLOR;
}
else if (d.id === id) {
return BASE_SELECTED_NODE_COLOR;
}
return BASE_NODE_COLOR;
});
}
/**
* Set colors to nodes based on 10 most used licenses.
*/
function setLicensesColors() {
graph_nodes.style("stroke", function (/** @type {any} */ d) {
var count = Math.min(LICENSE_COLORS.length, used_licenses.length);
for (var i = 0; i < count; i++) {
if (used_licenses[i].name === d.license) {
return LICENSE_COLORS[i];
}
}
return BASE_NODE_COLOR;
});
}
/**
* Find nodes in visualization. If no node is found, send info to VisualizationPanel.
* @param {string} search Name or pattern of the node that should be found.
*/
function findNodes(search) {
var isFound = false;
graph_nodes.style("stroke", function (/** @type {any} */ d) {
if (d.name.match(search)) {
isFound = true;
return BASE_FOUND_NODE_COLOR;
}
return BASE_NODE_COLOR;
});
if (!isFound) {
vscode.postMessage({
command: "node-not-found-v",
});
}
}
/**
* Remove node from visualization.
* @param {number} id ID of the node that will be removed.
*/
function removeNode(id) {
svg.selectAll("line").remove();
svg.selectAll("rect").remove();
svg.selectAll("text").remove();
var list_id = graph_data.nodes.findIndex((/** @type {{ id: number; }} */ node) => node.id === id);
// remove node from the JSON with graph data and store it in the list of removed nodes
var removedNode = graph_data.nodes.splice(list_id, 1)[0];
removedNodes.push(removedNode);
// remove affected links from the JSON with graph data and store them in the list of removed links
graph_data.links = graph_data.links.filter(function (/** @type {{ source: { id: any; }; target: { id: any; }; }} */ l) {
if (l.source.id === id || l.target.id === id) {
removedLinks.push(l);
}
return l.source.id !== id && l.target.id !== id;
});
linksUpdate();
nodesUpdate();
simulation.restart();
}
/**
* Return node to visualization.
* @param {string} name Name of the node that should be returned.
*/
function returnNode(name) {
svg.selectAll("line").remove();
svg.selectAll("rect").remove();
svg.selectAll("text").remove();
// remove node from list of removed nodes and return it to the JSON with graph data
var list_id = removedNodes.findIndex((node) => node.name === name);
var returnedNode = removedNodes.splice(list_id, 1)[0];
graph_data.nodes.push(returnedNode);
// remove affected links from list of removed links and return them to the JSON with graph data
removedLinks = removedLinks.filter(function (/** @type {{ source: { id: any; }; target: { id: any; }; }} */ l) {
if (
(l.source.id === returnedNode.id && !removedNodes.find((node) => node.id == l.target.id))
||
(l.target.id === returnedNode.id && !removedNodes.find((node) => node.id == l.source.id))
) {
graph_data.links.push(l);
}
return l.source.id !== returnedNode.id && l.target.id !== returnedNode.id;
});
linksUpdate();
nodesUpdate();
simulation.restart();
}
/**
* Update positions of nodes, node names and links.
*/
function simulationTicked() {
graph_links
.attr("x1", function (/** @type {{ source: { x: number; }; }} */ d) { return d.source.x; })
.attr("y1", function (/** @type {{ source: { y: number; }; }} */ d) { return d.source.y; })
.attr("x2", function (/** @type {{ target: { x: number; }; }} */ d) { return d.target.x; })
.attr("y2", function (/** @type {{ target: { y: number; }; }} */ d) { return d.target.y; });
graph_nodes
.attr("x", function (/** @type {{ x: number; }} */ d) { return d.x - node_width / 2; })
.attr("y", function (/** @type {{ y: number; }} */ d) { return d.y - node_height / 2; });
graph_package_names
.attr("x", function (/** @type {{ x: number; }} */ d) { return d.x + 3 - node_width / 2; })
.attr("y", function (/** @type {{ y: number; }} */ d) { return d.y + 10 - node_height / 2; });
}
/**
* Initialize the SVG element that will contain the visualization.
*/
function initSVG() {
svg = d3.select("#visualization")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "svg")
.style("background-color", "#161623")
// @ts-ignore
.call(d3.zoom().scaleExtent([0.01, 10]).on("zoom", function () { svg.attr("transform", d3.zoomTransform(this)) }))
.append("g");
//.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
}
/**
* Init dictionary storing which node is connected to which node.
*/
function initMatrix() {
graph_data.links.forEach(function (/** @type {any} */ link) {
// @ts-ignore
linkMatrix[link.source.id + "," + link.target.id] = 1;
});
}
/**
* Init the simulation with parameters from the HTML elements.
*/
function initSimulation() {
var input_distance = document.getElementById("distance");
var distance = "";
if (input_distance !== null) {
// @ts-ignore
distance = input_distance.value;
}
var input_iterations = document.getElementById("iterations");
var iterations = "do_prepare_recipe_sysroot";
if (input_iterations !== null) {
// @ts-ignore
iterations = input_iterations.value;
}
var input_strength = document.getElementById("strength");
var strength = "do_prepare_recipe_sysroot";
if (input_strength !== null) {
// @ts-ignore
strength = input_strength.value;
}
simulation = d3.forceSimulation(graph_data.nodes)
.force("link", d3.forceLink()
.distance(distance) // distance of links
.iterations(iterations) // number of iterations of the link force
.id(function (/** @type {{ id: any; }} */ d) { return d.id; })
.links(graph_data.links)
)
.force("charge", d3.forceManyBody().strength(strength)) // add force between nodes
.force("center", d3.forceCenter(width / 2, height / 2)) // make nodes centered
.on("end", simulationTicked);
}
/**
* Export the SVG element with visualization.
*/
function exportSVG() {
var svg = document.getElementById("svg");
// store current width and height
var curr_width = svg?.getAttribute("width")
var curr_height = svg?.getAttribute("height")
// @ts-ignore
var g = svg.querySelector('g')
// store current transform
// @ts-ignore
var curr_transform = g?.getAttribute("transform")
// center transform
// @ts-ignore
if (g.getBBox().width > width) {
// @ts-ignore
g.setAttribute("transform", "translate(" + g.getBBox().width / 2 + "," + g.getBBox().height / 2 + ")");
}
else {
// @ts-ignore
g.setAttribute("transform", "translate(" + 0 + "," + 0 + ")");
}
// set width and height od the SVG to the width and height of the entire graph
// @ts-ignore
svg.setAttribute('width', Math.max(g.getBBox().width, width))
// @ts-ignore
svg.setAttribute('height', Math.max(g.getBBox().height, height))
// serialize the SVG
var serializer = new XMLSerializer();
// @ts-ignore
var source = serializer.serializeToString(svg);
vscode.postMessage({
command: "export-svg-v",
svg: source
});
// return to original values
// @ts-ignore
g.setAttribute("transform", curr_transform); // clean transform
// @ts-ignore
svg.setAttribute('width', curr_width) // set svg to be the g dimensions
// @ts-ignore
svg.setAttribute('height', curr_height)
}
(
/**
* Main function of the script. Accept messages. Initialize visualization.
*/
function () {
setDimensions();
initData();
if (graph_data !== null) {
// append the svg object to the body of the page
initSVG();
arrowInit();
// Initialize the links
linksUpdate();
// Initialize the nodes
nodesUpdate();
initSimulation();
initMatrix();
}
window.addEventListener('message', event => {
const data = event.data;
switch (data.command) {
case "return-node-v":
returnNode(data.name);
break;
case "remove-node-v":
removeNode(data.id)
break;
case "select-node-from-list-v":
var selected_node = graph_data.nodes.find((/** @type {{ name: string; }} */ node) => node.name === data.name);
selectNode(selected_node);
break;
case "call-export-svg-v":
exportSVG();
break;
case "find-nodes-v":
findNodes(data.search);
break;
}
});
}()
);