From 020ab2aef5df58e41dbc98de6d2ee9307bdd9f19 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 10 Apr 2015 13:50:39 +0200 Subject: [PATCH] forcegraph: add labels --- lib/forcegraph.js | 200 +++++++++++++++++++++++++++++------------- scss/_forcegraph.scss | 15 ++-- 2 files changed, 149 insertions(+), 66 deletions(-) diff --git a/lib/forcegraph.js b/lib/forcegraph.js index 0a97e44..8a825b7 100644 --- a/lib/forcegraph.js +++ b/lib/forcegraph.js @@ -1,7 +1,7 @@ define(["d3"], function (d3) { return function (config, linkScale, sidebar, router) { var self = this - var svg, vis, link, node + var svg, vis, link, node, label var nodesDict, linksDict var zoomBehavior var force @@ -14,7 +14,7 @@ define(["d3"], function (d3) { var LINK_DISTANCE = 70 function graphDiameter(nodes) { - return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE + return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE * 1.41 } function savePositions() { @@ -51,6 +51,11 @@ define(["d3"], function (d3) { d.fixed &= 1 } + var draggableNode = d3.behavior.drag() + .on("dragstart", dragstart) + .on("drag", dragmove) + .on("dragend", dragend) + function animatePanzoom(translate, scale) { zoomBehavior.scale(scale) zoomBehavior.translate(translate) @@ -141,6 +146,112 @@ define(["d3"], function (d3) { panzoomTo([0, 0], force.size()) } + function updateLinks(vis, data) { + var link = vis.selectAll("g.link") + .data(data, function (d) { return d.o.id }) + + link.exit().remove() + + var linkEnter = link.enter().append("g") + .attr("class", "link") + .on("click", function (d) { + if (!d3.event.defaultPrevented) + router.link(d.o)() + }) + + linkEnter.append("line") + .append("title") + + link.selectAll("line") + .style("stroke", function (d) { return linkScale(d.o.tq).hex() }) + + link.selectAll("title").text(function (d) { return showTq(d.o) }) + + return link + } + + function updateNodes(vis, data) { + var node = vis.selectAll(".node") + .data(data, function(d) { return d.o.id }) + + node.exit().remove() + + node.enter().append("circle") + .attr("r", 8) + .on("click", function (d) { + if (!d3.event.defaultPrevented) + router.node(d.o.node)() + }) + .call(draggableNode) + + node.attr("class", function (d) { + var s = ["node"] + if (!d.o.node) + s.push("unknown") + + return s.join(" ") + }) + + return node + } + + function updateLabels(vis, data) { + var label = vis.selectAll("text") + .data(data, function(d) { return d.o.id }) + + label.exit().remove() + + var labelEnter = label.enter().append("text") + + label.text(nodeName) + .each(function (d) { + var bbox = this.getBBox() + d.labelHeight = bbox.height + d.labelWidth = bbox.width + }) + + labelEnter.each(function (d) { + d.labelAngle = Math.PI / 2 + }) + + return label + } + + function positionLabels() { + label.attr("transform", function(d) { + var neighbours = d.neighbours.map(function (n) { + var dx = n.x - d.x + var dy = n.y - d.y + + return (2 * Math.PI + Math.atan2(dy, dx)) % (2 * Math.PI) + }) + + var sumCos = neighbours.reduce(function (a, b) { + return a + Math.cos(b) + }, 0) + + var sumSin = neighbours.reduce(function (a, b) { + return a + Math.sin(b) + }, 0) + + if (neighbours.length > 0) + d.labelAngle = Math.PI + Math.atan2(sumSin, sumCos) + + var offset = 10 + + var a = offset + d.labelWidth / 2 + var b = offset + d.labelHeight / 2 + + var cos = Math.cos(d.labelAngle) + var sin = Math.sin(d.labelAngle) + + var x = d.x + a * Math.pow(Math.abs(cos), 2 / 5) * Math.sign(cos) + var y = d.y + b * Math.pow(Math.abs(sin), 2 / 5) * Math.sign(sin) + + return "translate(" + x + "," + y + ")" + }) + } + function tickEvent() { link.selectAll("line") .attr("x1", function(d) { return d.source.x }) @@ -148,9 +259,9 @@ define(["d3"], function (d3) { .attr("x2", function(d) { return d.target.x }) .attr("y2", function(d) { return d.target.y }) - node - .attr("cx", function(d) { return d.x }) - .attr("cy", function(d) { return d.y }) + node.attr("cx", function (d) { return d.x }) + .attr("cy", function (d) { return d.y }) + positionLabels() } el = document.createElement("div") @@ -168,26 +279,23 @@ define(["d3"], function (d3) { vis = svg.append("g") - vis.append("g").attr("class", "links") - vis.append("g").attr("class", "nodes") + var visLinks = vis.append("g").attr("class", "links") + var visLabels = vis.append("g").attr("class", "labels") + var visNodes = vis.append("g").attr("class", "nodes") force = d3.layout.force() - .charge(-70) - .gravity(0.05) + .charge(-80) + .gravity(0.01) + .chargeDistance(8 * LINK_DISTANCE) .linkDistance(LINK_DISTANCE) .linkStrength(function (d) { - return 1 / d.o.tq + return Math.max(0.5, 1 / d.o.tq) }) .on("tick", tickEvent) .on("end", savePositions) panzoom() - var draggableNode = d3.behavior.drag() - .on("dragstart", dragstart) - .on("drag", dragmove) - .on("dragend", dragend) - self.setData = function (data) { var oldNodes = {} @@ -235,26 +343,24 @@ define(["d3"], function (d3) { return e }) - link = vis.select("g.links") - .selectAll("g.link") - .data(intLinks, function (d) { return d.o.id }) + intNodes.forEach(function (d) { + d.neighbours = {} + }) - link.exit().remove() + intLinks.forEach(function (d) { + d.source.neighbours[d.target.o.id] = d.target + d.target.neighbours[d.source.o.id] = d.source + }) - var linkEnter = link.enter().append("g") - .attr("class", "link") - .on("click", function (d) { - if (!d3.event.defaultPrevented) - router.link(d.o)() - }) + intNodes.forEach(function (d) { + d.neighbours = Object.keys(d.neighbours).map(function (k) { + return d.neighbours[k] + }) + }) - linkEnter.append("line") - .append("title") - - link.selectAll("line") - .style("stroke", function (d) { return linkScale(d.o.tq).hex() }) - - link.selectAll("title").text(function (d) { return showTq(d.o) }) + link = updateLinks(visLinks, intLinks) + node = updateNodes(visNodes, intNodes) + label = updateLabels(visLabels, intNodes) linksDict = {} @@ -263,28 +369,6 @@ define(["d3"], function (d3) { linksDict[d.o.id] = d }) - node = vis.select("g.nodes") - .selectAll(".node") - .data(intNodes, function(d) { return d.o.id }) - - node.exit().remove() - - var nodeEnter = node.enter().append("circle") - .attr("r", 8) - .on("click", function (d) { - if (!d3.event.defaultPrevented) - router.node(d.o.node)() - }) - .call(draggableNode) - - node.attr("class", function (d) { - var s = ["node"] - if (!d.o.node) - s.push("unknown") - - return s.join(" ") - }) - nodesDict = {} node.each( function (d) { @@ -292,8 +376,6 @@ define(["d3"], function (d3) { nodesDict[d.o.node.nodeinfo.node_id] = d }) - nodeEnter.append("title") - if (localStorageTest()) { var save = JSON.parse(localStorage.getItem("graph/nodeposition")) @@ -303,8 +385,8 @@ define(["d3"], function (d3) { nodePositions[d.id] = d }) - nodeEnter.each( function (d) { - if (nodePositions[d.o.id]) { + node.each( function (d) { + if (nodePositions[d.o.id] && (d.x === undefined || d.y === undefined)) { d.x = nodePositions[d.o.id].x d.y = nodePositions[d.o.id].y } @@ -312,8 +394,6 @@ define(["d3"], function (d3) { } } - node.selectAll("title").text(nodeName) - var diameter = graphDiameter(intNodes) force.nodes(intNodes) diff --git a/scss/_forcegraph.scss b/scss/_forcegraph.scss index f8f94a4..54a6555 100644 --- a/scss/_forcegraph.scss +++ b/scss/_forcegraph.scss @@ -45,13 +45,16 @@ } } - .label { + .labels { text { - font-size: 0.8em; - } - - rect { - fill: rgba(255, 255, 255, 0.8); + font-size: 7pt; + font-family: Roboto; + alignment-baseline: central; + text-anchor: middle; + fill: rgba(0, 0, 0, 0.6); + paint-order: stroke; + stroke-width: 3pt; + stroke: rgba(255, 255, 255, 0.5); } } }