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.)
Related
I am working on a temporal network graph, with pie charts as nodes. Along with the nodes/links changing over time, the pie charts are supposed to change. It works fine without incorporating the changing pie slices. When I am incorporating the changing slices I get this weird behaviour where the nodes/pies restart from their initial position every time the (time) slider moves, which makes the whole thing jitter rather severely.
Each nodes data looks like:
{
"id": "Mike",
"start": "2022-08-09",
"location": [12.34, -56.74],
"received": [{
"giver": "Susan",
"receiver": "Mike",
"user_timestamp": "2022-08-09",
"message": "thanks!",
"location": [3.1415, 9.6535]
}, {
"giver": "Joe",
"receiver": "Mike",
"user_timestamp": "2022-08-11",
"message": "so cool!",
"location": [27.18, 2.818]
}]
}
The received array holds all the data pertinent to the pie - each slice is the same size, and there as many slices as their are elements in the array. I am changing the pie slices by filtering the received array based on the user_timestamp and the slider position. Another issue I'm having is that the pie slices are not updating properly when slices are added ...
rangeSlider.on("onchange", (val) => {
currentValue = Math.ceil(timeScale(val));
// this filters entire node/pie
const filteredNodes = userNetworkData.nodes.filter(
(d) => new Date(d.start) <= val
);
// filter the received array in each node
const filteredNodesReceived = filteredNodes.map((d) => {
return {
...d,
received: d.received.filter((r) => new Date(r.user_timestamp) <= val),
};
});
const filteredLinks = userNetworkData.links.filter(
(d) => new Date(d.start) <= val
);
// remove edge if either source or target is not present
const filteredLinksFiltered = filteredLinks.filter(
(d) => filteredNodesReceived.some(o => o.id == d.source.id) && filteredNodesReceived.some(o => o.id == d.target.id)
);
// point to new source, target structure
const filteredLinksMapped = filteredLinksFiltered.map((d) => {
return {
...d,
source: filteredNodesReceived.find(x => x.id==d.source.id),
target: filteredNodesReceived.find(x => x.id==d.target.id)
};
});
update(filteredNodesReceived, filteredLinksMapped);
});
The way its set up I am using userNetworkData to hold the data in some static version so I can bring it back after I have removed it. Maybe that doesn't make sense. I have tried updating the x,y,vx,vy each instance of userNetworkData.nodes on slider change but the same jittering occurs.
filteredLinksMapped is my attempt to re-associate the links with the nodes (which now have a different amount of elements in the received array).
The relevant parts of update(nodes,links):
function update(nodes, links) {
node = node
.data(nodes, (d) => d.id)
.join(
(enter) => enter.append("g").call(drag(simulation))
);
paths = node
.selectAll("path")
.data(function (d, i) {
return pie(d.received);
})
.join(
(enter)=>
enter.append("svg:path")
.attr("class", "path")
.attr("d", arc)
.attr("opacity", 1)
// for making each pie slice visible
.attr("stroke", function (d) {
return color(d.data.receiver);
})
.attr("stroke-width", radius * 0.2)
.attr("fill", function (d, i) {
return color(d.data.giver);
})
.attr("cursor", "pointer")
.on("mousemove", function (event, d) {
//tooltips bit
div.transition().duration(200).style("opacity", 0.9);
// this bit puts the tooltip near the slice of the pie chart
div
.html(parse_message(d.data))
.style("left", event.pageX + 20 + "px")
.style("top", event.pageY - 28 + "px");
})
.on("mouseout", function (d) {
div.transition().duration(500).style("opacity", 0);
})
)
link = link
.data(links, (d) => [d.source, d.target])
.join("line")
.attr("stroke-width", radius * 0.3)
.attr("stroke", (d) => color(d.source.id));
simulation.nodes(nodes);
simulation.force("link").links(links,function(d){return d.id;});
simulation.alpha(0.5).tick();
simulation.restart();
ticked();
}
I am initializing my selections and simulation outside of update like so:
const simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
.force(
"link",
d3.forceLink().id((d) => d.id)
)
// .force("collide", d3.forceCollide().radius(2*radius ).iterations(3))
.force(
"y",
d3.forceY((d) => projection(d.location)[1])
)
.force(
"x",
d3.forceX((d) => projection(d.location)[0])
)
.on("tick", ticked);
let link = svg.append("g").attr("class", "links").selectAll("line");
let node = svg.append("g").attr("class", "nodes").selectAll("g");
Note I am forcing the nodes towards the coordinates corresponding to their lat/lon as I am moving towards transitioning between a "map" view and a network view.
Unfortunately i'm having trouble getting to work on codepen, i'll keep trying but hopefully that's enough.
The problem was with the way I was copying over the x,y,vx,vy values from the node selection to the original data (which I would use to filter/add to my selection). Here is what I settled on.
const node_pos_vel = node.data().map(d=> (({ x,y,vx,vy,id }) => ({ x,y,vx,vy,id}))(d))
const node_pos_vel_map = new Map(node_pos_vel.map(d => [d.id, d]));
userNetworkData.nodes = userNetworkData.nodes.map(d => Object.assign(d, node_pos_vel_map.get(d.id)));
The first line is just taking the subset of the object that I want to update. See How to get a subset of a javascript object's properties for how it works.
The last line replaces just the values x,y,vx,vy values for each instance in userNetworkData.nodes when it is a part of the nodes currently in the DOM.
This was inspired by https://observablehq.com/#d3/temporal-force-directed-graph but the difference between their case (where they copy over all the data from node.data() is that I cannot copy over the received array, as the time filter is changing it, and I need to hold a full copy of it.
I have rewritten most of my d3 code to v4, but the new update pattern is throwing me off. The example below is for a force diagram. A duplicate circle is created within the first container upon every update. The data in my example does not actually change, but it's irrelevant. If I use new data, the same issue (a duplicate circle) occurs.
var w = 800,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var dataset = {};
function setData() {
dataset.nodes = [{
value: 200
}, {
value: 100
}, {
value: 50
}];
}
setData();
var rScale = d3.scaleSqrt()
.range([0, 100])
.domain([0, d3.max(dataset.nodes.map(function(d) {
return d.value;
}))]);
var node = svg.append("g")
.attr("class", "nodes")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
.selectAll(".node");
var simulation = d3.forceSimulation(dataset.nodes)
.force("charge", d3.forceManyBody().strength(-1600))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaDecay(.05)
.on("tick", ticked);
function ticked() {
node.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
function restart() {
// Apply the general update pattern to the nodes.
node = node.data(dataset.nodes, function(d) {
return d.id;
});
node.exit().remove();
node = node.enter().append("g")
.attr("class", "node")
.merge(node);
node.append("circle")
.attr("r", function(d) {
return rScale(d.value);
});
// Update and restart the simulation.
simulation.nodes(dataset.nodes);
simulation.alpha(1).restart();
}
restart();
function update() {
setData();
restart();
}
d3.select("#update").on("click", update);
If you click the Update button in this codepen (https://codepen.io/cplindem/pen/wpQbQe), you will see all three circles animate as the simulation restarts, but behind the largest circle, there is another, identical circle that does not animate. You can also see the new circle appear in the html if you inspect it.
What am I doing wrong?
Your first problem seems to be that you are keying the data on an 'id' field, but your data doesn't have any ids, so that needs changed or you just keep adding new groups:
function setData() {
dataset.nodes = [{
value: 200,
id: "A"
}, {
value: 100,
id: "B"
}, {
value: 50,
id: "C"
}];
console.log("dataset", dataset);
}
The second problem is you merge the new and updated selection and then append new circles to all of them, even the existing ones (so you have multiple circles per group on pressing update). I got it to work by doing this: make the new nodes, merge with existing selection, add circles to just the new nodes, update the circles in all the nodes:
node.exit().remove();
var newNodes = node.enter().append("g");
node = newNodes
.attr("class", "node")
.merge(node);
newNodes.append("circle");
node.select("circle")
.attr("r", function(d) {
return rScale(d.value);
});
Whether that 2nd bit is optimal I don't know, I'm still more anchored in v3 myself...
https://codepen.io/anon/pen/WdLexR
I'm a newbie to the d3 library and javascript in general.
I'm trying to achieve something like
this, where I have a sunburst partition but each node has a different height with respect to the radial center - but the padding to its parent/child stays the same.
I've tried looking around and couldn't come up with any solutions.
(trying to change the innerRadius/outerRadius parameters didn't seem to work :( ).
Here is my code:
var vis = d3.select("#chart").append("svg")
.style("margin", "auto")
.style("position", "relative")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.sort(function (a, b) { return d3.ascending(a.time, b.time); })
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.n_leaves+1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
//read data from json file and visualize it
d3.text("5rrasx_out.json", function(text) {
var data = JSON.parse(text);
var json = buildHierarchy(data,'5rrasx');
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json);
var dataSummary = [{label: 'pos', count: totalPos}, {label: 'neg', count: totalNeg}];
//set title
$("#title").text(json.title.replace(/\[.*\]/g,""));
//set chart
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("path")
.attr("class", "sunburst_node")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return (d.sentiment > 0) ? colors["pos"] : colors["neg"]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.on("click", click);
};
Any help would be much appreciated.
Thanks!
I know this is not a proper answer to the question above, but in case someone needs a sunburst with different dimensions for each node, here I post how to do it in R using the ggsunburst package.
# install ggsunburst
if (!require("ggplot2")) install.packages("ggplot2")
if (!require("rPython")) install.packages("rPython")
install.packages("http://genome.crg.es/~didac/ggsunburst/ggsunburst_0.0.10.tar.gz", repos=NULL, type="source")
library(ggsunburst)
# one possible input for ggsunburst is newick format
# consider the following newick "(((A,B),C),D,E);"
# you can define the distance in node A with "A:0.5"
# you can define size in node E with "E[&&NHX:size=5]"
# adding both attributes to the newick
nw <- '(((A:0.5,B),C:3),D[&&NHX:size=5],E[&&NHX:size=5]);'
sb <- sunburst_data(nw)
sunburst(sb, rects.fill.aes = "name") + scale_fill_discrete(guide=F)
as you can see in the code, these attributes can be defined independently, and as you can see in the plot they affect the dimennsions of the correponding nodes:
node "A" is 0.5 times shorter than "B", which is defined by the attribute "distance"
E has an angle 5 times wider than C, which is defined by the attribute "size".
and here an attempt to resemble the example posted in the question with a newick tree
nw <- "(((.:0[&&NHX:support=1.0:dist=0.0:name=.:size=3],a3:1[&&NHX:color=2:support=1.0:dist=1.0:name=a3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=a2])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=a1],b1:1.8[&&NHX:color=1:support=1.0:dist=1.8:name=b1:size=5],(((a4:1[&&NHX:color=1:support=1.0:dist=1.0:name=a4:size=1],b4:1.8[&&NHX:color=-1:support=1.0:dist=1.8:name=b4:size=1],c4:1.5[&&NHX:color=2:support=1.0:dist=1.5:name=c4:size=1],d4:0.8[&&NHX:color=-2:support=1.0:dist=0.8:name=d4:size=1])1:1[&&NHX:color=1:support=1.0:dist=1.0:name=b3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=b2:size=1],(c3:1[&&NHX:color=1:support=1.0:dist=1.0:name=c3:size=1],(e4:1[&&NHX:color=-2:support=1.0:dist=1.0:name=e4:size=1])1:0.5[&&NHX:color=-1:support=1.0:dist=0.5:name=d3:size=1])1:0.5[&&NHX:color=1:support=1.0:dist=0.5:name=c2:size=1])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=c1:size=1],d1:0.8[&&NHX:color=3:support=1.0:dist=0.8:name=d1:size=20]);"
sb <- sunburst_data(nw, node_attributes = "color")
sunburst(sb, leaf_labels.size = 4, node_labels.size = 4, node_labels = T, node_labels.min = 1, rects.fill.aes = "color") +
scale_fill_gradient2(guide=F) + ylim(-8,NA)
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
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!