d3 enter not placing data - javascript

Using d3, I am reading in 8760 data points (one for each hour of 2013) from a csv into a 2-d array and then trying to create them as circles in an <svg> element. The code successfully imports the data, as I can view it in the browser's console. However, the enter function does not seem to be applying the data to create the circles. No errors are raised.
Code:
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var w = 900, h = 500;
var dataset = [];
d3.csv("Jsorted2.csv", function(error, data) {
dataset = data.map(function(d) { return [ +d["Load"], new Date(d["dtDateTime"]), +d["Month"], +d["Hour"], +d["DayofYear"], +d["NetLoad"]]; });
});
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("circle")
.data( dataset )
.enter()
.append("circle")
.attr("cx", function(d,i) { return i; })
.attr("cy", function(d,i) { return d[0]; })
.attr("r", function(d,i) { return d[5]; });
</script>
The data loads properly:
Browser Inspector Console Screenshot
But the circles are missing:
Browser Inspector Source Screenshot
I have found different errors if I change the initial definition of dataset. If I use
var dataset = [[]];
the data still loads, but now I receive an error when d3 tries the grab the data:
d3.v4.min.js:3 Error: <circle> attribute cy: Expected length, "NaN".
(anonymous function)
# d3.v4.min.js:3Q_
# d3.v4.min.js:6K_
# d3.v4.min.js:6(anonymous function)
# D3test4LD.html:29
And d3 gets a little further in creating the circles:
<svg width="900" height="500">
<circle cx="0" cy="NaN"></circle>
</svg>
Why does d3 not load the data to circles? It appears as if d3 is not passing the data to d as it should, as d[0] has no length. If I cannot get this to work, I'll just write a Javascript for loop to place them myself, but I'd like to figure out why this doesn't work. It looks like a type conversion issue, but I import the data with a +, which is supposed to set the data as a number. I can even test that in the console:
> typeof(dataset[0][0])
"number"
Please do not include a js for loop as a solution.
UPDATE
So, I did solve my problem after researching asynchronous data loading, as per the answer below. For clear and easy future reference, here is one way to code a solution:
setInterval(drawLD, 2000);
function drawLD(){
svg.selectAll("circle")
.data( dataset )
.enter()
.append("circle")
.attr("cx", function(d,i) { return Math.round(i/8760); })
.attr("cy", function(d,i) { return Math.round(d[0]/maxload); })
.attr("r", function(d,i) { return 2; });
}

d3.csv is an asynchronous function. The code that creates the circles is executed before your csv data has returned. Place your d3 code inside the callback.
<script>
var w = 900, h = 500;
var dataset = [];
d3.csv("Jsorted2.csv", function(error, data) {
dataset = data.map(function(d) { return [ +d["Load"], new Date(d["dtDateTime"]), +d["Month"], +d["Hour"], +d["DayofYear"], +d["NetLoad"]]; });
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("circle")
.data( dataset )
.enter()
.append("circle")
.attr("cx", function(d,i) { return i; })
.attr("cy", function(d,i) { return d[0]; })
.attr("r", function(d,i) { return d[5]; });
});
</script>

Related

How to filter and omit input data for d3.js

I am new to D3.JS and, as a beginner, I am not sure how to handle a demanding program that will read a lot of data from a csv file and then make customized output based on the input. I am doing some test code in order to get the feel of the Here is the CSV file I am using:
location,age_group_id
USA,34
USA,34
USA,36
AFG,34
AFG,34
AFG,36
AFG,36
And the following code works fine to produce a simple bar graph based on this input:
<doctype html>
<html>
<head>
<title> Test </title>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
d3.csv("mydata.csv", function(error, data){
var canvas = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
canvas.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", function (d) { return d.age_group_id * 10} )
.attr("height", 48)
.attr("y", function (d, i){return i * 50} )
.attr("fill", "blue")
})
</script>
</body>
</html>
But what if I wanted to filter this data and only show out put for "location" == USA? I assume I would have to first read the data into a variable but I have yet to find documentation on how this is done. I also assume I will have to first define the display in a section and then load the data into this area.
When you do this:
d3.csv("mydata.csv", function(error, data){
All your CSV is loaded, as an array of objects, in a variable named data.
Thus, you can create a new dataset based on data:
var dataUsa = data.filter(function(d){
return d.location === "USA";
});
And use this dataset in the bars:
canvas.selectAll("rect")
.data(dataUsa)
var country = "USA";
d3.csv("mydata.csv", function(error, data){
var canvas = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
canvas.selectAll("rect")
.data(data)
.enter()
.filter(function(d) { return d.location == country })
.append("rect")
.attr("width", function (d) { return d.age_group_id * 10} )
.attr("height", 48)
.attr("y", function (d, i){return i * 50} )
.attr("fill", "blue")
})

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

reading nodes and edges from two distinct csv files using Force Layout

I have a problem when trying to display a graph using the force layout. I use two csv files, one for vertices and one for edges. I'm not sure, but I think that as the d3.csv method is asynchronous, and I'm using two of them, I need to insert one into the other to avoid "concurrency" issues (initially I tried to call d3.csv twice and separately, and I had troubles).
The structure of my csv's is the following:
For edges:
source,target
2,3
2,5
2,6
3,4
For nodes:
index,name
1,feature1
2,feature2
3,feature3
My initial attempt is:
// create the force layout
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var node, linked;
// we read the edges
d3.csv("csvEdges.csv", function(error, data) {
edg = data;
// we read the vertices
d3.csv("csvVertices.csv", function(error2, data2) {
ver = data2;
force.nodes(data2)
.links(data)
.start();
node = svg.selectAll(".node")
.data(data2)
.enter()
.append("circle")
.attr("class", "node")
.attr("r", 12)
.style("fill", function(d) {
return color(Math.round(Math.random()*18));
})
.call(force.drag);
linked = svg.selectAll(".link")
.data(data)
.enter()
.append("line")
.attr("class", "link");
force.on("tick", function() {
linked.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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
But I get:
"TypeError: Cannot call method 'push' of undefined."
I don't know what's the problem; I think it is related to the push of links. I read that maybe the problem is related to how d3 matches links with nodes' objects (in this case, my nodes have two fields). So I tried the following (I saw this in other question here):
// we read the edges
d3.csv("csvEdges.csv", function(error, data) {
edg = data;
// we read the vertices
d3.csv("csvVertices.csv", function(error2, data2) {
ver = data2;
force.nodes(data2)
.start();
//.links(data);
var findNode = function(id) {
for (var i in force.nodes()) {
if (force.nodes()[i]["index"] == id) return force.nodes()[i]
};
return null;
};
var pushLink = function (link) {
//console.log(link)
if(findNode(link.source)!= null && findNode(link.target)!= null) {
force.links().push ({
"source":findNode(link.source),
"target":findNode(link.target)
})
}
};
data.forEach(pushLink);
[...]
But in this case I get a bunch of:
Error: Invalid value for <circle> attribute cy="NaN"
and I don't know what's the problem in this case!
The way I have seen to do this is to queue the functions using queue.js, as described in the book "D3.js in Action" by Elijah Meeks. Sample code for chapter 6 is on the Manning website, see listing 6.7. (and buy the book, it's quite good) here's the basic structure slightly adapted to your use case:
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
queue()
.defer(d3.csv, "csvVertices.csv")
.defer(d3.csv, "csvEdges.csv")
.await(function(error, file1, file2) {createForceLayout(file1, file2);});
function createForceLayout(nodes, edges) {
var nodeHash = {};
for (x in nodes) {
nodeHash[nodes[x].id] = nodes[x];
}
for (x in edges) {
edges[x].weight = 1; //you have no weight
edges[x].source = nodeHash[edges[x].source];
edges[x].target = nodeHash[edges[x].target];
}
force = d3.layout.force()
.charge(-1000)
.gravity(.3)
.linkDistance(50)
.size([500,500])
.nodes(nodes)
.links(edges);
//etc.

Basics of D3's Force-directed Layout

I'm plowing into the exciting world of force-directed layouts with d3.js. I've got a grasp of the fundamentals of d3, but I can't figure out the basic system for setting up a force-directed layout.
Right now, I'm trying to create a simple layout with a few unconnected bubbles that float to the center. Pretty simple right!? The circles with the correct are created, but nothing happens.
Edit: the problem seems to be that the force.nodes() returns the initial data array. On working scripts, force.nodes() returns an array of objects.
Here's my code:
<script>
$(function(){
var width = 600,
height = 400;
var data = [2,5,7,3,4,6,3,6];
//create chart
var chart = d3.select('body').append('svg')
.attr('class','chart')
.attr('width', width)
.attr('height', height);
//create force layout
var force = d3.layout.force()
.gravity(30)
.alpha(.2)
.size([width, height])
.nodes(data)
.links([])
.charge(30)
.start();
//create visuals
var circles = chart.selectAll('.circle')
.data(force.nodes()) //what should I put here???
.enter()
.append('circle')
.attr('class','circles')
.attr('r', function(d) { return d; });
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
</script>
Here was the problem. The array you feed into force.nodes(array) needs to be an array of objects.
So:
var data = [{number:1, value:20}, {number:2, value:30}...];
works. Or even just
var data=[{value:1},{value:2}];
makes the script work fine.
You need to update cx and cy in force.on handler.
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x - deltaX(d, e) ; })
.attr("cy", function(d) { return d.y - deltaY(d, e); });
});
And functions deltaX and deltaY depends on your force model.

Categories