Related
I am working on a line graph in d3.js and am unsure how to to iterate through each country and update my graph's points. I want to draw each country on my map. In my code I have only hard coded the first country and the output shown in the following images. Have attached my csv file to show the column names. I am unsure whether I need to alter my csv file to do so.
any help is appreciated
function init(){
var w = 600;
var h = 600;
var barPadding = 20;
var dataset;
var rowConverter = function(d){
return {
year: parseFloat(d.year),
Afghanistan: (d.Afghanistan),
Albania: (d.Albania),
Algeria: (d.Algeria),
Andorra: (d.Andorra),
Angola: (d.Angola)
};
}
d3.csv("hello.csv", rowConverter, function(data){
dataset = data;
if (data==null){
alert("Error, data has not been loaded!");
}
else{
draw(dataset);
console.log(dataset);
}
});
function draw(){
var xScale = d3.scaleLinear()
.domain([d3.min(dataset,function(d){
return d.year;
}),
d3.max(dataset,function(d){
return d.year;
})])
.range([barPadding,w-barPadding]);
var yScale = d3.scaleLinear()
.domain([0,100])
.range([h-barPadding,barPadding*2]);
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
var valueline = d3.line()
.x(function(d) { return xScale(d.year); })
.y(function(d) { return yScale(d.Afghanistan); });
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d.year);
})
.attr("cy", function(d) {
return yScale(d.Afghanistan);
})
.attr("r", 5)
.attr("fill","slategrey")
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.year + "," + d.Afghanistan;
})
.attr("x", function(d) {
return xScale(d.year);
})
.attr("y", function(d) {
return yScale(d.Afghanistan);
})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "blue");
svg.append("path")
.data([dataset])
.attr("class", "line")
.attr("d", valueline);
svg.append("g")
.attr("class","axis")
.attr("transform", "translate(0," + (h - barPadding) + ")")
.call(xAxis);
svg.append("g")
.attr("class","axis")
.attr("transform", "translate(" + barPadding + ",0)")
.call(yAxis);
}
}
window.onload=init;
As a selectAll(null).data(dataArray).enter() uses a data array to enter an element for each item in the data array, we need to create an array for each line we wish to enter. Currently you have an array for each year, but we want to enter a path for each data series/country. So we need to create an array where each item in that array represents a path.
This requires altering the structure of our data from:
[
{year: 2000, series1: number, series2: number... },
{year: 2001, series1: number, series2: number... },
....
]
To an array with an item for each line:
[
{ year: 2000, series1: number },
{ year: 2001, series1: number },
...
{ year: 2000, series2: number },
{ year: 2001, series2: number },
...
]
I'm using this approach because it is commonly seen in d3 cannonical examples such as this.
This is relatively easy to do. After we parse in our csv/tsv/dsv with d3, we can access the columns of the dataset with dataset.columns. The first column isn't a series we want to plot, it represents the x axis, so we can slice it off with dataset.columns.slice(1). Ok, with the remaining columns we can iterate through each series and create the data array above:
I'm using csvParse in the snippet, which replicates d3.csv except that it doesn't use a callback function for the returned data, letting me define the dataset with var dataset = d3.csvParse(... rather than d3.csv("file.csv", function(error, dataset) {...})
var csv = "year,series1,series2,series3\n"+
"2000,5,2,8\n"+
"2001,6,4,7\n"+
"2002,9,3,5\n"+
"2003,10,6,7\n"+
"2004,9,7,8"
var data = d3.csvParse(csv);
var series = data.columns // get the columns
.slice(1) // drop the first column(years)
.map(function(series) { // for each series:
return { // return a new object:
series: series, // name it
values: data.map(function(d) { // get the data:
return { year: d.year, value: d[series] };
})
}
});
console.log(series);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="500" height="300"></svg>
Now we have an item in the data array for each series we want to draw a line for. Now we're cooking with gas. So we can now use selectAll().data(series) to enter a line for each item in the data array, creating a line for each series.
In keeping with Mike Bostock's example I linked to above, I've created an property which identifies which series each item represents, as well as a property which holds the arrays of year/value pairings.
Here's a quick demo:
var csv = "year,series1,series2,series3\n"+
"2000,5,2,8\n"+
"2001,6,4,7\n"+
"2002,9,3,5\n"+
"2003,10,6,7\n"+
"2004,9,7,8"
var data = d3.csvParse(csv);
var series = data.columns
.slice(1)
.map(function(series) {
return {
series: series,
values: data.map(function(d) {
return { year: d.year, value: d[series] };
})
}
});
var x = d3.scaleLinear().domain([2000,2004]).range([0,500]);
var y = d3.scaleLinear().domain([0,10]).range([300,0]);
var color = d3.scaleOrdinal().range(d3.schemeCategory20);
var line = d3.line()
.x(function(d) { return x(d.year); })
.y(function(d) { return y(d.value); });
d3.select("svg")
.selectAll("path")
.data(series)
.enter()
.append("path")
.attr("fill","none")
.attr("stroke", function(d,i) { return color(i) })
.attr("d",function(d) { return line(d.values) });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="500" height="300"></svg>
I'm trying to use attrTween in d3 to animate a pie chart when the page is loaded but it's not working for me. I've used attrTween before to animate a change in data and it's worked fine but this time I want to 'grow' the pie chart when the page is loaded first but it's not behaving as expected and I'm not getting any information as to why this is.
If I remove the line .attrTween('d' arcTweenStart); then everything works fine except of course it does not animate. If the line is left in then nothing is displayed and the arcTweenStart function is never entered. Can anyone spot where I'm going wrong?
function drawCharts()
{
// Create the chart and bind the data to it and position it
var pieChart = d3.select("#groupRisk").selectAll("svg")
.data(dataSet) // Bind the data to the chart
.enter().append("svg")
.attr("id", "pie")
.attr("width", w) // Set th width
.attr("height", h) // Set the height
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")"); // Position the chart
// Create the pie chart layout
var pie = d3.layout.pie()
.value(function(d) { return d.count; })
.sort(null); // Sort is set to null to allow for better looking tweens
// Create "slices" for each data element
var arcs = pieChart.selectAll("g.slice")
.data(pie) // Bind the pie layout to the slices
.attr("id", "arcs")
.enter()
.append("g")
.attr("class", "slice");
// Create the graphics for each slice and colour them
arcs.append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; })
.transition()
.duration(500)
.attrTween('d' arcTweenStart);
}
function arcTweenStart(b)
{
var start =
{
startAngle: b.startAngle,
endAngle: b.endAngle
};
var i = d3.interpolate(start, b);
return function(t)
{
return arc(i(t));
};
}
EDIT:
My data set looks like this:
var dataSet=
[
[
{ "label": "Green", "count": 40 },
{ "label": "Amber", "count": 50 },
{ "label": "Red", "count": 10 }
],
[
{ "label": "Green", "count": 20 },
{ "label": "Amber", "count": 30 },
{ "label": "Red", "count": 50 }
],
[
{ "label": "Green", "count": 50 },
{ "label": "Amber", "count": 20 },
{ "label": "Red", "count": 30 }
]
];
I have an array of data sets so I want to draw a chart for each one.
You don't show what your dataSet variable holds (that would have really helped answer the question!) but assuming your data looks like this:
var dataSet = [{
count: 4
}, {
count: 5
}, {
count: 6
}];
You don't need to do the first bind/enter:
d3.select("#groupRisk").selectAll("svg")
.data(dataSet) // Bind the data to the chart
.enter()
...
This would give you a pie chart for each entry in the data. Getting rid of that, your bind then becomes:
var arcs = pieChart.selectAll("g.slice")
.data(pie(dataSet)) //<-- call pie with the dataSet
.attr("id", "arcs")
.enter()
.append("g")
.attr("class", "slice");
But really to the heart of your question, your tween var start, has the same start/end angle as where you want to end. So, you animate the same thing over and over again. What I think you meant is:
function arcTweenStart(b) {
var start = {
startAngle: b.startAngle,
endAngle: b.startAngle //<-- set end to start and adjust on each call
};
var i = d3.interpolate(start, b);
return function(t) {
return arc(i(t));
};
}
Oh, and one typo in there too:
.attrTween('d' arcTweenStart); //<-- comma missing between 'd' and arcTweenStart
Example here.
I'm trying to create a circle pack graph using nest() and .rollup. I'm getting the following errors:
Error: Invalid value for <g> attribute transform="translate(undefined,undefined)"
Error: Invalid value for <circle> attribute r="NaN"
I want the circles to be sized according to the number of companies in each country. I'm attempting to adapt Mike Bostock's Flare circle-pack example.
If anyone could point me in the direction of any information, I'd be very grateful.
JS code:
var diameter = 960,
format = d3.format(",d");
var pack = d3.layout.pack()
.size([diameter - 4, diameter - 4])
.value(function(d) { return d.size; });
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(2,2)");
//Get data
d3.json("data/countriesNested.php", function(error, data){
var submissionsByCountry = d3.nest()
.key(function(d) { return d.Country; })
.key(function(d) { return d.Organisation; })
.rollup(function(leaves) { return leaves.length; })
.entries(data);
var node = svg.datum(data).selectAll(".node")
.data(pack.nodes)
.enter().append("g")
.attr("class", function(d) { return d.children ? "node" : "leaf node"; })
.attr("transform", function(d) { return "translate(" + d.cx + "," + d.cy + ")"; });
node.append("title")
.text(function(d) { return d.name + (d.children ? "" : ": " + format(d.size)); });
node.append("circle")
.attr("r", function(d) { return d.r; });
});
d3.select(self.frameElement).style("height", diameter + "px");
</script>
Data file (from MySQL using PHP script):
[
{
"Country":"USA",
"ID":4,
"Organisation":"Company 1"
},
{
"Country":"USA",
"ID":5,
"Organisation":"Company 2"
},
{
"Country":"USA",
"ID":6,
"Organisation":"Company 3"
},
{
"Country":"FRANCE",
"ID":19,
"Organisation":"Company 4"
},
{
"Country":"FRANCE",
"ID":24,
"Organisation":"Company 5"
},
{
"Country":"GERMANY",
"ID":10,
"Organisation":"Company 6"
},
{
"Country":"ITALY",
"ID":7,
"Organisation":"Company 7"
},
.....
Thanks for reading.
There are a few errors in your code that need to be fixed:
You need to set the accessor functions for children and values on your pack layout:
var pack = d3.layout.pack()
.size([diameter - 4, diameter - 4])
.children(function(d) {
return d.values; // accessor for children
})
.value(function(d) {
return d.values; // accessor for values
});
Your d3.nest() returns an array but d3.pack() requires you to supply a root object containing the hierarchy. You have to create a root object and put your nested array inside:
var countryRoot = {
key: "root",
values: submissionsByCountry
};
In your code you nest your data into submissionsByCountry but you are not using this variable anywhere else. So you obviously have to refer to it when binding data to your svg. This is accomplished by the above mentioned root object which is later on bound to the svg.
var node = svg.datum(countryRoot).selectAll(".node")
The attributes the pack layout is adding to your data nodes include values x and y, whereas you refered to them as cx and cy which are attributes to <svg:circle> but are not present in your data. Hence, you got your transform="translate(undefined,undefined)" error messages. You should use these attributes as such:
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
I put together a working plunk.
So, you want a parent circle (let's call it the world), with child circles representing each country sized with a count of entries in your JSON array?
d3.json("data/countriesNested.php", function(error, data) {
var submissionsByCountry = d3.nest()
.key(function(d) {
return d.Country;
})
.rollup(function(leaves) {
return leaves.length;
})
.entries(data);
var root = {
"key": "world",
"children": submissionsByCountry
};
...
This will give you something closely resembling the flare.json.
Next, you need to give d3 the right accessor for your circle size.
var pack = d3.layout.pack()
.size([diameter - 4, diameter - 4])
.value(function(d) {
return d.values; //<-- this comes from your roll-up and is the count.
});
Finally it looks like you changed the example code to access non-exist cx and cy attributes in the resulting nodes data:
var node = svg.datum(root).selectAll(".node")
.data(pack.nodes)
.enter().append("g")
.attr("class", function(d) {
return d.children ? "node" : "leaf node";
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; //<-- this is .x, .y
});
Here's an example.
I want to show on the chart D3 e-mail communications between users using data from JSON file on chart FLARE or other.
Users can be represented on a graph as a node and e-mails between them as links.
If it is possible to present in the D3 and someone knows the solution to this problem please let me know.
The following sample array of data for a single email.
In the other tables changes to email details: user names, titles, emails, dates and times.
{
"metadataAsStrings": {
"doc-from": "User 1",
"doc-sender": "User 1",
"caat-derived-recipients": "User 2",
"doc-subject": "Title Email 1"
"doc-recipient": "User 2",
"caat-normalized-author": "User 1",
"caat-derived-email-action": "REPLY"
"caat-derived-end-email": "true",
"caat-derived-inclusive-email-reason": "MESSAGE"
"doc-date": "2014/09/25 10:20:00",
"doc-is", "User 2"
}
}
This is pretty easy with the force layout.
Here's the plnkr:
http://plnkr.co/edit/1Mub7rTUKQuuAB6TAoJb?p=preview
What I actually did was create a json according to the structure that d3 force layout needs.
Assuming I have something similar to your data, I do a little bit of parsing:
{
"edges": [
{
"source":"1",
"target": "2",
"color": "yellow",
"weight": "1.0",
"doc-subject": "Title Email 1"
},
{
"source":"2",
"target": "3",
"color": "blue",
"weight": "1.0",
"doc-subject": "Title Email 2"
}
],
"nodes": [
{
"label":"user 1",
"x":-1015.1223754882812,"y":679.421875,
"id":"1","attributes":{},"color":"rgb(175,156,171)",
"size":20
},
{
"label":"user 2",
"x":-915.1223754882812,"y":659.421875,
"id":"2","attributes":{},"color":"rgb(175,156,171)",
"size":15
},
{
"label":"user 3",
"x":-1015.1223754882812,"y":579.421875,
"id":"3","attributes":{},"color":"rgb(175,156,171)",
"size":15
}
]
}
Then, in d3, I have this code to parse it:
d3.json("graph.json", function(error, graphData) {
//setup the data
var graph = {};
graph.nodes = [];
graph.links = [];
var test = [];
// set the node data
for (var nodeIndex in graphData.nodes){
var curr_node = graphData.nodes[nodeIndex];
graph.nodes[curr_node.id] = {
x: curr_node.x,
y: curr_node.y,
color: curr_node.color,
size: curr_node.size,
label: curr_node.label,
id: curr_node.id
};
test.push(Number(curr_node.id));
}
// sort the IDs
function sortNumber(a,b) {
return a - b;
}
test.sort(sortNumber);
// now go over each ID and set it in the
var tmpNodes = [];
for (var index in test){
tmpNodes.push(graph.nodes[test[index]]);
}
graph.nodes = tmpNodes;
// now setup the edges/links
for (edge in graphData.edges){
var curr_link = graphData.edges[edge];
graph.links.push({source: test.indexOf(Number(curr_link.source)), target: test.indexOf(Number(curr_link.target)), weight: 1.0});
}
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(drag)
.on("dblclick", dblclick)
.on("mouseover", function(d){
hover.html(d.id + ": " + d.label);
})
.on("mouseleave", function(d){
hover.html("");
})
;
node.append("circle")
.attr("r", function(d,i){
return d.size/2;
})
.attr("fill", function(d,i){
return d.color;
})
;
var textNode = node.append("g");
var text = textNode.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr("font-size", function(d){
return 12+(d.size-1)/7+"px";
})
.text(function(d) {
return d.label });
textNode.append("rect")
.attr("x",function(d,i){
var g = node[0][i].childNodes[1];
return -g.getBBox().width;
})
.attr("y",function(d,i){
var g = node[0][i].childNodes[1].childNodes[0];
return -g.getBBox().height+10;
})
.attr("fill","white")
.attr("fill-opacity",0.25)
.attr("width",function(d,i){
var g = node[0][i].childNodes[1];
return g.getBBox().width;
})
.attr("height",function(d,i){
var g = node[0][i].childNodes[1].childNodes[0];
return g.getBBox().height;
})
;
});
So what happens in the code, d3 gets the json, I do a little bit of parsing for d3 to get the data setup better (I made it like that so I could add more users, and sort them by their ID rather then the order in which they are set in the array) and then just give the links and nodes to d3 to plot.
Hope this helps.
My data looks like this:
[
{
name: "Joel Spolsky",
values: [
{
timestamp: 1380432214730,
value: 55
},
{
timestamp: 1380432215730,
value: 32
},
{
timestamp: 1380432216730,
value: 2
},
{
timestamp: 1380432217730,
value: 37
},
// etc
]
},
{
name: "Soul Jalopy",
values: [
{
timestamp: 1380432214730,
value: 35
},
{
timestamp: 1380432215730,
value: 72
},
{
timestamp: 1380432216730,
value: 23
},
{
timestamp: 1380432217730,
value: 3
},
// etc
]
},
// and so on
]
I pass this data into d3.layout.stack so a y and y0 get added. I then draw this stacked layout.
When the data changes, I join the new data to the old.
I can join the groups on name like this:
var nameGroups = this.chartBody.selectAll(".nameGroup")
.data(this.layers, function (d) {
return d.name;
});
But I'm having trouble joining the rectangles (or "bars") on timestamp. The best I can do (so far) is join them on the values key:
var rects = nameGroups.selectAll("rect")
.data(function (d) {
return d.values;
});
How do I join this "inner data" on the timestamp key?
I've tried including the array index:
var rects = nameGroups.selectAll("rect")
.data(function (d, i) {
return d.values[i].timestamp;
});
But that doesn't work because (I think) the timestamp is matched per array index. That is, the join isn't looking at all timestamp values for a match, just the one at that index.
UPDATE
Here is my complete update function:
updateChart: function (data) {
var that = this,
histogramContainer = d3.select(".histogram-container"),
histogramContainerWidth = parseInt(histogramContainer.style('width'), 10),
histogramContainerHeight = parseInt(histogramContainer.style('height'), 10),
width = histogramContainerWidth,
height = histogramContainerHeight,
nameGroups, rects;
/*
FWIW, here's my stack function created within my
init function:
this.stack = d3.layout.stack()
.values(function (d) { return d.values; })
.x(function (dd) { return dd.timestamp; })
.y(function (dd) { return dd.value; });
*/
// save the new data
this.layers = this.stack(data);
// join the new data to the old via the "name" key
nameGroups = this.chartBody.selectAll(".nameGroup")
.data(this.layers, function (d, i) {
return d.name;
});
// UPDATE
nameGroups.transition()
.duration(750);
// ENTER
nameGroups.enter().append("svg:g")
.attr("class", "nameGroup")
.style("fill", function(d,i) {
//console.log("entering a namegroup: ", d.name);
var color = (that.colors[d.name]) ?
that.colors[d.name].value :
Moonshadow.helpers.rw5(d.name);
return "#" + color;
});
// EXIT
nameGroups.exit()
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.remove();
rects = nameGroups.selectAll("rect")
.data(function (d) {
// I think that this is where the change needs to happen
return d.values;
});
// UPDATE
rects.transition()
.duration(750)
.attr("x", function (d) {
return that.xScale(d.timestamp);
})
.attr("y", function(d) {
return -that.yScale(d.y0) - that.yScale(d.y);
})
.attr("width", this.barWidth)
.attr("height", function(d) {
return +that.yScale(d.y);
});
// ENTER
rects.enter().append("svg:rect")
.attr("class", "stackedBar")
.attr("x", function (d) {
return that.xScale(d.timestamp); })
.attr("y", function (d) {
return -that.yScale(d.y0) - that.yScale(d.y); })
.attr("width", this.barWidth)
.attr("height",function (d) {
return +that.yScale(d.y); })
.style("fill-opacity", 1e-6)
.transition()
.duration(1250)
.style("fill-opacity", 1);
// EXIT
rects.exit()
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.transition()
.duration(750)
.remove();
}
You're not actually passing a key function in your code. The key function is the optional second argument to .data() (see the documentation). So in your case, the code should be
.data(function(d) { return d.values; },
function(d) { return d.timestamp; })
Here the first function tells D3 how to extract the values from the upper level of the nesting and the second how, for each item in the array extracted in the first argument, get the key.