Creating a d3.js Treemap zoomable with CSV - javascript

I want to create a Treemap with d3 but the enitities I want to show don't have a hierarchy. Is it still possible to create a Treemap?
My data includes crimes in Germany.
The I just wanted to show in the beginning all the crimes (Treemap not zoomed). then if I click on one of the boxes it will show me how many of them are male or female.
Sounds so easy but I really don't get it with my example.
I have tried so many examples but I dont get it because of the hierarchy.
First screenshot
Seccond Screenshot

You can arrange tabular data in a hierarchy using d3.nest(). This function groups the data under multiple key fields, similar to a GROUP BY statement in SQL - more detail can be found in the nest documentation. For an introduction to using d3.nest(), see this guide.
D3's treemap requires data to be arranged in a hierarchy with a root node, so you should nest all table entries under a single 'root' value as well as any fields that you wish to use to subdivide the treemap. The following code will arrange your data into a hierarchy that splits it by Sexus only:
var nested_data = d3.nest()
// Create a root node by nesting all data under the single value "root":
.key(function () { return "root"; })
// Nest by other fields of interest:
.key(function (d) { return d.Sexus; })
.entries(data);
For example, the following code shows a single rectangle in the treemap for all crimes and displays the number of crimes by Sexus when the user clicks on the rectangle:
<script src="js/d3.v4.js"></script>
<script>
var margin = { top: 40, right: 10, bottom: 10, left: 10 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var color = d3.scaleOrdinal().range(d3.schemeCategory20c);
var treemap = d3.treemap().size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", (width + margin.left + margin.right) + "px")
.attr("height", (height + margin.top + margin.bottom) + "px");
d3.csv("data/GermanyCrimeData.csv", function (error, data) {
if (error) throw error;
var nested_data = d3.nest()
// Create a root node by nesting all data under the single value "root":
.key(function () { return "root"; })
// Nest by other fields of interest:
.key(function (d) { return d.Sexus; })
.entries(data);
var root = d3.hierarchy(nested_data[0], function (d) { return d.values; })
.sum(function (d) { return d.Tatverdaechtige_Insgesamt_deutsch; });
var tree = treemap(root);
var nodes = svg.selectAll(".node")
.data([tree]) // Bind an array that contains the root node only.
.enter().append("g")
.attr("class", "node")
.attr('transform', function (d) {
return 'translate(' + [d.x0, d.y0] + ')'
})
.on("click", function () {
d3.select(this).select("text").style("visibility", "visible");
//See https://stackoverflow.com/questions/44074031/d3-show-hide-text-of-only-clicked-node
});
nodes.append("rect")
.attr("width", function (d) { return Math.max(0, d.x1 - d.x0 - 1) + "px"; })
.attr("height", function (d) { return Math.max(0, d.y1 - d.y0 - 1) + "px"; })
.attr("fill", function (d) { return color(d.data.key); });
nodes.append("text")
.attr("dx", 4)
.attr("dy", 14)
.style("visibility", "hidden") //Initially the text is not visible
.text(function (d) {
// Create an array of Tatverdaechtige_Insgesamt_deutsch counts for each Sexus:
var array = d.children.map(function (elt) { // Here, map is a native Javascript function, not a D3 function
// Return "Sexus: Tatverdaechtige_Insgesamt_deutsch":
return elt.data.key + ": " + elt.value;
// Note that d.data.key and d.value were created by d3.nest()
});
// Return a string representation of the array:
return d.data.key + " - " + array.join(", ");
});
});
</script>
To subdivide the treemap by a given field, the data must be nested by that field and tree.children must be bound to the DOM instead of [tree]:
//...
d3.csv("data/GermanyCrimeData.csv", function (error, data) {
if (error) throw error;
var nested_data = d3.nest()
.key(function () { return "root"; })
.key(function (d) { return d.Straftat; })
.key(function (d) { return d.Sexus; })
.entries(data);
var root = d3.hierarchy(nested_data[0], function (d) { return d.values; })
.sum(function (d) { return d.Tatverdaechtige_Insgesamt_deutsch; });
var tree = treemap(root);
var nodes = svg.selectAll(".node")
.data(tree.children) // Bind the children of the tree's root node
.enter().append("g")
.attr("class", "node")
.attr('transform', function (d) {
return 'translate(' + [d.x0, d.y0] + ')'
})
.on("click", function () {
d3.select(this).select("text").style("visibility", "visible");
//See https://stackoverflow.com/questions/44074031/d3-show-hide-text-of-only-clicked-node
});
//...
A simple way to make the treemap zoomable is to place the code that draws the treemap in a function, then use on("click", ...) to re-trigger the function whenever a rectangle is clicked, zooming in on the chosen node:
//...
d3.csv("data/GermanyCrimeData.csv", function (error, data) {
if (error) throw error;
var nested_data = d3.nest()
.key(function () { return "root"; })
.key(function (d) { return d.Stadt_Landkreis; })
.key(function (d) { return d.Straftat; })
.key(function (d) { return d.Sexus; })
.entries(data);
function drawTreemap(root) {
var hierarchical_data = d3.hierarchy(root, function (d) { return d.values; })
.sum(function (d) { return d.Tatverdaechtige_Insgesamt_deutsch; });
var tree = treemap(hierarchical_data);
// Clear the contents of the <svg>:
svg.selectAll("*").remove();
// Now populate the <svg>:
var nodes = svg.selectAll(".node")
.data(tree.children) //Bind the children of the root node in the tree
.enter().append("g")
.attr("class", "node")
.attr('transform', function (d) {
return 'translate(' + [d.x0, d.y0] + ')'
})
.on("click", function (d) {
if (d.children) { //The user clicked on a node that has children
drawTreemap(d.data);
} else { // The user clicked on a node that has no children (i.e. a leaf node).
// Do nothing.
}
});
nodes.append("rect")
.attr("width", function (d) { return Math.max(0, d.x1 - d.x0 - 1) + "px"; })
.attr("height", function (d) { return Math.max(0, d.y1 - d.y0 - 1) + "px"; })
.attr("fill", function (d) { return color(d.data.key); });
nodes.append("text")
.attr("dx", 4)
.attr("dy", 14)
//.style("visibility", "hidden") //Initially the text is not visible
.text(function (d) {
if (d.children) {
return d.data.key;
} else {
return d.data.Stadt_Landkreis + ", "
+ d.data.Straftat + ", "
+ d.data.Sexus + ": "
+ d.data.Tatverdaechtige_Insgesamt_deutsch;
}
});
}; // end of drawTreemap()
// Now call drawTreemap for the first time:
drawTreemap(nested_data[0]);
});
</script>
Finally, to animate the transitions, the following code is a zoomable treemap based on Mike Bostock's example, where flare.json has been replaced with the data from your CSV file, arranged with d3.nest() and d3.hierarchy() (this example uses D3 v3):
<!DOCTYPE html>
<html>
<head>
<style>
#chart {
width: 960px;
height: 500px;
background: #ddd;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
fill: none;
stroke: #fff;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
.grandparent rect {
fill: orange;
}
.grandparent:hover rect {
fill: #ee9700;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.parent {
fill: #bbb;
fill-opacity: .5;
}
.children:hover rect.child {
fill: #bbb;
}
</style>
</head>
<body>
<script src="js/d3.v3.js"></script>
<script>
var margin = {top: 20, right: 0, bottom: 0, left: 0},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
formatNumber = d3.format(",d"),
transitioning;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
var treemap = d3.layout.treemap()
.children(function(d, depth) { return depth ? null : d._children; })
.sort(function(a, b) { return a.value - b.value; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false);
var svg = d3.select("body").append("svg")
.attr("id", "chart") // Apply the #chart style to the <svg> element
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
d3.csv("data/GermanyCrimeData.csv", function (error, data) {
if (error) throw error;
var nested_data = d3.nest()
.key(function () { return "root"; })
.key(function (d) { return d.Stadt_Landkreis; })
.key(function (d) { return d.Straftat; })
.key(function (d) { return d.Sexus; })
.entries(data);
// In D3 v3, the syntax for arranging the data in a hierarchy is different:
var my_hierarchy = d3.layout.hierarchy()
.children(function (d) { return d.values; })
.value(function (d) { return d.Tatverdaechtige_Insgesamt_deutsch; });
// Apply the hierarchy to the nested data:
my_hierarchy(nested_data[0]); // This changes nested_data in-place
var root = nested_data[0];
initialize(root);
// my_hierarchy() has already added the aggregate values to each node
// in the tree, so instead we only need to add d._children to each node:
addUnderscoreChildren(root);
layout(root);
display(root);
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// NEW FUNCTION:
function addUnderscoreChildren(d) {
if (d.children) {
// d has children
d._children = d.children;
d.children.forEach(addUnderscoreChildren);
}
}
/* NOT NEEDED:
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.children)
? d.value = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0)
: d.value;
}*/
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
treemap.nodes({_children: d._children});
d._children.forEach(function(c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) { return d._children; })
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
.data(function(d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function(d) { return formatNumber(d.value); });
g.append("text")
.attr("dy", ".75em")
// Replace d.name with d.key; use a function to provide
// a key for leaf nodes, because nest() does not set their
// 'key' property:
.text(function(d) { return d.key || keyOfLeafNode(d); })
.call(text);
// NEW FUNCTION:
function keyOfLeafNode(d) {
return d.Stadt_Landkreis + " "
+ d.Straftat + " "
+ d.Sexus + ": "
+ d.value;
}
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.attr("x", function(d) { return x(d.x) + 6; })
.attr("y", function(d) { return y(d.y) + 6; });
}
function rect(rect) {
rect.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y); })
.attr("width", function(d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function(d) { return y(d.y + d.dy) - y(d.y); });
}
function name(d) {
// d3.nest() gives each node the property 'key' instead of 'name',
// so replace 'd.name' with 'd.key':
return d.parent
? name(d.parent) + "." + d.key
: d.key;
}
});
</script>
</body>
</html>

Related

gradient on path:hover when path is transitioned

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;
}
}

Return value from JSON, not the children count

Instead of printing the number of children a specific node in flare.json has, I want to print the real value of the child node from flare.json. When hovering over a parent node (a node with children), I want the sum of all children (and sub-children, ...) values printed.
There are similar questions (not specific d3) suggesting a recursive function. But all the code snippets did not help me so far. Does d3 ship with some function to achieve this behavior?
var width = 300,
height = 300,
radius = Math.min(width, height) / 2,
color = d3.scale.category20c();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height * .50 + ")");
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) { return 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); });
d3.json("https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json", function(error, root) {
if (error) throw error;
var data = root;
var path = svg.datum(root).selectAll("path")
.data(partition.nodes)
.enter()
.append("path")
.attr("display", function(d) { return d.depth ? null : "none"; }) // hide inner ring
.attr("d", arc)
.style("stroke", "#fff")
.style("fill-rule", "evenodd")
.style("fill", howToFill)
.each(stash)
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut);
var value = function(d) { return d.value; };
});
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
function arcTween(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
function howToFill(d) {
if ( d.name == "-" ) {
return "#fff";
} else {
return color((d.children ? d : d.parent).name);
}
}
// Mouseover
function onMouseOver(d) {
d3.select(this)
svg.append("text")
.style("text-anchor", "middle")
.text(function() { return d.name + " - " + d.value; });
}
function onMouseOut(d) {
d3.select(this)
svg
.select("text")
.remove();
}
d3.select(self.frameElement).style("height", height + "px");
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link rel="stylesheet" type="text/css" href="main.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
<script type="text/javascript" src="chart.js"></script>
</body>
Here is a version of your code which displays the actual counts:
var width = 300,
height = 300,
radius = Math.min(width, height) / 2,
color = d3.scale.category20c();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height * .50 + ")");
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) { return 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); });
d3.json("https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json", function(error, root) {
if (error) throw error;
// Deep copy of the root object (for use by the mouseover function)
// since the d3 partitioning will modify root and only keep
// partitioning details:
var initialData = JSON.parse(JSON.stringify(root));
var data = root;
var path = svg.datum(root).selectAll("path")
.data(partition.nodes)
.enter()
.append("path")
.attr("display", function(d) { return d.depth ? null : "none"; }) // hide inner ring
.attr("d", arc)
.style("stroke", "#fff")
.style("fill-rule", "evenodd")
.style("fill", howToFill)
.each(stash)
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut);
var value = function(d) { return d.value; };
// Let's move the onMouseOver function inside the json extractor in order
// to easily work with "initialData":
function onMouseOver(d) {
d3.select(this)
svg.append("text")
.style("text-anchor", "middle")
//.text(function() { return d.name + " - " + d.value; });
.text(function() {
return d.name + " - " + sumValue(findElem(initialData, d.name, d.depth, 0));
});
}
});
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
function arcTween(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
function howToFill(d) {
if ( d.name == "-" ) {
return "#fff";
} else {
return color((d.children ? d : d.parent).name);
}
}
function onMouseOut(d) {
d3.select(this)
svg
.select("text")
.remove();
}
d3.select(self.frameElement).style("height", height + "px");
function findElem(elmt, name, searchedDepth, currDepth) {
if (currDepth > searchedDepth)
return undefined
else if (elmt.name == name && searchedDepth == currDepth) {
return elmt;
}
else if (elmt.children) {
var result = elmt.children.map(c => findElem(c, name, searchedDepth, currDepth + 1)).filter(x => x);
return result.length == 1 ? result[0] : undefined;
}
else
return undefined;
}
function sumValue(elmt) {
try {
if (elmt.children)
return elmt.children.map(c => sumValue(c)).reduce((a, b) => a + b, 0);
else
return elmt.value;
} catch(err) {
// The try catch is there to hide the exception due to the "data"
// value which is not supported. Not sure how to cleanly fix this
// corner case:
return "error";
}
}
<meta charset="utf-8">
<head>
<link rel="stylesheet" type="text/css" href="main.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
<script type="text/javascript" src="chart.js"></script>
</body>
During the mouseover, let's first find back the actual data element which is hovered. The thing is, the d3 layout partitioning removes data information from the model... So that's why we need to get back to the original data to find back the node in question:
function findElem(elmt, name, searchedDepth, currDepth) {
if (currDepth > searchedDepth)
return undefined
else if (elmt.name == name && searchedDepth == currDepth) {
return elmt;
}
else if (elmt.children) {
var result = elmt.children.map(c => findElem(c, name, searchedDepth, currDepth + 1)).filter(x => x);
return result.length == 1 ? result[0] : undefined;
}
else
return undefined;
}
Which is called from the mouseover function this way:
.text( function() {
return d.name + " - " + sumValue(findElem(initialData, d.name, d.depth, 0));
});
where initialData is a deep copy of the json (otherwise, since the extracted json root is modified by the partitioning, we would loose original data information).
Notice the usage of the depth in order to handle the fact that different nodes can have the same label (for instance there are several nodes at different depths named "data").
Once we have the node in question, we can recursively sum all children values this way:
function sumValue(elmt) {
if (elmt.children)
return elmt.children.map(c => sumValue(c)).reduce((a, b) => a + b, 0);
else
return elmt.value;
}

updating/modifying d3 word cloud with new data

I'm trying to figure out how to modify and update from a selection of data with d3.js wordcloud.
Currently, I'm showing the top 10 results from a selection of data depending on indexed keys. I'd like to be able to switch this data depending on the keys, or if I want the top 10 or bottom 10 words.
here is a plnk so far;
http://plnkr.co/edit/cDTeGDaOoO5bXBZTHlhV?p=preview
I've been trying to refer to these guides, General Update Pattern, III and Animated d3 word cloud. However, I'm struggling to comprehend how to introduce a final update function, as almost all guides referring to this usually use a setTimeout to demonstrate how to update, and my brain just won't make the connection.
Any advice is most welcome!
Cheers,
(code here)
var width = 455;
var height = 310;
var fontScale = d3.scale.linear().range([0, 30]);
var fill = d3.scale.category20();
var svg = d3.select("#vis").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")")
// .selectAll("text")
d3.json("data.json", function(error, data) {
if (error) {
console.log(error)
}
else {
data = data
}
function sortObject(obj) {
var newValue = [];
var orgS = "MC";
var dateS = "Jan";
for (var question = 0; question < data.questions.length; question++) {
var organization = data.organizations.indexOf(orgS);
var date = data.dates.indexOf(dateS);
newValue.push({
label: data.questions[question],
value: data.values[question][organization][date]
});
}
newValue.sort(function(a, b) {
return b.value - a.value;
});
newValue.splice(10, 50)
return newValue;
}
var newValue = sortObject();
fontScale.domain([
d3.min(newValue, function(d) {
return d.value
}),
d3.max(newValue, function(d) {
return d.value
}),
]);
d3.layout.cloud().size([width, height])
.words(newValue)
.rotate(0)
.text(function(d) {
return d.label;
})
.font("Impact")
.fontSize(function(d) {
return fontScale(d.value)
})
.on("end", draw)
.start();
function draw(words) {
var selectVis = svg.selectAll("text")
.data(words)
selectVis
.enter().append("text")
.style("font-size", function(d) {
return fontScale(d.value)
})
.style("font-family", "Impact")
.style("fill", function(d, i) {
return fill(i);
})
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) {
return d.label;
})
selectVis
.transition()
.duration(600)
.style("font-size", function(d) {
return fontScale(d.value)
})
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.style("fill-opacity", 1);
selectVis.exit()
.transition()
.duration(200)
.style('fill-opacity', 1e-6)
.attr('font-size', 1)
.remove();
}
});
I did not see any update function within your code so I added that functionality in order to watch how the update works.
// Add a select elemnt to the page
var dropDown = d3.select("#drop")
.append("select")
.attr("name", "food-venues");
// Join with your venues
var foodVenues = data.organizations.map(function(d, i) {
return d;
})
// Append the venues as options
var options = dropDown.selectAll("option")
.data(foodVenues)
.enter()
.append("option")
.text(function(d) {
return d;
})
.attr("value", function(d) {
return d;
})
// On change call the update function
dropDown.on("change", update);
In order for a d3 word cloud to update correctly you need to calculate again the layout with the desired data
function update() {
// Using your function and the value of the venue to filter data
var filteredData = sortObject(data, this.value);
// Calculate the new domain with the new values
fontScale.domain([
d3.min(newValue, function(d) {
return d.value
}),
d3.max(newValue, function(d) {
return d.value
}),
]);
// Calculate the layout with new values
d3.layout.cloud()
.size([width, height])
.words(filteredData)
.rotate(0)
.text(function(d) {
return d.label;
})
.font("Impact")
.fontSize(function(d) {
return fontScale(d.value)
})
.on("end", draw)
.start();
}
I modified your sortObject function to receive an extra parameter which is the desired venue:
function sortObject(obj, venue) {
var newValue = [];
var orgS = venue || "MC";
// ....
}
Here is the working plnkr: http://plnkr.co/edit/B20h2bNRkyTtfs4SxE0v?p=preview
You should be able to use this approach to update with your desired restrictions. You may be able to add a checkbox with a event listener that will trigger the update function.
In your html:
<input checked type="checkbox" id="top" value="true"> <label for="top">Show top words</label>
In your javascript:
var topCheckbox = d3.select('#top')
.on("change", function() {
console.log('update!')
});

use d3 treemap to go upto child on going on clicking parent to child nodes

I have tried this code .I am new to d3.
but able to directly reach to child node directly
def city ,xyz city and abc city are displayed on treemap
data.json
{
"name":"country",
"children":
[
{
"name": "Verizona State",
"value": 100,
"children": [
{
"name": "xyz city",
"value": 30
},
{
"name": "abc city",
"value": 40
}
]
},
{
"name": "New Jersey",
"value": 50,
"children": [
{
"name": "def city",
"value": 30
}
]
}
]
}
index.html
<html>
<script src="http://d3js.org/d3.v3.min.js"></script>
<body>
<div id="heatmap">
<script>
var color = d3.scale.category10();
var canvas = d3.select("#heatmap").append("svg")
.attr("width",500)
.attr("height",500);
d3.json("data.json" ,function(data){
var treemap=d3.layout.treemap()
.size([500,500])
.nodes(data);
console.log(treemap);
var cells = canvas.selectAll(".cell")
.style("position", "relative")
.data(treemap)
.enter()
.append("g")
.attr("class","cell")
.attr("stroke","#fff");
cells.append("rect")
.attr( "x" , function(d) { console.log(d); return d.x; })
.attr("y", function(d) { return d.y; })
.attr("width", function(d) { return d.dx; })
.attr("height", function(d) { return d.dy; })
.attr("fill", function(d){return d.children ? null:color(d.parent.name); })
cells.append("text")
.attr("x",function(d) {return d.x + d.dx/2})
.attr("y",function(d) {return d.y + d.dy/2})
.text(function(d){ return d.children? null :d.name;})
})
</script>
</div>
</body>
</html>
I want a way to display first
Verizona State and New Jersey on treemap
and on clicking verizona state to get xyz city and abc city on treemap
Please suggest changes.
I solved this problem using this example of mike
http://bost.ocks.org/mike/treemap/
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script></script>
<script>
var margin = {top: 20, right: 0, bottom: 0, left: 0},
width = 960,
height = 500 - margin.top - margin.bottom,
formatNumber = d3.format(",d"),
transitioning;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
var treemap = d3.layout.treemap()
.children(function(d, depth) { return depth ? null : d._children; })
.sort(function(a, b) { return a.value - b.value; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false);
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
d3.json("flare.json", function(root) {
initialize(root);
accumulate(root);
layout(root);
display(root);
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.children)
? d.value = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0)
: d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
treemap.nodes({_children: d._children});
d._children.forEach(function(c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) { return d._children; })
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
.data(function(d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function(d) { return formatNumber(d.value); });
g.append("text")
.attr("dy", ".75em")
.text(function(d) { return d.name; })
.call(text);
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.attr("x", function(d) { return x(d.x) + 6; })
.attr("y", function(d) { return y(d.y) + 6; });
}
function rect(rect) {
rect.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y); })
.attr("width", function(d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function(d) { return y(d.y + d.dy) - y(d.y); });
}
function name(d) {
return d.parent
? name(d.parent) + "." + d.name
: d.name;
}
});
</script>
<script>
</script>

Adding link to d3js force layout cause bad data in nodes

I am working on a d3js side project where I am using the force layout to show nodes and lines to represent a graph. I have laid out the code such that the graph can be dynamically updated. I am doing this by:
Clearing force.nodes() then force.links()
Pushing all the nodes I want to add to force.nodes()
Updating the graph (join, enter, update, exit)
Pushing all the links I want with references to the nodes in force.nodes() to force.links
Updating the graph (join, enter, update, exit)
This works as long as I only have nodes to display, but as soon as I attempt to push a link to force.links then all hell breaks loose and the two nodes end up hiding in the top left corner.
Looking at the DOM I can see the following:
As you can see, the transform/translate parameters contain NaN values. So something blew up in my face but after two days of bug hunting I suspect that I am missing something here and need to take a cold shower.
I have stripped down my code to the smallest set that can reproduce the error. Please note from it that the nodes display fine until a link is pushed into force.links. No links are being drawn, only nodes, but the act of pushing a link where it belongs is disrupting the data in the nodes. This way of updating a graph should work as per the examples I have seen.
d3.json("data/fm.json", function(error, graph) {
if (error) throw error;
function chart(elementName) {
// look for the node in the d3 layout
var findNode = function(name) {
for (var i in nodes) {
if (nodes[i]["name"] === name) return nodes[i];
};
};
var width = 960, // default width
height = 450, // default height
color = d3.scale.category10(),
force = d3.layout.force(),
nodes = force.nodes(),
links = force.links(),
vis,
runOnceFlag = true;
vis = d3.select(elementName)
.append("svg:svg")
.attr("width", width)
.attr("height", height)
.attr("id", "svg")
.attr("pointer-events", "all")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("perserveAspectRatio", "xMinYMid")
.append('svg:g');
var update = function() {
var node = vis.selectAll("g.node")
.data(nodes, function (d) {
return d.name;
});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", 12)
.attr("id", function (d) {
return "Node;" + d.name;
})
.attr("class", "nodeStrokeClass")
.attr("fill", function(d) { return color(d.group); });
nodeEnter.append("svg:text")
.attr("class", "textClass")
.attr("x", 14)
.attr("y", ".31em")
.text(function (d) {
return d.name;
});
node.exit().remove();
force.on("tick", function () {
node.attr("transform", function (d) {
console.log(d);
return "translate(" + d.x + "," + d.y + ")";
});
});
// Restart the force layout.
force
.charge(-120)
.linkDistance( function(d) { return d.value * 10 } )
.size([width, height])
.start();
};
var a = graph.nodes[0];
var b = graph.nodes[1]
nodes.push(a);
update();
nodes.push(b);
update();
var c = {"source": findNode('a'), "target": findNode('b')}
// the line below causes the error
links.push(c);
update()
};
///
chart('body');
});
This is my data:
{
"nodes":[
{"name":"a", "group":1},
{"name":"b", "group":2},
{"name":"c", "group":3},
{"name":"d", "group":4},
{"name":"e", "group":5},
{"name":"f", "group":6},
{"name":"g", "group":7},
{"name":"h", "group":1},
{"name":"i", "group":2},
{"name":"j", "group":3},
{"name":"k", "group":4},
{"name":"l", "group":5},
{"name":"m", "group":6},
{"name":"n", "group":7}
],
"links":[
{"source":0,"target":1,"value":1},
{"source":2,"target":3,"value":1},
{"source":4,"target":5,"value":1},
{"source":7,"target":8,"value":1},
{"source":9,"target":10,"value":1},
{"source":11,"target":12,"value":1},
{"source":0,"target":5,"value":1},
{"source":1,"target":5,"value":1},
{"source":0,"target":6,"value":1},
{"source":1,"target":6,"value":1},
{"source":0,"target":7,"value":1},
{"source":1,"target":7,"value":1},
{"source":2,"target":8,"value":1},
{"source":3,"target":8,"value":1},
{"source":2,"target":9,"value":1},
{"source":3,"target":9,"value":1},
{"source":4,"target":11,"value":1},
{"source":5,"target":11,"value":1},
{"source":9,"target":12,"value":1},
{"source":10,"target":12,"value":1},
{"source":11,"target":13,"value":1},
{"source":12,"target":13,"value":1}
]
}
You have some basic problems with your code, see corrected concept below...
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<style>
.link {
stroke: #2E2E2E;
stroke-width: 2px;
}
.node {
stroke: #fff;
stroke-width: 2px;
}
.textClass {
stroke: #323232;
font-family: "Lucida Grande", "Droid Sans", Arial, Helvetica, sans-serif;
font-weight: normal;
stroke-width: .5;
font-size: 14px;
}
</style>
</head>
<body>
<!--<script src="d3 CB.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
d3.json("data.json", function (error, graph) {
if (error) throw error;
function chart(elementName) {
// look for the node in the d3 layout
var findNode = function (name) {
for (var i in nodes) {
if (nodes[i]["name"] === name) return nodes[i];
}
};
var width = 960, // default width
height = 450, // default height
color = d3.scale.category10(),
nodes = graph.nodes,
links = graph.links,
force = d3.layout.force()
.nodes(nodes)
.links([]),
vis,
runOnceFlag = true;
vis = d3.select(elementName)
.append("svg:svg")
.attr("width", width)
.attr("height", height)
.attr("id", "svg")
.attr("pointer-events", "all")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("perserveAspectRatio", "xMinYMid")
.append('svg:g');
var update = function () {
var link = vis.selectAll("line")
.data(force.links(), function (d) {
return d.source + "-" + d.target;
});
link.enter().insert("line", "g")
.attr("id", function (d) {
return d.source + "-" + d.target;
})
.attr("stroke-width", function (d) {
return d.value / 10;
})
.attr("class", "link")
.style("stroke", "red")
.transition().duration(5000).style("stroke", "black");
link.append("title")
.text(function (d) {
return d.value;
});
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function (d) {
return d.name;
});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", 12)
.attr("id", function (d) {
return "Node;" + d.name;
})
.attr("class", "nodeStrokeClass")
.attr("fill", function (d) {
return color(d.group);
});
nodeEnter.append("svg:text")
.attr("class", "textClass")
.attr("x", 14)
.attr("y", ".31em")
.text(function (d) {
return d.name;
});
node.exit().remove();
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;
});
node.attr("transform", function (d) {
console.log(d);
return "translate(" + d.x + "," + d.y + ")";
});
});
// Restart the force layout.
force
.charge(-120)
.linkDistance(function (d) {
return d.value * 100
})
.size([width, height])
.start();
};
update();
var c = {"source": findNode('a'), "target": findNode('b'), value: 1}
// the line below causes the error
window.setTimeout(function() {
force.links().push(c);
update()
},2000)
};
//
chart('body');
});
</script>
</body>
</html>

Categories

Resources