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!')
});
Related
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>
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've uploaded a block (FIXED) where you can toggle a sorting function.
What I want to add now is some kind of if statement when the checkbox is on, and when it is on I want the bars to sort automatically when you change year or category, and when you toggle it again it stops auto-sorting.
I thought a simple
if (document.getElementsByClassName('myCheckbox').checked) {
change();
}
Within the update function would work but nothing happens.
Any help is appreciated!
I started an answer your direct question, but soon realized that your code needed a bit of refactor. You had a bit too much copy/paste going on with redundant code and too many things drawing. When coding with d3 you should try for a single function that does all the drawing.
Here's the code running.
Here's a snippet of the new one update function to rule them all:
function update() {
file = d3.select('#year').property('value') == 'data2017' ? 'data.csv' : 'data2.csv';
catInt = d3.select('#category').property('value');
d3.csv(file, type, function(error,data) {
if(error) throw error;
var sortIndex = data.map(function(d){ return d.month});
// Update domain
y.domain([0, d3.max(data, function(d) {
return d["Category" + catInt]; })
]).nice();
// Update axis
g.selectAll(".axis.axis--y").transition()
.duration(750)
.call(yAxis);
g.selectAll(".axis.grid--y").transition()
.duration(750)
.call(yGrid);
// Sums and averages
let sumOfAll = d3.sum(data, function(d) {
return d["Category" + catInt];
});
let avgValue = d3.sum(data, function(d) {
return d["Category" + catInt];
}) / data.length;
//sort data
data.sort( d3.select("#myCheckbox").property("checked")
? function(a, b) { return b["Category" + catInt] - a["Category" + catInt]; }
: function(a, b) { return sortIndex.indexOf(a.month) - sortIndex.indexOf(b.month);})
// set x domain
x.domain(data.map(function(d) { return d.month; }));
g.selectAll(".axis.axis--x").transition()
.duration(750)
.call(xAxis);
// Update rectangles
let bars = g.selectAll(".barEnter")
.data(data, function(d){
return d.month;
});
bars = bars
.enter()
.append("rect")
.attr("class", "barEnter") // Enter data reference
.attr("width", x.bandwidth())
.merge(bars);
bars.transition()
.duration(750)
.attr("height", function(d) {
return height - y(d["Category" + catInt]);
})
.attr("x", function(d) {
return x(d.month);
})
.attr("y", function(d) {
return y(d["Category" + catInt]);
});
bars.exit().remove();
// Update text on rectangles
let textUpdate = g.selectAll(".textEnter")
.data(data, function(d){
return d.month;
});
textUpdate = textUpdate.enter()
.append("text")
.style("text-shadow","1px 1px #777")
.attr("class", "textEnter") // Enter data reference
.attr("text-anchor","middle")
.attr("font-size",11)
.attr("fill","#fff")
.merge(textUpdate);
textUpdate.transition()
.duration(750)
.attr("y", function(d) {
return y(d["Category" + catInt]) + 15;
})
// Update text value
.text( function(d) {
return d["Category" + catInt];
})
.attr("x", function(d) {
return x(d.month) + x.bandwidth()/2;
})
// Update sum and avg value
g.selectAll("#totalValue").transition()
.duration(750)
.text(sumOfAll + " Category " + catInt)
g.selectAll("#avgValue").transition()
.duration(750)
.text(formatValue(avgValue))
});
}
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 }
});
I currently am running a page which generates a graph with default values upon page load. The page takes the data from a TSV generated by a PHP script, modified by GET parameters.
The user can then input options, and update the graph through AJAX.
Currently the page is almost working, but it is is overlaying the new paths with the new data without removing the old paths.
The new data has the same x range and domain but different y coordinate values, sometimes with a different number of values.
Ideally I would like the old paths to fluidly transition from the old paths - how can I make this occur?
I've tried to include the relevant code below. Apologies for its poor quality, I am very new to d3.
...
var line = d3.svg.line()
.interpolate("basis")
.defined(function(d) {
return d.result != 0;
})
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.result);
});
var svg = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var txtDays = 7;
var txtStartDate = "01/01/2013";
var txtEndDate = "01/01/2014";
var txtInterval = 1;
requestDataURL = //removed for SO
d3.tsv("http://localhost" + requestDataURL, function(error, data) {
var varPolls = d3.keys(data[0]).filter(function(key) {
return key !== "date";
});
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var results = varPolls.map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
date: d.date,
result: +d[name]
};
})
};
});
x.domain(d3.extent(data, function(d) {
return d.date;
}));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
var group = svg.selectAll(".group")
.data(results)
.enter().append("g")
.attr("class", "group")
.attr("data-name", function(d) {
return d.name;
});
group.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return colors[d.name];
});
group.append("text")
.datum(function(d) {
return {
name: d.name,
value: d.values[d.values.length - 1]
};
})
.attr("transform", function(d) {
return "translate(" + x(d.value.date) + "," + y(d.value.result) + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) {
return Math.round(d.value.result);;
});
d3.select(".submit")
.attr('disabled', null);
});
$(".submit").click(function(event) {
var data = [];
//SORT OUT VALIDATION
var req = $.ajax({
url: requestDataURL,
dataType: 'text',
success: function(response) {
data = response;
}
});
requestDataURL = //new data removed for SO
$.when(req).done(function() {
d3.tsv("http://localhost" + requestDataURL, function(error, data) {
var varPolls = d3.keys(data[0]).filter(function(key) {
return key !== "date";
});
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var results = varPolls.map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
date: d.date,
result: +d[name]
};
})
};
});
x.domain(d3.extent(data, function(d) {
return d.date;
}));
var group = svg.selectAll(".chart")
.data(results);
group.exit().remove();
group.enter().append("g");
group.attr("class", "group")
.attr("data-name", function(d) {
return d.name;
});
group.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return colors[d.name];
});
group.transition()
.duration(500)
.ease("linear")
.attr("d", group);
});
});
});
The problem is that you're not handling the enter and update selections correctly. As a rule, append operations should only happen on enter selections, not update. When you're getting new data, you have the following code:
group.enter().append("g");
// ...
group.append("path");
This will append new path elements to the update selection, which is what you're seeing in the graph. The proper way to handle new data would look as follows:
var enterSel = group.enter().append("g");
// set attributes on the g elements
enterSel.append("path"); // append path elements to the new g elements
group.select("path") // select the path elements that are present, this includes the newly appended ones
.attr("d", function(d) { // update the d attribute
return line(d.values);
});
This code will append new elements for data items that have no corresponding elements and update the paths for existing elements.