Making a line with a different data format - d3.js

I'm trying to make some lines with this data format:
var data3 = [
{ "task": "Estructura", "data": [{"x":new Date(Date.parse("2014-01-03")),"y":0}, {"x":new Date(Date.parse("2014-03-09")),"y":8}] },
{ "task": "Mamposteria", "data": [{"x":new Date(Date.parse("2014-02-01")),"y":0}, {"x":new Date(Date.parse("2014-03-01")),"y":8}] },
{ "task": "Friso", "data": [{"x":new Date(Date.parse("2014-03-01")),"y":0}, {"x":new Date(Date.parse("2014-03-30")),"y":8}] },
{ "task": "Mortero", "data": [{"x":new Date(Date.parse("2014-05-01")),"y":8}, {"x":new Date(Date.parse("2014-07-01")),"y":0}] },
{ "task": "Pisos", "data": [{"x":new Date(Date.parse("2014-07-01")),"y":8}, {"x":new Date(Date.parse("2014-09-01")),"y":0}] }
];
And this the code for generating the lines:
var colors = [
'steelblue',
'green',
'red',
'purple',
'black'
]
var xScale = d3.time.scale()
.domain([
new Date(Date.parse('2014-01-01')),
new Date(Date.parse('2014-12-31'))
])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([0, 8])
.range([height, 0]);
var line = d3.svg.line() // line generator
.interpolate("linear")
.x(function(d) { return xScale(d.data.x); })
.y(function(d) { return yScale(d.data.y); });
var activities = svg.selectAll('.line')
.data(data3)
.enter()
.append("path");
var activitiesAttibutes = activities
.attr("class", "line")
.attr('stroke', function(d,i){
return colors[i%colors.length];
})
.attr("d", line)
.attr("transform", "translate(15,30)");
but I'm having trouble because it is not a two element array.
Do I have to arrange the data in another way or make changes in the line function generator? Could you provide me an example where I can solve this?

There are two things that need to change here - you need to iterate over the data array in each object for the line, and you need to change the accessors so that they apply to a single object in the data array:
var line = d3.svg.line() // line generator
.interpolate("linear")
// d is a single object in a data array
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); });
and then to apply the line, you're acting on a top-level object, so you need to pass in the data array:
var activitiesAttibutes = activities
.attr("d", function(d) {
return line(d.data);
});
See fiddle: http://jsfiddle.net/h07s5jc5/1/

Related

Updating each iteration using d3.js

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>

Projection in D3 v5 not working with coordinates data

I am trying to make a map of meteorite landings across the world using D3 v5. I have the map displaying. The coordinates (lat, long) from the meteorite json file are loading. I am trying to use them in .attr for "cx" and "cy". When I console.log the coordinates in .attr, they show up, but when I try to pass them through my projection so they will display properly on the map, I am getting the following error: Uncaught (in promise) TypeError: Cannot read property 'coordinates' of null.
Can anyone help me figure out how to get this working? Appreciate any help you can offer.
Here a link to a Codepen: https://codepen.io/lieberscott/pen/QryZPR?editors=0110
And my code:
const w = 960;
const h = 600;
const svg = d3.select(".map")
.append("svg")
.attr("height", h)
.attr("width", w);
let projection = d3.geoMercator()
.translate([w/2, h/2])
.scale(140);
const path = d3.geoPath()
.projection(projection);
let tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip");
const files = ["https://unpkg.com/world-atlas#1.1.4/world/110m.json", "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/meteorite-strike-data.json"];
Promise.all(files.map((url) => d3.json(url))).then(function(data) {
svg.append("g")
.attr("class", "country")
.selectAll("path")
.data(topojson.feature(data[0], data[0].objects.countries).features)
.enter().append("path")
.attr("d", path);
svg.selectAll(".meteor")
.data(data[1].features)
.enter().append("circle")
.attr("class", "meteor")
.attr("cx", (d) => {
console.log(d.geometry.coordinates[0]);
let coords = projection([d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return coords[0];
})
.attr("cy", (d) => {
let coords = projection([d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return coords[1];
})
.attr("r", 6);
});
Your data is missing coordinates for certain locations, eg:
{
"type": "Feature",
"geometry": null,
"properties": {
"mass": "2250",
"name": "Bulls Run",
"reclong": null,
"geolocation_address": null,
"geolocation_zip": null,
"year": "1964-01-01T00:00:00.000",
"geolocation_state": null,
"fall": "Fell",
"id": "5163",
"recclass": "Iron?",
"reclat": null,
"geolocation_city": null,
"nametype": "Valid"
}
},
This generates the error you see, stopping the appending of circles.
You could try to filter them out with something like:
svg.selectAll(".meteor")
.data(data[1].features.filter(function(d) {
return d.geometry; // return only features with a geometry
}) )
Giving us:
Updated pen: https://codepen.io/anon/pen/XqXQYy?editors=0110
Also, I'll quickly note that this:
projection([d.geometry.coordinates[0], d.geometry.coordinates[1]]);
Can be simplified to this:
projection(d.geometry.coordinates);

D3: It is possible to do in D3?

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.

How to update line chart points when the source data changes

I've created a multi-line chart from the following source data.
[
{
"key":"108",
"title":"Series 1",
"items":[
{
"key":"54048872e9c2021fd8231051",
"value":1.0
},
{
"key":"540488a1e9c2021fd823107b",
"value":2.0
}
]
},
{
"key":"15",
"title":"Series 2",
"items":[
{
"key":"54048872e9c2021fd8231051",
"value":1.0
},
{
"key":"540488a1e9c2021fd823107b",
"value":4.0
}
]
}
]
We render a line for each Series and circles for each data "point"
var series = svg.selectAll('.series')
.data(this.data)
.enter()
.append('g')
.attr('class', 'series');
series.append('path')
.attr('class', 'line');
// add points to chart
var points = series.append('g')
.attr('class', 'point')
.selectAll('circle')
.data(function (d) {
return d.items;
})
.enter()
.append('circle')
.attr('r', 5);
Then when we first render the chart or when the window resizes we actually set the coordinates of the line and circles:
this.svg.selectAll('.line')
.attr('d', function (d) {
return chart.line(d.items);
});
this.svg.selectAll('.point circle')
.attr('cx', function (d) { return chart.xScale(d.key); })
.attr('cy', function (d) { return chart.yScale(d.value); });
Here, chart.line corresponds to a d3 line generator previously created.
When the source data changes I can update the line by setting its data:
this.svg.selectAll('.line')
.data(this.data);
But I can't figure out how to do the same thing with the data points (circles).
I've tried the following but the data on the individual point is not actually updated. Here the first console log (after selecting the .point elements) returns the correct data but the cx attribute function is returning the old data.
var series = this.svg.selectAll('.series')
.data(this.data)
.selectAll('.point')
.data(function (d) {
console.log(d.items);
return d.items;
})
.selectAll('circle')
.attr('class', 'updated')
.attr('cx', function (d) { console.log(d); return chart.xScale(d.key); })
.attr('cy', function (d) { return chart.yScale(d.value); });

Using CSV for data

I wish to replace this portion of my script (from a dashingd3js tutorials) with a reference to a CSV file with the same data.
var lineData = [ { "x": 1, "y": 5}, { "x": 20, "y": 20},
{ "x": 40, "y": 10}, { "x": 60, "y": 40},
{ "x": 80, "y": 5}, { "x": 100, "y": 60}];
The csv is located in the same directory and named 'dataFile.csv'
dataFile.csv:
x,y
1,5
20,20
40,10
60,40
80,5
100,60
Edit: Trying to incorporate feedback from Lars and d3noob, this is what I tried:
//The data for our line
d3.csv("testData.csv", function(error, lineData){
//This is the accessor function we talked about above
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
//The SVG Container
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
//The line SVG Path we draw
var lineGraph = svgContainer.append("path")
.attr("d", lineFunction(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
}
Here is another version of the code which I am editing as I do more research. It currently does not work.
//The data for our line
d3.csv("testData.csv", function(d){
return{
lineData.x: d.x,
lineData.y: d.y };
}, function(error, rows) {
console.log(rows);
});
//This is the accessor function we talked about above
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
//The SVG Container
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
//The line SVG Path we draw
var lineGraph = svgContainer.append("path")
.attr("d", lineFunction(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
}
Your drawing code needs to stay inside csv callback:
d3.csv("testData.csv", function(data){
// this converts data to number
data.forEach(function(d) {
d.x = +d.x;
d.y = +d.y;
});
// rest of drawing code
...
});
See another example here:
http://vida.io/documents/QZZTrhk7SmfChczYp
It's easier to debug if you can post link to working code.
You need to load the file through an asynchronous request with d3.csv. Example code:
d3.csv("dataFile.csv", function(error, data) {
// do something exciting with data
}

Resources