D3 V4 Transition on entering elements using General Update Pattern with merge - javascript

To the best of my understanding it is not possible to include a transition on the entering elements in the standard enter/append/merge chain, since doing so would replace the entering element selection with a transition that cannot be merged with the update selection. (See here on the distinction between selections and transitions).
(Question edited in response to comment)
If the desired effect is sequenced transitions, one before and one after the merge, it can be accomplished as follows:
// Join data, store update selection
circ = svg.selectAll("circle")
.data(dataset);
// Add new circle and store entering circle selection
var newcirc = circ.enter().append("circle")
*attributes*
// Entering circle transition
newcirc
.transition()
.duration(1000)
*modify attributes*
.on("end", function () {
// Merge entering circle with existing circles, transition all
circ = newcirc.merge(circ)
.transition()
.duration(1000)
*modify attributes*
});
jsfiddle
I would like to know if there is a way to do this without breaking up the enter/append/merge chain.

There can be no doubt, that you have to have at least one break in your method chaining since you need to keep a reference to the update selection to be able to merge it into the enter selection later on. If you are fine with that, there is a way to keep the chain intact after that initial break.
I laid out the basic principal for this to work in my answer to "Can you chain a function after a transition without it being a part of the transition?". This uses transition.selection() which allows you to break free from the current transition and get access to the underlying selection the transition was started on. Your code is more complicated, though, as the chained transition adds to the complexity.
The first part is to store the update selection like you did before:
// Join data, store update selection
const circUpd = svg.selectAll("circle")
.data(dataset);
The second, uninterrupted part goes like this:
const circ = circUpd // 2. Store merged selection from 1.
.enter().append("circle")
// .attr()...
.transition()
// .duration(1000)
// .attr()...
.on("end", function () {
circ.transition() // 3. Use merged selection from 2.
// .duration(1000)
// .attr()...
})
.selection().merge(circUpd); // 1. Merge stored update into enter selection.
This might need some further explanations for the numbered steps above:
The last line is the most important one—after kicking off the transition, the code uses .selection() to get a hold of the selection the transition was based on, i.e. the enter selection, which in turn can be used to easily merge the stored update selection into it.
The merged selection comprising both the enter and the update selection is the result of the entire chain and is then stored in circ.
This is the tricky part! It is important to understand, that the function provided to .on("end", function() {...}) is a callback which is not executed before the transition ends. Although this line comes before the merging of the selections, it is actually executed after that merge. By referring to circ, however, it closes over—captures, if you will—the reference to circ. That way, when the callback is actually executed, circ will already refer to the previously merged selection.
Have a look at the following working snippet:
var w = 250;
var h = 250;
// Create SVG
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Create background rectangle
svg.append("rect")
.attr("x", "0")
.attr("y", "0")
.attr("width", w)
.attr("height", h)
.attr("fill", "aliceblue");
var dataset = [170, 220, 40, 120, 0, 300];
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([0, w]);
var yScale = d3.scaleLinear()
.domain([0, 300])
.range([75, 200])
var rad = xScale.bandwidth()/2
// Join data
var circ = svg.selectAll("circle")
.data(dataset);
// Create initial circles
circ.enter().append("circle")
.attr("cx", (d, i) => xScale(i)+rad)
.attr("cy", d => yScale(d))
.attr("r", rad)
.attr("fill", "blue");
// Trigger update on click
d3.select("h3")
.on("click", function () {
// Create new data value
var newvalue = Math.floor(Math.random()*300);
dataset.push(newvalue);
xScale.domain(d3.range(dataset.length));
rad = xScale.bandwidth()/2;
// Join data, store update selection
const circUpd = svg.selectAll("circle")
.data(dataset);
// Add new circle and store entering circle selection
const circ = circUpd // 2. Store merged selection from 1.
.enter().append("circle")
.attr("cx", "0")
.attr("cy", "25")
.attr("r", rad)
.attr("fill", "red")
.transition()
.duration(1000)
.attr("cx", (d, i) => xScale(i)+rad)
.on("end", function () {
circ.transition() // 3. Use merged selection from 2.
.duration(1000)
.attr("cx", (d, i) => xScale(i)+rad)
.attr("cy", d => yScale(d))
.attr("r", rad)
.attr("fill", "blue");
})
.selection().merge(circUpd); // 1. Merge stored update into enter selection.
});
<script src="https://d3js.org/d3.v4.js"></script>
<h3>Add a circle</h3>

Related

Pass the current parameter to function in a loop

I'm using D3 to print multiple rect out. Now I'hope the rect could allow user click on it and the active some function.
For example, there are 3 rectangles, "Tom", "Mary" and "Ben". When user click on different rect, it will pass the current value. Like when I click on "Tom" it will pass "Tom" to call the function.
However, I found that after finish print out the rect, no matter I click on which rect, they both return the least value of the dataset.
In my example, even I click on "Tom" or "Mary", both return "Ben".
for (var i = 0; i < ward_set.length; i++) {
var ward_id = ward_set[i];
legend.append("rect")
.attr("x", legend_x + 180 + 100 * n)
.attr("y", legend_y)
.attr("width", 18)
.attr("height", 18)
.attr("fill", colors[count])
.attr("class", "legend" + ward_set[i])
.on("click", function() {
console.log(ward_id);
});
}
Your question perfectly illustrates a very important principle, a rule of thumb if you like, when writing a D3 code: never use a loop to append elements. Use the enter/update/exit pattern instead.
What happens when you use a loop (be it for, while, forEach etc...) is that not only there is no data binding, but also you experience this strange outcome you described (getting always the last value), which is explained here: JavaScript closure inside loops – simple practical example
Therefore, the solution using a D3 idiomatic enter selection would be:
const data = ["Tom", "Mary", "Ben"];
const svg = d3.select("svg");
const rects = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
//etc...
Then, in the click listener, you get the first argument, which is the datum:
.on("click", function(d) {
console.log(d)
})
Here is a demo:
const data = ["Tom", "Mary", "Ben"];
const svg = d3.select("svg");
const rects = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("width", 50)
.attr("height", 150)
.attr("x", (_, i) => i * 60)
.on("click", function(d) {
console.log(d)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

D3 chart can't update -- enter and exit property of selection both empty

I'm trying to make a scatter plot using a .json file. It will let the user to select which group of data in the json file to be displayed. So I'm trying to use the update pattern.
The following code will make the first drawing, but every time selectGroup() is called(the code is in the html file), nothing got updated. The console.log(selection) did come back with a new array each time, but the enter and exit property of that selection is always empty.
Can anyone help me take a look? Thanks a lot!
var margin = {
top: 30,
right: 40,
bottom: 30,
left: 40
}
var width = 640 - margin.right - margin.left,
height = 360 - margin.top - margin.bottom;
var dataGroup;
var groupNumDefault = "I";
var maxX, maxY;
var svg, xAxis, xScale, yAxis, yScale;
//select and read data by group
function init() {
d3.json("data.json", function (d) {
maxX = d3.max(d, function (d) {
return d.x;
});
maxY = d3.max(d, function (d) {
return d.y;
});
console.log(maxY);
svg = d3.select("svg")
.attr("id", "scatter_plot")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("id", "drawing_area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//x-axis
xScale = d3.scale.linear().range([0, width]).domain([0, maxX]);
xAxis = d3.svg.axis()
.scale(xScale).orient("bottom").ticks(6);
//y-axis
yScale = d3.scale.linear().range([0, height]).domain([maxY, 0]);
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(6);
svg.append("g")
.attr("class", "x_axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y_axis")
.call(yAxis);
});
selectGroup(groupNumDefault);
}
//update data
function selectGroup(groupNum) {
d3.json("/data.json", function (d) {
dataGroup = d.filter(function (el) {
return el.group == groupNum;
});
console.log(dataGroup);
drawChart(dataGroup);
});
}
//drawing function
function drawChart(data) {
var selection = d3.select("svg").selectAll("circle")
.data(data);
console.log(selection);
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
selection.exit().remove();
}
init();
The problem here is on two fronts:
Firstly, your lack of a key function in your data() call means data is matched by index (position in data array) by default, which will mean no enter and exit selections if the old and current datasets sent to data() are of the same size. Instead, most (perhaps all) of the data will be put in the update selection when d3 matches by index (first datum in old dataset = first datum in new dataset, second datum in old dataset = second datum in new dataset etc etc)
var selection = d3.select("svg").selectAll("circle")
.data(data);
See: https://bl.ocks.org/mbostock/3808221
Basically, you need your data call adjusted to something like this (if your data has an .id property or anything else that can uniquely identify each datum)
var selection = d3.select("svg").selectAll("circle")
.data(data, function(d) { return d.id; });
This will generate enter() and exit() (and update) selections based on the data's actual contents rather than just their index.
Secondly, not everything the second time round is guaranteed be in the enter or exit selections. Some data may be just an update of existing data and not in either of those selections (in your case it may be intended to be completely new each time). However, given the situation just described above it's pretty much guaranteed most of your data will be in the update selection, some of it by mistake. To show updates you will need to alter the code like this (I'm assuming d3 v3 here, apparently it's slightly different for v4)
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
// this new bit is the update selection (which includes the just added enter selection
// now, the syntax is different in v4)
selection // v3 version
// .merge(selection) // v4 version (remove semi-colon off preceding enter statement)
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
selection.exit().remove();
Those two changes should see your visualisation working, unless of course the problem is something as simple as an empty set of data the second time around which would also explain things :-)

d3 v4 + react + es6 + crossfilter: Selection.exit().remove() not working

I'm using crossfilter.js and d3.js v4 with ES6 style React in an attempt to make dimensional charts with context brushing. Essentially, I took this example and converted it to ES6.
The problem I have is selection.exit().remove() is not working such that on each redraw, more and more circles are appended to the svg g element. Redraws are triggered when a brush is created. I checked by running
selection.exit()
.attr('class', d => {
console.log('anything');
return 'anything';
})
.remove();
but nothing was outputted so I'm thinking my data selection is not valid.
Here is the context:
componentDidMount() {
const el = ReactDOM.findDOMNode(this); // the mounted svg element
const svg = d3.select(el)
.attr('width', 300)
.attr('height', 500);
const g = svg.append('g')
.attr('transform', 'translate(32,32)');
let circles = g.append('g')
.selectAll('circle');
const brushGroup = g.append('g')
.attr('class', 'brush')
.call(brush);
function redraw() {
const all = group.all(); // crossfilter group returns an array
xAxisGroup.call(xAxis);
yAxisGroup.call(yAxis);
circles = circles.data(all, d => d); // keyFn for data constancy
circles.enter().append('circle')
.attr('r', radius)
.attr('cx', plotX) // radius + plotX/Y are simple curry functions
.attr('cy', plotY);
circles.exit().remove(); // not working!!
}
redraw();
}
I am also aware of this change in d3-selection in v4 but I'm not super confident which lines are my update and which ones are my update + enter.
Any help would be much appreciated. Thank you!
I suspect the problem is one of 2 things. Probably #1 below. It's a bit hard to tell with out a working example to test things out in, but here you go:
I believe that the selectAll and the data join should happen together in the redraw function. Because you never redo the selectAll in the redraw function, your selection will never contain any elements. If you check your enter and exit selections in your redraw function, your enter selection will always contain all your data points because the underlying selection is empty.
That your data key function returns an object. Since the object is the result of Crossfilter's group.all, they should be comparable by reference, but it would be safer to do circles.data(all, d => d.key).
The solution should be to do something like:
componentDidMount() {
const el = ReactDOM.findDOMNode(this); // the mounted svg element
const svg = d3.select(el)
.attr('width', 300)
.attr('height', 500);
const g = svg.append('g')
.attr('transform', 'translate(32,32)');
let circlesGroup = g.append('g'); // Just hang on to the group
const brushGroup = g.append('g')
.attr('class', 'brush')
.call(brush);
function redraw() {
const all = group.all(); // crossfilter group returns an array
xAxisGroup.call(xAxis);
yAxisGroup.call(yAxis);
let circles = circlesGroup // Do the selection and data join here
.selectAll('circle')
.data(all, d => d.key); // Have the key function return a key, not an object
circles.enter().append('circle')
.attr('r', radius)
.attr('cx', plotX) // radius + plotX/Y are simple curry functions
.attr('cy', plotY);
circles.exit().remove();
}
redraw();
}

d3: how to visualize a simple 2D matrix?

I'm stuck at trying to bind two-dimensional data in d3. I want to display a matrix of green squares. I'm building a matrix like this:
var size = 10;
dataset = [];
for(var y = 0; y<size; y++){
var tempData = [size];
for(var x = 0; x<size; x++){
tempData[x] = 5;
};
dataset.push(tempData);
};
I'm not sure how to bind the data correctly. I sort of understand Mike Bostock's tutorial on nested selections, but he's binding a matrix of fixed size to already existing elements. How would I use enter() to create new rectangles? This is how I tried to apply the tutorial's advice to first bind the outer, then the inner arrays.. not surprised that it doesn't work but I also don't know where to go from here..
svg.selectAll("rect")
.data(dataset)
.selectAll("rect")
.data(function (d,i) {return d;})
.enter()
.append("rect")
.attr("x", function(d,i){
return i*20})
.attr("y", function(d,i){
return i*20;})
.attr("height", 15)
.attr("width", 15)
.attr("fill", "green");
There are two problems. First, you have the second .selectAll() immediately after the first .data(), which means that you'll be operating on the update selection. This is empty as there are no elements in the DOM to start with. You need to operate on the enter selection instead (and it's good practice to use g elements here for the first level):
svg.selectAll("rect")
.data(dataset)
.enter()
.append("g")
.selectAll("rect")
.data(function (d,i) {return d;})
Second, you're putting the rectangles along the diagonal (same x and y coordinates), so even though the correct number of rect elements is there, you don't see all of them because they overlap. To fix, you need to take the index in the parent group into account for one of the coordinates (using the secret third argument):
.append("rect")
.attr("x", function(d,i){
return i*20;
})
.attr("y", function(d, i, j){
return j*20;
})
Complete demo here.

d3.js: Confusion about the order in which the code is executed

I am trying to make an interactive bar chart in D3.js
I uploaded everything to github for easy reference. I also included index.html at the end of my question.
My starting point is data.json containing an array of 7 items (i.e. countries). Each country has an attribute 'name' and four other attributes. These represent the exposition of private banks and the state to Greek debt for the years 2009 and 2014.
My goal is to create a bar chart that starts by showing the exposition of each country's banks and public sector in 2009 (so two bars for each country) and that changes to the year 2014 once the user clicks on the appropriate button.
I had managed to make it all work nicely! However, I had to create manually separate lists for each (sub-)dataset I needed to use. For example I created one called y2009 which included the exposition of bank and state for country 1, then the same for country 2, an so on..
(I left one of the list and commented it out on line 43)
I wanted to make my code more flexible so I created a for loop that extracts the data and creates the lists for me. (see lines 46-60). This did not work because the for loops would start before the data was actually loaded. Hence I would end up with empty lists.
So I grouped the for loops into a function (prepare()) and executed that function within the function that loads the data (lines 18-30). This fixed that issue...
..and created a new one! The two functions that should set the scales (see lines 67-73) do not work because their calculations require on one of the lists created by the for loops (namely 'total').
(I assume this is due to the list being created after the scale methods are called.)
The curious thing is that if I run the script, then copy in the console the xScale and yScale functions, and then copy the draw function (lines 101-212) everything works.
Hence I tried to group everything into functions (e.g. setScales, draw) so that I would call them in the order I want at the end of the script (lines 214-215) but this creates problem because certain variables (e.g. xScale and yScale) need to be global.
I also tried to first create them in the global space and then modify them through setScales. This did not work either.
Summing up, wait I don't understand is:
In which order should I write the code to make things work(again)? Is it a good idea to wrap operations within functions (e.g. setting the scales, drawing bars and labels) and then calling the function in the right order?
Which type of object is created with the scale method? I am confused on whether they are actual functions.
I hope this was not too much of a pain to read and thank everyone who made it through!
Fede
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="d3.min.js"></script>
</head>
<body>
<p>Introductory text here!</p>
<p>
<button id="change2009"> 2009 </button>
<button id="change2014"> 2014 </button>
</p>
<div id="country"></div>
<script type="text/javascript">
d3.json("data.json", function(error, json) {
if (error) {
console.log(error);
} else{
console.log(json);
dataset=json;
}
prepare (dataset);
});
//load data
var dataset;
var bank09=[];
var state09=[];
var bank14=[];
var state14=[];
var y2009=[];
var y2014=[];
var total=[];
var xScale;
var yScale;
//var total = [4.76, 0, 0.12, 6.36, 4.21, 0, 0.04, 7.96, 78.82, 0, 1.81, 46.56, 45, 0, 13.51, 61.74, 6.86, 0, 1.06, 40.87, 12.21, 0, 1.22, 13.06, 1.21, 0, 0.39, 27.35];
function prepare (dataset){
for (i in dataset) {bank09.push(dataset[i].bank09);
state09.push(dataset[i].state09);
bank14.push(dataset[i].bank14);
state14.push(dataset[i].state14);
y2009.push(dataset[i].bank09);
y2009.push(dataset[i].state09);
y2014.push(dataset[i].bank14);
y2014.push(dataset[i].state14);
total.push(dataset[i].bank09);
total.push(dataset[i].state09);
total.push(dataset[i].bank14);
total.push(dataset[i].state14);
}
}
//overwrite dataset
dataset2=y2009;
//scales
function setScales () {
var xScale = d3.scale.ordinal()
.domain(d3.range(total.length/2))
.rangeRoundBands([0, w], 0.1);
var yScale = d3.scale.linear()
.domain([0, d3.max(total)])
.range([0, h]);
console.log(yScale(89));
}
//layout
var w = 600;
var h = 600;
var barPadding = 1;
//coountry names
var country = ["Austria", "Belgium", "France", "Germany", "Italy", "Holland", "Spain"];
d3.select("#country")
.data(country)
.enter()
.append("rect")
.attr("class", "country")
//.append("text")
//.text(function(d){
// return d;
// })
//draw svg
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
function draw () {
//draw bars
svg.selectAll("rect")
.data(dataset2)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d){
return h - yScale(d);
})
.attr("width", xScale.rangeBand)
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "black");
//add labels
svg.selectAll("text")
.data(dataset2)
.enter()
.append("text")
.text(function(d){
return d;
})
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", "12px")
.attr("fill", "red")
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
//interactivity
d3.select("#change2014")
.on("click", function() {
//update data
dataset2=y2014;
//update bars
svg.selectAll("rect")
.data(dataset2)
.transition()
.duration(3000)
.attr("y", function(d){
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
//update labels
svg.selectAll("text")
.data(dataset2)
.transition()
.duration(3000)
.text(function(d){
return d;
})
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
})
d3.select("#change2009")
.on("click", function() {
//update data
dataset2=y2009;
//update bars
svg.selectAll("rect")
.data(dataset2)
.transition()
.duration(3000)
.attr("y", function(d){
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
//update labels
svg.selectAll("text")
.data(dataset2)
.transition()
.duration(3000)
.text(function(d){
return d;
})
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
})
}
setScales ();
draw();
</script>
In which order should I write the code to make things work(again)? Is
it a good idea to wrap operations within functions (e.g. setting the
scales, drawing bars and labels) and then calling the function in the
right order?
As Lars pointed out, you can put everything inside the d3.json callback. This is because you only want to start rendering with D3 once you have the data. The d3.json method is asynchronous, which means that after you call d3.json(), the code afterwards will execute first before the function inside the d3.json method has finished. Check out http://rowanmanning.com/posts/javascript-for-beginners-async/ for more on asynchronous behavior in Javascript.
Given that you only want to start rendering when the d3.json method has completed, you could also just organize the other parts of your code into smaller functions and call some sort of initializer function from within the d3.json success callback, sort of like what you are doing with the prepare function. This is a cleaner approach and starts taking you towards a model-view paradigm.
Which type of object is created with the scale method? I am confused
on whether they are actual functions.
The scale method does return a function, but with additional functions added to its prototype. Try printing out "someScale.prototype" to see all of the various methods you can use. I'd also highly recommend Scott Murray's tutorial on D3. Here is the chapter on scales: http://alignedleft.com/tutorials/d3/scales

Categories