Creating a force layout in D3.js visualisation library - d3.js

I'm working on a project, which visualises the references between books. It's worth to mention that I'm a total beginner in Javascript. So, I couldn't get far by reading the D3.js API reference. I used this example code, which works great.
The structure of my CSV file is like this:
source,target
"book 1","book 2"
"book 1","book 3"
etc.
The source and target are connected by a link. These are the points for the layout:
Create two different circles respectively for source and target node.
Set a specific color for source and target node.
The circles should be labeled by the book information, e.g., source node
is labeled by "book 1" and target node is labeled by "book 2".
If there is a link between targets, then make this specific link wider
than the others links from source to target.
I hope you can help me by creating these points.
Thanks in advance.
Best regards
Aeneas

d3.js plays much nicer with json data files than with csv files, so I would recommend transferring your csv data into a json format somehow. I recently coded something similar to this, and I had my nodes and links stored in a json file as a dictionary formatted as such:
{
'links': [{'source': 1, 'target': 2, 'value': 0.3}, {...}, ...],
'nodes': [{'name': 'something', 'size': 2}, {...}, ...]
}
This allows you to initialize your nodes and links as follows (after starting the view):
d3.json("data/nodesandlinks.json", function(json) {
var force = self.force = d3.layout.force()
.nodes(json.nodes)
.links(json.links)
.linkDistance(function(d) { return d.value; })
.linkStrength(function(d) { return d.value; })
.size([width, height])
.start();
var link = vis.selectAll("line.link")
.data(json.links)
.enter().append("svg:line")
.attr("class", "link")
.attr("source", function(d) { return d.source; })
.attr("target", function(d) { return d.target; })
.style("stroke-width", function(d) { return d.value; });
var node = vis.selectAll("g.node")
.data(json.nodes)
.enter().append("svg:g")
.attr("class", "node")
.attr("name", function(d) { return d.name; })
.call(force.drag);
Hope this helped!

Related

In d3.js map, points are hidden behind other features

I have tried to create a map of India with some points in it. I followed the codebase from here.
Everything is fine except the points. They are hidden behind other features on the map, and because of this are not visible. How do I layer the features so that the points are visible?
In d3.js map layering can be handled in two ways. If this is your code (paraphrasing from your example)
d3.json("path.json",function (json) {
g.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("path.csv",function (csv) {
g.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
Data will be added to the 'g' element based on the order in which the callback functions are completed, so it is possible that the csv data will be drawn first and the json data will be drawn after it.
The first method I'll present here is the cleanest way in most situations to specify data layer order (in my mind). SVG 'g' elements are appended in the order that they are specified. This gives you easy control over the layering of data:
var gBackground = svg.append("g"); // appended first
var gDataPoints = svg.append("g"); // appended second
// ... and so forth
Then, all you have to do is specify to which 'g' element/layer data gets appended/inserted into. So, your code would look more like:
d3.json("path.json",function (json) {
gBackground.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("path.csv",function (csv) {
gDataPoints.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
The second option appends data to the same 'g' element but ensures the order in which this is done is controlled, by drawing the second layer in the callback function that draws the first, after the first is drawn:
To control the ordering of the data with this method we would modify the code to something like:
d3.json("path.json",function (json) {
g.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
// once the json is drawn, draw the csv:
d3.csv("path.csv",function (csv) {
g.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
});

adding class names to arcs from data in d3.layout.pie()

I'm creating a pie chart from a JSON file. I wonder if there is a way I can take some names from the JSON file and assign them as class names of the arcs created by d3.layout.pie().
Here is an example I created: http://blockbuilder.org/jinlong25/532d889e01d02cef2d24
Essentially, I want to do something like the last line of code below:
var data = [
{
'name': 'apple',
'value': 250
},
{
'name': 'banana',
'value': 100
},
{
'name': 'orange',
'value': 150
}
];
var arcs = svg.selectAll('g.arc')
.data(pie(data.map(function(d) { return d.value; })))
.enter().append('g')
.attr('transform', 'translate(70, 70)')
.attr('class', function(d) { return d.name; };
but since the data has been transformed by pie(), I wonder if there is anyway to add class names to the data generated by pie().
thanks!
d3's layouts helpfully provide a .value() accessor which allows you to specify how get the value of the datum, instead of doing the data.map() operation. So, you could do:
var pie = d3.layout.pie().value(function(d) { return d.value; })
That way, your original datum is preserved in d.data.
So using that definition of pie, your code would change to this:
var arcs = svg.selectAll('g.arc')
.data(pie(data))
.enter().append('g')
.attr('transform', 'translate(70, 70)')
.attr('class', function(d) { return d.data.name; };
edit: added link the relevant documentation.
Some D3 layouts mutate the original dataset but others create a new dataset (like voronoi). In those cases, you can use the array position from the original dataset when working with the new dataset. So from your example:
var arcs = svg.selectAll('g.arc')
.data(pie(data.map(function(d) { return d.value; })))
.enter().append('g')
.attr('transform', 'translate(70, 70)')
.attr('class', function(d,i) { return data[i].name; };

D3.js nested data - how can i display my circles

I am a beginner on D3.js and hit the wall on the following:
I wish to display the score given by an attendee of an event over time. Then as the attendee can give also comments, I would like to place a circle on the curve of the score in the same colour as the scoring.
I succeeded to do this for a single user.
The code is on JS Fiddle http://jsfiddle.net/roestigraben/8s1t8hb3/
Then, trying to extend this to multiple attendees, I run into problems.
The JSFiddle http://jsfiddle.net/roestigraben/Lk2kf1gh/
This code displays nicely the score data for the 3 attendees simulated. However the circles to display the possible comments (there is only one in the data set) from the attendees do not work
I try to filter the attendees array
svg.selectAll("circle")
.data(data.filter(function(d, i){ if(d.comment){return d}; })) // condition here
.enter().append("circle")
.attr("class", "dotLarge")
.attr({r: 5})
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.status); })
I think I need to go deeper into the nesting, but ....my ignorance.
Thanks a lot
Peter
The code where you're displaying your circles doesn't even come close to matching the format of your data. I don't know that you're having a problem with the nest, but you probably want a slightly different data structure when it comes to graphing your comments.
I have updated your fiddle here: http://jsfiddle.net/Lk2kf1gh/7/
The important bit is:
var comments = attendees.map(function(d) {
return {
name: d.name,
comments: d.values.filter(function(e) {
return e.comment != undefined;
})
};
});
//generation of the circles to indicate a comment
// needs to be done with a filter
svg.selectAll("g.comments")
.data(comments)
.enter()
.append("g")
.attr("class", function(d) { return "comments " + d.name })
.selectAll("circle.comment")
.data(function(d) {
return d.comments;
})
.enter()
.append("circle")
.attr("class", "dotLarge comment")
.attr("r", 5)
.attr("cx", function(e) { return x(e.time); })
.attr("cy", function(e) { return y(e.status); });
The first part of this code creates a new data structure that is more focused on the comment information. The second part creates the circles.
Note: I've grouped each person into their own g element and then created a circle for each of their comments. This makes use of d3 nested selections and data binding. Hopefully you can follow what is happening.
I've also added a few more comments to your data for testing. I didn't bother to fix any of the cosmetic issues, I'll leave that up to you.

D3js force directed graph linkStrength blocked to 0

I'm encountering some problem with d3js and the force directed layout:
Links are weak like if linkStrength() were set to 0. But changing it doesn't change anything.
When i drag a node, the others doesn't move...
EDIT :
I've found that by changing data to classic integer-indexed array, everything is ok!
I don't know why key-value arrays or object doesn't work ...
Here is my code:
tick = ->
link
.attr "x1", (d) ->
nodes[d.source].x
.attr "y1", (d) ->
nodes[d.source].y
.attr "x2", (d) ->
nodes[d.target].x
.attr "y2", (d) ->
nodes[d.target].y
circles
.attr "cx", (d) ->
d.x
.attr "cy", (d) ->
d.y
nodes_values = d3.values nodes
force = d3.layout.force()
.nodes nodes_values
.links links
.size([width, height])
.charge(-120)
.linkDistance(30)
.on 'tick', tick
.start()
link = svg.selectAll(".link")
.data links
.enter()
.append("line")
.attr("class", "link")
.attr "marker-end", "url(#arrow)"
groups = svg.selectAll(".node")
.data nodes_values
.enter()
.append 'g'
circles = groups
.append("circle")
.attr("class", "node")
.attr "r", (d)->
if d.weigth
return d.weigth * 5
else
return 5
.style "fill", (d) -> color d.group
.call(force.drag)
And data looks like:
Links:
"[
{
"source": "xxxx.xxxx#xxxxx.xx",
"target": "NIWT",
},
{
"source": "yyyyy.yyyyy#yyyyyy.yyy",
"target": "NIUT",
}
]"
Nodes:
{
"xxxxx.xxxxx#xxxxx.xxx" : {
"name":"xxxxx.xxxxx#xxxxx.xxx",
"group":"Operateurs",
"weight":0,
"x":386.20246469091313,
"y":282.4477932203487,
"px":386.337157279126,
"py":282.4570376593727,
},
"yyyyy.yyyyy#yyyyy.yyyy": {
"name":"yyyyy.yyyyy#yyyyy.yyyy",
"group":"Operateurs",
"weight":0,
"x":853.3548980089732,
"y":395.80903774295444,
"px":853.2517240837253,
"py":395.7616750529105
}
}
Did you have any idea?
The problem is in the links array that you pass to the force layout.
The source and target values of your links need to be pointers to the actual node data objects, not just string ids. That way, when the d3 force layout scans through the array of links, it can access the data objects and adjust the x and y values according to the link strength.
To fix, you need to add an extra routine to go through your links array and use the strings to extract the data object from your nodes hashmap.
var links_pointers = links.map(function(link){
return {source:nodes[link.source], target:nodes[target.source]};
});
var nodes_values = d3.values(nodes);
force = d3.layout.force()
.nodes(nodes_values)
.links(links_pointers)
/* etc. */
Then of course you can use the links_pointers array as the data for your link selection and change your tick function accordingly (to use d.source.x instead of nodes[d.source].x, etc.)

Show info when hovering over voronoi polygons (in D3.js)

I want to show the city name and population related to the voronoi area hovered over. However, with how I made the voronoi areas, I had to either only send coordinate data and have all of the drawings work, or send more data and none of the voronoi areas are drawn (b/c it can't read the non-coordinate data, and I don't know how to specify within an array or object, at least when creating voronois). I can enter static or irrelevant data for the tooltip (as I did below), but not anything from the actual dataset.
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("a simple tooltip");
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [w, h]]);
d3.csv("us-cities1.csv", function(d) {
return [projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat])[1]];
}, function(error, rows) {
vertices = rows;
drawV(vertices);
}
);
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function drawV(d) {
svg.append("g")
.selectAll("path")
.data(voronoi(d), polygon)
.enter().append("path")
.attr("class", "test")
.attr("d", polygon)
.attr("id", function(d, i){return i;})
.on("mouseover", function(){return tooltip.style("visibility", "visible");})
.on("mousemove", function(){return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px").text((this).id);})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");});
svg.selectAll("circle")
.data(d)
.enter().append("circle")
.attr("class", "city")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2);
}
I've put together an example using your data to demonstrate what Lars mentions. I created a variable for Voronoi like this:
var voronoi = d3.geom.voronoi()
.x(function(d) { return (d.coords[0]); })
.y(function(d) { return (d.coords[1]); });
which was taken from this Bl.ock by Mike. This allows you to specify the array of coordinates and still have them connected to the descriptive data you want to display.
I then created the object to store all the data in a format that could be used in the Voronio polygons using:
cities.forEach(function (d,i) {
var element = {
coords: projection([+d.lon, +d.lat]),
place: d.place,
rank: d.rank,
population: d.population
};
locCities.push(element);
});
I could have specified the translation of the coordinates in the voronio variable and then just used the cities variable, but I didn't.
The title attribute was used for the tooltips, but this can be replaced with something more appropriate such as what you have in your code. The relevant code is :
.append("title") // using titles instead of tooltips
.text(function (d,i) { return d.point.place + " ranked " + d.point.rank; });
There were a few issues with the data. I had to use an older version of d3 (3.1.5) to get the geojson to render correctly. I know there have been a number of chnanges to the AlberUsa projection so beware there is an issue there.
The location of some of the cities seems wrong to me for instance San Fancisco appears somewhere in Florida (this caused me some confusion). So I checked the original csv file and the coordinates seem to be wrong in there and the data is rendering where it should (just not where I'd expect according to the labels).
Now putting it all together you can find it here

Resources