diff --git a/lib/forcegraph.js b/lib/forcegraph.js index 9287122..aa11d59 100644 --- a/lib/forcegraph.js +++ b/lib/forcegraph.js @@ -1,10 +1,11 @@ define(["d3"], function (d3) { var margin = 200 + var NODE_RADIUS = 15 + var LINE_RADIUS = 12 return function (config, linkScale, sidebar, router) { var self = this - var svg, canvas, ctx, screenRect - var svgNodes, svgLinks + var canvas, ctx, screenRect var nodesDict, linksDict var zoomBehavior var force @@ -18,6 +19,8 @@ define(["d3"], function (d3) { var nodes = [] var unknownNodes = [] + var draggedNode + var LINK_DISTANCE = 70 function graphDiameter(nodes) { @@ -42,20 +45,39 @@ define(["d3"], function (d3) { return d.o.id } - function dragstart(d) { + function dragstart() { + var e = translateXY(d3.mouse(el)) + + var nodes = intNodes.filter(function (d) { + return distancePoint(e, d) < NODE_RADIUS + }) + + if (nodes.length === 0) + return + + draggedNode = nodes[0] d3.event.sourceEvent.stopPropagation() - d.fixed |= 2 + d3.event.sourceEvent.preventDefault() + draggedNode.fixed |= 2 } - function dragmove(d) { - d.px = d3.event.x - d.py = d3.event.y - force.resume() + function dragmove() { + if (draggedNode) { + var e = translateXY(d3.mouse(el)) + + draggedNode.px = e.x + draggedNode.py = e.y + force.resume() + } } - function dragend(d) { - d3.event.sourceEvent.stopPropagation() - d.fixed &= 1 + function dragend() { + if (draggedNode) { + d3.event.sourceEvent.stopPropagation() + d3.event.sourceEvent.preventDefault() + draggedNode.fixed &= 1 + draggedNode = undefined + } } var draggableNode = d3.behavior.drag() @@ -112,9 +134,6 @@ define(["d3"], function (d3) { right: (canvas.width - translate[0]) / scale, bottom: (canvas.height - translate[1]) / scale} - svg.attr("transform", "translate(" + translate + ") " + - "scale(" + scale + ")") - redraw() } @@ -183,38 +202,6 @@ define(["d3"], function (d3) { panzoomTo([0, 0], force.size()) } - function updateLinks(vis, data) { - var link = vis.selectAll("line") - .data(data, function (d) { return d.o.id }) - - link.exit().remove() - - link.enter().append("line") - .on("click", function (d) { - if (!d3.event.defaultPrevented) - router.link(d.o)() - }) - - return link - } - - function updateNodes(vis, data) { - var node = vis.selectAll("circle") - .data(data, function(d) { return d.o.id }) - - node.exit().remove() - - node.enter().append("circle") - .attr("r", 12) - .on("click", function (d) { - if (!d3.event.defaultPrevented) - router.node(d.o.node)() - }) - .call(draggableNode) - - return node - } - function drawLabel(d) { var sum = d.neighbours.reduce(function (a, b) { return [a[0] + b.x, a[1] + b.y] @@ -354,14 +341,6 @@ define(["d3"], function (d3) { function tickEvent() { redraw() - - svgLinks.attr("x1", function(d) { return d.source.x }) - .attr("y1", function(d) { return d.source.y }) - .attr("x2", function(d) { return d.target.x }) - .attr("y2", function(d) { return d.target.y }) - - svgNodes.attr("cx", function (d) { return d.x }) - .attr("cy", function (d) { return d.y }) } function resizeCanvas() { @@ -375,6 +354,68 @@ define(["d3"], function (d3) { redraw() } + function distance(a, b) { + return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2) + } + + function distancePoint(a, b) { + return Math.sqrt(distance(a, b)) + } + + function distanceLink(p, a, b) { + /* http://stackoverflow.com/questions/849211 */ + + var l2 = distance(a, b) + + if (l2 === 0) + return distance(p, a) + + var t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2 + + if (t < 0) + return distance(p, a) + + if (t > 1) + return distance(p, b) + + return Math.sqrt(distance(p, { x: a.x + t * (b.x - a.x), + y: a.y + t * (b.y - a.y) })) + } + + function translateXY(d) { + var translate = zoomBehavior.translate() + var scale = zoomBehavior.scale() + + return {x: (d[0] - translate[0]) / scale, + y: (d[1] - translate[1]) / scale + } + } + + function onClick() { + if (d3.event.defaultPrevented) + return + + var e = translateXY(d3.mouse(el)) + + var nodes = intNodes.filter(function (d) { + return distancePoint(e, d) < NODE_RADIUS + }) + + if (nodes.length > 0) { + router.node(nodes[0].o.node)() + return + } + + var links = intLinks.filter(function (d) { + return distanceLink(e, d.source, d.target) < LINE_RADIUS + }) + + if (links.length > 0) { + router.link(links[0].o)() + return + } + } + el = document.createElement("div") el.classList.add("graph") self.div = el @@ -384,15 +425,13 @@ define(["d3"], function (d3) { .on("zoom", panzoom) .translate([sidebar.getWidth(), 0]) - canvas = d3.select(el).append("canvas").node() - - svg = d3.select(el).append("svg") - .attr("pointer-events", "all") - .call(zoomBehavior) - .append("g") - - var visLinks = svg.append("g") - var visNodes = svg.append("g") + canvas = d3.select(el) + .call(zoomBehavior) + .append("canvas") + .attr("pointer-events", "all") + .on("click", onClick) + .call(draggableNode) + .node() ctx = canvas.getContext("2d") @@ -512,9 +551,6 @@ define(["d3"], function (d3) { }) }) - svgLinks = updateLinks(visLinks, intLinks) - svgNodes = updateNodes(visNodes, intNodes) - nodes = intNodes.filter(function (d) { return d.o.node }) unknownNodes = intNodes.filter(function (d) { return !d.o.node }) @@ -570,7 +606,6 @@ define(["d3"], function (d3) { force.stop() canvas.remove() force = null - svg = null } return self