I'm really struggling to understand d3's area and stack layout stuff. I tried making what I thought was the smallest example but nothing appears and in fact it prints errors in the console. What am I not getting? Here's the code.
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 300)
var testData = [
[ 0, 10],
[10, 20],
[20, 30],
[30, 20],
];
svg.selectAll("path.area")
.data(testData)
.enter().append("path")
.style("fill", "#ff0000")
.attr("d", d3.svg.area());
The dimension of the data is not correct. Each area path needs a 2D array, like this:
d3.svg.area()([[ 0, 10], [10, 20], [20, 30], [30, 20]])
results in:
"M0,10L10,20L20,30L30,20L30,0L20,0L10,0L0,0Z"
That means that you need to bind a 3D array to the selection. Each element (i.e. path) in the selection will then receive a 2D array.
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 300)
var testData = [
[ 0, 10],
[10, 20],
[20, 30],
[30, 20],
];
svg.selectAll("path.area")
.data([testData]) // dimension of data should be 3D
.enter().append("path")
.style("fill", "#ff0000")
.attr("class", "area") // not the cause of your problem
.attr("d", d3.svg.area());
Sometimes it's easier to picture what is going on by imagining that you would like to create multiple areas. Then it would look like:
var testData1 = [
[ 0, 10],
[10, 20],
[20, 30],
[30, 20],
];
var testData2 = [
[100, 110],
[110, 120],
[120, 130],
[130, 120],
];
svg.selectAll("path.area")
.data([testData1, testData2])
.enter().append("path")
.style("fill", "#ff0000")
.attr("class", "area")
.attr("d", d3.svg.area());
Related
I have two series of scale, one is linear and the other is band, how can I make them to match up if there is some caps in the data.
Take a look at the example if necessary.
Mouse over and you see the boxes are not matching with the breaks of line.
If you want your scaleBand to be scaled (widened) where data is missing, I don't think that the scaleBand is the proper method for this, but it is unclear if that is something you want. Band scales are intended to provide equal spacing for each data value and that all values are present - it is an ordinal scale.
Assuming you only want the band scale to be aligned with your data where it is present:
If you log the domains of each of your x scales (scaleBand and scaleLinear) we find that the scaleBand has a domain of:
[ "1", "2", "8", "9", "13", "14", "20", "22" ] // 8 elements
And the scaleLinear has a domain of:
[ 1, 22 ] // a span of 22 'elements'
The scaleBand will need an equivalent domain to the scaleLinear. You can do this statically ( which I show mostly to demonstrate how d3.range will work):
let xBand = d3.scaleBand()
.domain(d3.range(1,23))
.rangeRound([0, width]);
This actually produces a domain that has 22 elements from 1 through 22.
or dynamically:
let xBand = d3.scaleBand()
.domain(d3.range(d3.min(testData1, d => d[0],
d3.max(testData1, d => d[0]+1)))
You could do this other ways, but the d3.range() function is nice and easy.
However, there is still one issue that remains, this is aligning the ticks between the two scales. For the linear scale, the tick for the first value (1) is on the y axis, but the band gap scale starts (and is not centered) on the y axis and fills the gap between 1 and 2. In other words, the center point of the band does not align vertically with the vertices of the line graph.
This can be addressed by adding 0.5 to both the lower and upper bounds of the linear scale's domain:
let xDomain = [
d3.min(testData1, d => d[0]-0.5),
d3.max(testData1, d => d[0]+0.5)
];
I've udpated your codepen with the relevant changes: codepen.
And in the event that that disappears, here is a snippet (the mouse over does not work for me for some reason in the snippet, it does in the codepen )
let width = 1000;
let height = 300;
let svg = d3.select(".wrapper-area-simple").append("svg")
.attr("width", width + 80)
.attr("height", height + 80)
.append('svg:g')
.attr('transform', 'translate(40, 30)');
let testData1 = [
[ 1, 10],
[ 2, 30],
[ 8, 34],
[ 9, 26],
[13, 37],
[14, 12],
[20, 23],
[22, 16],
];
let xDomain = [
d3.min(testData1, d => d[0]-0.5),
d3.max(testData1, d => d[0]+0.5)
];
let x = d3.scaleLinear()
.rangeRound([0, width])
.domain(xDomain);
let y = d3.scaleLinear()
.range([height, 0])
.domain(d3.extent(testData1, d => d[1]));
let line = d3.line()
.x(d => x(d[0]))
.y(d => y(d[1]));
svg.append('svg:g')
.datum(testData1)
.append('svg:path')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', '#000');
let xAxis = d3.axisBottom(x)
.ticks(testData1.length);
svg.append('svg:g')
.call(xAxis)
.attr('transform', `translate(0, 300)`);
let xBand = d3.scaleBand()
.domain(d3.range(d3.min(testData1, d => d[0]),
d3.max(testData1, d => d[0]+1)
))
.rangeRound([0, width]);
svg.append('svg:g')
.selectAll('rect')
.data(testData1)
.enter()
.append('svg:rect')
.attr('x', d => xBand(d[0]))
.attr('width', xBand.bandwidth())
.attr('height', height)
.attr('fill', '#000')
.on('mouseover', function() {
d3.select(this).classed('over', true);
})
.on('mouseout', function() {
d3.select(this).classed('over', false);
});
svg {
border: 1px solid red;
}
rect {
opacity: .1;
}
rect.over {
opacity: .2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"> </script>
<div class="wrapper-area-simple"></div>
Well, bad news for you: they will never match up (in your case). Let's see why.
This is your data:
let testData1 = [
[1, 10],
[2, 30],
[8, 34],
[9, 26],
[13, 37],
[14, 12],
[20, 23],
[22, 16],
];
As you can see, regarding the x coordinate, the line jumps from 1 to 2, but then from 2 to 8, from 8 to 9, and then from 9 to 13... That is, the x range intervals are not regular, evenly spaced. So far, so good.
However, when you pass the same data to the band scale, this is what it does: it divides the range ([0, width], which is basically the width) by testData1.length, that is, it divides the range by 8, and creates 8 equal intervals. Those are your bands, and that's the expected behaviour of the band scale. From the documentation:
Discrete output values are automatically computed by the scale by dividing the continuous range into uniform bands. (emphasis mine)
One solution here is simply using another linear scale:
let xBand = d3.scaleLinear()
.domain(xDomain)
.rangeRound([0, width]);
And this math to the width of the rectangles:
.attr('width', (d,i) => testData1[i+1] ? xBand(testData1[i+1][0]) - xBand(d[0]) : 0)
Here is your updated Codepen: http://codepen.io/anon/pen/MJdGyY?editors=0010
I recently started working on D3 and found what feels like a good introductory tutorial by Mr Scott Murray at alignedleft.com. I'm currently trying to replicate his information on scale, but I'm running into a problem that I can't seem to solve. I've gone so far as to copy and paste his code and it isn't working.
I'm probably using a newer version of D3 than the tutorial is written on and I'm just missing something that changed in a version?
I'm currently using d3 version 4.3.0
The code I'm working with is
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
];
var w = 500;
var h = 200;
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([0, h]);
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([0, w]);
var svg = d3.select("body").append("svg").attr("height", h).attr("width", w);
svg.selectAll("circle").data(dataset).enter().append("circle").attr("cx", function(d) {
return xScale(d[0]);
}).attr("cy", function(d){
return yScale(d[1]);
}).attr("r", 5);
Any guidance or reason for this not working would be appreciated
D3.js version four will break a fair amount of code from version three. The reason being the "great namespace flattening" of version 4.
The following methods relating to scales have changed:
d3.scale.linear ↦ d3.scaleLinear
d3.scale.sqrt ↦ d3.scaleSqrt
d3.scale.pow ↦ d3.scalePow
d3.scale.log ↦ d3.scaleLog
d3.scale.quantize ↦ d3.scaleQuantize
d3.scale.threshold ↦ d3.scaleThreshold
d3.scale.quantile ↦ d3.scaleQuantile
d3.scale.identity ↦ d3.scaleIdentity
d3.scale.ordinal ↦ d3.scaleOrdinal
d3.time.scale ↦ d3.scaleTime
d3.time.scale.utc ↦ d3.scaleUtc
So, your d3.scale.linear() should read d3.scaleLinear()
I have timeseries data. On the server, I generate an x array, and three arrays of values (all of equal length).
However, the x array doesn't always go month to month. Sometimes it skips a couple of months. When this happens, the x-axis is very spaced out. Is there a good way to generate range of labels on the x-axis, and pass key values for different lines so that the entire line's values are still represented in the chart.
Currently I have:
var chart = c3.generate({
bindto: "#" + this.chart.chartId,
data: {
x: 'x',
columns: [
["x", "2015-01-01", "2015-02-01, "2015-06-01", "2016-01-01"],
["data1", 5, 8, 2, 9]
["data2", 3, 10, 2, 1]
["data3", 1, 8, 4, 9]
},
subchart: {
show: true
},
axis: {
x: {
type: 'timeseries',
extent: ['2015-01-01', '2016-01-01'],
tick: {
format: '%Y-%m-%d'
}
}
}
});
Any advice is appreciated to solve this spaced out issue for timeseries.
You appear to be asking about addressing x axes with c3, but your tags suggest that you are open to a solution with D3. Looking at your JSON string and seeing the node x.extent I would suggest trying something like this:
// setup x axes
var minDate = yourJSON.x.extent[0], maxDate = yourJSON.x.extent[1],
xScale = d3.time.scale().domain([maxDate,maxDate]).range([0, width]),
xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickFormat(d3.time.format("%Y-%m-%d"));
var svg = d3.select("#chart-content").append("svg")...etc;
// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end");
Since you are specifying a range, D3 will expose time in between those dates.
Code available here http://jsfiddle.net/zeleniy/ea1uL7w9/
data = [47, 13, 61, 46, 26, 32, 6, 85, 1, 14, 86, 77, 13, 66, 0, 20, 11, 87, 5, 15];
data = [52, 33, 53, 45, 59, 45, 42, 50, 53, 50, 37, 45, 52, 50, 46, 48, 52, 56, 58, 59];
width = 300;
height = 100;
xScale = d3.scale.linear()
.domain([0, data.length])
.range([0, width]);
yScale = d3.scale.linear()
.domain([d3.min(data), d3.max(data)])
.range([0, height])
area = d3.svg.area()
.interpolate("basis")
.x(function(d, i) { return xScale(i); })
.y0(function(d) { return yScale(-d / 2); })
.y1(function(d) { return yScale(d / 2); });
svg = d3.select("#stream")
.append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("path")
.data([this.data])
.enter()
.append("path")
.attr("transform", "translate(0," + (height / 2) + ")")
.style("fill", "red")
.attr("d", area);
With first data set chart drawn in the center of svg element, as i expect. But with second data set stream shifts to the top of svg element. And i can't understand why. So why?
The first array contains values close to 0 and it's opening up your range. This line, then, is a fudge to shift the path into that open window:
.attr("transform", "translate(0," + (height / 2) + ")")
That said, you are setting up your scales in a confusing way (to me at least). I think about my domain as the min/max of my (plotted) dataset, in your case -max(d/2) and max(d/2). Further, I also think about my y-scale going from bottom to top as it would in a normal plot. With these changes, you don't need to artificially move anything:
var dataMax = d3.max(data);
yScale = d3.scale.linear()
.domain([ -dataMax/2, dataMax/2 ]) // real min/max of plotted data
.range([height, 0]) //<- bottom to top, although it still works without this change...
In this example, I left an axis overlayed for illustration.
Up until now, I've been using loops to add line elements to a D3 visualization, but this doesn't seem in the spirit of the API.
Let's say I have got some data,
var data = {time: 1, value: 2, value2: 5, value3: 3,value4: 2},
{time: 2, value: 4, value2: 9, value3: 2,value4: 4},
{time: 3, value: 8, value2:12, value3: 2,value4:15}]);
I'd like four lines, with time as the X for all 4.
I can do something like this:
var l = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[1]]);})
.interpolate("basis");
var l2 = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[2]]);})
.interpolate("basis");
var l3 = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[3]]);})
.interpolate("basis");
var l4 = d3.svg.line()
.x(function(d){return xScale(d[keys[0]]);})
.y(function(d,i){
return yScale(d[keys[4]]);})
.interpolate("basis");
And then add these one by one (or by a loop).
var line1 = group.selectAll("path.path1")
.attr("d",l(data));
var line2 = group.selectAll("path.path2")
.attr("d",l2(data));
var line3 = group.selectAll("path.path3")
.attr("d",l3(data));
var line4 = group.selectAll("path.path4")
.attr("d",l4(data));
Is there a better more general way of adding these paths?
Yes. First I would restructure your data for easier iteration, like this:
var series = [
[{time: 1, value: 2}, {time: 2, value: 4}, {time: 3, value: 8}],
[{time: 1, value: 5}, {time: 2, value: 9}, {time: 3, value: 12}],
[{time: 1, value: 3}, {time: 2, value: 2}, {time: 3, value: 2}],
[{time: 1, value: 2}, {time: 2, value: 4}, {time: 3, value: 15}]
];
Now you need just a single generic line:
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.value); });
And, you can then add all of the path elements in one go:
group.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", line);
If you want to make the data structure format smaller, you could also extract the times into a separate array, and then use a 2D array for the values. That would look like this:
var times = [1, 2, 3];
var values = [
[2, 4, 8],
[5, 9, 12],
[3, 2, 2],
[2, 4, 15]
];
Since the matrix doesn't include the time value, you need to look it up from the x-accessor of the line generator. On the other hand, the y-accessor is simplified since you can pass the matrix value directly to the y-scale:
var line = d3.svg.line()
.interpolate("basis")
.x(function(d, i) { return x(times[i]); })
.y(y);
Creating the elements stays the same:
group.selectAll(".line")
.data(values)
.enter().append("path")
.attr("class", "line")
.attr("d", line);