I'm tring to create a multi-line chart using D3.js. Here is my sample csv data:
A,B,C,D,E,F,G,date
53831,72169.87,54219,72555,63466,115312,126390,4/26/16
53031,70901.11,5976,5111,62388,111626,123198,7/10/16
51834,69917.12,5449,4902,62990,114296,124833,4/24/16
54637,73016.92,58535,77379,63090,113216,125261,6/14/16
54801,73072.4,57997,75674,63090,113216,125261,6/27/16
53578,71718.19,51085,69152,63370,115061,125949,5/3/16
51679,68897.14,6021,5421,61514,110330,121972,7/24/16
Here is my code snippet. However I keep seeing the error like d is not an expected number(as title shows). Can anyone please point me out?
Also I feel like the way I'm parsing data is ugly (two for loop). Any suggestions are welcome.
// Set the dimensions of the canvas / graph
var margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
},
width = 800 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%b %Y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom");
var yAxis = d3.svg.axis().scale(y)
.orient("left");
var line = d3.svg.line()
.interpolate("basis")
.x(function (d) {
return x(d.date);
})
.y(function (d) {
return y(d.value);
});
// Adds the svg canvas
var svg = d3.select("#d3-line-chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
//get the data
d3.csv("test.csv", function (error, data) {
var res = [];
var cols = d3.keys(data[0])
.filter(function (key) {
return key;
});
for (var j = 0; j < cols.length - 1; j++) {
var col = cols[j];
var row = [];
for (var i = 0; i < data.length; i++) {
row.push({
symbol: col,
date: data[i]["date"],
value: +data[i][col]
});
}
res.push(row);
}
// Scale the range of the data
x.domain(d3.extent(res, function (d) {
return d.date;
}));
y.domain([0, d3.max(res, function (d) {
return d.value;
})]);
svg.selectAll(".line")
.data(res)
.enter().append("path")
.attr("class", "line")
.attr("d", line);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
Firstly, provide sorted data in the CSV on the basis of date so:
Instead of this:
A,B,C,D,E,F,G,date
53831,72169.87,54219,72555,63466,115312,126390,4/26/16
53031,70901.11,5976,5111,62388,111626,123198,7/10/16
51834,69917.12,5449,4902,62990,114296,124833,4/24/16
54637,73016.92,58535,77379,63090,113216,125261,6/14/16
54801,73072.4,57997,75674,63090,113216,125261,6/27/16
53578,71718.19,51085,69152,63370,115061,125949,5/3/16
51679,68897.14,6021,5421,61514,110330,121972,7/24/16
Provide sorted CSV:
A,B,C,D,E,F,G,date
51834,69917.12,5449,4902,62990,114296,124833,4/24/16
53831,72169.87,54219,72555,63466,115312,126390,4/26/16
53578,71718.19,51085,69152,63370,115061,125949,5/3/16
54637,73016.92,58535,77379,63090,113216,125261,6/14/16
54801,73072.4,57997,75674,63090,113216,125261,6/27/16
53031,70901.11,5976,5111,62388,111626,123198,7/10/16
51679,68897.14,6021,5421,61514,110330,121972,7/24/16
Secondly:
The date parser you providing is incorrect:
var parseDate = d3.time.format("%b %Y").parse;
Should have been this:
var parseDate = d3.time.format("%m/%d/%Y").parse;
Because your date is in the format ,4/26/16.
Thirdly,
The way you are calculating the x and y domain extent is wrong:
So instead of this:
// Scale the range of the data
x.domain(d3.extent(res, function (d) {
return d.date;
}));
y.domain([0, d3.max(res, function (d) {
return d.value;
})]);
It should have been:
x.domain(d3.extent(data, function (d) {
return parseDate(d.date);
}));
y.domain([0, d3.max(res, function (d) {
return d3.max(d, function(d2){console.log(d2);return d2.value;});
})]);
Reason: the res array you creating is an array inside an array so need that handling in here.
Working code here
I have created a linechart with D3.js, but can't seem to figure out how to control the X-axis completely.
You can see my example her: http://codepen.io/DesignMonkey/pen/KdOZNQ
I have sat the ticks count for the x-axis to 3 days, and I have an Array with a range of 31 days, that should show a day in each end of the x-axis and skip to every third day. But for some reason when the a-axis pass the 1st in the month, it shows both 2015-08-31 and 2015-09-01 and doesn't skip to 2015-09-03 as it is supposed to.
My code for the linechart is here:
// Set the dimensions of the canvas / graph
let margin = {top: 30, right: 20, bottom: 30, left: 40},
width = 330 - margin.left - margin.right,
height = 180 - margin.top - margin.bottom;
// Set the ranges
var x = d3.time.scale().range([0, width]).nice(10);
var y = d3.scale.linear().rangeRound([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom")
.ticks(d3.time.days, 3)
.tickFormat(d3.time.format('%e'))
.innerTickSize(-height)
.outerTickSize(0)
.tickPadding(10)
var yAxis = d3.svg.axis().scale(y)
.orient("left")
.ticks(5)
.innerTickSize(-width)
.outerTickSize(0)
.tickPadding(10)
// Define the line
var valueline = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.value); });
// Adds the svg canvas
let svg = d3.select(template.find(".chart"))
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// For each line
data.forEach((item) => {
// Scale the range of the data
x.domain(d3.extent(item.data, function(d) {
if(d.value != undefined)
return d.date;
}));
// Create a perfect looking domainrange to the nearest 10th
y.domain([
Math.floor(d3.min(item.data, function(d) {
if(d.value != undefined)
return d.value;
})/10)*10
,
Math.ceil(d3.max(item.data, function(d) {
if(d.value != undefined)
return d.value;
})/10)*10
]);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Add only points that have value
svg.append("path")
.attr("class", "line color-"+ item.colorClass)
.attr("d", valueline(item.data.filter((pnt) => pnt.value != undefined)));
});
Can somebody tell me what I'm doing wrong here? :)
/Peter
UPDATE:
I found out it's like, that it want to show the new month. See this pen: http://codepen.io/DesignMonkey/pen/jbgYZx
It doesn't say "Septemper 1" but only "Septemper" like it wants to label the change of month. How do I disable this? :)
Solution:
var count = 0;
var tickRange = data[0].data.map((item) => {
count = (count == 3) ? 0 : count+1;
if(count == 1) {
return item.date;
}
})
.filter((d) => d != undefined);
and then:
var xAxis = d3.svg.axis().scale(x)
.orient("bottom")
.ticks(d3.time.days, 3)
.tickValues(tickRange)
.tickFormat(d3.time.format('%e'))
.innerTickSize(-height)
.outerTickSize(0)
.tickPadding(10)
Thanks :)
I have created a line chart with D3.js that displays time-based series of values. The chart can be panned and zoomed. To improve the user experience, I also implemented auto-scaling on the Y-axis, which sets the domain of the y-axis to the values [min, max] extent of the currently visible data.
Now I want to add a transition to the chart that animates the change of the y-axis scale (like in this example). It works, but unfortunately it looks and behaves terrible when panning the chart, because the transition does not only animate the y-part of each line point, but also the x-part, which leads to a very unpleasant effect of smearing the line horizontally as well as a noticable lag while panning the chart. It just feels wrong.
So what I would like to achieve is this: the x-property of the line's data should be set instantly without transition to avoid the lag, the y-property should be animated. This is the part where I update the chart:
self.svg.select("path.line").transition().attr("d", self.valueline);
Where valueline looks like this:
self.valueline = d3.svg.line()
.x(function (d) { return self.x(d.t); })
.y(function (d) { return self.y(d.v); });
Is there a way to apply the transition to d.v (the value) only?
First of all, the way it's done in the example is not very idiomatic, it may be better to bind the data to the path first and then apply the transition.
Then you could bind some null data at the start with all d.v == 0 and then bind the real data with a transition. This gives a y-value-only transition. The following edits in the example will show what I mean...
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%d-%b-%y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
var valueline = d3.svg.line()
.x(function (d) {
return x(d.date);
})
.y(function (d) {
return y(d.close);
});
// Adds the svg canvas
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
//EDIT ********************************************
var line;
// Get the data
d3.csv("data.csv", function (error, data) {
data.forEach(function (d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
var entryData = data.map(function (d) {
return ({date: d.date, close: 0})
})
// Scale the range of the data
x.domain(d3.extent(data, function (d) {
return d.date;
}));
y.domain([0, d3.max(data, function (d) {
return d.close;
})]);
//Bind the data first THEN add the valueline path.
//Start with all zero y values
line = svg.selectAll("line").data([entryData]);
line.enter().append("path")
.attr("class", "line")
.attr("d", valueline);
// Transition initial data, y values only
line.data([data]);
line.transition().delay(500).call(trans, "entry")
// Add the valueline path.
.attr("d", valueline);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis with entry transition
var gYaxis = svg.append("g")
.attr("class", "y axis");
//Null axis starting point
y.domain([0, 0]);
gYaxis.call(yAxis);
//Final axis after entry transition
y.domain([0, d3.max(data, function (d) {
return d.close;
})]);
gYaxis.transition().delay(500).call(trans, "entry")
.call(yAxis);
});
// ** Update data section (Called from the onclick)
function updateData() {
// Get the data again
d3.csv("data-alt.csv", function (error, data) {
data.forEach(function (d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data again
x.domain(d3.extent(data, function (d) {
return d.date;
}));
y.domain([0, d3.max(data, function (d) {
return d.close;
})]);
//EDIT ********************************************
// Bind the new data and then transition
line = svg.selectAll(".line").data([data]);
line.enter().append("path")
.attr("class", "line")
line.transition().call(trans)
// Add the valueline path.
.attr("d", valueline);
// Select the section we want to apply our changes to
var svgTrans = d3.select("body").transition();
// Make the changes
svgTrans.select(".x.axis") // change the x axis
.call(trans)
.call(xAxis);
svgTrans.select(".y.axis") // change the y axis
.call(trans)
.call(yAxis);
});
}
function trans (transition, name){
var delays = {normal:0, entry: 500};
name = name || "normal";
transition.duration(750).delay(delays[name]).ease("sin-in-out")
}
The JSFiddle of my code is at this URL http://jsfiddle.net/b8eLJ/1/
Within the code, I have implemented a back button. The objective is that the calendar on x-axis should go back in time. As this happens, the lines should also move back in time.
I have the x-axis working correctly with the new domain - but am struggling with the lines and circles.
I have also looked at the following example http://bl.ocks.org/mbostock/1166403
I am a little confused about how to either "select the line and move it" or "destroy the line and recreate it". I have tried both and neither seem to work.
function startFunction() {
console.log("start");
endDate.setDate(endDate.getDate() - 7);
startDate.setDate(startDate.getDate() - 7);
//change the domain to reflect the new dates
xScale.domain([startDate, endDate]);
var t = svg.transition().duration(750);
t.select(".x.axis").call(xAxis);
//???
var pl = svg.selectAll("path.line");
pl.exit().remove();
}
// INPUT
dataset2 = [{
movie: "test",
results: [{
week: "20140102",
revenue: "5"
}, {
week: "20140109",
revenue: "10"
}, {
week: "20140116",
revenue: "17"
}, ]
}, {
movie: "test",
results: [{
week: "20140206",
revenue: "31"
}, {
week: "20140213",
revenue: "42"
}]
}];
console.log("1");
var parseDate = d3.time.format("%Y%m%d").parse;
var lineFunction = d3.svg.line()
.x(function (d) {
return xScale(parseDate(String(d.week)));
})
.y(function (d) {
return yScale(d.revenue);
})
.interpolate("linear");
console.log("2");
var endDate = new Date();
var startDate = new Date();
startDate.setDate(startDate.getDate() - 84);
//SVG Width and height
var margin = {
top: 20,
right: 10,
bottom: 20,
left: 40
};
var w = 750 - margin.left - margin.right;
var h = 250 - margin.top - margin.bottom;
//X SCALE AND AXIS STUFF
//var xMin = 0;
//var xMax = 1000;
var xScale = d3.time.scale()
.domain([startDate, endDate])
.range([0, w]);
console.log(parseDate("20130101"));
console.log("3");
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(12);
console.log("4S");
//Y SCALE AND AXIS STUFF
//max and min test
var minY = d3.min(dataset2, function (kv) {
return d3.min(kv.results, function (d) {
return +d.revenue;
})
});
var maxY = d3.max(dataset2, function (kv) {
return d3.max(kv.results, function (d) {
return +d.revenue;
})
});
console.log("min y " + minY);
console.log("max y " + maxY);
var yScale = d3.scale.linear()
.domain([minY, maxY])
.range([h, 0]);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(10);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
console.log("4S1");
//CREATE X-AXIS
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(,0)")
.call(yAxis);
//create circle
var movie_groups = svg.selectAll("g.metric_group")
.data(dataset2).enter()
.append("g")
.attr("class", "metric_group");
var circles = movie_groups.selectAll("circle")
.data(function (d) {
return d.results;
});
svg.selectAll("g.circle")
.data(dataset2)
.enter()
.append("g")
.attr("class", "circle")
.selectAll("circle")
.data(function (d) {
return d.results;
})
.enter()
.append("circle")
.attr("cx", function (d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
//create line
var lineGraph = svg.selectAll("path.line")
.data(dataset2).enter().append("path")
.attr("d", function (d) {
return lineFunction(d.results);
})
.attr("class", "line");
To clarify, lines moving back in time will actually translate to lines moving forward to the right since their date is not changing and the time axis is moving to the right. I hope I am thinking clear here...it is late.
Importantly, your data is not changing between clicks so there will be no new data to be bound in the enter() selection after the initial call. Lines will be drawn once and only once.
Since your axis is the movable part, one quick solution would be to keep removing the lines and rebuilding them again inside startFunction, like this:
var lineGraph = svg.selectAll("path.line").remove();
lineGraph = svg.selectAll("path.line")
.data(dataset2);
I tried this and it works. Note however that I am not removing the lines off of the exit selection and thus not truly leveraging the EUE pattern (Enter/Update/Exit). But, since your data is not really changing, I am not sure what you could add to it between clicks that could be used as the key to selection.data. I hope this helps...
NOTE: I am not particularly fond of this solution and considered writing it as a comment but there is too much verbiage. For one thing, object constancy is lost since we veered away from the EUE pattern and the "graph movement" is ugly. This can be mitigated some by adding a transition delay in the drawing of lines (path) as shown below...but still...
lineGraph.enter().append("path").transition().delay(750)
.attr("d", function (d) {
return lineFunction(d.results);
})
I'm plotting the graphs with "number of clicks" as Y axis and "date" as X axis. Because of the large amount of data, the X axis is jumbled and couldn't display all the dates. I tried to use ticks(d3.time.months, 1) and tickFormat('%b %Y') to cur off some of the ticks. When I run the code, I got "getMonth() is not defined" for my data.
.tsv file:
date count
2013-01-01 4
2013-03-02 5
sample code:
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1, 0);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(d3.time.months, 1)
.tickFormat(d3.time.format('%b %Y'))
d3.tsv("data_Bar_Chart_Paino_Kristen.tsv", type, function(error, data) {
x.domain(data.map(function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.hits; })]);
var temp = height + 30; //+15
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(-8," + temp + ")")
.call(xAxis);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.date); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.hits); })
.attr("height", function(d) {return height - y(d.hits); });
}
Is there a way to solve my problem and show ticks properly?
You'll need to tell D3 that your axis is a date. Try this:
//assumes the data is sorted chronologically
var xMin = data[0].dateFieldName;
var xMax = data[data.length-1].dateFieldName;
//set the scale for the x axis
var x = d3.time.scale().domain([xMin, xMax]).range([0, width]);
//straight from your code
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(d3.time.months, 1)
.tickFormat(d3.time.format('%b %Y'))
The trick here is to use the d3.time.scale() method.
If you use the d3.time.scale() you have another problem, you can't use x.rangeBand() when you draw the rect.