const canvas = document.getElementById("canvas"); // Layouts var width = canvas.offsetWidth; var height = canvas.offsetHeight; const node_size = 55; var stage = new Konva.Stage({ container: canvas, width: width, height: height }); const node_layout = { radius: node_size, fill: 'white', stroke: 'black', strokeWidth: 2, shadowColor: 'black', shadowBlur: 10, shadowOffset: { x: 2, y: 2 }, shadowOpacity: 0.3, }; const node_text_layout = { x: 1 - node_size, y: 2 - node_size, text: name, fontSize: 22, width: 2 * node_size-1, height: 2 * node_size-1, fontFamily: 'Roboto', fill: 'black', verticalAlign: 'middle', align: 'center' }; const edge_layout = { stroke: 'black', strokeWidth: 3, } const edge_text_layout = { fontSize: 22, width: 2 * node_size, height: 2 * node_size, fontFamily: 'Roboto', fill: 'black', verticalAlign: 'middle', align: 'center' }; //----------------------------------------------------------------------------- //-------------------------------Init------------------------------------------ //----------------------------------------------------------------------------- var edge_layer = new Konva.Layer(); var node_layer = new Konva.Layer(); var konva_edges = []; var konva_nodes = []; var graph_copy = {}; var state = "idle"; // selected nodes var selected = []; var path_elements = []; // (additions) auto layout var auto_layout = false; function initialisation(){ stage.add(edge_layer); stage.add(node_layer); // avoid flickering edge_layer.hide(); // draw initial graph for (id in nodes) { draw_new_node(nodes[id], node_size + Math.random() * (width - 2 * node_size), node_size + Math.random() * (height - 2 * node_size)); } for (id in edges) { draw_new_edge(edges[id]); } // (additions) auto layout if ( sessionStorage.getItem("auto_layout") !== null) { // Restore the contents of the text field auto_layout = (sessionStorage.getItem("auto_layout") == 'true'); if (auto_layout){ document.querySelector("[data-activeon=auto_layout]").classList.add("active"); } } // Precalculate a good graph presentation for (var i = 0; i < 1000; i++) { force_directed_graph(); } node_layer.draw(); edge_layer.show(); } initialisation(); //----------------------------------------------------------------------------- //------------------------------utils------------------------------------------ //----------------------------------------------------------------------------- function sort(array) { return array.sort(function(a, b) { return a - b; }) } //----------------------------------------------------------------------------- //---------------------------draw methods-------------------------------------- //----------------------------------------------------------------------------- function draw_new_node(name, x, y) { const pos = { x: x, y: y }; let group = new Konva.Group({ ...pos, draggable: true, dragBoundFunc: function(pos) { return { x: sort([node_size, pos.x, width - node_size])[1], y: sort([node_size, pos.y, height - node_size])[1], }; }, id: name }); let box = new Konva.Circle(node_layout); let simpleText = new Konva.Text({...node_text_layout, text: name}); group.add(box); group.add(simpleText); group.on("click", on_node_click); group.on('dragstart', function() { group.attrs.dragging = true; }); group.on('dragend', function() { group.attrs.dragging = false; }); group.on('mouseover', function() { document.body.style.cursor = 'pointer'; }); group.on('mouseout', function() { document.body.style.cursor = 'default'; }); graph_copy[name] = []; konva_nodes.push(group); node_layer.add(group); node_layer.draw(); } function draw_new_edge(edge) { let start_node = stage.findOne('#' + edge["start"]); let end_node = stage.findOne('#' + edge["end"]); let weight = edge["weight"]; let group = new Konva.Group({ start_node: start_node, end_node: end_node, weight: weight, id: edge["start"]+edge["end"] }); let line = new Konva.Line(edge_layout); let simpleText = new Konva.Text({...edge_text_layout, text: weight}); graph_copy[edge["start"]].push(edge["end"]); graph_copy[edge["end"]].push(edge["start"]); group.add(line); group.add(simpleText); konva_edges.push(group); update_edge(group); edge_layer.add(group); edge_layer.draw(); return group; } function update_edge(konva_edge) { let line = konva_edge.children[0]; let text = konva_edge.children[1]; let start_node = konva_edge.attrs.start_node; let end_node = konva_edge.attrs.end_node; let size = 15+text.text().length*15; let line_length = Math.sqrt((start_node.getX() - end_node.getX()) ** 2 + (start_node.getY() - end_node.getY()) ** 2) - size; line.points([start_node.getX(), start_node.getY(), end_node.getX(), end_node.getY()]); line.dash([line_length / 2, size, line_length / 2]); text.x((start_node.getX() + end_node.getX()) / 2 - node_size); text.y((start_node.getY() + end_node.getY()) / 2 - node_size), edge_layer.draw(); } function reset_node_style(){ for (let id in konva_nodes) { konva_nodes[id].children[0].strokeWidth(node_layout.strokeWidth); konva_nodes[id].children[0].stroke("black"); } } function reset_edge_style(){ for (let id in konva_edges) { konva_edges[id].children[0].strokeWidth(edge_layout.strokeWidth); konva_edges[id].children[0].stroke("black"); } } function draw_selected_nodes() { reset_node_style(); reset_edge_style(); for (let id in selected) { selected[id].children[0].strokeWidth(node_layout.strokeWidth + 1); selected[id].children[0].stroke("rgb(240, 141, 25)"); } node_layer.draw(); } function draw_path(path){ let path_elements = []; for (let id in path){ path_elements.push(stage.findOne("#"+path[id])); if (id == 0) { continue } let current_edge = stage.findOne("#"+path[id-1]+path[id]); if (current_edge){ path_elements.push(current_edge); }else{ path_elements.push(stage.findOne("#"+path[id]+path[id-1])); } } reset_node_style(); reset_edge_style(); for (let id in path_elements) { path_elements[id].children[0].strokeWidth(node_layout.strokeWidth + 1); path_elements[id].children[0].stroke("rgb(240, 141, 25)"); } } function on_node_click(e) { e.cancelBubble = true; // let mousePos = stage.getPointerPosition(); let idx = selected.indexOf(this); if (idx >= 0) { selected.splice(idx, 1); } else if (selected.length < 2) { selected.push(this); } else { selected.shift(); selected.push(this); } draw_selected_nodes(); }; function create_node(){ if (state != "create_node"){ state = "create_node"; // let mousePos = stage.getPointerPosition(); let pos = {x: width/2, y: height/2}; create_input(pos.x, pos.y).then(function(text) { draw_new_node(text, pos.x, pos.y); state = "idle"; }, function(){state = "idle"}); } } function create_edge() { if (selected.length < 2) { show_error("Select at least 2 nodes."); return; } let x = (selected[0].getX() + selected[1].getX()) / 2; let y = (selected[0].getY() + selected[1].getY()) / 2; create_input(x, y).then(function(text) { draw_new_edge({ "start": selected[0].children[1].text(), "end": selected[1].children[1].text(), "weight": text }); selected = []; draw_selected_nodes(); },function(){}); } function create_input(x, y) { return new Promise(function(resolve, reject) { var input = document.createElement("input"); input.setAttribute("type", "text"); input.style.left = x + "px"; input.style.top = y + "px"; canvas.appendChild(input); input.focus(); stage.listening(false); window.addEventListener("keyup", function(event) { if (event.key === "Escape") { input.remove(); stage.listening(true); reject(); } }); input.addEventListener("keyup", function(event) { if (event.key === "Enter") { resolve(input.value); input.remove(); stage.listening(true); } }); }); } function find_path(){ draw_path(elem); } function update_edges(edges) { for (id in edges) { update_edge(edges[id]); } } node_layer.on('beforeDraw', function() { update_edges(konva_edges); }); // adapt the stage on any window resize function fitStageIntoParentContainer() { width = canvas.offsetWidth; height = canvas.offsetHeight; stage.width(canvas.offsetWidth); stage.height(canvas.offsetHeight); stage.draw(); } window.addEventListener('resize', fitStageIntoParentContainer); //----------------------------------------------------------------------------- //---------------------(additions) auto layout--------------------------------- //----------------------------------------------------------------------------- function toggle_auto_layout() { auto_layout = !auto_layout; document.querySelector("[data-activeon=auto_layout]").classList.toggle("active"); sessionStorage.setItem("auto_layout", auto_layout); } function normalize(vec) { let l = Math.sqrt(vec[0]**2+vec[1]**2); return [vec[0]/l,vec[1]/l]; } function in_edges(start_node, end_node){ let idx = graph_copy[start_node.attrs.id].indexOf(end_node.attrs.id); if (idx < 0){ return false; } return true; } function force_directed_graph(spring_length = 300, step = 0.004){ for (var idx in konva_nodes){ if (konva_nodes[idx].attrs.dragging){ continue } var x1 = konva_nodes[idx].getX(); var y1 = konva_nodes[idx].getY(); var rep = [0,0]; var attr = [0,0]; for (let jdx in konva_nodes){ if (idx == jdx){ continue; } let x2 = konva_nodes[jdx].getX(); let y2 = konva_nodes[jdx].getY(); let dir = normalize([x2-x1,y2-y1]); let dist = ((x1-x2)**2+(y1-y2)**2); if (in_edges(konva_nodes[idx], konva_nodes[jdx])){ let f = (dist/spring_length); attr[0] += f*dir[0]; attr[1] += f*dir[1]; } let f = (spring_length**2/Math.sqrt(dist)); rep[0] -= f*dir[0]; rep[1] -= f*dir[1]; } let new_x = x1+step*(rep[0]+attr[0]); let new_y = y1+step*(rep[1]+attr[1]); if (new_x < 2*node_size) { new_x += 10*step*((2*node_size-new_x)); } else if (new_x > width-2*node_size) { new_x += 10*step*((width-2*node_size-new_x)); } if (new_y < 2*node_size) { new_y += 10*step*((2*node_size-new_y)); } else if (new_y > height-2*node_size) { new_y += 10*step*((height-2*node_size-new_y)); } konva_nodes[idx].x(new_x); konva_nodes[idx].y(new_y); } } var anim = new Konva.Animation(function(frame) { if (auto_layout) { const spring_length = 300; const step = 0.00025*frame.timeDiff; force_directed_graph(spring_length, step) } }, node_layer); anim.start(); function show_error(msg){ let infobar = document.querySelector("#infobar"); infobar.innerHTML = msg; infobar.classList.add("active"); window.setTimeout(function(){ infobar.classList.remove("active"); }, 4000); } // bug: this avoids glitches window.onfocus = function () {auto_layout = true;}; window.onblur = function () {auto_layout = false;};