Change d3 force layout link style to match d3-tree look - d3.js

I am undertaking yet another attempt to draw a family tree with d3. For this, I would like to use the usual node-link graph (like this one):
But with a link style like that usually found in d3 trees, i.e. be the Bezier curves with horizontal (or vertical) ends:
Is it possible to change the links accordingly, without diving into the d3-force code?

If you are just looking to match the style of the links, no need to dive into the d3-force code, it only calculates position, not anything related to styling.
Each link has a x and y values for both the source and the target. If you replace the line that is found linking source and target in most force layout examples with a path, you can use these x and y values to pretty much style any type of link you want.
I'm using d3v4+ below - your examples use d3v3.
Option 1 - Use the Built In Links
In d3v3 you would use d3.svg.diagonal, but now there is d3.linkVertical() and d3.linkHorizontal() to achieve the same thing. With this we can use:
d3.linkVertical()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; }));
And then shape paths representing links with:
link.attr("d",d3.linkVertical()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; }));
I've only done a vertical styling below - but you could determine if the difference in the x coordinates is greater than the y coordinates to determine if you should apply horizontal or vertical styling.
var svg = d3.select("svg");
var nodes = "abcdefg".split("").map(function(d) {
return {name:d};
})
var links = "bcdef".split("").map(function(d) {
return {target:"a", source:d}
})
links.push({target:"d", source:"b"},{target:"d", source:"g"})
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.name; }))
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(250,150));
var node = svg.append("g")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 5)
var link = svg.append("g")
.selectAll("path")
.data(links)
.enter().append("path")
simulation
.nodes(nodes)
.on("tick", ticked)
.force("link")
.links(links);
function ticked() {
link.attr("d", d3.linkVertical()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; }));
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
path {
stroke: black;
stroke-width: 2px;
fill:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300">
Option 2 - Manually Specify Path
We can substitute the line used to connect nodes with a path, we can supply the d attribute of the path manually given the datum of the path contains the x,y of the target and the source. Perhaps something like:
path.attr("d", function(d) {
var x0 = d.source.x;
var y0 = d.source.y;
var x1 = d.target.x;
var y1 = d.target.y;
var xcontrol = x1 * 0.5 + x0 * 0.5;
return ["M",x0,y0,"C",xcontrol,y0,xcontrol,y1,x1,y1].join(" ");
})
Again, I've only done only one styling here, this time horizontal, but adding a check to see if horizontal or vertical links are needed should be fairly straightforward:
var svg = d3.select("svg");
var nodes = "abcdefg".split("").map(function(d) {
return {name:d};
})
var links = "bcdef".split("").map(function(d) {
return {target:"a", source:d}
})
links.push({target:"d", source:"b"},{target:"d", source:"g"})
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.name; }))
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(250,150));
var node = svg.append("g")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 5)
var link = svg.append("g")
.selectAll("path")
.data(links)
.enter().append("path")
simulation
.nodes(nodes)
.on("tick", ticked)
.force("link")
.links(links);
function ticked() {
link.attr("d", function(d) {
var x0 = d.source.x;
var y0 = d.source.y;
var x1 = d.target.x;
var y1 = d.target.y;
var xcontrol = x1 * 0.5 + x0 * 0.5;
return ["M",x0,y0,"C",xcontrol,y0,xcontrol,y1,x1,y1].join(" ");
})
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
path {
stroke: black;
stroke-width: 2px;
fill:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300">
Option 3 - Use a Custom Curve Generator
I include this because I just recently answered a question on custom curves, that by chance uses the same styling. This way we can define the path of each link with:
var line = d3.line().curve(d3.someCurve))
and
link.attr("d", function(d) {
return line([[d.source.x,d.source.y],[d.target.x,d.target.y]]);
})
I've added a couple lines to build on the example above too, the curves can be either vertical or horizontal:
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
if (Math.abs(this.x0 - x) > Math.abs(this.y0 - y)) {
var x1 = this.x0 * 0.5 + x * 0.5;
this._context.bezierCurveTo(x1,this.y0,x1,y,x,y);
}
else {
var y1 = this.y0 * 0.5 + y * 0.5;
this._context.bezierCurveTo(this.x0,y1,x,y1,x,y);
}
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
var svg = d3.select("svg");
var line = d3.line()
.curve(curve);
var nodes = "abcdefg".split("").map(function(d) {
return {name:d};
})
var links = "bcdef".split("").map(function(d) {
return {target:"a", source:d}
})
links.push({target:"d", source:"b"},{target:"d", source:"g"})
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.name; }))
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(250,150));
var node = svg.append("g")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 5)
var link = svg.append("g")
.selectAll("path")
.data(links)
.enter().append("path")
simulation
.nodes(nodes)
.on("tick", ticked)
.force("link")
.links(links);
function ticked() {
link.
attr("d", function(d) {
return line([[d.source.x,d.source.y],[d.target.x,d.target.y]]);
})
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
path {
stroke: black;
stroke-width: 2px;
fill:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300">
This option will work with canvas as well (as will option 1 if I'm not mistaken).

Related

How to build a zoomable choropleth map with insets to handle discrete data in d3.v5?

Very new to D3 and coming from R but looking to develop some interactive choropleth maps.
I'm wanting to develop this zoomable choropleth: https://bl.ocks.org/cmgiven/d39ec773c4f063a463137748097ff52f
into something like this V5 choropleth with easily attachable data: https://bl.ocks.org/denisemauldin/3436a3ae06f73a492228059a515821fe
I can mainipulate the V5 choropleth fairly easily, still figuring out how to adjust continuous scale to a discrete scale but I don't really know how to build the zoomable svg in V5.
I think in your question you are asking about continuous scale, here is a code snippet for continuous scaling for an zoomable choropleth, What is just apply the same mechanism in this bl.ocks.org just wrap everything in <g> group tag and apply the zoom on this <g> tag:
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on('zoom', zoomed);
svg.call(zoom);
function zoomed() {
zoomG.selectAll('path') // To prevent stroke width from scaling
.attr('transform', d3.event.transform);
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var unemployment = d3.map();
var stateNames = d3.map();
var path = d3.geoPath();
var x = d3.scaleLinear()
.domain([1, 10])
.rangeRound([600, 860]);
var color = d3.scaleThreshold()
.domain(d3.range(0, 10))
.range(d3.schemeBlues[9]);
var g = svg.append("g")
.attr("class", "key")
.attr("transform", "translate(0,40)");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.attr("fill", function(d) { return color(d[0]); });
g.append("text")
.attr("class", "caption")
.attr("x", x.range()[0])
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Unemployment rate");
g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(function(x, i) { return i ? x : x + "%"; })
.tickValues(color.domain()))
.select(".domain")
.remove();
var promises = [
d3.json("https://d3js.org/us-10m.v1.json"),
d3.tsv("https://gist.githubusercontent.com/denisemauldin/3436a3ae06f73a492228059a515821fe/raw/954210175a18c646eb751d59e04d5a4814fe3b03/us-state-names.tsv", function(d) {
stateNames.set(d.id, d.name)
}),
d3.tsv("https://gist.githubusercontent.com/denisemauldin/3436a3ae06f73a492228059a515821fe/raw/954210175a18c646eb751d59e04d5a4814fe3b03/map.tsv", function(d) {
unemployment.set(d.name, +d.value);
})
]
Promise.all(promises).then(ready)
function ready([us]) {
let zoomG = svg.append("g");
zoomG.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("fill", function(d) {
var sn = stateNames.get(d.id)
d.rate = unemployment.get(stateNames.get(d.id)) || 0
var col = color(d.rate);
if (col) {
return col
} else {
return '#ffffff'
}
})
.attr("d", path)
.append("title")
.text(function(d) {
return d.rate + "%"; });
zoomG.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("class", "states")
.attr("d", path);
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on('zoom', zoomed);
svg.call(zoom);
function zoomed() {
zoomG.selectAll('path') // To prevent stroke width from scaling
.attr('transform', d3.event.transform);
}
}
.counties {
fill: none;
}
.states {
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
<!DOCTYPE html>
<meta charset="utf-8">
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>

Get bounding box of individual countries from topojson

I want to get bounding boxes for each country from a topojson, but when I add them as svg rectangles they are bundled together towards 0,0.
Ive re-read the API and played around with the order of the bound coordinates, but that didn't change anything! Also, I tried to use the SVG method getBBox() on the country paths but that produced the same result.
Any ideas?
var width = 700,
height = 400,
bboxes = [];
d3.queue()
.defer(d3.json, "data/world.topojson")
.await(ready);
//Map projection
var proj = d3.geoMercator()
.scale(100)
.center([-0.0018057527730242487, 11.258678472759552]) //projection center
.translate([width / 2, height / 2]) //translate to center the map in view
//Generate paths based on projection
var myPath = d3.geoPath().projection(proj);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//Group for the map features
var map = svg.append("g")
.attr("class", "map");
function ready(error, geodata) {
if (error) return console.log(error); //unknown error, check the console
//Create a path for each map feature in the data
map.selectAll("path")
.data(topojson.feature(geodata, geodata.objects.subunits).features) //generate features from TopoJSON
.enter()
.append("path")
.attr("class", "country")
.attr("id", function(d) {
return d.id;
})
.attr("d", myPath);
bboxes = boundingExtent(topojson.feature(geodata, geodata.objects.subunits).features);
svg.selectAll("rect")
.data(bboxes)
.enter()
.append("rect")
.attr("id", function(d){
return d.id;
})
.attr("class", "bb")
.attr("x1", function(d) {
return d.x;
})
.attr("y1", function(d) {
return d.y;
})
.attr("width", function(d) {
return d.width;
})
.attr("height", function(d) {
return d.height;
})
}
function boundingExtent(features) {
var bounds= [];
for (var x in features) {
var boundObj = {};
thisBounds = myPath.bounds(features[x]);
boundObj.id = features[x].id;
boundObj.x = thisBounds[0][0];
boundObj.y = thisBounds[0][1];
boundObj.width = thisBounds[1][0] - thisBounds[0][0];
boundObj.height = thisBounds[1][1] - thisBounds[0][1];
boundObj.path = thisBounds;
bounds.push(boundObj)
}
return bounds;
}
function boundExtentBySvg(){
var countries = svg.selectAll(".country")
countries.each(function(d){
var box = d3.select(this).node().getBBox();
bboxes.push({id: d.id, x: box.x, y : box.y, width: box.width, height : box.height})
})
}
In these lines:
.attr("x1", function(d) {
return d.x;
})
.attr("y1", function(d) {
return d.y;
})
rect does not have an attribute of x1 or y1, I think you meant just x and y.
Here's your code running (note, I switched out the topojson file which caused slight code changes):
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
<script data-require="topojson.min.js#3.0.0" data-semver="3.0.0" src="https://unpkg.com/topojson#3.0.0"></script>
</head>
<body>
<svg width="700" height="400"></svg>
<script>
var width = 700,
height = 400,
bboxes = [];
d3.queue()
.defer(d3.json, "https://unpkg.com/world-atlas#1/world/110m.json")
.await(ready);
//Map projection
var proj = d3.geoMercator()
.scale(100)
.center([-0.0018057527730242487, 11.258678472759552]) //projection center
.translate([width / 2, height / 2]) //translate to center the map in view
//Generate paths based on projection
var myPath = d3.geoPath().projection(proj);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//Group for the map features
var map = svg.append("g")
.attr("class", "map");
function ready(error, geodata) {
if (error) return console.log(error); //unknown error, check the console
//Create a path for each map feature in the data
map.selectAll("path")
.data(topojson.feature(geodata, geodata.objects.countries).features) //generate features from TopoJSON
.enter()
.append("path")
.attr("class", "country")
.attr("id", function(d) {
return d.id;
})
.attr("d", myPath);
bboxes = boundingExtent(topojson.feature(geodata, geodata.objects.countries).features);
svg.selectAll("rect")
.data(bboxes)
.enter()
.append("rect")
.attr("id", function(d) {
return d.id;
})
.attr("class", "bb")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("width", function(d) {
return d.width;
})
.attr("height", function(d) {
return d.height;
})
.style("fill", "none")
.style("stroke", "steelblue");
}
function boundingExtent(features) {
var bounds = [];
for (var x in features) {
var boundObj = {};
thisBounds = myPath.bounds(features[x]);
boundObj.id = features[x].id;
boundObj.x = thisBounds[0][0];
boundObj.y = thisBounds[0][1];
boundObj.width = thisBounds[1][0] - thisBounds[0][0];
boundObj.height = thisBounds[1][1] - thisBounds[0][1];
boundObj.path = thisBounds;
bounds.push(boundObj)
}
console.log(bounds)
return bounds;
}
function boundExtentBySvg() {
var countries = svg.selectAll(".country")
countries.each(function(d) {
var box = d3.select(this).node().getBBox();
bboxes.push({
id: d.id,
x: box.x,
y: box.y,
width: box.width,
height: box.height
})
})
}
</script>
</body>
</html>

How to modify a d3 force layout with voronoi polygons to trigger events on grouped elements?

The goal is to combine d3 force simulation, g elements, and voronoi polygons to make trigger events on nodes easier, such as dragging, mouseovers, tooltips and so on with a graph that can be dynamically modified. This follows the d3 Circle Dragging IV example.
In the following code, when adding the clip path attribute to the g element and clippath elements:
Why does dragging not trigger on the cells?
Why do the nodes become obscured and the
paths lose their styles on edges?
How can this be fixed to drag the nodes and trigger events on them like mouseovers?
var data = [
{
"index" : 0,
"vx" : 0,
"vy" : 0,
"x" : 842,
"y" : 106
},
{
"index" : 1,
"vx" : 0,
"vy" : 0,
"x" : 839,
"y" : 56
},
{
"index" : 2,
"vx" : 0,
"vy" : 0,
"x" : 771,
"y" : 72
}
]
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var simulation = d3.forceSimulation(data)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
var nodes = svg.append("g").attr("class", "nodes"),
node = nodes.selectAll("g"),
paths = svg.append("g").attr("class", "paths"),
path = paths.selectAll("path");
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[0, 0], [width, height]]);
var update = function() {
node = nodes.selectAll("g").data(data);
var nodeEnter = node.enter()
.append("g")
.attr("class", "node")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; });
nodeEnter.append("circle");
nodeEnter.append("text")
.text(function(d, i) { return i; });
node.merge(nodeEnter);
path = paths.selectAll(".path")
.data(data)
.enter().append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("path")
.attr("class", "path");
simulation.nodes(data);
simulation.restart();
}();
function ticked() {
var node = nodes.selectAll("g");
var diagram = voronoi(node.data()).polygons();
paths.selectAll("path")
.data(diagram)
.enter()
.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("path")
.attr("class", "path");
paths.selectAll("path")
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
node.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
svg {
border: 1px solid #888888;
}
circle {
r: 3;
cursor: move;
fill: black;
}
.node {
pointer-events: all;
}
path {
fill: none;
stroke: #999;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.1/d3.js"></script>
<svg width="400" height="400"></svg>
(Separate question, but nesting the paths in the g elements as in the Circle Dragging IV element causes undesired positioning of the paths off to the side of the graph.)
In a related question, using polygons instead of paths and clippaths, I can get the dragging to work, but am trying to use the clippath version as a comparison and not sure what are the differences, other than clippath seems to be preferred by Mike Bostock (d3 creator).
If the goal is:
is to combine d3 force simulation, g elements, and voronoi polygons to
make trigger events on nodes easier, such as dragging, mouseovers,
tooltips and so on with a graph that can be dynamically updated.
I'm going to step back a bit from the specifics of your code and try to get to the goal. I will use two primary sources (one which you reference) in this attempt to get there (and I may be way off base in doing so).
Source one: Mike Bostock's block circle dragging example.
Source two: Mike Bostock's Force-directed Graph example.
I hope that this approach at least helps to get to your goal (I took it partly as I was having difficulty with your snippet). It should be useful as a minimal example and proof of concept.
As with you, I'll use the circle dragging example as the foundation, and then I'll try to incorporate the force-directed example.
The key portions of the force directed graph that need to be imported are defining the simulation:
var simulation = d3.forceSimulation()
Assigning the nodes:
simulation
.nodes(circle)
.on("tick", ticked);
( .nodes(graph.nodes) in original )
Instructing what to do on tick:
force.nodes(circles)
.on('tick',ticked);
The ticked function:
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
( we don't need the link portion, and we want to update the circles (rather than a variable named node )
And the portions that fall in the drag events.
If we import all that into a snippet (combining drag events, adding a ticked function, and we get:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("id", function(d, i) { return "cell-" + i; });
circle.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("use")
.attr("xlink:href", function(d, i) { return "#cell-" + i; });
circle.append("circle")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d, i) { return color(i); });
simulation
.nodes(circles)
.on("tick", ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d, i) {
d3.select(this).classed("active", false);
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="600" height="400"></svg>
The obvious problem is that the cells don't update unless there is a drag. To solve this we just need to take the line that updates the cells on drag and put it in the ticked function so it updates on tick:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("id", function(d, i) { return "cell-" + i; });
circle.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("use")
.attr("xlink:href", function(d, i) { return "#cell-" + i; });
circle.append("circle")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d, i) { return color(i); });
circle.append("text")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d, i) { return i; })
simulation
.nodes(circles)
.on("tick", ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
circle.selectAll('text')
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d, i) {
d3.select(this).classed("active", false);
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="600" height="400"></svg>
update: updating nodes:
Adding and removing nodes is where it got complicated for me at least. The primary issue was that the code above rearranged the svg groups with d3.selection.raise() on drag events, which could mess up my clip path ordering if using only the data element increment. Likewise with removing items from within the middle of the array, this would cause pairing issues between cells, groups, and circles. This pairing was the primary challenge - along with ensuring any appended nodes were in the proper parent and in the right order.
To solve the pairing issues, I used a new property in the data to use as an identifier, rather than the increment. Secondly, I do a couple specific manipulations of the cells when adding: ensuring they are in the right parent and that the cell appears above the circle in the DOM (using d3.selection.lower()).
Note: I haven't managed a good way to remove a circle and keep the voronoi working with a typical update cycle, so I've just recreated for each removal - and since as far as I know the Voronoi is recalculated every tick, this shouldn't be an issue.
The result is (click to remove/add, click the button to toggle remove/add):
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var n = 0;
var circles = d3.range(15).map(function() {
return {
n: n++,
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
// control add/remove
var addNew = false;
d3.select('#control').append('input')
.attr('type','button')
.attr('value', addNew ? "Add" : "Remove")
.on('click', function(d) {
addNew = !addNew;
d3.select(this).attr('value', addNew ? "Add" : "Remove")
d3.selectAll('g').on('click', (addNew) ? add : remove);
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.attr('id',function(d) { return 'g-'+d.n })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on('click', (addNew) ? add : remove);
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
circle.append("clipPath")
.attr("id", function(d) { return "clip-" + d.n; })
.append("use")
.attr("xlink:href", function(d) { return "#cell-" + d.n; });
circle.append("circle")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d) { return color(d.n); });
circle.append("text")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d) { return d.n; })
var simulation = d3.forceSimulation()
.nodes(circles)
.force('charge', d3.forceManyBody());
simulation.nodes(circles)
.on('tick',ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
circle.selectAll('text')
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function remove () {
d3.select(this).raise();
var id = d3.select(this).attr('id').split('-')[1];
id = +id;
// Get the clicked item:
var index = circles.map(function(d) {
return d.n;
}).indexOf(id);
circles.splice(index,1);
// Update circle data:
var circle = svg.selectAll("g")
.data(circles);
circle.exit().remove();
circle.selectAll("clipPath").exit().remove();
circle.selectAll("circle").exit().remove();
circle.selectAll("text").exit().remove();
//// Update voronoi:
d3.selectAll('.cell').remove();
cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
simulation.nodes(circles)
.on('tick',ticked);
}
function add() {
// Add circle to circles:
var coord = d3.mouse(this);
var newIndex = d3.max(circles, function(d) { return d.n; }) + 1;
circles.push({x: coord[0], y: coord[1], n: newIndex });
// Enter and Append:
circle = svg.selectAll("g").data(circles).enter()
var newCircle = circle.append("g")
.attr('id',function(d) { return 'g-'+d.n })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on('click',add)
cell = circle.selectAll("path")
.data(voronoi.polygons(circles)).enter();
cell.select('#g-'+newIndex).append('path')
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
newCircle.data(circles).enter();
newCircle.append("clipPath")
.attr("id", function(d) { return "clip-" + d.n; })
.append("use")
.attr("xlink:href", function(d) { return "#cell-" + d.n; });
newCircle.append("circle")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d) { return color(d.n); });
newCircle.append("text")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d) { return d.n; })
cell = d3.selectAll('.cell');
d3.select("#cell-"+newIndex).lower(); // ensure the path is above the circle in svg.
simulation.nodes(circles)
.on('tick',ticked);
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
.cell {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
svg {
background: #eeeeee;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="control"> </div>
<svg width="960" height="500"></svg>
In terms of specific parts of your question, I found that the dragging and clip path issues in the first two bullets of your question were a largely problem of pairing clip paths, cells, and circles as well as finding the right manner in which to add new elements to the chart - which I hope I demonstrated above.
I hope this is last snippet is closer to the specific problems you were encountering, and I hope that the code above is clear - but it likely went from the clear and concise Bostockian to some other lower standard.
Block version.
Why does dragging not trigger on the cells?
Because if the cell attribute has fill:none, then it must have pointer-events:all.
Why do the nodes become obscured and the paths lose their styles on
edges?
Because the clip path was targeting the g elements position instead of the circles position.
How can this be fixed to drag the nodes and trigger events on
them like mouseovers?
use path attr pointer-events: all, path { pointer-events: all; }
select the desired child element such as circle, or text, in the drag or tick event for positioning parent.select(child).attr('d' function(d) { ..do stuff.. });
use node id's for references to simplify data array updates or deletions node.data(data, function(d) { return d.id; })
Thanks Andrew Reid for your help.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
color = d3.scaleOrdinal(d3.schemeCategory10);
var a = {id: "a"},
b = {id: "b"},
c = {id: "c"},
data = [a, b, c],
links = [];
var simulation = d3.forceSimulation(data)
.force("charge", d3.forceManyBody().strength(-10))
.force("link", d3.forceLink(links).distance(200))
.force("center", d3.forceCenter(width / 2, height / 2))
.alphaTarget(1)
.on("tick", ticked);
var link = svg.append("g").attr("class", "links").selectAll(".link"),
node = svg.append("g").attr("class", "nodes").selectAll(".node");
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, 1], [width + 1, height + 1]]);
update();
d3.timeout(function() {
links.push({source: a, target: b}); // Add a-b.
links.push({source: b, target: c}); // Add b-c.
links.push({source: c, target: a}); // Add c-a.
update();
}, 1000);
d3.interval(function() {
data.pop(); // Remove c.
links.pop(); // Remove c-a.
links.pop(); // Remove b-c.
update();
}, 5000, d3.now());
d3.interval(function() {
data.push(c); // Re-add c.
links.push({source: b, target: c}); // Re-add b-c.
links.push({source: c, target: a}); // Re-add c-a.
update();
}, 5000, d3.now() + 1000);
function update() {
node = node.data(data, function(d) { return d.id; });
node.exit().remove();
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout);
nodeEnter.append("circle").attr("fill", function(d) { return color(d.id); }).attr("r", 8);
nodeEnter.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.id; });
nodeEnter.append("path").attr("class", "path");
nodeEnter.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node = node.merge(nodeEnter);
// Apply the general update pattern to the links.
link = link.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.exit().remove();
link = link.enter().append("line").merge(link);
// Update and restart the simulation.
simulation.nodes(data);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
function mouseover(d) {
d3.select(this).raise().classed("active", true);
}
function mouseout(d) {
d3.select(this).raise().classed("active", false);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.fx = d3.event.x).attr("cy", d.fy = d3.event.y);
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function ticked() {
node.select("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
node.select("path")
.data(voronoi.polygons(data))
.attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
node.select("text")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" });
link.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; });
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active path {
fill: #111;
opacity: 0.05;
}
.active text {
visibility: visible;
}
.active circle {
stroke: #000;
stroke-width: 1.5px;
}
svg {
border: 1px solid #888;
}
.links {
stroke: #000;
stroke-width: 1.5;
}
.nodes {
stroke-width: 1.5;
}
text {
pointer-events: none;
font: 1.8em sans-serif;
visibility: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="400" height="400"></svg>

How to control the coordinates of the nodes of d3

I have created a d3 force layout, and it works very well. Now I will add a group of data to my graph. I hope I could control the center of my new nodes. For example, supposed the center is (100,100), I hope the new nodes lay out into rectangle area like [(50,50) to (150,150)] as a whole.
var width = 500,
height = 500;
var nodes = [{id:0, n:'Tom'}, {id:1, n:'Join'}, {id:2, n:'John'}, {id:3, n:'Bob'}, {id:4, n:'4'}, {id:5, n:'5'}, {id:6, n:'6'}];
var links = [{source:0,target:1},{source:0,target:2},{source:0,target:3},{source:0,target:4},{source:0,target:5},{source:1,target:5},{source:1,target:6}];
// init force
var force = d3.layout.force()
.charge(-120)
.linkDistance(120)
.size([width, height]);
// init svg
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// set tick function
force.on("tick", function () {
d3.selectAll(".link").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;
});
// controll the coordinates here
d3.selectAll(".node").attr("transform", function(d){
if(d.flag == 1){
d.x = Math.max(50, Math.min(150, d.x));
d.y = Math.max(50, Math.min(150, d.y));
}
return "translate("+d.x+","+d.y+")";
});
}).on('end', function(){
svg.selectAll(".node").each(function(d){d.fixed=true;});
});
function setData(ns, ls){
var update = svg.selectAll(".link").data(ls);
update.enter().append("line")
.attr("class", "link")
.style("stroke-width", 1);
update.exit().remove();
update = svg.selectAll(".node").data(ns);
update.enter().append("g")
.attr("class", "node")
.attr("id", function(d){return d.id})
.call(force.drag)
.call(function(p){
p.append("image")
.attr("class", "nodeimage")
.attr("width", "30px")
.attr("height", "30px")
.attr("x", "-15px")
.attr("y", "-15px");
p.append("text")
.attr("class", "nodetext")
.attr("dx", "-10px")
.attr("dy", "20px")
.style("font-size", "15px")
.text(function(d){return d.n});
});
update.exit().remove();
update.selectAll(".nodeimage")
.each(function() {
d3.select(this).datum(d3.select(this.parentNode).datum());
})
.attr("xlink:href", function(d){
var img;
if(d.flag == 1){
img = "http://www.gravatar.com/avatar/1eccef322f0beef11e0e47ed7963189b/?default=&s=80"
}else{
img = "http://www.gravatar.com/avatar/a1338368fe0b4f3d301398a79c171987/?default=&s=80";
}
return img;
});
force.nodes(ns)
.links(ls)
.start();
}
//init
setData(nodes, links);
setTimeout(function(){
//generate new data and merge to old data
nodes = nodes.concat(generateNewData());
setData(nodes, links);
//how do i control the coordinate of new nodes?
}, 3000);
function generateNewData(){
var ns = [];
for(var i = 0; i < 10; i++){
ns.push({id:i+100,n:'n'+i,flag:1});
}
return ns;
}
Here is my demo of jsfiddle:http://jsfiddle.net/cs4xhs7s/4/
The latest demo shows that the nodes can display in the rectangle, however, their coordinates are the same. I hope it is an available force layout.
https://jsfiddle.net/wpnq15mf/1/
var width = 500,
height = 500;
var nodes = [{id:0, n:'Tom'}, {id:1, n:'Join'}, {id:2, n:'John'}, {id:3, n:'Bob'}, {id:4, n:'4'}, {id:5, n:'5'}, {id:6, n:'6'}];
var links = [{source:0,target:1},{source:0,target:2},{source:0,target:3},{source:0,target:4},{source:0,target:5},{source:1,target:5},{source:1,target:6}];
// init force
var force = d3.layout.force()
.charge(-500)
.linkDistance(120)
.gravity(0.1)
.size([width, height]);
// init svg
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// set tick function
force.on("tick", function () {
d3.selectAll(".link").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;
});
// controll the coordinates here
d3.selectAll(".node").attr("transform", function(d){
if(d.flag == 1){
d.x = Math.max(50, Math.min(150, d.x));
d.y = Math.max(50, Math.min(150, d.y));
}
return "translate("+d.x+","+d.y+")";
});
}).on('end', function(){
svg.selectAll(".node").each(function(d){d.fixed=true;});
});
function setData(ns, ls){
var update = svg.selectAll(".link").data(ls);
update.enter().append("line")
.attr("class", "link")
.style("stroke-width", 1);
update.exit().remove();
update = svg.selectAll(".node").data(ns);
update.enter().append("g")
.attr("class", "node")
.attr("id", function(d){return d.id})
.call(force.drag)
.call(function(p){
p.append("image")
.attr("class", "nodeimage")
.attr("width", "30px")
.attr("height", "30px")
.attr("x", "-15px")
.attr("y", "-15px");
p.append("text")
.attr("class", "nodetext")
.attr("dx", "-10px")
.attr("dy", "20px")
.style("font-size", "15px")
.text(function(d){return d.n});
});
update.exit().remove();
update.selectAll(".nodeimage")
.each(function() {
d3.select(this).datum(d3.select(this.parentNode).datum());
})
.attr("xlink:href", function(d){
var img;
if(d.flagx == 1){
img = "http://www.gravatar.com/avatar/1eccef322f0beef11e0e47ed7963189b/?default=&s=80"
}else{
img = "http://www.gravatar.com/avatar/a1338368fe0b4f3d301398a79c171987/?default=&s=80";
}
return img;
});
force.nodes(ns)
.links(ls)
.start();
}
//init
setData(nodes, links);
setTimeout(function(){
//generate new data and merge to old data
nodes = nodes.concat(generateNewData());
links = links.concat(generateNewLinks());
setData(nodes, links);
//how do i control the coordinate of new nodes?
}, 3000);
function generateNewData(){
var ns = [];
ns.push({id:6,n:'n'+i,flag:1, flagx:1});
for(var i = 1; i < 10; i++){
ns.push({id:i+6,n:'n'+i, flagx:1});
}
return ns;
}
function generateNewLinks(){
var ns = [];
ns.push({source:7,target:8});
ns.push({source:7,target:9});
ns.push({source:7,target:10});
ns.push({source:7,target:11});
ns.push({source:7,target:12});
ns.push({source:7,target:13});
ns.push({source:7,target:14});
ns.push({source:7,target:15});
ns.push({source:7,target:16});
return ns;
}
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

dagre-d3 how to click node and run an event after that

I am using dagre-d3.js to create hierarchical graph. Now I have a requirement to make the node clickable and perform a function. I am unable to achieve that.
current some of my code looks like
var g = new dagreD3.graphlib.Graph().setGraph({});
g.setNode("TEST", { label: "TEST"})
g.setNode("TEST1", { label: "TEST1"})
g.setEdge("TEST", "TEST1", { label: "open", style: "stroke: green; stroke-width: 2px;fill: none", arrowheadStyle: "fill: green" });
var svg = d3.select("svg"),
inner = svg.select("g");
var render = new dagreD3.render();
render(inner, g);
var initialScale = 0.75;
zoom
.translate([(svg.attr("width") - g.graph().width * initialScale) / 2, 20])
.scale(initialScale)
.event(svg);
svg.attr('height', g.graph().height * initialScale + 40);
I just need to be able to click on TEST or TEST1 and run a function that I wrote to go to that div with same name on page(TEST, TEST1)
I have looked through this, but it doesn't help me.
https://github.com/cpettitt/dagre-d3/issues/13
Also this seems to use different method which is not available to me.
Please guide me
Thanks,
Nihir
Here are 4 mouse events:
d3.selectAll('svg g.comp')
.on('mouseover', function(d) {
console.log('mouseover');
})
.on('mouseout', function(d) {
console.log('mouseout');
})
.on('mousedown', function(d) {
console.log('mousedown');
})
.on('mouseup', function(d) {
console.log('mouseup');
});
This sounds like an interesting approach.
But there were some inbuilt method available to which I just figured out
here is my solution
var selections = inner.selectAll("g.node");
selections
.on('click', function (d) { ScrollToID(d); });
You can use jquery to select the node tag on click, then parse out the node name and pass it into your function. Something like this:
$(document).ready(function() {
$('.node').click(function() {
// This gets the node name from the 'class' attribute
var class_header = $(this).attr('class').split(' ');
var node_name = class_header[class_header.length - 1]
// Execute your function
myFunction(node_name)
})
})
var json = {"nodes": [{"name": "Node1", "group": 2},{"name": "Node2","group": 1},{"name": "Node3","group": 1}],
"links": [{"source": 0,"target": 1,"value": 2},{"source": 0,"target": 2,"value": 2}]};
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
force.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", function(d){ return ["link", d.source.name, d.target.name].join(" "); })
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
// Set up dictionary of neighbors
var node2neighbors = {};
for (var i =0; i < json.nodes.length; i++){
var name = json.nodes[i].name;
node2neighbors[name] = json.links.filter(function(d){
return d.source.name == name || d.target.name == name;
}).map(function(d){
return d.source.name == name ? d.target.name : d.source.name;
});
}
var clickableNodes = ["Node1"];
var nodes = svg.selectAll(".node")
.data(json.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("id", function(n){ return n.name; })
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag)
nodes.filter(function(n){ return clickableNodes.indexOf(n.name) != -1; })
.on("click", function(n){
// Determine if current node's neighbors and their links are visible
var active = n.active ? false : true // toggle whether node is active
, newOpacity = active ? 0 : 1;
// Extract node's name and the names of its neighbors
var name = n.name
, neighbors = node2neighbors[name];
// Hide the neighbors and their links
for (var i = 0; i < neighbors.length; i++){
d3.select("circle#" + neighbors[i]).style("opacity", newOpacity);
d3.selectAll("line." + neighbors[i]).style("opacity", newOpacity);
}
// Update whether or not the node is active
n.active = active;
});
nodes.append("title")
.text(function(d) { return d.name; });
force.on("tick", function() {
link.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; });
nodes.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});

Resources