I managed to create a bubble chart which works fine when it is a single dataset. But something goes wrong if I need to update it with other datasets. Please help me with my update function at http://jsfiddle.net/9jL64/.
function changebubble(root)
{
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root))
.filter(function(d) { return !d.children; }));
node.enter()
.append("g")
.attr("class", "node")
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; });
node.select("circle")
.transition()
.duration(1000)
.attr("r", function(d) { return d.r; })
.style("fill", function(d,i) { return color(i); });
node.transition().attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("circle")
.attr("r", function (d) { return d.r; })
.style("fill", function (d, i) { return color(i); });
node.exit()
.remove();
// Returns a flattened hierarchy containing all leaf nodes under the root.
function classes(root)
{
var classes = [];
function recurse(name, node) {
if (node.children)
node.children.forEach(function(child) { recurse(node.name, child); });
else
classes.push({packageName: name, className: node.name, value: node.size});
}
recurse(null, root);
return {children: classes};
}
This is the classic case when you need to capture the enter selection on the svg:g group element in order to apply the enter/update/exit pattern correctly. But, to keep object constancy, and so that your labels still point to the right elements, you also need to key your data based on some data property of interest (d.className, which is generated from d.name).
Here is the main segment of your revised bubble update function:
var node = svg.selectAll(".node")
.data(
bubble.nodes(classes(root)).filter(function (d){return !d.children;}),
function(d) {return d.className} // key data based on className to keep object constancy
);
// capture the enter selection
var nodeEnter = node.enter()
.append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
// re-use enter selection for circles
nodeEnter
.append("circle")
.attr("r", function (d) {return d.r;})
.style("fill", function (d, i) {return color(i);})
// re-use enter selection for titles
nodeEnter
.append("title")
.text(function (d) {
return d.className + ": " + format(d.value);
});
The complete FIDDLE is here.
I also blogged on the matter of applying the enter/update/exit pattern to svg:g elements, if you are interested.
Related
I'm newbie with D3.js and I have created a grouped bar chart. And I would like to put the value at the top of echa bar. But, I'm not able to do it :(
I have found several solutiones but I cannot do it works fine.
You can find all the code of my development here:
The function where I create all the bars is:
setBars(canvas, data, scales, keys, colors) {
let height = HEIGHT - OFFSET_TOP - OFFSET_BOTTOM;
let bar = canvas
.append("g")
.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + scales.x0Scale(d.shoot) + ",0)";
})
.selectAll("rect")
.data(function(d) {
return keys.map(function(key) {
return { key: key, value: d[key] };
});
})
.enter()
.append("rect")
.attr("class", "rect")
.attr("x", function(d) {
return scales.x1Scale(d.key);
})
.attr("y", function(d) {
return scales.yScale(d.value);
})
.attr("width", scales.x1Scale.bandwidth())
.attr("height", function(d) {
return height - scales.yScale(d.value);
})
.attr("fill", function(d) {
return colors(d.key);
});
//set label over bar
bar
.selectAll("g")
.data(function(d) {
return d.value;
})
.enter()
.append("text")
.attr("class", "bar-text")
.attr("fill", "#000")
.attr("text-anchor", "middle")
.text(function(d) {
console.log("d.value: " + d.value);
return d.value;
})
.attr("x", function(d, i) {
return scales.x1Scale.bandwidth() * (i + 0.5);
})
.attr("y", function(d, i) {
return scales.yScale(d.value) + 8;
});
}
What am I doing wrong?
If you look at the chain in your bar selection you'll see that you're trying to append text elements to rects: bar is a selection of rectangles, not a selection of groups. Appending texts to rectangles, of course, won't work in an SVG.
The solution is breaking that selection. For instance:
//This is the group selection:
let bar = canvas
.append("g")
.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + scales.x0Scale(d.shoot) + ",0)";
});
//Here, you append rects to the groups:
bar.selectAll("rect")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
})
.enter()
.append("rect")
//etc...
//Finally, here, you append texts to the groups:
bar.selectAll("text")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
})
.enter()
.append("text")
//etc...
As you can see, you'll have to change the data() method as well.
I am pretty much new to d3 and i'm working on a d3 project with a friend for a couple of weeks now.
We built a website containing a sankey diagram and a filter that influences the thickness of links and nodes. Therefore the filter has updateSankey() as an event Handler for the change event.
The links are black with stroke-opacity: 0.15
Lately we tried to introduce a feature that appends a linear gradient to a path onmouseover and removes it onmouseout
To make this work we added an eventHandler to each path which calls a function on both the events. in the functions we append or remove the linear gradient. The gradient goes from the color of the source-node to the color of the target-node.
The problem: after filtering, when all the links have been transitioned the source-node and target-node inside the eventhandler isn't updated and therefore the gradient has wrong colors.
this is how it should look like, it works properly if i don't change the filter on the left
as soon as i change the filter on the left, the colors get messed up
I think we have to do a transition to update these colors, but i have absolutely no idea how and where i have to do this, so i would be glad if you guys could help me.
Down below you find all relevant functions as they currently are.
Greetings and thanks alot in advance
bäsi
/**
* Initialize Sankey
*/
function initSankey() {
/*simple initialisation of the sankey, should explain itself*/
svg = d3.select("svg"),
width = +svg.attr("width") - 2*marginleft,
height = +svg.attr("height") - margintop;
formatNumber = d3.format(",.0f"),
format = function (d) { return formatNumber(d) + " %"; },
color = d3.scaleOrdinal(d3.schemeCategory10);
sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 1], [width - 1, height - 6]])
.iterations(0);
t = d3.transition()
.duration(1500)
.ease(d3.easeLinear);
//set attributes for all links
titleGroup = svg.append("g")
.attr("class", "titles")
.attr("font-family", "sans-serif")
.attr("font-size", "150%");
diagram= svg.append("g")
.attr("class", "sankey")
.attr("transform", "translate(" + marginleft + "," + margintop + ")");
linkGroup = diagram.append("g")
.attr("class", "links")
.attr("fill", "none");
//.attr("stroke", "#000")
//.attr("stroke-opacity", 0.2);
//set attributes for all nodes
nodeGroup = diagram.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", 10);
}
/**
* for the filtering and transition by selecting a filter we need to update the sankey and "draw" it new
* */
function updateSankey() {
flush();
filter();
calculateLinks();
switch (lang)
{
case "ger":
d3.json("data/labels-ger.json", helper);
break;
case "fra":
d3.json("data/labels-fr.json", helper);
break;
case "eng":
d3.json("data/labels-en.json", helper);
break;
default:
d3.json("data/labels.json", helper);
}
}
/**
* the main function for "drawing" the saneky, takes the customLinks that where calculated and returns the saneky
* */
function helper(error, labels) {
if (error)
throw error;
labels.links = customLinks;
sankey(labels);
var links = linkGroup.selectAll('path')
.data(labels.links);
//Set attributes for each link separately
links.enter().append("g")
.attr("id",function (d,i) {return "path"+i;})
.attr("from",function (d) { return d.source.name; })
.attr("to",function (d) { return d.target.name; })
.append("path")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.15)
.attr("display", function (d) {
/* don't display a link if the link is smaller than 4%, else it will be just displayed*/
if(d.value < 4.0){return "none";}
else{return "inline";}
})
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke-width", function (d) {return Math.max(1, d.width); })
.on("mouseover",function (d,id) {
var pathGroup = svg.select('#path' + id);
var path = pathGroup.select("path");
/*var from = document.getElementById("path" + id).__data__.source;
var to = document.getElementById("path" + id).__data__.target;
console.log(from)
console.log(to)
*/
var pathGradient = pathGroup.append("defs")
.append("linearGradient")
.attr("id","grad" + id)
.attr("gradientUnit","userSpaceOnUse")
.attr("style","mix-blend-mode: multiply;")
.attr("x1","0%")
.attr("x2","100%")
.attr("y1","0%")
.attr("y2","0%");
pathGradient.append("stop")
.attr("class","from")
.attr("offset","0%")
.attr("style", function (d) {
var color = setColor(d.source);
return "stop-color:" + color + ";stop-opacity:1";
});
pathGradient.append("stop")
.attr("class","to")
.attr("offset","100%")
.attr("style",function (d) {
var color = setColor(d.target);
return "stop-color:" + color + ";stop-opacity:1";
});
path.attr("stroke","url(#grad"+id+")")
.attr("stroke-opacity","0.95");
})
//.attr("onmouseover",function (d,i) { return "appendGradient(" + i + ")" })
.on("mouseout",function (d, id) {
pathGroup = svg.select('#path' + id);
var path = pathGroup.select("path");
var pathGradient = pathGroup.select("defs")
.remove();
path.attr("stroke","#000")
.attr("stroke-opacity","0.15");
})
//.attr("onmouseout",function (d,i) { return "removeGradient(" + i + ")" })
.append("title")
.text(function (d) {
//tooltip info for the links
return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
linkGroup.selectAll("g").transition(t)
.attr("id",function (d,i) {return "path"+i;})
.attr("from",function (d) { return d.source.name; })
.attr("to",function (d) { return d.target.name; });
links.transition(t)
.attr("display", function (d) {
//again if the link is smaller than 4% don't display it, we have to do this method again because of the
// transition, if another filter is selected
if(d.value < 4.0){return "none";}
else{return "inline";}
})
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke-width", function (d) { return Math.max(1, d.width); })
.select('title')
.text(function (d) {
//same argumentation as above, we need the method again for the transition
return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
//remove the unneeded links
links.exit().remove();
var nodes = nodeGroup.selectAll('.node')
.data(labels.nodes);
var nodesEnter = nodes.enter()
.append("g")
.attr('class', 'node');
//set attributes for each node separately
nodesEnter.append("rect")
.attr("x", function (d) { return d.x0; })
.attr("y", function (d) { return d.y0; })
.attr("height", function (d) { return d.y1 - d.y0; })
.attr("width", function (d) {
var width = d.x1 - d.x0;
if(d.value > 0)
{
//this is used for the years above the nodes, every x position of all nodes is pushed in an array
columnCoord.push(d.x0 + width/2);
}
return width;
})
.attr("fill", setColor)
.attr("stroke", "#000")
.attr("fill-opacity", 0.5)
//specify Pop-Up when hovering over node
nodesEnter.append("title")
.text(function (d) { return d.name + "\n" + format(d.value); });
//Update selection
var nodesUpdate = nodes.transition(t);
//same as the links we have to state the methods again in the update
nodesUpdate.select("rect")
.attr("y", function (d) { return d.y0; })
.attr("x", function (d) { return d.x0; })
.attr("height", function (d) { return d.y1 - d.y0; });
nodesUpdate.select("title")
.text(function (d) { return d.name + "\n" + format(d.value); });
//Exit selection
nodes.exit().remove();
//we filter all arrays
columnCoord = filterArray(columnCoord);
if(!titlesDrawn)
{
drawTitles();
titlesDrawn = true;
}
}
I am creating a sankey diagram using D3. I am trying to redraw the diagram with additional node and link and using transition to animate the previous diagram to the new diagram. I was able to add in new node and link but the old nodes and links did not change position. Since the new node and link could be added at any place within the diagram, I do not want to clear and redraw the entire svg, but use transition to get from the old diagram to the new one. The code to draw the sankey diagram is this:
function draw(data){
// Set the sankey diagram properties
var sankey = d3sankey()
.nodeWidth(17)
.nodePadding(27)
.size([width, height]);
var path = sankey.link();
var graph = data;
sankey.nodes(graph.nodes)
.links(graph.links)
.layout(32);
sankey.relayout();
// add in the links
link.selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("fill", "none")
.style("stroke", function(d){
return "grey";
})
.style("stroke-opacity", "0.4")
.on("mouseover", function() { d3.select(this).style("stroke-opacity", "0.7") } )
.on("mouseout", function() { d3.select(this).style("stroke-opacity", "0.4") } )
.style("stroke-width", function (d) {
return Math.max(1, d.dy);
})
.sort(function (a, b) {
return b.dy - a.dy;
});
link.transition().duration(750);
//link.exit();
// add in the nodes
var node = nodes.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
// add the rectangles for the nodes
node.append("rect")
.attr("height", function (d) {
return d.dy;
})
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function (d) {
return d.dy / 2;
})
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("text-shadow", "0 1px 0 #fff")
.attr("transform", null)
.text(function (d) {
return d.name;
})
.filter(function (d) {
return d.x < width / 2;
})
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
node.transition().duration(750);
}
The JSFiddle
Is it possible to use transition to add in new node and link and reposition
old nodes and links?
Thanks!
I was able to do this by using moving the nodes and links to new position. The code for that is:
var nodes = d3.selectAll(".node")
.transition().duration(750)
.attr('opacity', 1.0)
.attr("transform", function (d) {
if(d.node == 3){
console.log(d.x, d.y);
}
return "translate(" + d.x + "," + d.y + ")";
});
var nodeRects = d3.selectAll(".node rect")
.attr("height", function (d) {
if(d.node == 3){
console.log(d.dy);
}
return d.dy;
})
var links = d3.selectAll(".link")
.transition().duration(750)
.attr('d', path)
.attr('opacity', 1.0)
Updated JSFiddle
I adapted Ger Hobbelt's excellent example of group/bundle nodes
https://gist.github.com/GerHobbelt/3071239
as a JSFiddle here:
https://jsfiddle.net/NovasTaylor/tco2fkad/
The display demonstrates both collapsible nodes and regions (hulls).
The one tweak that eludes me is how to add labels to expanded nodes. I have successfully added labels to nodes in my other force network diagrams using code similar to:
nodes.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
// d.name is a label for the node, present in the JSON source
return d.name;
});
Is anyone familiar enough with Ger's example to guide me in the right direction?
On enter, instead of appending circle append a g with a circle and a text. Then re-factor a bit to fix the movement of the g instead of the circle. Finally, append write out the .text() only if the node has a name (meaning it's a leaf):
node = nodeg.selectAll("g.node").data(net.nodes, nodeid);
node.exit().remove();
var onEnter = node.enter();
var g = onEnter
.append("g")
.attr("class", function(d) { return "node" + (d.size?"":" leaf"); })
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
g.append("circle")
// if (d.size) -- d.size > 0 when d is a group node.
.attr("r", function(d) { return d.size ? d.size + dr : dr+1; })
.style("fill", function(d) { return fill(d.group); })
.on("click", function(d) {
expand[d.group] = !expand[d.group];
init();
});
g.append("text")
.attr("fill","black")
.text(function(d,i){
if (d['name']){
return d['name'];
}
});
And refactored tick to use g instead of circle:
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
Updated fiddle.
I am having some issues with d3js and I can't figure out what is going on. The idea is to draw initial graph from some endpoint data (first img), that's fine works well. Each node is clickable, on click ajax call is made for that node and data is returned, based on some criteria at that point nodes.push(xx), links.push(xx) happens to add new nodes and restart() is called to draw new nodes and links. The issue is that the main graph is doing the correct thing (Not showed on screenshots as I had to put fake data on the first graph i.e. calling an endpoint /record/id/first doesn't return a data) but there are bunch of random nodes showing up in the right bottom corner.
You can also see on the example below, even if the data doesn't change after clicking on first/second/third something wrong goes with node.enter() after restart() with the same data passed in...
JS FIDDLE: http://jsfiddle.net/5754j86e/
var w = 1200,
h = 1200;
var nodes = [];
var links = [];
var node;
var link;
var texts;
var ids = [];
var circleWidth = 10;
var initialIdentifier = "marcin";
nodes = initialBuildNodes(initialIdentifier, sparql);
links = initialBuildLinks(sparql);
//Add SVG
var svg = d3.select('#chart').append('svg')
.attr('width', w)
.attr('height', h);
var linkGroup = svg.append("svg:g").attr("id", "link-group");
var nodeGroup = svg.append("svg:g").attr("id", "node-group");
var textGroup = svg.append("svg:g").attr("id", "text-group");
//Add Force Layout
var force = d3.layout.force()
.size([w, h])
.gravity(.05)
.charge(-1040);
force.linkDistance(120);
restart();
function restart() {
force.links(links)
console.log("LINKS ARE: ", links)
link = linkGroup.selectAll(".link").data (links);
link.enter().append('line')
.attr("class", "link");
link.exit().remove();
force.nodes(nodes)
console.log("NODES ARE: ", nodes)
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
})
.on("click", function(d) {
nodeClicked (d);
})
.on('mouseenter', function(d){
nodeMouseEnter(d)
})
.on('mouseout', function(d){
nodeMouseOut(d)
});
node.exit().remove();
var annotation = textGroup.selectAll(".annotation").data (nodes);
annotation.enter().append("svg:g")
.attr("class", "annotation")
.append("text")
.attr("x", function(d) { return d.radius + 4 })
.attr("y", ".31em")
.attr("class", "label")
.text(function(d) { return d.name; });
annotation.exit().remove();
force.start();
}
function nodeClicked (d) {
// AJAX CALL happens here and bunch of nodes.push({name: "new name"}) happen
}
force.on('tick', function(e) {
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 })
node.attr('transform', function(d, i) {
return 'translate('+ d.x +', '+ d.y +')';
})
svg.selectAll(".annotation").attr("transform", function(d) {
var labelx = d.x + 13;
return "translate(" + labelx + "," + d.y + ")";
})
});
Okay I got it, based on the docs (https://github.com/mbostock/d3/wiki/Selections#enter):
var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */
From my code you can see I do enter() and then once again I add circle on node in a separate statement.
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
Adding circle should be within the scope of enter() otherwise it happens to all nodes not only the new nodes therefore it should be :
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag)
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});