I have a simple d3 area chart, with two areas plotted using the following data:
var data = [
[{'year':0,'amount':2},{'year':1,'amount':3},{'year':2,'amount':9},{'year':3,'amount':5},{'year':4,'amount':6},{'year':5,'amount':7},{'year':6,'amount':8},{'year':7,'amount':9},{'year':8,'amount':10},{'year':9,'amount':11},{'year':10,'amount':12}],
[{'year':0,'amount':1},{'year':1,'amount':2},{'year':2,'amount':8},{'year':3,'amount':4},{'year':4,'amount':5},{'year':5,'amount':6},{'year':6,'amount':7},{'year':7,'amount':8},{'year':8,'amount':9},{'year':9,'amount':10},{'year':10,'amount':11}]
];
The two separate arrays of objects allow me to plot two areas on one chart using the code below:
var colors = [
'steelblue',
'lightblue',
];
var margin = {top: 20, right: 30, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain(d3.extent(data.map(function(d,i) { console.log(d); return d[i].year; })))
.range([0, width]);
var y = d3.scale.linear()
.domain([-1, 16])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(-height)
.tickPadding(10)
.tickSubdivide(true)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.tickPadding(10)
.tickSize(-width)
.tickSubdivide(true)
.orient("left");
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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "y axis")
.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", (-margin.left) + 10)
.attr("x", -height/2)
.text('Axis Label');
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
//************************************************************
// Create D3 line object and draw data on our SVG object
//************************************************************
var line = d3.svg.area()
.interpolate("cardinal")
.x(function(d) { return x(d.year); })
.y0(height)
.y1(function(d) { return y(d.amount); });
svg.selectAll('.line')
.data(data)
.enter()
.append("path")
.attr("class", "area")
.attr('fill', function(d,i){
return colors[i%colors.length];
})
.attr("d", line);
Thing is I need to set the domains based on the data. I've tried doing:
.domain(d3.extent(data.map(function(d) { return d.amount; })))
...when creating my linear scale but obviously this doesn't work as the array map in the call above just maps out the nested arrays instead of the objects inside.
How do I set the domain using data in this format? Or is there a better way to structure my data whilst still allowing for multiple areas to be drawn?
To get the overall extent of all the amount values contained in both arrays you need to somehow merge these arrays into one. There are several ways this could be done:
d3.merge() to merge both arrays into one:
var allValues = d3.merge(data);
The main advantage of this approach over the following ones is the fact, that this will work with any number of nested arrays in data without any changes to the code.
Built-in method Array.prototype.concat():
var allValues = data[0].concat(data[1])
If you want to show off and don't need to be compatible with older version of JavaScript, you can apply the spread operator new to ES6:
var allValues = [...data[0], ...data[1]];
Having this flattened array containing all values you can pass it to d3.extent() to calculate the overall extent.
var extent = d3.extent(allValues, function(d) { return d.amount; });
var data = [
[{'year':0,'amount':2},{'year':1,'amount':3},{'year':2,'amount':9},{'year':3,'amount':5},{'year':4,'amount':6},{'year':5,'amount':7},{'year':6,'amount':8},{'year':7,'amount':9},{'year':8,'amount':10},{'year':9,'amount':11},{'year':10,'amount':12}],
[{'year':0,'amount':1},{'year':1,'amount':2},{'year':2,'amount':8},{'year':3,'amount':4},{'year':4,'amount':5},{'year':5,'amount':6},{'year':6,'amount':7},{'year':7,'amount':8},{'year':8,'amount':9},{'year':9,'amount':10},{'year':10,'amount':11}]
];
console.log(d3.extent(d3.merge(data), function(d) { return d.amount; })); // using d3.merge()
console.log(d3.extent(data[0].concat(data[1]), function(d) { return d.amount; })); // using Array.prototype.concat()
// This will only work in compatible browsers which support the new ES6 spread operator
console.log(d3.extent([...data[0], ...data[1]], function(d) { return d.amount; })); // using ES6 spread operator
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Related
I am able to see glucose readings but time shows up as: 0NaN-NaN-NaNTNaN:NaN:NaN.NaNZ
I am trying to parse a dataset of time of the format "Y-M-D H:M:S.MS". I need it to be formatted properly so that I can show it on the x axis. I have attached sample dataset to this code.
My code looks like this:
<script>
function overview(){
// Set the dimensions of the canvas / graph
var margin = {top: 10, right: 20, bottom: 30, left: 30},
width = 600 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// // Parse the date / time
var parseDate = d3.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ");
var x = d3.scaleTime()
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom()
.tickFormat(d3.timeFormat("%H"));
var yAxis = d3.axisLeft();
// Define the line
var valueline = d3.line()
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.glucoseReading); });
// 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 + ")");
// Get the data
d3.csv("glucose.csv", function(error, data) {
data.forEach(function(d) {
d.time = parseDate(d.time);
d.glucoseReading = +d.glucoseReading;
console.log(d.time);
console.log(d.glucoseReading);
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.time; }));
y.domain([0, d3.max(data, function(d) { return d.glucoseReading; })]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the scatterplot
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.glucoseReading); });
// 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);
});
}
overview();
</script>
My Dataset looks like:
You want to convert (parse) strings to dates, not the other way around. Therefore, instead of d3.utcFormat(), you have to use d3.utcParse(). On top of that, your specifier is incorrect: there is no timezone in your strings.
So, this should be your parseDate function and specifier:
var parseDate = d3.utcParse("%Y-%m-%d %H:%M:%S.%L")
Here is it working (check your browse console, not the snippet's one):
var parseDate = d3.utcParse("%Y-%m-%d %H:%M:%S.%L")
var string = "2017-08-23 00:03:52.591";
console.log(parseDate(string))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I am trying to import single-column tsv data in order to create a line chart with d3.js, but I am getting an error message, and I don't know how to solve this.
Here is my code (based on already existing examples):
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 50}, // as if margin would be an object
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis() // we create a new axis
.scale(x) // we set the scale based on the var x
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.x(function(d,i) {
console.log('Plotting X value for data point: ' + d + ' using index: ' + i + ' to be at: ' + x(i) + ' using our xScale.');
return x(i);
})
.y(function(d) { return y(d.values); });
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 + ")");
d3.tsv("data.tsv", type, function(error, data) {
if (error) throw error;
x.domain(d3.extent(data, function(d) { return d.length; })); // get min and max date values
y.domain(d3.extent(data, function(d) { return d.values; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis); // we call also a var!!
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Values");
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
});
function type(d) {
d.values = +d.values; // converts string to number
return d;
}
</script>
And here is the tsv file:
values
1.5
2.6
3.4
7.8
3.8
1.0
6.7
The error I get is:
"Invalid value for <path> attribute d="MNaN,416.9117647058824LNaN,344.1176470588235LNaN,291.17647058823525LNaN,0LNaN,264.70588235294116LNaN,450LNaN,72.79411764705881"
Apparently my data index is not correctly read.
Could someone maybe help?
One change instead of
x.domain(d3.extent(data, function(d) { return d.length; })); // get min and max
Do this since the x axis is the length of the array.
x.domain([0, data.length-1]); // get min and max
Working code here
Hope this helps!
I am trying do some path interpolation in D3. I'd like to produce an area plot like this, but I want to transition the area along the y-axis, starting from the bottom of the xaxis up to the final position shown in the example. Here's a quick sketch to explain what I'd like to do:
I'd like to start the transition with no area:
and transition it up along the y-axis:
Using the code, copied from the example, here's what I'd trying to do:
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var area = d3.svg.area()
.x(function(d) { return x(d.date); })
.y0(height)
.y1(function(d) { return y(d.close); });
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 + ")");
d3.tsv("data.tsv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
var dataSel = svg.selectAll('.area').data(data)
dataSel.exit().remove()
dataSel.enter()
.append('path')
.attr("class", "area")
.attr("d", 'M0,0h' + width) // my idea here was to draw a path that
// has no area along the x-axis and then
// interpolate the path up to the final area
dataSel.transition() // transition the path to its final position
.duration(1000)
.attr("d", area)
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Price ($)");
});
Can anyone explain/show how I can transition a path along one axis as I'm trying to do?
D3 path transitions only really work if the starting and ending paths have the same number of control points. So, for example, D3 can't transition a line into an area. What you could do, however, is something like the following:
Use d3.area to generate the final path for the area.
Make a copy of the path and, in the copy, change all the control points that correspond to the "top" of the area to control points on the "bottom" of the area. (In other words, modify their y-values.)
Draw the area using this modified path.
Transition to the final path.
I try to make a bar chart on the basis of 2D data array (I din`t want to use 2D array initially, so there is a function "mergingAr", which merges them) using d3.js. Here is the code:
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
var arr1 = [399200,100000, 352108, 600150, 39000, 17005, 4278];
var arr2 = [839, 149, 146, 200, 200, 121, 63];
function mergingAr (array1, array2)
{
var i, out = [];//literal new array
for(i=0;i<array1.length;i++)
{
out.push([array1[i],array2[i]]);
}
return out;
}
var data = mergingAr(arr1, arr2);
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) {
return d[0]; })])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d[0]); })
//.attr("width", x.rangeBand())
.attr("width", width/a1.length)
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return height - y(d[1]); });
Te problem is - the bars cover each other, there are no distance between them, even if I used rangeRoundBands.
There are 2 issues in your code.
The first one is that the data array is not sorted. In order to sort it you can do:
out = out.sort(function(a,b) { return d3.ascending(a[0],b[0]) })
before returning out in your mergeAt function. Sorting the array makes sure that you process bars in the right order.
The second issue is that your intervals are not equal. To remediate to this, I made the width of a block equal to the distance to the next one (but you might want to do something different):
.attr("width", function(d,i){
if(i!=(data.length-1)) {
return x(data[i+1][0])-x(data[i][0])
} else {
return 10; // the last block is of width 10. a cleaner way is to add a
// marker at the end of the array to know where to finish
// the axis
}
})
JsFiddle: http://jsfiddle.net/chrisJamesC/6WJPA/
Edit
In order to have the same interval between each bar and the same width, you have to change the scale to an ordinal one:
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(data.map(function(d){return d[0]}))
Then, you need to change the way you compute the width to:
.attr("width", x.rangeBand())
JsFiddle: http://jsfiddle.net/chrisJamesC/6WJPA/2/
My chart is working completely as expected, except that my parseDate function doesn't want to give me correct dates on the x-axis. I'm sure this is something simple.
Currently I'm adding d3.v2.min.js, without any additional helper libraries - do I need something else to get d3.time.format() working?
Without parsing the date my data returns an x-axis with:
.960 .965 .970 .975
rather than
1960 1965 1970 1975
The JSON is structured like this:
[{"year":1959,"average":315.97},
{"year":1960,"average":316.91},
{"year":1961,"average":317.64},
...etc
{"year":2011,"average":391.57}]
The code with the // commented sections being the issue:
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 920 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// parseDate below not working
var parseDate = d3.time.format("%Y").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// data taking format
d3.json("{{ asset('js/data_co2.json') }}", function(data) {
// parseDate() not working below
Gives error: Uncaught TypeError: Object 1959 has no method 'substring'
data.forEach(function(d) {
d.year = parseDate(d.year);
d.average = +d.average;
});
x.domain(d3.extent(data, function(d) { return d.year; }));
y.domain([260, 420]);
var area = d3.svg.area()
.x(function(d) { return x(d.year); })
.y0(height)
.y1(function(d) { return y(d.average); });
var svg = d3.select("#co2").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 + ")");
svg.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("CO2 Levels (ppm)");
});
</script>
parseDate, the function "prepared" by d3, expects to be passed a string for parsing. Your years are (appropriately) numbers, so you would need to convert them to strings:
parseDate(String(d.year));
or
parseDate(d.year.toString());
However, keep in mind that the same result – a Date object – could be achieved natively, like this:
var date = new Date(d.year, 0);// The required 0, is for January