d3: how to visualize a simple 2D matrix? - javascript

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.

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.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

d3 lines and circles data

I am trying to draw a set of lines and circle points, but I cant figure out how to get the circles to work.
The line function needs an array of points, but for the circle it needs just the x/y of each point.
How do I append a circle (to the same group as the line), for each x/y point?
// Data join
var join = svg.selectAll("g")
.data(lineData)
// Enter
var group = join.enter()
.append("g");
group.append("path")
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
group.append('circle')
.attr("r", 10)
.attr('fill', 'blue');
// Update
join.select("path")
.attr('d', line);
join.select("circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); });
Full code is here: http://jsfiddle.net/dxxddvL4/1/
The basic pattern you need to use are nested selections -- for each line, there are multiple circles. It's easier to do the lines and circles separately, lines and g elements first:
var join = svg.selectAll("g")
.data(lineData);
// Enter
join.enter()
.append("g")
.append("path")
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
// Update
join.select("path")
.attr('d', line);
join.exit().remove();
The code is basically the same as yours, except that the appended g elements aren't saved in a separate selection and the exit selection is handled by removing the elements. Now the circles, along the same lines:
var circles = join.selectAll("circle")
.data(function(d) { return d; });
circles.enter()
.append('circle')
.attr("r", 10)
.attr('fill', 'blue');
circles.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); });
circles.exit().remove();
The first line here is the nested selection -- for each element in the array that denotes the line, we want a circle. Note that this is operating on the update selection of the g elements. This is ok because the elements in the enter selection are merged into the update selection when the g elements are appended. That is, even though we only handle the update selection, any newly-appended elements are included in this.
After that, we handle the selections as usual. The enter selection has elements appended, the update selection sets the coordinates, the exit selection removes elements. All the magic happens in that first line, where we tell D3 to, for each g element at the top level, bind each point from the line to any circles underneath.
Complete example here.

d3.js create objects on top of each other

I create rectangles in my SVG element using this code:
var rectangles = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect");
rectangles.attr("x", function (d) {
return xScale(getDate(d));
//return xScale(d.start);
})
.attr("y", function (d, i) {
return (i * 33);
})
.attr("height", 30)
.transition()
.duration(1000)
.attr("width", function (d) {
return d.length;
})
.attr("rx", 5)
.attr("ry", 5)
.attr("class", "rectangle")
.attr("onclick", function (d) {
return "runaction(" + d.start + ")";
});
How can I create new rectangles on top of the previous ones?
This is an answer to this question I got from Scott Murray, author of great introductory tutorials for d3.js http://alignedleft.com/tutorials/d3/ which helped me a lot with understanding its functionality. I hope he won't mind me putting his answer here for everyone's benefit.
Thank you very much Scott!
And yes, that's absolutely possible. Taking your example, let's say you want to draw one set of circles with the dataset called "giraffeData" bound to them. You would use:
svg.selectAll("circle")
.data(giraffeData)
.enter()
.append("circle");
But then you have a second data set (really just an array of values) called "zebraData". So you could use the same code, but change which data set you reference here:
svg.selectAll("circle")
.data(zebraData)
.enter()
.append("circle");
Of course, this will inadvertently select all the circles you already created and bind the new data to them — which isn't really what you want. So you'll have to help D3 differentiate between the giraffe circles and the zebra circles. You could do that by assigning them classes:
svg.selectAll("circle.giraffe")
.data(giraffeData)
.enter()
.append("circle")
.attr("class", "giraffe");
svg.selectAll("circle.zebra")
.data(zebraData)
.enter()
.append("circle")
.attr("class", "zebra");
Or, you could group the circles of each type into a separate SVG 'g' element:
var giraffes = svg.append("g")
.attr("class", "giraffe");
giraffes.selectAll("circle")
.data(giraffeData)
.enter()
.append("circle");
var zebras = svg.append("g")
.attr("class", "zebra");
zebras.selectAll("circle")
.data(zebraData)
.enter()
.append("circle");
I'd probably choose the latter, as then your DOM is more cleanly organized, and you don't have to add a class to every circle. You could just know that any circle inside the g with class zebra is a "zebra circle".

D3js - Creating and easily updating a multi-line chart

I've created a little test line chart using D3, but since I am quite new to the library I am not sure what the best way would be to add multiple lines to a chart, at the moment I only have one line displayed in this fiddle.
I would like to display 2 lines on the chart, but I am unsure of how to achieve that without copy pasting code, which I am sure would be very inefficient as I would like to update/animate the graph at regular intervals based on user selection.
Instead of this,
var data = [12345,22345,32345,42345,52345,62345,72345,82345,92345,102345,112345,122345,132345,142345];
I would like to display something like this,
var data = [
[12345,42345,3234,22345,72345,62345,32345,92345,52345,22345], // line one
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234] // line two
];
Would this be a possibility? If so, what would be the best way to approach this, so that I can easily update/animate the graph when needed?
Note: I am merely trying to learn and to familiarize myself with D3 best practices and the library as a whole. Thanks.
This is possible and reasonable.
There is a tutorial that approaches this at the
D3 Nested Selection Tutorial
which describes nesting of data.
Below is code that I hacked from your fiddle to demonstrate this.
var data = [
[12345,42345,3234,22345,72345,62345,32345,92345,52345,22345],
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234]
];
var width = 625,
height = 350;
var x = d3.scale.linear()
.domain([0,data[0].length]) // Hack. Computing x-domain from 1st array
.range([0, width]);
var y = d3.scale.linear()
.domain([0,d3.max(data[0])]) // Hack. Computing y-domain from 1st array
.range([height, 0]);
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return y(d); });
var area = d3.svg.area()
.x(line.x())
.y1(line.y())
.y0(y(0));
var svg = d3.select("body").append("svg")
//.datum(data)
.attr("width", width)
.attr("height", height)
//.append("g");
var lines = svg.selectAll( "g" )
.data( data ); // The data here is two arrays
// for each array, create a 'g' line container
var aLineContainer = lines
.enter().append("g");
/*svg.append("path")
.attr("class", "area")
.attr("d", area);*/
aLineContainer.append("path")
.attr("class", "area")
.attr("d", area);
/*svg.append("path")
.attr("class", "line")
.attr("d", line);*/
aLineContainer.append("path")
.attr("class", "line")
.attr("d", line);
/*svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);*/
// Access the nested data, which are values within the array here
aLineContainer.selectAll(".dot")
.data( function( d, i ) { return d; } ) // This is the nested data call
.enter()
.append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);
​
One deficiency is that I computed the domain for the x and y axes off the first array, which is a hack, but not pertinent to your question exactly.

Categories