From 241dfc93b2083775ca5ca4494b9371fac9df9076 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Thu, 17 Oct 2019 19:08:19 +0200 Subject: [PATCH 01/16] added "use strict" added parameter fontsize, scaleFactor, zoomFactor added load node position from user data changed constant for avoid massive forces at small distances added function getNodePositions changed requestAnimationFrame to use function from window instead of this added function findNode added function getCanvasPos added function getNodePositions --- springy.js | 66 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/springy.js b/springy.js index 0bf5ba4..ede00f9 100644 --- a/springy.js +++ b/springy.js @@ -24,6 +24,7 @@ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ +"use strict"; (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. @@ -327,22 +328,27 @@ // ----------- var Layout = Springy.Layout = {}; - Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed) { + Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, zoomFactor) { this.graph = graph; this.stiffness = stiffness; // spring stiffness constant this.repulsion = repulsion; // repulsion constant this.damping = damping; // velocity damping factor this.minEnergyThreshold = minEnergyThreshold || 0.01; //threshold used to determine render stop this.maxSpeed = maxSpeed || Infinity; // nodes aren't allowed to exceed this speed - + this.fontsize = fontsize || 8.0; + this.scaleFactor = 1.025; // scale factor for each wheel click. + this.zoomFactor = zoomFactor || 1.0; // current zoom factor for the whole canvas. this.nodePoints = {}; // keep track of points associated with nodes this.edgeSprings = {}; // keep track of springs associated with edges }; Layout.ForceDirected.prototype.point = function(node) { if (!(node.id in this.nodePoints)) { - var mass = (node.data.mass !== undefined) ? node.data.mass : 1.0; - this.nodePoints[node.id] = new Layout.ForceDirected.Point(Vector.random(), mass); + var mass = (node.data.mass !== undefined) ? parseFloat(node.data.mass) : 1.0; + // DS: load positions from user data + var x = (node.data.x !== undefined) ? parseFloat(node.data.x) : 10.0 * (Math.random() - 0.5); + var y = (node.data.y !== undefined) ? parseFloat(node.data.y) : 10.0 * (Math.random() - 0.5); + this.nodePoints[node.id] = new Layout.ForceDirected.Point(new Vector(x, y), mass); } return this.nodePoints[node.id]; @@ -416,7 +422,7 @@ if (point1 !== point2) { var d = point1.p.subtract(point2.p); - var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero) + var distance = d.magnitude() + 0.3; // DS 0.1 is too small: avoid massive forces at small distances (and divide by zero) var direction = d.normalise(); // apply force to each end point @@ -478,16 +484,25 @@ return energy; }; + Layout.ForceDirected.prototype.getNodePositions = function() { + var nodes_array = []; + this.eachNode(function(node, point) { + var element = {id:node.data.name, x:point.p.x, y:point.p.y, mass:point.m}; + nodes_array.push(element); + }); + return nodes_array; + }; + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-) - Springy.requestAnimationFrame = __bind(this.requestAnimationFrame || - this.webkitRequestAnimationFrame || - this.mozRequestAnimationFrame || - this.oRequestAnimationFrame || - this.msRequestAnimationFrame || + Springy.requestAnimationFrame = __bind(window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || (function(callback, element) { - this.setTimeout(callback, 10); - }), this); + window.setTimeout(callback, 10); + }), window); /** @@ -548,6 +563,22 @@ return min; }; + Layout.ForceDirected.prototype.findNode = function(node_id) { + var min = null; + var pos = new Springy.Vector(0, 0); + var t = this; + this.graph.nodes.forEach(function(n){ + var point = t.point(n); + var distance = point.p.subtract(pos).magnitude(); + if (n.data.name === node_id) { + min = {node: n, point: point, distance: distance, inside:true}; + return min; + } + }); + + return min; + } + // returns [bottomleft, topright] Layout.ForceDirected.prototype.getBoundingBox = function() { var bottomleft = new Vector(-2,-2); @@ -647,11 +678,12 @@ * @param onRenderStart optional callback function that gets executed whenever rendering starts. * @param onRenderFrame optional callback function that gets executed after each frame is rendered. */ - var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, onRenderStop, onRenderStart, onRenderFrame) { + var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, getCanvasPos, onRenderStop, onRenderStart, onRenderFrame) { this.layout = layout; this.clear = clear; this.drawEdge = drawEdge; this.drawNode = drawNode; + this.getCanvasPos = getCanvasPos; this.onRenderStop = onRenderStop; this.onRenderStart = onRenderStart; this.onRenderFrame = onRenderFrame; @@ -663,6 +695,14 @@ this.start(); }; + Renderer.prototype.getNodePositions = function(e) { + return JSON.stringify(this.layout.getNodePositions()); + }; + + Renderer.prototype.getCanvasPos = function(e) { + return this.getCanvasPos(); + }; + /** * Starts the simulation of the layout in use. * From f89247691a60b20ae5e282d0c8408091804cae09 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Thu, 17 Oct 2019 19:50:44 +0200 Subject: [PATCH 02/16] added "use strict" added parameter fontsize, zoomFactor, maxSpeed, nodePositions, pinWeight, edgeLabelBoxes, selected, x_offset, y_offset added node shadows added node shapes added zoom canvas, drag canvas, zoom nodes, drag & pin nodes. added function zoom added function trackTransforms added function getCanvasPos --- springyui.js | 681 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 610 insertions(+), 71 deletions(-) diff --git a/springyui.js b/springyui.js index acc35eb..7e6e99e 100755 --- a/springyui.js +++ b/springyui.js @@ -22,30 +22,50 @@ Copyright (c) 2010 Dennis Hotson FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +"use strict"; (function() { jQuery.fn.springy = function(params) { var graph = this.graph = params.graph || new Springy.Graph(); - var nodeFont = "16px Verdana, sans-serif"; - var edgeFont = "8px Verdana, sans-serif"; + var nodeFont = "Verdana, sans-serif"; + var edgeFont = "Verdana, sans-serif"; var stiffness = params.stiffness || 400.0; var repulsion = params.repulsion || 400.0; var damping = params.damping || 0.5; var minEnergyThreshold = params.minEnergyThreshold || 0.00001; + var maxSpeed = params.maxSpeed || Infinity; // nodes aren't allowed to exceed this speed var nodeSelected = params.nodeSelected || null; + var nodePositions = params.nodePositions || null; + var pinWeight = params.pinWeight || 1000.0; var nodeImages = {}; var edgeLabelsUpright = true; - + var edgeLabelBoxes = params.edgeLabelBoxes || false; + var fontsize = params.fontsize * 1.0 || Math.max(12 - Math.round(Math.sqrt(graph.nodes.length)), 4); + var zoomFactor = params.zoomFactor * 1.0 || 1.0; var canvas = this[0]; var ctx = canvas.getContext("2d"); - var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold); + var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, zoomFactor); + var selected = null; + var color1 = "#7FEFFF"; // blue + var color2 = "#50C0FF"; + var shadowColor = "rgba(50, 50, 50, 0.3)"; + var shadowOffset = 10; + trackTransforms(ctx); // calculate bounding box of graph layout.. with ease-in var currentBB = layout.getBoundingBox(); var targetBB = {bottomleft: new Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)}; - + if (params.selected) { + selected = layout.findNode(params.selected); + } + if (zoomFactor !== !.0) { + ctx.scale(zoomFactor,zoomFactor); + } + if (params.x_offset || params.y_offset) { + ctx.translate(params.x_offset, params.y_offset); + } // auto adjusting bounding box Springy.requestAnimationFrame(function adjust() { targetBB = layout.getBoundingBox(); @@ -75,22 +95,279 @@ jQuery.fn.springy = function(params) { return new Springy.Vector(px, py); }; - // half-assed drag and drop - var selected = null; + var set_colors = function() { + var grd = ctx.createLinearGradient(-100, 100, 100, -100); + grd.addColorStop(0, color1); + grd.addColorStop(1, color2); + ctx.fillStyle = grd; + ctx.shadowColor = shadowColor; + ctx.shadowBlur = 10; + ctx.shadowOffsetX = shadowOffset; + ctx.shadowOffsetY = shadowOffset; + }; + + var box_shape = function(pos, width, height, shape){ + height = (height*8/14); + width = width / 2.0 + height / 2.0; + var hh = height / 2.0; + ctx.save(); + ctx.translate(pos.x, pos.y); + set_colors(); + switch(shape) { + case 'box3d': + ctx.beginPath(); + ctx.moveTo(width, height); ctx.lineTo(width+hh, height-hh); + ctx.lineTo(width+hh, -height-hh, 0); ctx.lineTo(width, -height); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(width, -height); ctx.lineTo(-width, -height); + ctx.lineTo(-width+hh, -height-hh, 0); ctx.lineTo(width+hh, -height-hh); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + break; + case 'folder': + case 'note': + break; + case 'tab': + ctx.beginPath(); + ctx.rect(-width, -height-hh, height+hh, hh); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + break; + case 'Msquare': + break; + } + ctx.beginPath(); + ctx.rect(-width, -height, width*2, height*2); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + switch(shape) { + case 'component': + ctx.beginPath(); + ctx.rect(-width-hh, -hh-hh/2, height, hh); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.beginPath(); + ctx.rect(-width-hh, hh-hh/2, height, hh); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + break; + } + + ctx.restore(); + }; + + var house = function(pos, width, height, inv){ + width = width / 2 + height / 3; + height = height / 3 * 2; + var hh = height / 2; + ctx.save(); + ctx.translate(pos.x, pos.y); + set_colors(); + if (inv) { + ctx.rotate(Math.PI); + } + ctx.beginPath(); + ctx.moveTo(-width, height); ctx.lineTo(width, height); + ctx.lineTo(width, 0); ctx.lineTo(0, -height-hh); + ctx.lineTo(-width, 0); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + ctx.stroke(); + }; + + var parallelogram = function(pos, width, height){ + width = width / 2 + height / 2; + var hh = height; + height = height / 3 * 2; + ctx.save(); + ctx.translate(pos.x, pos.y); + set_colors(); + ctx.beginPath(); + ctx.moveTo(-width, -height); ctx.lineTo(width, -height); + ctx.lineTo(width+hh, height); ctx.lineTo(-width+hh, height); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + ctx.stroke(); + }; + + var trapezium = function(pos, width, height, inv){ + width = width / 2 + height / 2; + var hh = height; + height = height / 3 * 2; + ctx.save(); + ctx.translate(pos.x, pos.y); + set_colors(); + if (inv) { + ctx.rotate(Math.PI); + } + ctx.beginPath(); + ctx.moveTo(-width, -height); ctx.lineTo(width, -height); + ctx.lineTo(width+hh, height); ctx.lineTo(-width-hh, height); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + ctx.stroke(); + }; + + var ellipse = function(pos, width, height){ + width = width / 2 + height; + ctx.save(); + ctx.translate(pos.x, pos.y); + ctx.scale(1, height / width); + set_colors(); + ctx.beginPath(); + ctx.arc(0, 0, width, 0, Math.PI * 2, true); + ctx.fill(); + ctx.restore(); + ctx.stroke(); + }; + + var triangle = function(pos, width, height){ + var dim = width / 2 + height / 3 * 2; + var c1x = pos.x, + c1y = pos.y - height, + c2x = c1x - dim, + c2y = pos.y + 8, + c3x = c1x + dim, + c3y = c2y; + set_colors(); + ctx.beginPath(); + ctx.moveTo(c1x, c1y); + ctx.lineTo(c2x, c2y); + ctx.lineTo(c3x, c3y); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + }; + + var polygon = function(pos, width, height, n, even){ + var pix = 2*Math.PI/n; // angel in circle + var fy = (3*height)/(2*width); // deformation of circle + var dim = (n+1)*(3*height+2*width)/(4*n); // radius + ctx.save(); + ctx.translate(pos.x, pos.y); + ctx.scale(1, fy); + set_colors(); + if (even) { + ctx.rotate(pix/2 + Math.PI/2); // flat bottom line + } else { + ctx.rotate(Math.PI/2); // standing on corner + } + ctx.beginPath(); + ctx.moveTo(dim, 0); + while(n--) { + ctx.rotate(pix); + ctx.lineTo(dim, 0); + } + ctx.fill(); + ctx.restore(); + ctx.stroke(); + }; + + var star = function(pos, width, height){ + // var dim = width / 2 + height / 3 * 2; + var dim = height * 4 / 3; + var pi5 = Math.PI / 5; + ctx.save(); + ctx.translate(pos.x, pos.y); + set_colors(); + ctx.beginPath(); + ctx.moveTo(dim, 0); + for (var i = 0; i < 9; i++) { + ctx.rotate(pi5); + if (i % 2 == 0) { + ctx.lineTo((dim / 0.525731) * 0.200811, 0); + } else { + ctx.lineTo(dim, 0); + } + } + ctx.closePath(); + ctx.fill(); + ctx.restore(); + ctx.stroke(); + }; + + // drag and drop var nearest = null; var dragged = null; + var point_clicked = null; + var inside_node = false; + var lastX=canvas.width/2, lastY=canvas.height/2; + var dragStart = null; + var canvas_dragged; + + var mouse_inside_node = function(item, mp) { + if (item !== null && item.node !== null && typeof(item.inside) == 'undefined') { + var node = item.node; + var boxWidth = node.getWidth(); + var boxHeight = node.getHeight(); + var pos = toScreen(item.point.p); + var p = toScreen(mp); + var diffx = Math.abs(pos.x - p.x); + var diffy = Math.abs(pos.y - p.y); + + inside_node = (diffx <= boxWidth/2 && diffy <= boxHeight) ? true : false; + item.inside = inside_node; + } + }; + + var snap_to_canvas = function() { + // move upper left corner and lower right corner inside canvas + var diffx = 0; + var diffy = 0; + var diffx2 = 0; + var diffy2 = 0; + + var xform = ctx.getTransform(); + var xsize = canvas.width * xform.a; + var ysize = canvas.height * xform.a; + var xoffset = xform.e; + var yoffset = xform.f; + + if (xoffset > 0) + diffx = -xoffset; + if (xoffset < 0 && xoffset + xsize < canvas.width) + diffx = canvas.width - (xoffset + xsize); + + if (yoffset > 0) + diffy = -yoffset; + if (yoffset < 0 && yoffset + ysize < canvas.height) + diffy = canvas.height - (yoffset + ysize); + ctx.translate(diffx, diffy); + }; jQuery(canvas).mousedown(function(e) { var pos = jQuery(this).offset(); - var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top}); + var p1 = ctx.transformedPoint(e.pageX - pos.left, e.pageY - pos.top); + var p = fromScreen(p1); selected = nearest = dragged = layout.nearest(p); - + point_clicked = p; if (selected.node !== null) { - dragged.point.m = 10000.0; - + // DS 13.Oct 2019 : fix or just move selected node depending on pinWeight + dragged.point.m = pinWeight; + } + mouse_inside_node(selected, p); + if (selected.inside) { if (nodeSelected) { nodeSelected(selected.node); } + } else { + lastX = e.offsetX || (e.pageX - pos.left); + lastY = e.offsetY || (e.pageY - pos.top); + + dragStart = ctx.transformedPoint(lastX,lastY); + canvas_dragged = false; } renderer.start(); @@ -101,7 +378,7 @@ jQuery.fn.springy = function(params) { var pos = jQuery(this).offset(); var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top}); selected = layout.nearest(p); - node = selected.node; + var node = selected.node; if (node && node.data && node.data.ondoubleclick) { node.data.ondoubleclick(); } @@ -109,39 +386,181 @@ jQuery.fn.springy = function(params) { jQuery(canvas).mousemove(function(e) { var pos = jQuery(this).offset(); - var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top}); + var p1 = ctx.transformedPoint(e.pageX - pos.left, e.pageY - pos.top); + var p = fromScreen(p1); nearest = layout.nearest(p); - - if (dragged !== null && dragged.node !== null) { + mouse_inside_node(nearest, p); + if (dragged !== null && dragged.node !== null && dragged.inside) { dragged.point.p.x = p.x; dragged.point.p.y = p.y; + } else { + lastX = e.offsetX || (e.pageX - pos.left); + lastY = e.offsetY || (e.pageY - pos.top); + canvas_dragged = true; + if (dragStart !== null){ + var pt = ctx.transformedPoint(lastX,lastY); + var diffx = pt.x-dragStart.x; + var diffy = pt.y-dragStart.y; + var xform = ctx.getTransform(); + var xsize = canvas.width * xform.a; + var ysize = canvas.height * xform.a; + var xoffset = xform.e; + var yoffset = xform.f; + // 0 limit left: + if (diffx > 0 && xoffset + diffx > 0) { + diffx = 0; + } + // 0 limit right: + if (diffx < 0 && (xoffset + diffx + xsize) < canvas.width) { + diffx = 0; + } + // 0 limit top: + if (diffy > 0 && yoffset+diffy > 0) { + diffy = 0; + } + // 0 limit bottom: + if (diffy < 0 && (yoffset + diffy + ysize) < canvas.height) { + diffy = 0; + } + ctx.translate(diffx, diffy); + snap_to_canvas(); + } } + renderer.start(); + }); + jQuery(canvas).mouseleave(function(e) { + nearest = null; + dragged = null; + dragStart = null; renderer.start(); }); jQuery(window).bind('mouseup',function(e) { dragged = null; + dragStart = null; }); + // ------------------------------------------------- + + var zoom = function(clicks){ + if (! inside_node) { + var factor = Math.pow(layout.scaleFactor,clicks); + var pt = ctx.transformedPoint(lastX,lastY); + var zoomFactor = layout.zoomFactor; + ctx.translate(pt.x,pt.y); + if (factor < 1) { + // avoid negative zoom + if (zoomFactor * factor < 1) factor = 1.0/zoomFactor; + ctx.scale(factor,factor); + zoomFactor = zoomFactor * factor; + } + if (factor > 1 && zoomFactor < 8) { + ctx.scale(factor,factor); + zoomFactor = zoomFactor * factor; + } + ctx.translate(-pt.x,-pt.y); + if (clicks < 0) + snap_to_canvas(); + layout.zoomFactor = zoomFactor; + } else { + var factor = Math.pow(layout.scaleFactor,clicks); + var fontsize = layout.fontsize * factor; + if (fontsize < 1) fontsize = 1; + if (fontsize > 30) fontsize = 30; + layout.fontsize = fontsize; + } + renderer.start(); + } + + var handleScroll = function(evt){ + var delta = evt.wheelDelta ? evt.wheelDelta/40 : evt.detail ? -evt.detail : 0; + if (delta) zoom(delta); + return evt.preventDefault() && false; + }; + + canvas.addEventListener('DOMMouseScroll',handleScroll,false); + canvas.addEventListener('mousewheel',handleScroll,false); + // ------------------------------------------------- + + // Adds ctx.getTransform() - returns an SVGMatrix + // Adds ctx.transformedPoint(x,y) - returns an SVGPoint + function trackTransforms(ctx){ + var svg = document.createElementNS("http://www.w3.org/2000/svg",'svg'); + var xform = svg.createSVGMatrix(); + ctx.getTransform = function(){ return xform; }; + + var savedTransforms = []; + var save = ctx.save; + ctx.save = function(){ + savedTransforms.push(xform.translate(0,0)); + return save.call(ctx); + }; + var restore = ctx.restore; + ctx.restore = function(){ + xform = savedTransforms.pop(); + return restore.call(ctx); + }; + + var scale = ctx.scale; + ctx.scale = function(sx,sy){ + xform = xform.scaleNonUniform(sx,sy); + return scale.call(ctx,sx,sy); + }; + var rotate = ctx.rotate; + ctx.rotate = function(radians){ + xform = xform.rotate(radians*180/Math.PI); + return rotate.call(ctx,radians); + }; + var translate = ctx.translate; + ctx.translate = function(dx,dy){ + xform = xform.translate(dx,dy); + return translate.call(ctx,dx,dy); + }; + var transform = ctx.transform; + ctx.transform = function(a,b,c,d,e,f){ + var m2 = svg.createSVGMatrix(); + m2.a=a; m2.b=b; m2.c=c; m2.d=d; m2.e=e; m2.f=f; + xform = xform.multiply(m2); + return transform.call(ctx,a,b,c,d,e,f); + }; + var setTransform = ctx.setTransform; + ctx.setTransform = function(a,b,c,d,e,f){ + xform.a = a; + xform.b = b; + xform.c = c; + xform.d = d; + xform.e = e; + xform.f = f; + return setTransform.call(ctx,a,b,c,d,e,f); + }; + var pt = svg.createSVGPoint(); + ctx.transformedPoint = function(x,y){ + pt.x=x; pt.y=y; + return pt.matrixTransform(xform.inverse()); + } + } + var getTextWidth = function(node) { var text = (node.data.label !== undefined) ? node.data.label : node.id; - if (node._width && node._width[text]) + var fontsize = layout.fontsize; + if (node._width && node.fontsize === fontsize && node._width[text]) return node._width[text]; ctx.save(); - ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont; + ctx.font = fontsize.toString() + 'px ' + nodeFont; var width = ctx.measureText(text).width; ctx.restore(); node._width || (node._width = {}); node._width[text] = width; + node.fontsize = fontsize; return width; }; var getTextHeight = function(node) { - return 16; + return layout.fontsize; // In a more modular world, this would actually read the font size, but I think leaving it a constant is sufficient for now. // If you change the font size, I'd adjust this too. }; @@ -212,14 +631,11 @@ jQuery.fn.springy = function(params) { // Figure out how far off center the line should be drawn var offset = normal.multiply(-((total - 1) * spacing)/2.0 + (n * spacing)); - var paddingX = 6; - var paddingY = 6; - var s1 = toScreen(p1).add(offset); var s2 = toScreen(p2).add(offset); - var boxWidth = edge.target.getWidth() + paddingX; - var boxHeight = edge.target.getHeight() + paddingY; + var boxWidth = edge.target.getWidth() * 1.2; + var boxHeight = edge.target.getHeight() * 2.0; // extra space for target polygons var intersection = intersect_line_box(s1, s2, {x: x2-boxWidth/2.0, y: y2-boxHeight/2.0}, boxWidth, boxHeight); @@ -227,16 +643,34 @@ jQuery.fn.springy = function(params) { intersection = s2; } - var stroke = (edge.data.color !== undefined) ? edge.data.color : '#000000'; + boxWidth = edge.source.getWidth() * 1.2; + boxHeight = edge.source.getHeight() * 2.0; // extra space for source polygons + + // DS: respect source node! + var lineStart = intersect_line_box(s1, s2, {x: x1-boxWidth/2.0, y: y1-boxHeight/2.0}, boxWidth, boxHeight); + + if (!lineStart) { + lineStart = s1; + } + var stroke = (edge.data.color !== undefined) ? edge.data.color : '#000000'; + var fontsize = layout.fontsize; var arrowWidth; var arrowLength; var weight = (edge.data.weight !== undefined) ? edge.data.weight : 1.0; - - ctx.lineWidth = Math.max(weight * 2, 0.1); + weight = weight * (fontsize/8); + if (selected !== null && selected.node !== null + && (selected.node.id === edge.source.id || selected.node.id === edge.target.id) + && selected.inside) { + // highlight edges of the selected node + weight = weight * 2; + stroke = "rgba(255, 140, 0,0.7)"; + } + ctx.save(); // DS: add save + ctx.lineWidth = Math.max(weight, 0.1); arrowWidth = 1 + ctx.lineWidth; - arrowLength = 8; + arrowLength = fontsize*1.8; var directional = (edge.data.directional !== undefined) ? edge.data.directional : true; @@ -245,15 +679,15 @@ jQuery.fn.springy = function(params) { if (directional) { lineEnd = intersection.subtract(direction.normalise().multiply(arrowLength * 0.5)); } else { - lineEnd = s2; + lineEnd = intersection; // DS: respect target node! } ctx.strokeStyle = stroke; ctx.beginPath(); - ctx.moveTo(s1.x, s1.y); + ctx.moveTo(lineStart.x, lineStart.y); // DS: respect source node! ctx.lineTo(lineEnd.x, lineEnd.y); ctx.stroke(); - + ctx.restore(); // DS: add restore // arrow if (directional) { ctx.save(); @@ -271,62 +705,159 @@ jQuery.fn.springy = function(params) { } // label - if (edge.data.label !== undefined) { - text = edge.data.label + if (edge.data.label !== undefined && edge.data.label.length) { + var text = edge.data.label; + var l_fontsize = fontsize * 9 / 10; ctx.save(); ctx.textAlign = "center"; ctx.textBaseline = "top"; - ctx.font = (edge.data.font !== undefined) ? edge.data.font : edgeFont; + ctx.font = l_fontsize.toString() + 'px ' + edgeFont; + if (edgeLabelBoxes) { + var boxWidth = ctx.measureText(text).width * 1.1; + var px = (x1+x2)/2; + var py = (y1+y2)/2 - fontsize/2; + ctx.fillStyle = "#EEEEEE"; // label background + ctx.fillRect(px-boxWidth/2, py, boxWidth, fontsize); + + ctx.fillStyle = "darkred"; + ctx.fillText(text, px, py); + } else { ctx.fillStyle = stroke; var angle = Math.atan2(s2.y - s1.y, s2.x - s1.x); - var displacement = -8; - if (edgeLabelsUpright && (angle > Math.PI/2 || angle < -Math.PI/2)) { - displacement = 8; - angle += Math.PI; + var displacement = -(fontsize*2/3); + if (edgeLabelsUpright && (angle > Math.PI/2 || angle < -Math.PI/2)) { + displacement = fontsize*2/3; + angle += Math.PI; + } + var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement)); + ctx.translate(textPos.x, textPos.y); + ctx.rotate(angle); + ctx.fillText(text, 0,-2); } - var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement)); - ctx.translate(textPos.x, textPos.y); - ctx.rotate(angle); - ctx.fillText(text, 0,-2); ctx.restore(); } }, function drawNode(node, p) { var s = toScreen(p); - - ctx.save(); - - // Pulled out the padding aspect sso that the size functions could be used in multiple places - // These should probably be settable by the user (and scoped higher) but this suffices for now - var paddingX = 6; - var paddingY = 6; - - var contentWidth = node.getWidth(); - var contentHeight = node.getHeight(); - var boxWidth = contentWidth + paddingX; - var boxHeight = contentHeight + paddingY; - - // clear background - ctx.clearRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight); - + var boxWidth = node.getWidth(); + var boxHeight = node.getHeight() * 1.2; + var alpha = '0.9'; + var color = typeof(node.data.color) !== 'undefined' ? node.data.color : ''; + var textColor = 'Black'; // fill background - if (selected !== null && selected.node !== null && selected.node.id === node.id) { - ctx.fillStyle = "#FFFFE0"; - } else if (nearest !== null && nearest.node !== null && nearest.node.id === node.id) { - ctx.fillStyle = "#EEEEEE"; + if (selected !== null && selected.node !== null && selected.node.id === node.id && selected.inside) { + shadowColor = 'DarkOrange'; + shadowOffset = 0; } else { - ctx.fillStyle = "#FFFFFF"; + shadowColor = "rgba(50, 50, 50, 0.3)"; + shadowOffset = 10; + } + if (color.length > 0) { + color1 = color; + color2 = color; + } else { + color1 = "rgba(176, 224, 230,"+alpha+")"; // PowderBlue - rgb(176, 224, 230) + color2 = "rgba(176, 196, 222,"+alpha+")"; // LightSteelBlue - rgb(176, 196, 222) } - ctx.fillRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight); - if (node.data.image == undefined) { - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont; - ctx.fillStyle = (node.data.color !== undefined) ? node.data.color : "#000000"; - var text = (node.data.label !== undefined) ? node.data.label : node.id; - ctx.fillText(text, s.x - contentWidth/2, s.y - contentHeight/2); + ctx.save(); + ctx.lineWidth = layout.fontsize/12; + ctx.strokeStyle = "SlateGray"; + + /********* Draw Shape **********/ + /*******************************/ + var shape = typeof(node.data.shape) !== 'undefined' ? node.data.shape : 'box'; + switch(shape) { + case 'plaintext': + case 'none': + break; + case 'box': + box_shape(s, boxWidth, boxHeight, shape); + break; + case 'doublebox': + box_shape(s, boxWidth*1.1, boxHeight*1.2, shape); + box_shape(s, boxWidth, boxHeight, shape); + break; + case 'house': + house(s, boxWidth, boxHeight, false); + break; + case 'invhouse': + house(s, boxWidth, boxHeight, true); + break; + case 'circle': + ellipse(s, boxWidth, boxWidth); + break; + case 'ellipse': + ellipse(s, boxWidth, boxHeight); + break; + case 'doublecircle': + ellipse(s, boxWidth*1.1, boxHeight*1.2); + ellipse(s, boxWidth, boxHeight); + break; + case 'point': + textColor = 'DarkGray'; + ellipse(s, boxHeight, boxHeight); + break; + case 'triangle': + triangle(s, boxWidth, boxHeight); + // polygon(s, boxWidth, boxHeight, 3, true); + break; + case 'invtriangle': + polygon(s, boxWidth, boxHeight, 3, false); + break; + case 'rectangle': + polygon(s, boxWidth, boxHeight, 4, true); + break; + case 'diamond': + polygon(s, boxWidth, boxHeight, 4, false); + break; + case 'pentagon': + polygon(s, boxWidth, boxHeight, 5, true); + break; + case 'hexagon': + polygon(s, boxWidth, boxHeight, 6, true); + break; + case 'septagon': + polygon(s, boxWidth, boxHeight, 7, true); + break; + case 'octagon': + polygon(s, boxWidth, boxHeight, 8, true); + break; + case 'doubleoctagon': + polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true); + polygon(s, boxWidth, boxHeight, 8, true); + break; + case 'tripleoctagon': + polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true); + polygon(s, boxWidth, boxHeight, 8, true); + polygon(s, boxWidth*0.9, boxHeight*0.8, 8, true); + break; + case 'star': + textColor = 'DarkGray'; + star(s, boxWidth, boxHeight); + break; + case 'trapezium': + trapezium(s, boxWidth, boxHeight, false); + break; + case 'invtrapezium': + trapezium(s, boxWidth, boxHeight, true); + break; + case 'parallelogram': + parallelogram(s, boxWidth, boxHeight); + break; + default: + box(s, boxWidth, boxHeight); + } + ctx.translate(s.x, s.y); + // Node Label Text + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.font = node.fontsize.toString() + 'px ' + nodeFont; + ctx.fillStyle = textColor; + var text = typeof(node.data.label) !== 'undefined' ? node.data.label : node.id; + ctx.fillText(text, 0, 0); + ctx.restore(); } else { // Currently we just ignore any labels if the image object is set. One might want to extend this logic to allow for both, or other composite nodes. var src = node.data.image.src; // There should probably be a sanity check here too, but un-src-ed images aren't exaclty a disaster. @@ -348,7 +879,15 @@ jQuery.fn.springy = function(params) { img.src = src; } } - ctx.restore(); + }, + function getCanvasPos() { + var xform = ctx.getTransform(); + var canvasPos = {}; + canvasPos.fontsize = layout.fontsize; + canvasPos.zoomFactor = layout.zoomFactor; + canvasPos.x_offset = xform.e / layout.zoomFactor; + canvasPos.y_offset = xform.f / layout.zoomFactor; + return canvasPos; } ); From 5a528f17ffafdcac142487c6b466dcc9d5a417d1 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Mon, 28 Oct 2019 13:05:35 +0100 Subject: [PATCH 03/16] bug fixes for shapes parallelogram and circle --- springyui.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/springyui.js b/springyui.js index 7e6e99e..35562d3 100755 --- a/springyui.js +++ b/springyui.js @@ -190,7 +190,7 @@ jQuery.fn.springy = function(params) { var hh = height; height = height / 3 * 2; ctx.save(); - ctx.translate(pos.x, pos.y); + ctx.translate(pos.x-(hh/2), pos.y); set_colors(); ctx.beginPath(); ctx.moveTo(-width, -height); ctx.lineTo(width, -height); @@ -221,10 +221,14 @@ jQuery.fn.springy = function(params) { }; var ellipse = function(pos, width, height){ - width = width / 2 + height; ctx.save(); ctx.translate(pos.x, pos.y); - ctx.scale(1, height / width); + if (width > height) { + width = width / 2 + height; + ctx.scale(1, height / width); + } else { + width = width / 2; + } set_colors(); ctx.beginPath(); ctx.arc(0, 0, width, 0, Math.PI * 2, true); @@ -742,7 +746,7 @@ jQuery.fn.springy = function(params) { var s = toScreen(p); var boxWidth = node.getWidth(); var boxHeight = node.getHeight() * 1.2; - var alpha = '0.9'; + var alpha = '0.8'; var color = typeof(node.data.color) !== 'undefined' ? node.data.color : ''; var textColor = 'Black'; // fill background @@ -786,7 +790,7 @@ jQuery.fn.springy = function(params) { house(s, boxWidth, boxHeight, true); break; case 'circle': - ellipse(s, boxWidth, boxWidth); + ellipse(s, boxWidth+boxHeight, boxWidth+boxHeight); break; case 'ellipse': ellipse(s, boxWidth, boxHeight); @@ -844,7 +848,7 @@ jQuery.fn.springy = function(params) { trapezium(s, boxWidth, boxHeight, true); break; case 'parallelogram': - parallelogram(s, boxWidth, boxHeight); + parallelogram(s, boxWidth, boxHeight*1.1); break; default: box(s, boxWidth, boxHeight); From 9f6fb751cde24077f9703f08442ae94b2a91bf3a Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Mon, 18 Nov 2019 19:52:40 +0100 Subject: [PATCH 04/16] improved for padding of the canvas . --- springy.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/springy.js b/springy.js index ede00f9..ed8a17f 100644 --- a/springy.js +++ b/springy.js @@ -599,7 +599,7 @@ } }); - var padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding + var padding = topright.subtract(bottomleft).multiply2(0.14, 0.07); // ~5% padding return {bottomleft: bottomleft.subtract(padding), topright: topright.add(padding)}; }; @@ -627,6 +627,10 @@ return new Vector(this.x * n, this.y * n); }; + Vector.prototype.multiply2 = function(n, m) { + return new Vector(this.x * n, this.y * m); + }; + Vector.prototype.divide = function(n) { return new Vector((this.x / n) || 0, (this.y / n) || 0); // Avoid divide by zero errors.. }; From 6cf20338f1bec87bc4a19ecab0f31c594dba148d Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Tue, 3 Dec 2019 18:58:23 +0100 Subject: [PATCH 05/16] improved shadows for shapes. improved response time for larger graphics. --- springy.js | 67 +++++++++++++++--- springyui.js | 192 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 180 insertions(+), 79 deletions(-) diff --git a/springy.js b/springy.js index ed8a17f..d3e6eb9 100644 --- a/springy.js +++ b/springy.js @@ -338,6 +338,7 @@ this.fontsize = fontsize || 8.0; this.scaleFactor = 1.025; // scale factor for each wheel click. this.zoomFactor = zoomFactor || 1.0; // current zoom factor for the whole canvas. + this.energy = 0; this.nodePoints = {}; // keep track of points associated with nodes this.edgeSprings = {}; // keep track of springs associated with edges }; @@ -390,6 +391,22 @@ return this.edgeSprings[edge.id]; }; + // produce a random sample: callback should accept two arguments: Node, Point + Layout.ForceDirected.prototype.sampleNode = function(callback, limit) { + var t = this; + var sample = []; + var length = this.graph.nodes.length; + var n = Math.max(Math.min(limit, length), 0); + while (n--) { + var rand = Math.floor(Math.random() * length); + sample[rand] = t.graph.nodes[rand]; // deduplicate + //callback.call(t, rand, t.point(t.graph.nodes[rand])); + } + sample.forEach(function(n){ + callback.call(t, n, t.point(n)); + }); + }; + // callback should accept two arguments: Node, Point Layout.ForceDirected.prototype.eachNode = function(callback) { var t = this; @@ -414,7 +431,28 @@ }); }; - + Layout.ForceDirected.prototype.scaleFontSize = function(factor) { + var t = this; + t.fontsize *= factor; + }; + + + var timeslice = 1000000; + var loops_cnt = timeslice; + var sliceTimer = null; + function tic_fork (cnt, stage) { + loops_cnt -= cnt; + if (loops_cnt <= 0) { // DS: with 1,000 nodes we have 1,000,000 iterations, thats why i slice the time. + loops_cnt = timeslice; + console.log('tic'+stage); + if (! sliceTimer) { + sliceTimer = window.setTimeout(function (){ + console.log('tic-slice'+stage); + sliceTimer = null; + }, 10); + } + } + } // Physics stuff Layout.ForceDirected.prototype.applyCoulombsLaw = function() { this.eachNode(function(n1, point1) { @@ -430,6 +468,7 @@ point2.applyForce(direction.multiply(this.repulsion).divide(distance * distance * -0.5)); } }); + tic_fork (this.graph.nodes.length, 1); }); }; @@ -509,7 +548,7 @@ * Start simulation if it's not running already. * In case it's running then the call is ignored, and none of the callbacks passed is ever executed. */ - Layout.ForceDirected.prototype.start = function(render, onRenderStop, onRenderStart) { + Layout.ForceDirected.prototype.start = function(render, onRenderStop, onRenderStart, do_update) { var t = this; if (this._started) return; @@ -517,16 +556,17 @@ this._stop = false; if (onRenderStart !== undefined) { onRenderStart(); } - Springy.requestAnimationFrame(function step() { - t.tick(0.03); - + if (do_update) { + t.tick(0.03); + } if (render !== undefined) { render(); } - + // console.log('toc'); // stop simulation when energy of the system goes below a threshold - if (t._stop || t.totalEnergy() < t.minEnergyThreshold) { + t.energy = t.totalEnergy(); + if (t._stop || t.energy < t.minEnergyThreshold) { t._started = false; if (onRenderStop !== undefined) { onRenderStop(); } } else { @@ -542,9 +582,11 @@ Layout.ForceDirected.prototype.tick = function(timestep) { this.applyCoulombsLaw(); this.applyHookesLaw(); + tic_fork (this.graph.edges.length, 2); this.attractToCentre(); this.updateVelocity(timestep); this.updatePosition(timestep); + tic_fork (this.graph.nodes.length * 3, 3); }; // Find the nearest point to a particular position @@ -707,6 +749,11 @@ return this.getCanvasPos(); }; + Renderer.prototype.scaleFontSize = function(factor) { + this.layout.scaleFontSize(factor); + this.start(); + }; + /** * Starts the simulation of the layout in use. * @@ -717,7 +764,7 @@ * @param done An optional callback function that gets executed when the springy algorithm stops, * either because it ended or because stop() was called. */ - Renderer.prototype.start = function(done) { + Renderer.prototype.start = function(do_update) { var t = this; this.layout.start(function render() { t.clear(); @@ -725,13 +772,15 @@ t.layout.eachEdge(function(edge, spring) { t.drawEdge(edge, spring.point1.p, spring.point2.p); }); + tic_fork (t.layout.graph.edges.length*100, 4); t.layout.eachNode(function(node, point) { t.drawNode(node, point.p); }); + tic_fork (t.layout.graph.nodes.length*100, 5); if (t.onRenderFrame !== undefined) { t.onRenderFrame(); } - }, this.onRenderStop, this.onRenderStart); + }, this.onRenderStop, this.onRenderStart, do_update); }; Renderer.prototype.stop = function() { diff --git a/springyui.js b/springyui.js index 35562d3..5852f92 100755 --- a/springyui.js +++ b/springyui.js @@ -37,10 +37,14 @@ jQuery.fn.springy = function(params) { var maxSpeed = params.maxSpeed || Infinity; // nodes aren't allowed to exceed this speed var nodeSelected = params.nodeSelected || null; var nodePositions = params.nodePositions || null; + var RenderFrameCall = params.onRenderFrame || null; + var RenderStopCall = params.onRenderStop || null; + var RenderStartCall = params.onRenderStart || null; var pinWeight = params.pinWeight || 1000.0; var nodeImages = {}; var edgeLabelsUpright = true; var edgeLabelBoxes = params.edgeLabelBoxes || false; + var useGradient = params.useGradient || false; var fontsize = params.fontsize * 1.0 || Math.max(12 - Math.round(Math.sqrt(graph.nodes.length)), 4); var zoomFactor = params.zoomFactor * 1.0 || 1.0; var canvas = this[0]; @@ -52,7 +56,7 @@ jQuery.fn.springy = function(params) { var color1 = "#7FEFFF"; // blue var color2 = "#50C0FF"; var shadowColor = "rgba(50, 50, 50, 0.3)"; - var shadowOffset = 10; + var shadowOffset = fontsize * zoomFactor; trackTransforms(ctx); // calculate bounding box of graph layout.. with ease-in var currentBB = layout.getBoundingBox(); @@ -95,24 +99,36 @@ jQuery.fn.springy = function(params) { return new Springy.Vector(px, py); }; - var set_colors = function() { + var set_2colors = function() { var grd = ctx.createLinearGradient(-100, 100, 100, -100); grd.addColorStop(0, color1); grd.addColorStop(1, color2); ctx.fillStyle = grd; ctx.shadowColor = shadowColor; - ctx.shadowBlur = 10; + ctx.shadowBlur = layout.fontsize*layout.zoomFactor; ctx.shadowOffsetX = shadowOffset; ctx.shadowOffsetY = shadowOffset; }; + var set_1colors = function() { + ctx.fillStyle = color1; + ctx.shadowColor = shadowColor; + ctx.shadowBlur = layout.fontsize*layout.zoomFactor; + ctx.shadowOffsetX = shadowOffset; + ctx.shadowOffsetY = shadowOffset; + }; + var set_colors = (useGradient)? set_2colors : set_1colors; - var box_shape = function(pos, width, height, shape){ + var box_shape = function(pos, width, height, shape, first){ height = (height*8/14); width = width / 2.0 + height / 2.0; var hh = height / 2.0; ctx.save(); ctx.translate(pos.x, pos.y); - set_colors(); + if (first) { + set_colors(); + } else { + ctx.fillStyle = color1; + } switch(shape) { case 'box3d': ctx.beginPath(); @@ -220,7 +236,7 @@ jQuery.fn.springy = function(params) { ctx.stroke(); }; - var ellipse = function(pos, width, height){ + var ellipse = function(pos, width, height, first){ ctx.save(); ctx.translate(pos.x, pos.y); if (width > height) { @@ -229,7 +245,11 @@ jQuery.fn.springy = function(params) { } else { width = width / 2; } - set_colors(); + if (first) { + set_colors(); + } else { + ctx.fillStyle = color1; + } ctx.beginPath(); ctx.arc(0, 0, width, 0, Math.PI * 2, true); ctx.fill(); @@ -255,16 +275,20 @@ jQuery.fn.springy = function(params) { ctx.stroke(); }; - var polygon = function(pos, width, height, n, even){ + var polygon = function(pos, width, height, n, even, first){ var pix = 2*Math.PI/n; // angel in circle var fy = (3*height)/(2*width); // deformation of circle var dim = (n+1)*(3*height+2*width)/(4*n); // radius ctx.save(); ctx.translate(pos.x, pos.y); ctx.scale(1, fy); - set_colors(); + if (first) { + set_colors(); + } else { + ctx.fillStyle = color1; + } if (even) { - ctx.rotate(pix/2 + Math.PI/2); // flat bottom line + ctx.rotate(pix/2 + Math.PI/2*even); // flat bottom line } else { ctx.rotate(Math.PI/2); // standing on corner } @@ -374,7 +398,7 @@ jQuery.fn.springy = function(params) { canvas_dragged = false; } - renderer.start(); + renderer.start(selected.inside); }); // Basic double click handler @@ -430,14 +454,14 @@ jQuery.fn.springy = function(params) { snap_to_canvas(); } } - renderer.start(); + renderer.start(dragged !== null && dragged.inside); }); jQuery(canvas).mouseleave(function(e) { nearest = null; dragged = null; dragStart = null; - renderer.start(); + renderer.start(true); }); jQuery(window).bind('mouseup',function(e) { @@ -473,7 +497,7 @@ jQuery.fn.springy = function(params) { if (fontsize > 30) fontsize = 30; layout.fontsize = fontsize; } - renderer.start(); + renderer.start(true); } var handleScroll = function(evt){ @@ -712,33 +736,36 @@ jQuery.fn.springy = function(params) { if (edge.data.label !== undefined && edge.data.label.length) { var text = edge.data.label; var l_fontsize = fontsize * 9 / 10; - ctx.save(); - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - ctx.font = l_fontsize.toString() + 'px ' + edgeFont; - if (edgeLabelBoxes) { - var boxWidth = ctx.measureText(text).width * 1.1; - var px = (x1+x2)/2; - var py = (y1+y2)/2 - fontsize/2; - ctx.fillStyle = "#EEEEEE"; // label background - ctx.fillRect(px-boxWidth/2, py, boxWidth, fontsize); - - ctx.fillStyle = "darkred"; - ctx.fillText(text, px, py); - } else { - ctx.fillStyle = stroke; - var angle = Math.atan2(s2.y - s1.y, s2.x - s1.x); - var displacement = -(fontsize*2/3); - if (edgeLabelsUpright && (angle > Math.PI/2 || angle < -Math.PI/2)) { - displacement = fontsize*2/3; - angle += Math.PI; + if (l_fontsize * layout.zoomFactor > 2.4) { // DS: hide tiny label + ctx.save(); + ctx.textAlign = "center"; + ctx.font = l_fontsize.toString() + 'px ' + edgeFont; + if (edgeLabelBoxes) { + var boxWidth = ctx.measureText(text).width * 1.1; + var px = (x1+x2)/2; + var py = (y1+y2)/2 - fontsize/2; + ctx.textBaseline = "top"; + ctx.fillStyle = "#EEEEEE"; // label background + ctx.fillRect(px-boxWidth/2, py, boxWidth, fontsize); + + ctx.fillStyle = "darkred"; + ctx.fillText(text, px, py); + } else { + ctx.textBaseline = "middle"; + ctx.fillStyle = stroke; + var angle = Math.atan2(s2.y - s1.y, s2.x - s1.x); + var displacement = -(l_fontsize / 3.0); + if (edgeLabelsUpright && (angle > Math.PI/2 || angle < -Math.PI/2)) { + displacement = l_fontsize / 3.0; + angle += Math.PI; + } + var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement)); + ctx.translate(textPos.x, textPos.y); + ctx.rotate(angle); + ctx.fillText(text, 0,-2); } - var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement)); - ctx.translate(textPos.x, textPos.y); - ctx.rotate(angle); - ctx.fillText(text, 0,-2); + ctx.restore(); } - ctx.restore(); } }, @@ -755,7 +782,7 @@ jQuery.fn.springy = function(params) { shadowOffset = 0; } else { shadowColor = "rgba(50, 50, 50, 0.3)"; - shadowOffset = 10; + shadowOffset = layout.fontsize * layout.zoomFactor; } if (color.length > 0) { color1 = color; @@ -777,11 +804,17 @@ jQuery.fn.springy = function(params) { case 'none': break; case 'box': - box_shape(s, boxWidth, boxHeight, shape); + case 'box3d': + case 'folder': + case 'note': + case 'tab': + case 'component': + case 'Msquare': + box_shape(s, boxWidth, boxHeight, shape, true); break; case 'doublebox': - box_shape(s, boxWidth*1.1, boxHeight*1.2, shape); - box_shape(s, boxWidth, boxHeight, shape); + box_shape(s, boxWidth*1.1, boxHeight*1.2, shape, true); + box_shape(s, boxWidth, boxHeight, shape, false); break; case 'house': house(s, boxWidth, boxHeight, false); @@ -790,52 +823,57 @@ jQuery.fn.springy = function(params) { house(s, boxWidth, boxHeight, true); break; case 'circle': - ellipse(s, boxWidth+boxHeight, boxWidth+boxHeight); + ellipse(s, boxWidth+boxHeight, boxWidth+boxHeight, true); break; case 'ellipse': - ellipse(s, boxWidth, boxHeight); + ellipse(s, boxWidth, boxHeight, true); break; case 'doublecircle': - ellipse(s, boxWidth*1.1, boxHeight*1.2); - ellipse(s, boxWidth, boxHeight); + ellipse(s, boxWidth*1.1, boxHeight*1.2, true); + ellipse(s, boxWidth, boxHeight, false); break; case 'point': textColor = 'DarkGray'; - ellipse(s, boxHeight, boxHeight); + ellipse(s, boxHeight, boxHeight, true); break; case 'triangle': triangle(s, boxWidth, boxHeight); - // polygon(s, boxWidth, boxHeight, 3, true); + break; + case 'righttriangle': + polygon(s, boxWidth*1.2, boxHeight, 3, 2, true); + break; + case 'lefttriangle': + polygon(s, boxWidth*1.2, boxHeight, 3, 4, true); break; case 'invtriangle': - polygon(s, boxWidth, boxHeight, 3, false); + polygon(s, boxWidth, boxHeight, 3, false, true); break; case 'rectangle': - polygon(s, boxWidth, boxHeight, 4, true); + polygon(s, boxWidth, boxHeight, 4, true, true); break; case 'diamond': - polygon(s, boxWidth, boxHeight, 4, false); + polygon(s, boxWidth, boxHeight, 4, false, true); break; case 'pentagon': - polygon(s, boxWidth, boxHeight, 5, true); + polygon(s, boxWidth, boxHeight, 5, true, true); break; case 'hexagon': - polygon(s, boxWidth, boxHeight, 6, true); + polygon(s, boxWidth, boxHeight, 6, true, true); break; case 'septagon': - polygon(s, boxWidth, boxHeight, 7, true); + polygon(s, boxWidth, boxHeight, 7, true, true); break; case 'octagon': - polygon(s, boxWidth, boxHeight, 8, true); + polygon(s, boxWidth, boxHeight, 8, true, true); break; case 'doubleoctagon': - polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true); + polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true, true); polygon(s, boxWidth, boxHeight, 8, true); break; case 'tripleoctagon': - polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true); - polygon(s, boxWidth, boxHeight, 8, true); - polygon(s, boxWidth*0.9, boxHeight*0.8, 8, true); + polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true, true); + polygon(s, boxWidth, boxHeight, 8, true, false); + polygon(s, boxWidth*0.9, boxHeight*0.8, 8, true, false); break; case 'star': textColor = 'DarkGray'; @@ -851,16 +889,18 @@ jQuery.fn.springy = function(params) { parallelogram(s, boxWidth, boxHeight*1.1); break; default: - box(s, boxWidth, boxHeight); + box_shape(s, boxWidth, boxHeight, shape, true); } ctx.translate(s.x, s.y); // Node Label Text - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.font = node.fontsize.toString() + 'px ' + nodeFont; - ctx.fillStyle = textColor; - var text = typeof(node.data.label) !== 'undefined' ? node.data.label : node.id; - ctx.fillText(text, 0, 0); + if (node.fontsize * layout.zoomFactor > 2.4) { // DS: hide tiny label + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.font = node.fontsize.toString() + 'px ' + nodeFont; + ctx.fillStyle = textColor; + var text = typeof(node.data.label) !== 'undefined' ? node.data.label : node.id; + ctx.fillText(text, 0, 0); + } ctx.restore(); } else { // Currently we just ignore any labels if the image object is set. One might want to extend this logic to allow for both, or other composite nodes. @@ -888,14 +928,27 @@ jQuery.fn.springy = function(params) { var xform = ctx.getTransform(); var canvasPos = {}; canvasPos.fontsize = layout.fontsize; - canvasPos.zoomFactor = layout.zoomFactor; + canvasPos.zoomFactor = layout.zoomFactor; // = xform.e = xform.d canvasPos.x_offset = xform.e / layout.zoomFactor; canvasPos.y_offset = xform.f / layout.zoomFactor; + canvasPos.energy = layout.energy; return canvasPos; + }, + function onRenderStop() { + if (RenderStopCall) + RenderStopCall(renderer); + }, + function onRenderStart() { + if (RenderStartCall) + RenderStartCall(renderer); + }, + function onRenderFrame() { + if (RenderFrameCall) + RenderFrameCall(renderer); } ); - renderer.start(); + renderer.start(true); // helpers for figuring out where to draw arrows function intersect_line_line(p1, p2, p3, p4) { @@ -930,7 +983,6 @@ jQuery.fn.springy = function(params) { return false; } - return this; } From 0b74d84a9d0ba7d8e9d170bebc05ed7f65e75786 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Thu, 5 Dec 2019 23:38:10 +0100 Subject: [PATCH 06/16] new parameter fontname; new function scaleFontSize loop optimisation: moved assignments; use of const and let for loop variables. --- springy.js | 29 +++++++++++----- springyui.js | 95 +++++++++++++++++++++++++++------------------------- 2 files changed, 69 insertions(+), 55 deletions(-) diff --git a/springy.js b/springy.js index d3e6eb9..d6fa843 100644 --- a/springy.js +++ b/springy.js @@ -328,7 +328,7 @@ // ----------- var Layout = Springy.Layout = {}; - Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, zoomFactor) { + Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor) { this.graph = graph; this.stiffness = stiffness; // spring stiffness constant this.repulsion = repulsion; // repulsion constant @@ -336,6 +336,10 @@ this.minEnergyThreshold = minEnergyThreshold || 0.01; //threshold used to determine render stop this.maxSpeed = maxSpeed || Infinity; // nodes aren't allowed to exceed this speed this.fontsize = fontsize || 8.0; + this.edgeFontsize = this.fontsize * 9 / 10; + this.fontname = fontname || "Verdana, sans-serif"; + this.nodeFont = this.fontsize.toString() + 'px ' + this.fontname; + this.edgeFont = this.edgeFontsize.toString() + 'px ' + this.fontname; this.scaleFactor = 1.025; // scale factor for each wheel click. this.zoomFactor = zoomFactor || 1.0; // current zoom factor for the whole canvas. this.energy = 0; @@ -432,24 +436,31 @@ }; Layout.ForceDirected.prototype.scaleFontSize = function(factor) { - var t = this; - t.fontsize *= factor; + const t = this; + let fontsize = t.fontsize * factor; + if (fontsize < 1) fontsize = 1; + if (fontsize > 30) fontsize = 30; + t.edgeFontsize = fontsize * 9 / 10; + t.fontsize = fontsize; + t.nodeFont = t.fontsize.toString() + 'px ' + t.fontname; + t.edgeFont = t.edgeFontsize.toString() + 'px ' + t.fontname; }; - var timeslice = 1000000; + var timeslice = 200000; var loops_cnt = timeslice; var sliceTimer = null; function tic_fork (cnt, stage) { loops_cnt -= cnt; if (loops_cnt <= 0) { // DS: with 1,000 nodes we have 1,000,000 iterations, thats why i slice the time. loops_cnt = timeslice; - console.log('tic'+stage); + //window.setTimeout(function () {console.log('tic-'+stage);}); + // console.log('tic'+stage); if (! sliceTimer) { sliceTimer = window.setTimeout(function (){ - console.log('tic-slice'+stage); + // console.log('tic-slice'+stage); sliceTimer = null; - }, 10); + }, 1); } } } @@ -738,7 +749,7 @@ } Renderer.prototype.graphChanged = function(e) { - this.start(); + this.start(true); }; Renderer.prototype.getNodePositions = function(e) { @@ -751,7 +762,7 @@ Renderer.prototype.scaleFontSize = function(factor) { this.layout.scaleFontSize(factor); - this.start(); + this.start(true); }; /** diff --git a/springyui.js b/springyui.js index 5852f92..d1ded01 100755 --- a/springyui.js +++ b/springyui.js @@ -27,30 +27,39 @@ Copyright (c) 2010 Dennis Hotson (function() { jQuery.fn.springy = function(params) { - var graph = this.graph = params.graph || new Springy.Graph(); - var nodeFont = "Verdana, sans-serif"; - var edgeFont = "Verdana, sans-serif"; - var stiffness = params.stiffness || 400.0; - var repulsion = params.repulsion || 400.0; - var damping = params.damping || 0.5; - var minEnergyThreshold = params.minEnergyThreshold || 0.00001; - var maxSpeed = params.maxSpeed || Infinity; // nodes aren't allowed to exceed this speed - var nodeSelected = params.nodeSelected || null; - var nodePositions = params.nodePositions || null; - var RenderFrameCall = params.onRenderFrame || null; - var RenderStopCall = params.onRenderStop || null; - var RenderStartCall = params.onRenderStart || null; - var pinWeight = params.pinWeight || 1000.0; + const graph = this.graph = params.graph || new Springy.Graph(); + const fontname = "Verdana, sans-serif"; + const nodeTextColor = 'Black'; + const nodeTextAlign = "center"; + const nodeTextBaseline = "middle"; + const activeShadowColor = 'DarkOrange'; + const activeShadowOffset = 0; + const passiveShadowColor = "rgba(50, 50, 50, 0.3)"; + const nodeAlpha = '0.8'; + const nodestrokeStyle = "SlateGray"; + const defaultColor1 = "rgba(176, 224, 230,"+nodeAlpha+")"; // PowderBlue - rgb(176, 224, 230) + const defaultColor2 = "rgba(176, 196, 222,"+nodeAlpha+")"; // LightSteelBlue - rgb(176, 196, 222) + const stiffness = params.stiffness || 400.0; + const repulsion = params.repulsion || 400.0; + const damping = params.damping || 0.5; + const minEnergyThreshold = params.minEnergyThreshold || 0.00001; + const maxSpeed = params.maxSpeed || Infinity; // nodes aren't allowed to exceed this speed + const nodeSelected = params.nodeSelected || null; + const nodePositions = params.nodePositions || null; + const RenderFrameCall = params.onRenderFrame || null; + const RenderStopCall = params.onRenderStop || null; + const RenderStartCall = params.onRenderStart || null; + const pinWeight = params.pinWeight || 1000.0; var nodeImages = {}; - var edgeLabelsUpright = true; - var edgeLabelBoxes = params.edgeLabelBoxes || false; - var useGradient = params.useGradient || false; + const edgeLabelsUpright = true; + const edgeLabelBoxes = params.edgeLabelBoxes || false; + const useGradient = params.useGradient || false; var fontsize = params.fontsize * 1.0 || Math.max(12 - Math.round(Math.sqrt(graph.nodes.length)), 4); var zoomFactor = params.zoomFactor * 1.0 || 1.0; var canvas = this[0]; var ctx = canvas.getContext("2d"); - var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, zoomFactor); + var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor); var selected = null; var color1 = "#7FEFFF"; // blue @@ -472,9 +481,9 @@ jQuery.fn.springy = function(params) { var zoom = function(clicks){ if (! inside_node) { - var factor = Math.pow(layout.scaleFactor,clicks); - var pt = ctx.transformedPoint(lastX,lastY); - var zoomFactor = layout.zoomFactor; + let factor = Math.pow(layout.scaleFactor,clicks); + let pt = ctx.transformedPoint(lastX,lastY); + let zoomFactor = layout.zoomFactor; ctx.translate(pt.x,pt.y); if (factor < 1) { // avoid negative zoom @@ -491,11 +500,8 @@ jQuery.fn.springy = function(params) { snap_to_canvas(); layout.zoomFactor = zoomFactor; } else { - var factor = Math.pow(layout.scaleFactor,clicks); - var fontsize = layout.fontsize * factor; - if (fontsize < 1) fontsize = 1; - if (fontsize > 30) fontsize = 30; - layout.fontsize = fontsize; + let factor = Math.pow(layout.scaleFactor,clicks); + layout.scaleFontSize(factor); } renderer.start(true); } @@ -571,18 +577,17 @@ jQuery.fn.springy = function(params) { var getTextWidth = function(node) { var text = (node.data.label !== undefined) ? node.data.label : node.id; - var fontsize = layout.fontsize; - if (node._width && node.fontsize === fontsize && node._width[text]) + if (node._width && node.fontsize === layout.fontsize && node._width[text]) return node._width[text]; ctx.save(); - ctx.font = fontsize.toString() + 'px ' + nodeFont; + ctx.font = layout.nodeFont; var width = ctx.measureText(text).width; ctx.restore(); node._width || (node._width = {}); node._width[text] = width; - node.fontsize = fontsize; + node.fontsize = layout.fontsize; return width; }; @@ -735,11 +740,10 @@ jQuery.fn.springy = function(params) { // label if (edge.data.label !== undefined && edge.data.label.length) { var text = edge.data.label; - var l_fontsize = fontsize * 9 / 10; - if (l_fontsize * layout.zoomFactor > 2.4) { // DS: hide tiny label + if (layout.edgeFontsize * layout.zoomFactor > 2.4) { // DS: hide tiny label ctx.save(); ctx.textAlign = "center"; - ctx.font = l_fontsize.toString() + 'px ' + edgeFont; + ctx.font = layout.edgeFont; if (edgeLabelBoxes) { var boxWidth = ctx.measureText(text).width * 1.1; var px = (x1+x2)/2; @@ -754,9 +758,9 @@ jQuery.fn.springy = function(params) { ctx.textBaseline = "middle"; ctx.fillStyle = stroke; var angle = Math.atan2(s2.y - s1.y, s2.x - s1.x); - var displacement = -(l_fontsize / 3.0); + var displacement = -(layout.edgeFontsize / 10.0); if (edgeLabelsUpright && (angle > Math.PI/2 || angle < -Math.PI/2)) { - displacement = l_fontsize / 3.0; + displacement = layout.edgeFontsize / 3.0; angle += Math.PI; } var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement)); @@ -773,28 +777,27 @@ jQuery.fn.springy = function(params) { var s = toScreen(p); var boxWidth = node.getWidth(); var boxHeight = node.getHeight() * 1.2; - var alpha = '0.8'; var color = typeof(node.data.color) !== 'undefined' ? node.data.color : ''; - var textColor = 'Black'; + var textColor = nodeTextColor; // fill background if (selected !== null && selected.node !== null && selected.node.id === node.id && selected.inside) { - shadowColor = 'DarkOrange'; - shadowOffset = 0; + shadowColor = activeShadowColor; + shadowOffset = activeShadowOffset; } else { - shadowColor = "rgba(50, 50, 50, 0.3)"; + shadowColor = passiveShadowColor; shadowOffset = layout.fontsize * layout.zoomFactor; } if (color.length > 0) { color1 = color; color2 = color; } else { - color1 = "rgba(176, 224, 230,"+alpha+")"; // PowderBlue - rgb(176, 224, 230) - color2 = "rgba(176, 196, 222,"+alpha+")"; // LightSteelBlue - rgb(176, 196, 222) + color1 = defaultColor1; + color2 = defaultColor2; } if (node.data.image == undefined) { ctx.save(); ctx.lineWidth = layout.fontsize/12; - ctx.strokeStyle = "SlateGray"; + ctx.strokeStyle = nodestrokeStyle; /********* Draw Shape **********/ /*******************************/ @@ -894,9 +897,9 @@ jQuery.fn.springy = function(params) { ctx.translate(s.x, s.y); // Node Label Text if (node.fontsize * layout.zoomFactor > 2.4) { // DS: hide tiny label - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.font = node.fontsize.toString() + 'px ' + nodeFont; + ctx.textAlign = nodeTextAlign; + ctx.textBaseline = nodeTextBaseline; + ctx.font = layout.nodeFont; // node.fontsize.toString() + 'px ' + nodeFont; ctx.fillStyle = textColor; var text = typeof(node.data.label) !== 'undefined' ? node.data.label : node.id; ctx.fillText(text, 0, 0); From e369c8cd7225e3c543fe9634b989feb3ff5796a0 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Fri, 6 Dec 2019 02:37:46 +0100 Subject: [PATCH 07/16] added parameter pinWeight -- fixate or just move selected node depending on pinWeight added functions selectNode, isSelectedNode, isSelectedEdge, setNodeProperties --- springy.js | 44 +++++++++++++++++++++++++++++++++++++++++++- springyui.js | 31 +++++++++++++------------------ 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/springy.js b/springy.js index d6fa843..690028e 100644 --- a/springy.js +++ b/springy.js @@ -328,7 +328,7 @@ // ----------- var Layout = Springy.Layout = {}; - Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor) { + Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor, pinWeight) { this.graph = graph; this.stiffness = stiffness; // spring stiffness constant this.repulsion = repulsion; // repulsion constant @@ -340,8 +340,10 @@ this.fontname = fontname || "Verdana, sans-serif"; this.nodeFont = this.fontsize.toString() + 'px ' + this.fontname; this.edgeFont = this.edgeFontsize.toString() + 'px ' + this.fontname; + this.pinWeight = pinWeight || 10; this.scaleFactor = 1.025; // scale factor for each wheel click. this.zoomFactor = zoomFactor || 1.0; // current zoom factor for the whole canvas. + this.selected = null; this.energy = 0; this.nodePoints = {}; // keep track of points associated with nodes this.edgeSprings = {}; // keep track of springs associated with edges @@ -446,6 +448,9 @@ t.edgeFont = t.edgeFontsize.toString() + 'px ' + t.fontname; }; + Layout.ForceDirected.prototype.setPinWeight = function(weight) { + this.pinWeight = weight; + }; var timeslice = 200000; var loops_cnt = timeslice; @@ -632,6 +637,35 @@ return min; } + Layout.ForceDirected.prototype.selectNode = function(node_id) { + var t = this; + t.selected = t.findNode(node_id); + return t.selected; + } + + Layout.ForceDirected.prototype.isSelectedNode = function(node_id) { + var t = this; + return (t.selected !== null && t.selected.node !== null + && (t.selected.node.id === node_id || node_id === null) + && t.selected.inside); + } + + Layout.ForceDirected.prototype.isSelectedEdge = function(edge) { + var t = this; + return (t.selected !== null && t.selected.node !== null + && (t.selected.node.id === edge.source.id || t.selected.node.id === edge.target.id) + && t.selected.inside); + } + + Layout.ForceDirected.prototype.setNodeProperties = function(label, color, shape) { + var t = this; + if (t.isSelectedNode(null)) { + t.selected.node.data.label = label; + t.selected.node.data.color = color; + t.selected.node.data.shape = shape; + } + } + // returns [bottomleft, topright] Layout.ForceDirected.prototype.getBoundingBox = function() { var bottomleft = new Vector(-2,-2); @@ -765,6 +799,14 @@ this.start(true); }; + Renderer.prototype.setPinWeight = function(weight) { + this.layout.setPinWeight(weight); + }; + + Renderer.prototype.setNodeProperties = function(label, color, shape) { + this.layout.setNodeProperties(label, color, shape); + }; + /** * Starts the simulation of the layout in use. * diff --git a/springyui.js b/springyui.js index d1ded01..e603732 100755 --- a/springyui.js +++ b/springyui.js @@ -59,8 +59,7 @@ jQuery.fn.springy = function(params) { var canvas = this[0]; var ctx = canvas.getContext("2d"); - var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor); - var selected = null; + var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor, pinWeight); var color1 = "#7FEFFF"; // blue var color2 = "#50C0FF"; @@ -71,7 +70,7 @@ jQuery.fn.springy = function(params) { var currentBB = layout.getBoundingBox(); var targetBB = {bottomleft: new Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)}; if (params.selected) { - selected = layout.findNode(params.selected); + layout.selectNode(params.selected); } if (zoomFactor !== !.0) { ctx.scale(zoomFactor,zoomFactor); @@ -388,16 +387,14 @@ jQuery.fn.springy = function(params) { var pos = jQuery(this).offset(); var p1 = ctx.transformedPoint(e.pageX - pos.left, e.pageY - pos.top); var p = fromScreen(p1); - selected = nearest = dragged = layout.nearest(p); + layout.selected = nearest = dragged = layout.nearest(p); point_clicked = p; - if (selected.node !== null) { - // DS 13.Oct 2019 : fix or just move selected node depending on pinWeight - dragged.point.m = pinWeight; - } - mouse_inside_node(selected, p); - if (selected.inside) { + mouse_inside_node(layout.selected, p); + if (layout.selected.inside) { + // DS 13.Oct 2019 : fixate or just move selected node depending on pinWeight + dragged.point.m = layout.pinWeight; if (nodeSelected) { - nodeSelected(selected.node); + nodeSelected(layout.selected.node); } } else { lastX = e.offsetX || (e.pageX - pos.left); @@ -407,15 +404,15 @@ jQuery.fn.springy = function(params) { canvas_dragged = false; } - renderer.start(selected.inside); + renderer.start(layout.selected.inside); }); // Basic double click handler jQuery(canvas).dblclick(function(e) { var pos = jQuery(this).offset(); var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top}); - selected = layout.nearest(p); - var node = selected.node; + layout.selected = layout.nearest(p); + var node = layout.selected.node; if (node && node.data && node.data.ondoubleclick) { node.data.ondoubleclick(); } @@ -693,9 +690,7 @@ jQuery.fn.springy = function(params) { var weight = (edge.data.weight !== undefined) ? edge.data.weight : 1.0; weight = weight * (fontsize/8); - if (selected !== null && selected.node !== null - && (selected.node.id === edge.source.id || selected.node.id === edge.target.id) - && selected.inside) { + if (layout.isSelectedEdge(edge)) { // highlight edges of the selected node weight = weight * 2; stroke = "rgba(255, 140, 0,0.7)"; @@ -780,7 +775,7 @@ jQuery.fn.springy = function(params) { var color = typeof(node.data.color) !== 'undefined' ? node.data.color : ''; var textColor = nodeTextColor; // fill background - if (selected !== null && selected.node !== null && selected.node.id === node.id && selected.inside) { + if (layout.isSelectedNode(node.id)) { shadowColor = activeShadowColor; shadowOffset = activeShadowOffset; } else { From e2a407e87a127064282f128de1b7900a5aead3b3 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Sun, 8 Dec 2019 06:39:42 +0100 Subject: [PATCH 08/16] fixed typo --- springyui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/springyui.js b/springyui.js index e603732..4818f81 100755 --- a/springyui.js +++ b/springyui.js @@ -72,7 +72,7 @@ jQuery.fn.springy = function(params) { if (params.selected) { layout.selectNode(params.selected); } - if (zoomFactor !== !.0) { + if (zoomFactor !== 1.0) { ctx.scale(zoomFactor,zoomFactor); } if (params.x_offset || params.y_offset) { @@ -488,7 +488,7 @@ jQuery.fn.springy = function(params) { ctx.scale(factor,factor); zoomFactor = zoomFactor * factor; } - if (factor > 1 && zoomFactor < 8) { + if (factor > 1 && zoomFactor < 12) { ctx.scale(factor,factor); zoomFactor = zoomFactor * factor; } From 1be094ae5f72ba45c84417ca0389cbe3501c0a0e Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Mon, 9 Dec 2019 02:21:49 +0100 Subject: [PATCH 09/16] tuning of math, memory usage and repeated calculations for applyCoulombsLaw, applyHookesLaw and render. added functions zoomCanvas, scaleZoomFactor. --- springy.js | 121 ++++++++++++++++++++++++++++++++++++++++++------ springyui.js | 128 +++++++++++++++++++++++++++++---------------------- 2 files changed, 180 insertions(+), 69 deletions(-) diff --git a/springy.js b/springy.js index 690028e..0815dd5 100644 --- a/springy.js +++ b/springy.js @@ -41,7 +41,7 @@ root.Springy = factory(); } }(this, function() { - + const boosted = true; // DS: 2.7fps vs 11.7fps var Springy = {}; var Graph = Springy.Graph = function() { @@ -343,10 +343,14 @@ this.pinWeight = pinWeight || 10; this.scaleFactor = 1.025; // scale factor for each wheel click. this.zoomFactor = zoomFactor || 1.0; // current zoom factor for the whole canvas. + this.realFontsize = this.fontsize * this.zoomFactor; + this.realEdgeFontsize = this.edgeFontsize * this.zoomFactor; this.selected = null; this.energy = 0; this.nodePoints = {}; // keep track of points associated with nodes this.edgeSprings = {}; // keep track of springs associated with edges + this.times = []; + this.fps = 0; }; Layout.ForceDirected.prototype.point = function(node) { @@ -439,27 +443,38 @@ Layout.ForceDirected.prototype.scaleFontSize = function(factor) { const t = this; - let fontsize = t.fontsize * factor; - if (fontsize < 1) fontsize = 1; - if (fontsize > 30) fontsize = 30; - t.edgeFontsize = fontsize * 9 / 10; - t.fontsize = fontsize; + let realFontsize = t.fontsize * t.zoomFactor * factor; + realFontsize = Math.max(Math.min(realFontsize, 30), 0.5); + t.realFontsize = realFontsize; + t.fontsize = realFontsize / t.zoomFactor; + t.realEdgeFontsize = realFontsize * 9 / 10; + t.edgeFontsize = t.fontsize * 9 / 10; t.nodeFont = t.fontsize.toString() + 'px ' + t.fontname; t.edgeFont = t.edgeFontsize.toString() + 'px ' + t.fontname; }; + Layout.ForceDirected.prototype.scaleZoomFactor = function(factor) { + let zoomFactor = this.zoomFactor * factor; + zoomFactor = Math.max(Math.min(zoomFactor, 12.0), 1.0); + if (this.zoomFactor !== zoomFactor) { + this.zoomFactor = zoomFactor; + return true; + } else { + return false; + } + }; + Layout.ForceDirected.prototype.setPinWeight = function(weight) { this.pinWeight = weight; }; - var timeslice = 200000; - var loops_cnt = timeslice; - var sliceTimer = null; + let timeslice = 200000; + let loops_cnt = timeslice; + let sliceTimer = null; function tic_fork (cnt, stage) { loops_cnt -= cnt; if (loops_cnt <= 0) { // DS: with 1,000 nodes we have 1,000,000 iterations, thats why i slice the time. loops_cnt = timeslice; - //window.setTimeout(function () {console.log('tic-'+stage);}); // console.log('tic'+stage); if (! sliceTimer) { sliceTimer = window.setTimeout(function (){ @@ -470,7 +485,30 @@ } } // Physics stuff - Layout.ForceDirected.prototype.applyCoulombsLaw = function() { + Layout.ForceDirected.prototype.applyCoulombsLaw = boosted ? function() { + const len = this.graph.nodes.length; + let dir = new Vector(0,0); + for(let n=0;n 0 && t.times[0] <= now - 10000) { + t.times.shift(); + } + t.times.push(now); + t.fps = t.times.length / 10; + if (do_update) { t.tick(0.03); } @@ -769,7 +833,7 @@ * @param onRenderStart optional callback function that gets executed whenever rendering starts. * @param onRenderFrame optional callback function that gets executed after each frame is rendered. */ - var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, getCanvasPos, onRenderStop, onRenderStart, onRenderFrame) { + var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, getCanvasPos, onRenderStop, onRenderStart, onRenderFrame, zoomCanvas) { this.layout = layout; this.clear = clear; this.drawEdge = drawEdge; @@ -778,7 +842,7 @@ this.onRenderStop = onRenderStop; this.onRenderStart = onRenderStart; this.onRenderFrame = onRenderFrame; - + this.zoomCanvas = zoomCanvas; this.layout.graph.addGraphListener(this); } @@ -799,6 +863,15 @@ this.start(true); }; + Renderer.prototype.scaleZoomFactor = function(factor) { + var t = this; + if (t.layout.scaleZoomFactor(factor)) { + t.layout.scaleFontSize(1.0); + if (t.zoomCanvas !== undefined) { t.zoomCanvas(factor); } + t.start(true); + } + }; + Renderer.prototype.setPinWeight = function(weight) { this.layout.setPinWeight(weight); }; @@ -817,7 +890,25 @@ * @param done An optional callback function that gets executed when the springy algorithm stops, * either because it ended or because stop() was called. */ - Renderer.prototype.start = function(do_update) { + Renderer.prototype.start = boosted ? function(do_update) { + var t = this; + this.layout.start(function render() { + t.clear(); + for(let n=0;n 2.4) { // DS: hide tiny label + let text = edge.data.label; + if (layout.realEdgeFontsize > 2.4) { // DS: hide tiny label ctx.save(); ctx.textAlign = "center"; ctx.font = layout.edgeFont; if (edgeLabelBoxes) { - var boxWidth = ctx.measureText(text).width * 1.1; - var px = (x1+x2)/2; - var py = (y1+y2)/2 - fontsize/2; + let boxWidth = ctx.measureText(text).width * 1.1; + let px = (x1+x2)/2; + let py = (y1+y2)/2 - layout.fontsize/2; ctx.textBaseline = "top"; ctx.fillStyle = "#EEEEEE"; // label background - ctx.fillRect(px-boxWidth/2, py, boxWidth, fontsize); + ctx.fillRect(px-boxWidth/2, py, boxWidth, layout.fontsize); ctx.fillStyle = "darkred"; ctx.fillText(text, px, py); } else { ctx.textBaseline = "middle"; ctx.fillStyle = stroke; - var angle = Math.atan2(s2.y - s1.y, s2.x - s1.x); - var displacement = -(layout.edgeFontsize / 10.0); + let angle = Math.atan2(s2.y - s1.y, s2.x - s1.x); + let displacement = -(layout.edgeFontsize / 10.0); if (edgeLabelsUpright && (angle > Math.PI/2 || angle < -Math.PI/2)) { displacement = layout.edgeFontsize / 3.0; angle += Math.PI; } - var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement)); + let textPos = s1.add(s2).divide(2).add(normal.multiply(displacement)); ctx.translate(textPos.x, textPos.y); ctx.rotate(angle); ctx.fillText(text, 0,-2); @@ -769,10 +778,9 @@ jQuery.fn.springy = function(params) { }, function drawNode(node, p) { - var s = toScreen(p); - var boxWidth = node.getWidth(); - var boxHeight = node.getHeight() * 1.2; - var color = typeof(node.data.color) !== 'undefined' ? node.data.color : ''; + const s = toScreen(p); + const boxWidth = node.getWidth(); + const boxHeight = node.getHeight() * 1.2; var textColor = nodeTextColor; // fill background if (layout.isSelectedNode(node.id)) { @@ -780,11 +788,11 @@ jQuery.fn.springy = function(params) { shadowOffset = activeShadowOffset; } else { shadowColor = passiveShadowColor; - shadowOffset = layout.fontsize * layout.zoomFactor; + shadowOffset = layout.realFontsize; } - if (color.length > 0) { - color1 = color; - color2 = color; + if (typeof(node.data.color) !== 'undefined') { + color1 = node.data.color; + color2 = node.data.color; } else { color1 = defaultColor1; color2 = defaultColor2; @@ -891,12 +899,12 @@ jQuery.fn.springy = function(params) { } ctx.translate(s.x, s.y); // Node Label Text - if (node.fontsize * layout.zoomFactor > 2.4) { // DS: hide tiny label + if (layout.realFontsize > 2.4) { // DS: hide tiny label ctx.textAlign = nodeTextAlign; ctx.textBaseline = nodeTextBaseline; - ctx.font = layout.nodeFont; // node.fontsize.toString() + 'px ' + nodeFont; + ctx.font = layout.nodeFont; ctx.fillStyle = textColor; - var text = typeof(node.data.label) !== 'undefined' ? node.data.label : node.id; + let text = typeof(node.data.label) !== 'undefined' ? node.data.label : node.id; ctx.fillText(text, 0, 0); } ctx.restore(); @@ -912,7 +920,7 @@ jQuery.fn.springy = function(params) { // First time seeing an image with this src address, so add it to our set of image objects // Note: we index images by their src to avoid making too many duplicates nodeImages[src] = {}; - var img = new Image(); + let img = new Image(); nodeImages[src].object = img; img.addEventListener("load", function () { // HTMLImageElement objects are very finicky about being used before they are loaded, so we set a flag when it is done @@ -930,6 +938,7 @@ jQuery.fn.springy = function(params) { canvasPos.x_offset = xform.e / layout.zoomFactor; canvasPos.y_offset = xform.f / layout.zoomFactor; canvasPos.energy = layout.energy; + canvasPos.fps = layout.fps; return canvasPos; }, function onRenderStop() { @@ -943,6 +952,17 @@ jQuery.fn.springy = function(params) { function onRenderFrame() { if (RenderFrameCall) RenderFrameCall(renderer); + }, + function zoomCanvas(factor) { + let pt = ctx.transformedPoint(canvas.width/2,canvas.height/2); + ctx.translate(pt.x,pt.y); + ctx.scale(factor,factor); + ctx.translate(-pt.x,-pt.y); + if (factor < 1) { + snap_to_canvas(); + } else { + ctx.clearRect(0,0,canvas.width,canvas.height); + } } ); From 3ad26744842e55a5fd3e5cc0a8f288ea373160ca Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Thu, 12 Dec 2019 18:12:40 +0100 Subject: [PATCH 10/16] new functions: setExciteMethod, -- set method for smart selection - none, downstream, upstream, connected propagateExcitement -- smart selection of related nodes improved functions: applyCoulombsLaw -- Boosted method 2 - loops variables are transformed into static memory array addresses --- springy.js | 115 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 8 deletions(-) diff --git a/springy.js b/springy.js index 0815dd5..e35a40b 100644 --- a/springy.js +++ b/springy.js @@ -41,7 +41,7 @@ root.Springy = factory(); } }(this, function() { - const boosted = true; // DS: 2.7fps vs 11.7fps + const boosted = typeof(Float64Array) !== 'undefined' ? 2 : 1; // DS: 0: 2.7fps vs 1: 11.7fps vs 2: 18.4fps var Springy = {}; var Graph = Springy.Graph = function() { @@ -61,8 +61,11 @@ // Data fields used by layout algorithm in this file: // this.data.mass + // this.data.insulator // Data used by default renderer in springyui.js // this.data.label + // this.data.color + // this.inside }; var Edge = Springy.Edge = function(id, source, target, data) { @@ -346,6 +349,7 @@ this.realFontsize = this.fontsize * this.zoomFactor; this.realEdgeFontsize = this.edgeFontsize * this.zoomFactor; this.selected = null; + this.exciteMethod = 'none'; // none, downstream, upstream, connected this.energy = 0; this.nodePoints = {}; // keep track of points associated with nodes this.edgeSprings = {}; // keep track of springs associated with edges @@ -356,10 +360,11 @@ Layout.ForceDirected.prototype.point = function(node) { if (!(node.id in this.nodePoints)) { var mass = (node.data.mass !== undefined) ? parseFloat(node.data.mass) : 1.0; + var insulator = (node.data.insulator !== undefined) ? node.data.insulator : false; // DS: load positions from user data var x = (node.data.x !== undefined) ? parseFloat(node.data.x) : 10.0 * (Math.random() - 0.5); var y = (node.data.y !== undefined) ? parseFloat(node.data.y) : 10.0 * (Math.random() - 0.5); - this.nodePoints[node.id] = new Layout.ForceDirected.Point(new Vector(x, y), mass); + this.nodePoints[node.id] = new Layout.ForceDirected.Point(new Vector(x, y), mass, insulator); } return this.nodePoints[node.id]; @@ -410,7 +415,6 @@ while (n--) { var rand = Math.floor(Math.random() * length); sample[rand] = t.graph.nodes[rand]; // deduplicate - //callback.call(t, rand, t.point(t.graph.nodes[rand])); } sample.forEach(function(n){ callback.call(t, n, t.point(n)); @@ -468,6 +472,10 @@ this.pinWeight = weight; }; + Layout.ForceDirected.prototype.setExciteMethod = function(exciteMethod) { + this.exciteMethod = exciteMethod; + }; + let timeslice = 200000; let loops_cnt = timeslice; let sliceTimer = null; @@ -485,7 +493,38 @@ } } // Physics stuff - Layout.ForceDirected.prototype.applyCoulombsLaw = boosted ? function() { + Layout.ForceDirected.prototype.applyCoulombsLaw = boosted === 2 ? function() { + // Boosted method 2 -- hand written assembler code - loops variables are transformed into static memory array addresses + const len = this.graph.nodes.length; + let dir = new Float64Array(8); + dir[7] = this.repulsion; + // dir[0]=dir_x; dir[1]=dir_y; dir[2]=distance; dir[3]=force + // dir[4]=p1.x; dir[5]=p1.y; dir[6]=1/p1.m; dir[7]=repulsion + for(let n=0;n Date: Thu, 12 Dec 2019 18:13:06 +0100 Subject: [PATCH 11/16] fixed bugs - drawing error on zoom out, draw edge : set mimimal visual line width. feature - new param.exciteMethod -- set method for smart selection: none, downstream, upstream, connected --- springyui.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/springyui.js b/springyui.js index 44b50dc..ee73c5f 100755 --- a/springyui.js +++ b/springyui.js @@ -51,6 +51,7 @@ jQuery.fn.springy = function(params) { const RenderStopCall = params.onRenderStop || null; const RenderStartCall = params.onRenderStart || null; const pinWeight = params.pinWeight || 1000.0; + const exciteMethod = params.exciteMethod || 'none'; var nodeImages = {}; const edgeLabelsUpright = true; const edgeLabelBoxes = params.edgeLabelBoxes || false; @@ -122,7 +123,7 @@ jQuery.fn.springy = function(params) { var set_1colors = function() { ctx.fillStyle = color1; ctx.shadowColor = shadowColor; - ctx.shadowBlur = layout.fontsize*layout.zoomFactor; + ctx.shadowBlur = layout.fontsize*layout.zoomFactor*(shadowOffset===0 ? 2 : 1); ctx.shadowOffsetX = shadowOffset; ctx.shadowOffsetY = shadowOffset; }; @@ -395,6 +396,8 @@ jQuery.fn.springy = function(params) { if (layout.selected.inside) { // DS 13.Oct 2019 : fixate or just move selected node depending on pinWeight dragged.point.m = layout.pinWeight; + layout.propagateExcitement(); + if (nodeSelected) { nodeSelected(layout.selected.node); } @@ -698,7 +701,7 @@ jQuery.fn.springy = function(params) { let stroke = (edge.data.color !== undefined) ? edge.data.color : '#000000'; let weight = (edge.data.weight !== undefined) ? edge.data.weight : 1.0; - weight = weight * (layout.fontsize/8); + weight *= Math.max(layout.fontsize/8, 1/layout.zoomFactor*0.3333); if (layout.isSelectedEdge(edge)) { // highlight edges of the selected node weight = weight * 2; @@ -783,7 +786,7 @@ jQuery.fn.springy = function(params) { const boxHeight = node.getHeight() * 1.2; var textColor = nodeTextColor; // fill background - if (layout.isSelectedNode(node.id)) { + if (layout.isSelectedNode(node.id) || layout.isExcitedNode(node.id)) { shadowColor = activeShadowColor; shadowOffset = activeShadowOffset; } else { @@ -959,13 +962,12 @@ jQuery.fn.springy = function(params) { ctx.scale(factor,factor); ctx.translate(-pt.x,-pt.y); if (factor < 1) { - snap_to_canvas(); - } else { ctx.clearRect(0,0,canvas.width,canvas.height); + snap_to_canvas(); } } ); - + renderer.setExciteMethod(exciteMethod); renderer.start(true); // helpers for figuring out where to draw arrows From 0c7f8fa9d73eb3b2f8dc97acc9c5ac2547fa8b61 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Mon, 16 Dec 2019 02:14:18 +0100 Subject: [PATCH 12/16] springy.js - new function setParameter - added timeout after each frame for Safari to keep the browser responsive (when ui.js is included) springyui.js - inproved look for arrowheads, righttriangle and lefttriangle - default maxspeed is now 100 --- springy.js | 24 +++++++++++++++++++++++- springyui.js | 8 ++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/springy.js b/springy.js index e35a40b..594a7bb 100644 --- a/springy.js +++ b/springy.js @@ -337,7 +337,7 @@ this.repulsion = repulsion; // repulsion constant this.damping = damping; // velocity damping factor this.minEnergyThreshold = minEnergyThreshold || 0.01; //threshold used to determine render stop - this.maxSpeed = maxSpeed || Infinity; // nodes aren't allowed to exceed this speed + this.maxSpeed = maxSpeed || 100.0; // nodes aren't allowed to exceed this speed this.fontsize = fontsize || 8.0; this.edgeFontsize = this.fontsize * 9 / 10; this.fontname = fontname || "Verdana, sans-serif"; @@ -476,6 +476,19 @@ this.exciteMethod = exciteMethod; }; + Layout.ForceDirected.prototype.setParameter = function(param) { + const t = this; + t.stiffness = param.stiffness || t.stiffness; // spring stiffness constant + t.repulsion = param.repulsion || t.repulsion; // repulsion constant + t.damping = param.damping || t.damping; // velocity damping factor + t.minEnergyThreshold = param.minEnergyThreshold || t.minEnergyThreshold; //threshold used to determine render stop + t.maxSpeed = param.maxSpeed || t.maxSpeed; // nodes aren't allowed to exceed this speed + const len = this.graph.edges.length; + for(let n=0;n Date: Mon, 6 Jan 2020 20:39:49 +0100 Subject: [PATCH 13/16] improved display for selected node. --- springy.js | 20 +++++++++++++------- springyui.js | 12 ++++++++---- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/springy.js b/springy.js index 594a7bb..d6d81b3 100644 --- a/springy.js +++ b/springy.js @@ -546,7 +546,7 @@ const point2 = this.nodePoints[m]; dir.x = point1.p.x - point2.p.x; // subtract dir.y = point1.p.y - point2.p.y; - const distance = Math.sqrt(dir.x*dir.x+dir.y*dir.y) + 0.3; // magnitude + const distance = Math.sqrt(dir.x*dir.x+dir.y*dir.y) + 0.1; // magnitude dir.x /= distance; // normalise dir.y /= distance; const force = this.repulsion / (distance * distance * 0.5); @@ -566,7 +566,7 @@ if (point1 !== point2) { var d = point1.p.subtract(point2.p); - var distance = d.magnitude() + 0.3; // DS 0.1 is too small: avoid massive forces at small distances (and divide by zero) + var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero) var direction = d.normalise(); // apply force to each end point @@ -626,23 +626,23 @@ if (method === 'none') return; let method_fn = method === 'downstream' ? function(spring){ - if (spring.point1.e && ! spring.point2.e && ! spring.point1.i) { + if (spring.point1.e && ! spring.point2.e && (! spring.point1.i || spring.point1 === t.selected.point)) { spring.point2.e = true; cnt++; } } : method === 'upstream' ? function(spring){ - if (spring.point2.e && ! spring.point1.e && ! spring.point2.i) { + if (spring.point2.e && ! spring.point1.e && (! spring.point2.i || spring.point2 === t.selected.point)) { spring.point1.e = true; cnt++; } } : method === 'connected' ? function(spring){ - if (spring.point1.e && ! spring.point2.e && ! spring.point1.i) { + if (spring.point1.e && ! spring.point2.e) { spring.point2.e = true; cnt++; } - if (spring.point2.e && ! spring.point1.e && ! spring.point2.i) { + if (spring.point2.e && ! spring.point1.e) { spring.point1.e = true; cnt++; } @@ -692,7 +692,7 @@ Layout.ForceDirected.prototype.getNodePositions = function() { var nodes_array = []; this.eachNode(function(node, point) { - var element = {id:node.data.name, x:point.p.x, y:point.p.y, mass:point.m}; + var element = {id:node.data.name, x:point.p.x, y:point.p.y, mass:point.m, active:point.e}; nodes_array.push(element); }); return nodes_array; @@ -959,6 +959,12 @@ this.propagateExcitement(method); }; + Renderer.prototype.selectNode = function(name) { + this.layout.selectNode(name); + this.layout.propagateExcitement(); + this.start(true); + }; + Renderer.prototype.getNodePositions = function(e) { return JSON.stringify(this.layout.getNodePositions()); }; diff --git a/springyui.js b/springyui.js index 95a963a..ea70bb9 100755 --- a/springyui.js +++ b/springyui.js @@ -33,6 +33,7 @@ jQuery.fn.springy = function(params) { const nodeTextColor = 'Black'; const nodeTextAlign = "center"; const nodeTextBaseline = "middle"; + const selectedShadowColor = 'Aqua'; const activeShadowColor = 'DarkOrange'; const activeShadowOffset = 0; const passiveShadowColor = "rgba(50, 50, 50, 0.3)"; @@ -56,7 +57,7 @@ jQuery.fn.springy = function(params) { const edgeLabelsUpright = true; const edgeLabelBoxes = params.edgeLabelBoxes || false; const useGradient = params.useGradient || false; - var fontsize = params.fontsize * 1.0 || Math.max(12.0 - Math.sqrt(graph.nodes.length) / 3.0, 0.5); + var fontsize = params.fontsize * 1.0 || Math.max(10.0 - Math.sqrt(graph.nodes.length) / 3.0, 0.5); var zoomFactor = params.zoomFactor * 1.0 || 1.0; zoomFactor = Math.max(Math.min(zoomFactor, 12), 1); fontsize = Math.max(Math.min(fontsize, 30), 1); @@ -580,7 +581,7 @@ jQuery.fn.springy = function(params) { var getTextWidth = boosted ? function(node) { if (node._width && node.fontsize === layout.fontsize) return node._width; - var text = (node.data.label !== undefined) ? node.data.label : node.id; + var text = (node.data.label !== undefined && node.data.label) ? node.data.label : node.id; ctx.save(); ctx.font = layout.nodeFont; node._width = ctx.measureText(text).width; @@ -672,7 +673,7 @@ jQuery.fn.springy = function(params) { } //change default to 10.0 to allow text fit between edges - const spacing = 12.0; + const spacing = layout.fontsize; // Figure out how far off center the line should be drawn const offset = normal.multiply(-((total - 1) * spacing)/2.0 + (n * spacing)); @@ -786,7 +787,10 @@ jQuery.fn.springy = function(params) { const boxHeight = node.getHeight() * 1.2; var textColor = nodeTextColor; // fill background - if (layout.isSelectedNode(node.id) || layout.isExcitedNode(node.id)) { + if (layout.isSelectedNode(node.id)) { + shadowColor = selectedShadowColor; + shadowOffset = activeShadowOffset; + } else if (layout.isExcitedNode(node.id)) { shadowColor = activeShadowColor; shadowOffset = activeShadowOffset; } else { From 1adc754beee7fee7a594ff12c284f33a3a8790f5 Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Fri, 17 Jan 2020 17:31:46 +0100 Subject: [PATCH 14/16] improved display for selected node. --- springy.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/springy.js b/springy.js index d6d81b3..0199839 100644 --- a/springy.js +++ b/springy.js @@ -623,7 +623,12 @@ this.eachNode(function(node, point) { point.e = false; }); - if (method === 'none') return; + if (method === 'none') { + if (t.selected !== null && t.selected.node !== null) { + t.selected.point.e = true; // set selected node Excitement + } + return; + } let method_fn = method === 'downstream' ? function(spring){ if (spring.point1.e && ! spring.point2.e && (! spring.point1.i || spring.point1 === t.selected.point)) { From 7c518c395cdb86c262bd078ef8e18ec6c1d16a8e Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Sat, 22 Feb 2020 21:35:37 +0100 Subject: [PATCH 15/16] multiple diagrams per window. Support for zoom and fontsize new function optimizeMass when nodes have many edges the calculation of forces causes shaking and jumping of the nodes. by adding a counter mass equal to the number of edges can stop that errors. new function focusSelected zoom and move the canvas to the selected node. support for touchpad select node move node move canvas support for generic double click handler --- springy.js | 82 +++++++++++++++++------ springyui.js | 181 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 180 insertions(+), 83 deletions(-) diff --git a/springy.js b/springy.js index 0199839..49a9474 100644 --- a/springy.js +++ b/springy.js @@ -331,8 +331,9 @@ // ----------- var Layout = Springy.Layout = {}; - Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor, pinWeight) { + Layout.ForceDirected = function(graph, ctx, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed, fontsize, fontname, zoomFactor, pinWeight) { this.graph = graph; + this.ctx = ctx; this.stiffness = stiffness; // spring stiffness constant this.repulsion = repulsion; // repulsion constant this.damping = damping; // velocity damping factor @@ -483,10 +484,22 @@ t.damping = param.damping || t.damping; // velocity damping factor t.minEnergyThreshold = param.minEnergyThreshold || t.minEnergyThreshold; //threshold used to determine render stop t.maxSpeed = param.maxSpeed || t.maxSpeed; // nodes aren't allowed to exceed this speed - const len = this.graph.edges.length; - for(let n=0;n 0 && xoffset + diffx > 0) { + diffx = 0; + } + // 0 limit right: + if (diffx < 0 && (xoffset + diffx + xsize) < canvas.width) { + diffx = 0; + } + // 0 limit top: + if (diffy > 0 && yoffset+diffy > 0) { + diffy = 0; + } + // 0 limit bottom: + if (diffy < 0 && (yoffset + diffy + ysize) < canvas.height) { + diffy = 0; + } + ctx.translate(diffx, diffy); + snap_to_canvas(); + } + + var mousemove = function(canvas, pageX, pageY, offsetX, offsetY){ + var pos = $(canvas).offset(); + var p1 = ctx.transformedPoint(pageX - pos.left, pageY - pos.top); var p = fromScreen(p1); nearest = layout.nearest(p); mouse_inside_node(nearest, p); @@ -434,39 +473,38 @@ jQuery.fn.springy = function(params) { dragged.point.p.x = p.x; dragged.point.p.y = p.y; } else { - lastX = e.offsetX || (e.pageX - pos.left); - lastY = e.offsetY || (e.pageY - pos.top); + lastX = offsetX || (pageX - pos.left); + lastY = offsetY || (pageY - pos.top); canvas_dragged = true; if (dragStart !== null){ - var pt = ctx.transformedPoint(lastX,lastY); - var diffx = pt.x-dragStart.x; - var diffy = pt.y-dragStart.y; - var xform = ctx.getTransform(); - var xsize = canvas.width * xform.a; - var ysize = canvas.height * xform.a; - var xoffset = xform.e; - var yoffset = xform.f; - // 0 limit left: - if (diffx > 0 && xoffset + diffx > 0) { - diffx = 0; - } - // 0 limit right: - if (diffx < 0 && (xoffset + diffx + xsize) < canvas.width) { - diffx = 0; - } - // 0 limit top: - if (diffy > 0 && yoffset+diffy > 0) { - diffy = 0; - } - // 0 limit bottom: - if (diffy < 0 && (yoffset + diffy + ysize) < canvas.height) { - diffy = 0; - } - ctx.translate(diffx, diffy); - snap_to_canvas(); + moveViewport (canvas, dragStart.x, dragStart.y, lastX, lastY); } } renderer.start(dragged !== null && dragged.inside); + }; + + jQuery(canvas).mousemove(function(e) { + mousemove(this, e.pageX, e.pageY, e.offsetX, e.offsetY); + }); + + jQuery(canvas).on('touchstart', function(e){ + let t = e.changedTouches[0]; // erster Finger + mousedown(this, t.pageX, t.pageY, t.offsetX, t.offsetY); + e.preventDefault(); + }); + + jQuery(canvas).on('touchmove', function(e){ + let t = e.changedTouches[0]; // erster Finger + mousemove(this, t.pageX, t.pageY, t.offsetX, t.offsetY); + e.preventDefault(); + }); + + jQuery(canvas).on('touchend', function(e){ + nearest = null; + dragged = null; + dragStart = null; + renderer.start(true); + e.preventDefault(); }); jQuery(canvas).mouseleave(function(e) { @@ -578,7 +616,8 @@ jQuery.fn.springy = function(params) { } } - var getTextWidth = boosted ? function(node) { + var getTextWidth = boosted ? function(node, layout) { + var ctx = layout.ctx; if (node._width && node.fontsize === layout.fontsize) return node._width; var text = (node.data.label !== undefined && node.data.label) ? node.data.label : node.id; @@ -589,7 +628,8 @@ jQuery.fn.springy = function(params) { ctx.restore(); return node._width; } : - function(node) { + function(node, layout) { + var ctx = layout.ctx; var text = (node.data.label !== undefined) ? node.data.label : node.id; if (node._width && node.fontsize === layout.fontsize && node._width[text]) return node._width[text]; @@ -606,7 +646,7 @@ jQuery.fn.springy = function(params) { return width; }; - var getTextHeight = function(node) { + var getTextHeight = function(node, layout) { return layout.fontsize; // In a more modular world, this would actually read the font size, but I think leaving it a constant is sufficient for now. // If you change the font size, I'd adjust this too. @@ -622,10 +662,10 @@ jQuery.fn.springy = function(params) { return height; } - Springy.Node.prototype.getHeight = function() { + Springy.Node.prototype.getHeight = function(layout) { var height; if (this.data.image == undefined) { - height = getTextHeight(this); + height = layout.fontsize; } else { if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) { height = getImageHeight(this); @@ -634,10 +674,10 @@ jQuery.fn.springy = function(params) { return height; } - Springy.Node.prototype.getWidth = function() { + Springy.Node.prototype.getWidth = function(layout) { var width; if (this.data.image == undefined) { - width = getTextWidth(this); + width = getTextWidth(this, layout); } else { if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) { width = getImageWidth(this); @@ -681,8 +721,8 @@ jQuery.fn.springy = function(params) { const s1 = toScreen(p1).add(offset); const s2 = toScreen(p2).add(offset); - let boxWidth = edge.target.getWidth() * 1.2; - let boxHeight = edge.target.getHeight() * 2.0; // extra space for target polygons + let boxWidth = edge.target.getWidth(layout) * 1.2; + let boxHeight = edge.target.getHeight(layout) * 2.0; // extra space for target polygons let intersection = intersect_line_box(s1, s2, {x: x2-boxWidth/2.0, y: y2-boxHeight/2.0}, boxWidth, boxHeight); @@ -690,8 +730,8 @@ jQuery.fn.springy = function(params) { intersection = s2; } - boxWidth = edge.source.getWidth() * 1.2; - boxHeight = edge.source.getHeight() * 2.0; // extra space for source polygons + boxWidth = edge.source.getWidth(layout) * 1.2; + boxHeight = edge.source.getHeight(layout) * 2.0; // extra space for source polygons // DS: respect source node! let lineStart = intersect_line_box(s1, s2, {x: x1-boxWidth/2.0, y: y1-boxHeight/2.0}, boxWidth, boxHeight); @@ -783,8 +823,8 @@ jQuery.fn.springy = function(params) { }, function drawNode(node, p) { const s = toScreen(p); - const boxWidth = node.getWidth(); - const boxHeight = node.getHeight() * 1.2; + const boxWidth = node.getWidth(layout); + const boxHeight = node.getHeight(layout) * 1.2; var textColor = nodeTextColor; // fill background if (layout.isSelectedNode(node.id)) { @@ -881,7 +921,7 @@ jQuery.fn.springy = function(params) { break; case 'doubleoctagon': polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true, true); - polygon(s, boxWidth, boxHeight, 8, true); + polygon(s, boxWidth, boxHeight, 8, true, false); break; case 'tripleoctagon': polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true, true); @@ -969,9 +1009,24 @@ jQuery.fn.springy = function(params) { ctx.clearRect(0,0,canvas.width,canvas.height); snap_to_canvas(); } + }, + function moveCanvas(p) { + // center view port over selected node + var pos = toScreen(p); // selected node point + var zoomFactor = layout.zoomFactor; + //console.log('moveCanvas - pos :'+(pos.x * zoomFactor)+', '+(pos.y * zoomFactor)); + //console.log('moveCanvas - canvas/2 :'+(canvas.width/2)+', '+(canvas.height/2) ); + var xform = ctx.getTransform(); + //console.log('moveCanvas - offset :'+xform.e+', '+xform.f+', factor '+layout.zoomFactor); + let diffx = -((pos.x * zoomFactor) - (canvas.width/2) + xform.e)/layout.zoomFactor; + let diffy = -((pos.y * zoomFactor) - (canvas.height/2) + xform.f)/layout.zoomFactor; + //console.log('moveCanvas - diff :'+diffx+', '+diffy); + ctx.translate(diffx, diffy); + snap_to_canvas(); } ); renderer.setExciteMethod(exciteMethod); + renderer.optimizeMass(1); renderer.start(true); // helpers for figuring out where to draw arrows From f43240ddb0086b4a6cab55b0c2eb6e910312b53a Mon Sep 17 00:00:00 2001 From: Dirk Strack Date: Wed, 4 Mar 2020 01:37:59 +0100 Subject: [PATCH 16/16] support for zoom gesture and double tap --- springyui.js | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/springyui.js b/springyui.js index 15860d0..e40af96 100755 --- a/springyui.js +++ b/springyui.js @@ -316,7 +316,6 @@ jQuery.fn.springy = function(params) { }; var star = function(pos, width, height){ - // var dim = width / 2 + height / 3 * 2; var dim = height * 4 / 3; var pi5 = Math.PI / 5; ctx.save(); @@ -346,6 +345,7 @@ jQuery.fn.springy = function(params) { var lastX=canvas.width/2, lastY=canvas.height/2; var dragStart = null; var canvas_dragged; + var tapedTwice = false; var mouse_inside_node = function(item, mp) { if (item !== null && item.node !== null && typeof(item.inside) == 'undefined') { @@ -418,9 +418,9 @@ jQuery.fn.springy = function(params) { }); // Basic double click handler - jQuery(canvas).dblclick(function(e) { - var pos = jQuery(this).offset(); - var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top}); + var doubleclick = function(canvas, pageX, pageY){ + var pos = jQuery(canvas).offset(); + var p = fromScreen({x: pageX - pos.left, y: pageY - pos.top}); if (layout.selected && layout.selected.inside) { var node = layout.selected.node; if (node && node.data) { @@ -432,6 +432,10 @@ jQuery.fn.springy = function(params) { } } } + }; + + jQuery(canvas).dblclick(function(e) { + doubleclick(this, e.pageX, e.pageY); }); var moveViewport = function(canvas, startx, starty, lastX, lastY) { @@ -488,8 +492,15 @@ jQuery.fn.springy = function(params) { }); jQuery(canvas).on('touchstart', function(e){ - let t = e.changedTouches[0]; // erster Finger - mousedown(this, t.pageX, t.pageY, t.offsetX, t.offsetY); + let t = e.changedTouches[0]; // first finger + if (!tapedTwice) { + tapedTwice = true; + setTimeout( function() { tapedTwice = false; }, 300 ); + + mousedown(this, t.pageX, t.pageY, t.offsetX, t.offsetY); + } else { + doubleclick(this, t.pageX, t.pageY); + } e.preventDefault(); }); @@ -556,6 +567,16 @@ jQuery.fn.springy = function(params) { canvas.addEventListener('DOMMouseScroll',handleScroll,false); canvas.addEventListener('mousewheel',handleScroll,false); + + var handleGesture = function(evt){ + var delta = evt.scale ? evt.scale / 20 : 0; + if (delta) zoom(delta); + return evt.preventDefault() && false; + }; + + canvas.addEventListener('gesturestart',handleGesture,false); + canvas.addEventListener('gesturechange',handleGesture,false); + canvas.addEventListener('gestureend',handleGesture,false); // ------------------------------------------------- // Adds ctx.getTransform() - returns an SVGMatrix @@ -920,13 +941,13 @@ jQuery.fn.springy = function(params) { polygon(s, boxWidth, boxHeight, 8, true, true); break; case 'doubleoctagon': - polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true, true); - polygon(s, boxWidth, boxHeight, 8, true, false); + polygon(s, boxWidth*0.91, boxHeight*1.2, 8, true, true); + polygon(s, boxWidth*0.9, boxHeight, 8, true, false); break; case 'tripleoctagon': - polygon(s, boxWidth*1.1, boxHeight*1.2, 8, true, true); + polygon(s, boxWidth*1.05, boxHeight*1.2, 8, true, true); polygon(s, boxWidth, boxHeight, 8, true, false); - polygon(s, boxWidth*0.9, boxHeight*0.8, 8, true, false); + polygon(s, boxWidth*0.97, boxHeight*0.8, 8, true, false); break; case 'star': textColor = 'DarkGray'; @@ -1022,6 +1043,7 @@ jQuery.fn.springy = function(params) { let diffy = -((pos.y * zoomFactor) - (canvas.height/2) + xform.f)/layout.zoomFactor; //console.log('moveCanvas - diff :'+diffx+', '+diffy); ctx.translate(diffx, diffy); + ctx.clearRect(0,0,canvas.width,canvas.height); snap_to_canvas(); } );