Conditionally build group in d3 - javascript

I am trying to build a graph using the force layout in D3. I would like to build different looking nodes depending on the data. Currently all nodes have a category and a name. So I draw an svg:g consisting of two rect and two text elements.
My code currently looks something like this:
// nodes are in the form: { group: 'string', name: 'string2' }
this.node = this.node.data(this.node, function(d) { return d.id; });
var g = this.node.enter().
append('svg:g').
attr('transform', function(d) { return 'translate('+ d.x +','+ d.y +')'; });
g.append('svg:rect').attr('h', 20).attr('w', 100);
g.append('svg:rect').attr('y', 20).attr('h', 20).attr('w', 100);
g.append('svg:text').text(function(d) { d.group; });
g.append('svg:text').attr('y', 20).text(function(d) { d.name; });
If the node doesn't have a name, however, I'd like to supress the creation of the second rect and text. Logically, if it wasn't for the implicit iterator in d3 I'd be doing something like:
var g = this.node.enter().
append('svg:g').
attr('transform', function(d) { return 'translate('+ d.x +','+ d.y +')'; });
g.append('svg:rect').attr('h', 20).attr('w', 100);
g.append('svg:text').text(function(d) { d.group; });
// unfortunately 'd' isn't defined out here.
// EDIT: based on comment from the answer below; the conditional should
// be for the text and the related rectangle.
if(d.name) {
g.append('svg:rect').attr('y', 20).attr('h', 20).attr('w', 100);
g.append('svg:text').attr('y', 20).text(function(d) { d.name; });
}

You could use an each call on your g selection to decide whether or not to add the label.
g.each(function(d) {
if (d.name){
var thisGroup = d3.select(this);
thisGroup.append("text")
.text(d.group);
thisGroup.append("text")
.attr("y", 20)
.text(d.name);
});
However, be aware that this structure could get confusing if you're going to be updating the data.
If you want to be able to update neatly, I would recommend doing a nested selection:
var labels = g.selectAll("text")
.data(function(d){ d.name? [d.group, d.name]:[]; });
labels.enter().append("text");
labels.exit().remove();
labels.text(function(d){return d;})
.attr("y", function(d,i){return i*20;});
The data-join function tests the parent's data object, and based on it either passes an array containing the two values you want to use for the label text, or an empty array. If it passes the empty array, then no labels are created; otherwise, each label has it's text set by the value in the array and it's vertical position set by the index.

Related

D3 - forEach is not a function when upgrading from v3 to v4

I am trying to upgrade this stackable bar chart to v4.
Everything works except for one thing.
When I filter one category the bars don't drop to the start of the x-axis. I get an error which says:
state.selectAll(...).forEach is not a function
I've tried multiple things but I can't figure this one out.
This is the broken code:
function plotSingle(d) {
class_keep = d.id.split("id").pop();
idx = legendClassArray.indexOf(class_keep);
//erase all but selected bars by setting opacity to 0
d3.selectAll(".bars:not(.class" + class_keep + ")")
.transition()
.duration(1000)
.attr("width", 0) // use because svg has no zindex to hide bars so can't select visible bar underneath
.style("opacity", 0);
//lower the bars to start on x-axis
state.selectAll("rect").forEach(function(d, i) {
//get height and y posn of base bar and selected bar
h_keep = d3.select(d[idx]).attr("height");
y_keep = d3.select(d[idx]).attr("y");
h_base = d3.select(d[0]).attr("height");
y_base = d3.select(d[0]).attr("y");
h_shift = h_keep - h_base;
y_new = y_base - h_shift;
//reposition selected bars
d3.select(d[idx])
.transition()
.ease("bounce")
.duration(1000)
.delay(750)
.attr("y", y_new);
})
}
I find it strange that this works flawlessly in D3 v3, why wouldn't this work in v4?
In d3 v3 selectAll returned an array, in d3 v4 it returns an object.
From the v3 notes:
Selections are arrays of elements—literally (maybe not literally...).
D3 binds additional methods to the array so that you can apply
operators to the selected elements, such as setting an attribute on
all the selected elements.
Where as changes in v4 include:
Selections no longer subclass Array using prototype chain injection;
they are now plain objects, improving performance. The internal fields
(selection._groups, selection._parents) are private; please use the
documented public API to manipulate selections. The new
selection.nodes method generates an array of all nodes in a selection.
If you want to access each node in v4 try:
selection.nodes().forEach( function(d,i) { ... })
But, this is just the node, to get the data you would need to select each node:
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.nodes().forEach(function(d,i) {
console.log(d3.select(d).data());
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, if you need the data or to modify the selection properties, it could be easier to use selection.each(). d3.each iterates through each element of a d3 selection itself, and allows you to invoke a function for each element in a selection (see API docs here):
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.each( function() {
console.log(d3.select(this).data());
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
In v3 of this bar chart, in the forEach loop
`states.selectAll("rect").forEach(function(d,i) {`
d is an array of nodes (the rectangles in each .g).
But, in v4 d3 selections aren't arrays, you can't use a forEach loop in the same way. But you can still get the nodes in it without much modification using selection.nodes() and than get the childNodes to replicate the array in the v3 version:
state.nodes().forEach(function(d, i) {
var nodes = d.childNodes;
Here we go through each element/node in state and get the child rects, returned as an array. Here's an updated fiddle.

Restart D3 bar chart animation

I am working on a widget that shows several D3 bar charts with different values, one after the other, in a sliding carousel.
When the page loads, the bar chart animate as it should, but when the page goes on to the next chart - whether it be on click or by itself - I would like it to restart the animation again each time.
I have tried calling animateChart() in the console but this doesn't work.
I am looking for a function that I can call from the console or from another function, like animateChart(), that will reload the D3 bar chart animation.
Here is a link to my widget:
http://jsfiddle.net/alocdk/oa5tg1qu/1/
I've found where you could enhance your animateChart function.
In fact you were modifying only data that were enterring your graph.
By calling :
d3.select(svg)
.selectAll("rect")
.data(data)
.enter().append("rect")
[...]
Everything following this, will only apply on the new data.
You may want to read these to understand the pattern to follow with data update in D3.
General Update Pattern, I
General Update Pattern, II
General Update Pattern, III
Here is my shot now http://jsfiddle.net/uknynmqa/1/
I've removed the loop you were doing on all your svg, because I assumed you wanted to only animate the current one.
And your function is updating all of the data, and not only those enterring thanks to :
// Update the data for all
var join = d3.select(svg)
.selectAll("rect")
.data(data);
// Append new data.
join.enter().append("rect")
.attr("class", function (d, i) {
var low = ""
i == minIndex ? low = " low" : "";
return "bar" + " " + "index_" + i + low;
})
// Update everyone.
join.attr("width", barWidth)
.attr("x", function (d, i) {
return barWidth * i + barSpace * i;
})
.attr("y", chartHeight)
.attr("height", 0)
.transition()
.delay(function (d, i) {
return i * 100;
})
.attr("y", function (d, i) {
return chartHeight - y(d);
})
.attr("height", function (d) {
return y(d);
});
D3 is following a really specific data update pattern.
Depending on what you want to do, you can follow this. It's up to you what you want to animate or not.
// Link data to your graph and get the join
var join = svg.selectAll('rect')
.data(data);
// Update data already there
join.attr('x', 0);
// Append the new data
join.enter().append('rect')
.attr('x', 0);
// Remove exiting elements not linked to any data anymore
join.exit().remove();
// Update all resulting elements
join.attr('x', 0);

How to update both the content and location of text labels on a D3 pie chart

I'm trying to make a pie chart with d3.js that looks like this:
Note that the labels are placed along the edges of the pie chart. Initially, I am able to draw the pie charts and properly place the text nodes (the fiddle only displays one pie chart; assume, however, that they all have data that works and is appropriate, as this one does). However, when I go to adjust the data, I can't seem to .attr(translate, transform) them to the correct region along the edge of the pie chart (or do anything to them, for that matter):
changeFunctions[i] = function (data, i) {
path.data(pie(data))
.transition()
.duration(750)
.attrTween("d", arcTween);
text.each(function (d, num) {
d3.select(this)
.text(function (t) {
return t.data.name+". "+(100 * t.data.votes/totalVotes).toFixed(0) + "%";
})
/*
.attr("transform", function (d) {
//console.log("the d", d)
var c = arc.centroid(d),
x = c[0], y = c[1],
h = Math.sqrt(x * x + y * y);
return "translate(" + (x/h * 100) + ',' + (y/h * 100) + ")";
})*/
.attr("opacity", function (t) {
return t.data.votes == 0 ? 0 : 1;
});
})
}
I have omitted the general code to draw the pie chart; it's in the jsfiddle. Basically, I draw each of the pie charts in a for loop and store this function, changeFunctions[i], in a closure, so that I have access to variables like path and text.
The path.data part of this function works; the pie chart properly adjusts its wedges. The text.each part, however, does not.
How should I go about making the text nodes update both their values and locations?
fiddle
When updating the text elements, you also need to update the data that's bound to them, else nothing will happen. When you create the elements, you're binding the data to the g element that contains the arc segment and text. By then appending path and text, the data is "inherited" to those elements. You're exploiting this fact by referencing d when setting attributes for those elements.
Probably the best way to make it work is to use the same pattern on update. That is, instead of updating only the data bound to the path elements as you're doing at the moment, update the data for the g elements. Then you can .select() the descendant path and text elements, which will again inherit the new data to them. Then you can set the attributes in the usual manner.
This requires a few changes to your code. In particular, there should be a variable for the g element selection and not just for the paths to make things easier:
var g = svg.selectAll("g.arc")
.data(pie(data));
g.enter()
.append("g").attr("class", "arc");
var path = g.append("path");
The changeFunction code changes as follows:
var gnew = g.data(pie(data));
gnew.select("path")
.transition()
.duration(750)
.attrTween("d", arcTween);
Now, to update the text, you just need to select it and reset the attributes:
gnew.select("text")
.attr("transform", function(d) { ... });
Complete demo here.

multiple circles / lines using d3.js

I have a d3.js problem and have struggled with this for a while and just can not seem to solve it. I believe it is pretty easy, but am missing something very basic.
Specifically, I have the following code, which generates a line and 2 circles for the 1st entry in the JSON - I have 'hardcoded' it for the first entry.
I'd like to now add the 2nd and 3rd entries of the JSON file to the graph and have control over line and circle colors and then generalize the code.
From reading the documentation and StackOverflow, it seems like the proper approach is to use nesting, but I can't seem to make it work?
The code is on jsfiddle at the following URL and the javascript is below.
http://jsfiddle.net/GVmVk/
// INPUT
dataset2 =
[
{
movie : "test",
results :
[
{ week: "20130101", revenue: "60"},
{ week: "20130201", revenue: "80"}
]
},
{
movie : "beta",
results :
[
{ week: "20130101", revenue: "40"},
{ week: "20130201", revenue: "50"}
]
},
{
movie : "gamm",
results :
[
{ week: "20130101", revenue: "10"},
{ week: "20130201", revenue: "20"}
]
}
];
console.log("1");
var parseDate = d3.time.format("%Y%m%d").parse;
var lineFunction = d3.svg.line()
.x(function(d) { return xScale(parseDate(String(d.week))); })
.y(function(d) { return yScale(d.revenue); })
.interpolate("linear");
console.log("2");
//SVG Width and height
var w = 750;
var h = 250;
//X SCALE AND AXIS STUFF
//var xMin = 0;
//var xMax = 1000;
var xScale = d3.time.scale()
.domain([parseDate("20130101"),parseDate("20131231")])
.range([0, w]);
console.log(parseDate("20130101"));
console.log("3");
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
console.log("4S");
//Y SCALE AND AXIS STUFF
var yScale = d3.scale.linear()
.domain([0, 100])
.range([h, 0]);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("4S1");
//CREATE X-AXIS
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - 30) + ")")
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + 25 + ",0)")
.call(yAxis);
svg.selectAll("circle")
.data(dataset2[0].results)
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.append("path")
.attr("d", lineFunction(dataset2[0].results))
.attr("class", "line");
The word "nesting" comes up in two contexts in d3 -- creating nested data arrays with d3.nest, and using nested data to create nested selections.
Your data is already in the correct format for a nested selection -- an array of objects, each of which has a sub-array of individual data points. So you don't need to worry about manipulating the data, you just need to go straight to joining your data to your elements in nested d3 selections:
I'm going to take you through it quickly, but the following tutorials will be good reference for the future:
Thinking with Joins
Nested Selections
How Selections Work
On to your example: you have a top-level data structure that is an array of movie objects, each of which contains a sub-array of weekly revenue values. The first thing you need to decide is what type of elements you want associated with each level of data. You're drawing a line and a set of circles for the data in the sub-array, but aren't currently adding anything for the top-level array objects (the movies). You need to add something for them in order for nested selections to work, and it needs to be something that can contain your line and circle. In SVG, that's almost always going to be a <g> (grouping) element.
To efficiently create one <g> element for every object in your data array -- and to attach the data objects to the elements for future reference -- you create an empty selection, join your data to it, then use the enter() method of the data join selection to add elements for each data object that didn't match an element. In this case, since we don't have any elements to start, all the data objects will be in the enter() selection. However, the same pattern also works when updating some of the data.
var movies = svg //start with your svg selection,
//it will become the parent to the entering <g> elements
.selectAll("g.movie") //select all <g> elements with class "movie"
//that are children of the <svg> element
//contained in the `svg` selection
//this selection will currently be empty
.data( dataset2 ); //join the selection to a data array
//each object in the array will be associated with
//an element in the selection, if those elements exist
//This data-joined selection is now saved as `movies`
movies.enter() //create a selection for the data objects that didn't match elements
.append("g") //add a new <g> element for each data object
.attr("class", "movie") //set it's class to match our selection criteria
//for each movie group, we're going to add *one* line (<path> element),
//and then a create subselection for the circles
.append("path") //add a <path> within *each* new movie <g> element
//the path will *inherit* the data from the <g> element
.attr("class", "line"); //set the class for your CSS
var lineGraph = movies.select("path.line")
//All the entered elements are now available within the movies selection
//(along with any existing elements that we were updating).
//Using select("path") selects the first (and only) path within the group
//regardless of whether we just created it or are updating it.
.attr("d", function(d){ return lineFunction(d.results); });
//the "d" attribute of a path describes its shape;
//the lineFunction creates a "d" definition based on a data array.
//If the data object attached to the path had *only* been the d.results array
//we could have just done .attr("d", lineFunction), since d3
//automatically passes the data object to any function given as the value
//of an .attr(name, value) command. Instead, we needed to create an
//anonymous function to accept the data object and extract the sub-array.
var circles = movies.selectAll("circle")
//there will be multiple circles for each movie group, so we need a
//sub-selection, created with `.selectAll`.
//again, this selection will initially be empty.
.data( function(d) {return d.results; });
//for the circles, we define the data as a function
//The function will be called once for each *movie* element,
//and passed the movie element's data object.
//The resulting array will be assigned to circles within that particular
//movie element (or to an `enter()` selection, if the circles don't exist).
circles.enter() //get the data objects that don't have matching <circle> elements
.append("circle") //create a circle for each
//the circles will be added to the appropriate "g.movie"
//because of the nested selection structure
.attr("r", 3); //the radius doesn't depend on the data,
//so you can set it here, when the circle is created,
//the same as you would set a class.
circles //for attributes that depend on the data, they are set on the entire
//selection (including updating elements), after having created the
//newly entered circles.
.attr("cx", function(d) { return xScale( parseDate(d.week) ); })
.attr("cy", function(d) { return yScale( d.revenue ); });
Live version with the rest of your code: http://jsfiddle.net/GVmVk/3/
You'll need to adjust the domain of your x-scale so that the first data points aren't cut off, and you'll need to decide how you want to use your movie title property, but that should get you going.
Yes indeed, nested selection are the way to go for the circles, although you don't need them for the paths:
svg.selectAll("g.circle")
.data(dataset2)
.enter()
.append("g")
.attr("class", "circle")
.selectAll("circle")
.data(function(d) { return d.results; })
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.selectAll("path.line")
.data(dataset2).enter().append("path")
.attr("d", function(d) { return lineFunction(d.results); })
.attr("class", "line");
Complete example here.

Updating graph with new data

Fairly new to d3.js, so hoping there's something obvious that I'm missing. Have been looking at this code over and over again, not sure where it's going wrong.
I have a bargraph, which displays 28 bars. I'm trying to:
Replicate this tutorial, where new data is added to graph, and oldest data is removed.
Instead of using shift to remove data, I'd like to push to the graph/array, but only display the last 28 numbers. I'd like to use the whole array for another display. This said, I can't get the above to work.
This is a jsFiddle to the troublesome code.
I have a graph located within a group (with a unique ID, #kI1data, plan on having multiple graphs later). When that group is clicked, a value from the data array is shifted, and another pushed. The graph is then redrawn. I believe it's this redraw function that is causing an issue; it removes rectangles, but doesn't add any new ones after the first click.
graphGroup.append("g")
.attr("id", "kInv1")
.append("rect")
.attr("class", "invBack")
.attr("x", 0).attr("y", 0)
.attr("width", kI1width).attr("height", kI1height)
.attr("fill", "grey")
.on("click", dataChange); // Add/remove data, and update graph
function dataChange() {
kInvd1.shift();
kInvd1.push(30); // 30 a placeholder number
updateGraph();
};
function updateGraph() {
// Select the group containing the rectangles that need updating
// (to avoid selecting other rectangles within the svg)
var kI3dataSelect = d3.select("#kI1data").selectAll("rect").data(kInvd1, function(d) { return d; });
// Enter new data
kI3dataSelect.enter().append("rect")
.attr("x", function(d, i) { return 1 + ((i+1) * ((kI1width)/28)); })
.attr("y", function(d) { return ykI1(d); })
.attr("height", function(d) { return (kI1height) - ykI1(d); })
.attr("width", (kI1width)/28)
.attr("fill", "black");
// Update positions (shift one bar left)
kI3dataSelect
.transition().duration(100)
.attr("x", function(d, i) { return 1 + (i * ((kI1width)/28)); });
kI3dataSelect.exit()
.transition().duration(100)
.attr("x", function(d, i) { return 1 + ((i-1) * ((kI1width)/28)); })
.remove();
};
For now I'm just trying to get the newly added data to display, whilst the oldest data is removed. Once that's done, if there are any pointers on how to display just the last 28 numbers in the array, it'd be very much appreciated!
The jsFiddle shows how a new bar is added on the first click, but subsequent clicks only translate and remove data, whilst new bars are not added.
Changing your dataChange function to the following gets you new insertions and limits the over all array to 28 elements.
function dataChange() {
if (kInvd1.length >= 28) kInvd1.shift()
kInvd1.push(Math.random() * 60); // 30 a placeholder number
updateGraph();
};

Categories

Resources