D3 monotone interpolation causing little loops - javascript

I'm displaying data on a line graph with monotone interpolation. But in some cases little loops appear.
Is there away to fix this while keeping the same look as monotone (so not just using linear) and having the line hit all the points?
JSFiddle showing the issue here: http://jsfiddle.net/WkvMx/3/
Code to replicate:
var data =
[
{date: '2013-08-01', value: 234},
{date: '2013-08-02', value: 244},
{date: '2013-08-04', value: 1034},
{date: '2013-08-06', value: 1004},
{date: '2013-08-28', value: 234},
{date: '2013-08-29', value: 233}
]
var width = 500;
var height = 250;
var parse = d3.time.format("%Y-%m-%d").parse;
var x = d3.time.scale()
.range([0, width])
.domain([parse('2013-08-01'), parse('2013-09-01')]);
var max = d3.max(data, function (v) { return v.value; });
var min = d3.min(data, function (v) { return v.value; });
var y = d3.scale.linear()
.range([height, 0])
.domain([min, max]).nice();
var line = d3.svg.line()
.interpolate("monotone")
.x(function(d) { return x(parse(d.date)); })
.y(function(d) { return y(d.value); });
var svg = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height)
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line)
.attr("stroke", 'red')
var circlegroup = svg.append("g")
circlegroup.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(parse(d.date)); })
.attr("cy", function(d) { return y(d.value); })

The monotone interpolation is intended to interpolate monotone datasets, that's it, datasets whose y-coordinate increases, and even in that case, the monotonicity of the interpolation function is not guaranteed, although it can be achieved by modifying the tangents of the curve. The tangents can be controled globally (kind of) using the tension method, but it can't be set point by point.
As #Lars pointed out, you can try other interpolation methods better suited for your data, and tweak the tension to get a smooth line that passes by all the input points.

Related

d3.js stacked bar chart - modify stack order logic

I would like to create a stacked bar chart whereby the order of the rects is determined by data values (i.e. largest to smallest, tallest to shortest, richest to poorest, ect). To the best of my knowledge, after stacking data, the initial order seems to be preserved. This can be seen in my snippet, hardcoded data lets us see what's happening before and after d3.stack(). Note that the third rect fmc3 goes from being the third largest in t1 to the largest of all rects in t3 despite its position in the stack remaining the same:
var margins = {top:100, bottom:300, left:100, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
{period:'t1', fmc1:2, fmc2:5, fmc3:6, fmc4:9, fmc5:10},
{period:'t2', fmc1:3, fmc2:4, fmc3:9, fmc4:8, fmc5:11},
{period:'t3', fmc1:3, fmc2:5, fmc3:15, fmc4:12, fmc5:10},
];
var groups = d3.map(data, function(d){return(d.period)}).keys();
var subgroups = Object.keys(data[0]).slice(1);
var stackedData = d3.stack()
.keys(subgroups)
(data);
//console.log(stackedData);
var yScale = d3.scaleLinear()
.domain([0,80])
.range([height,0]);
var xScale = d3.scaleBand()
.domain(['t1','t2','t3'])
.range([0,width])
.padding([.5]);
var colorScale = d3.scaleOrdinal()
.domain(subgroups)
.range(["#003366","#366092","#4f81b9","#95b3d7","#b8cce4","#e7eef8","#a6a6a6","#d9d9d9","#ffffcc","#f6d18b","#e4a733","#b29866","#a6a6a6","#d9d9d9","#e7eef8","#b8cce4","#95b3d7","#4f81b9","#366092","#003366"].reverse());
graphGroup.append("g")
.selectAll("g")
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) { return colorScale(d.key); })
.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { return xScale(d.data.period); })
.attr("y", function(d) { return yScale(d[1]); })
.attr("height", function(d) { return yScale(d[0]) - yScale(d[1]); })
.attr("width",xScale.bandwidth());
<script src="https://d3js.org/d3.v5.min.js"></script>
I suspect preserving the initial order may be somewhat necessary to calculate adjacent rects in the stack. However, on the other hand, ordering data before visualizing it is a very common, even preferred practice in the field of visualization and I would be surprised if no one has found a solution to this issue yet.
Question
Given there are no built-in features to specify the ordering of the rects in a stack, how should I approach the sort logic to achieve largest to smallest ordering?
Well, there is a built-in feature to specify the order, which is stack.order(). However, it specifies the order computing the entire series, not every single value the stack (which I believe is what you want... in that case, you'll have to create your own function).
So, for instance, using stack.order(d3.stackOrderDescending):
var margins = {
top: 0,
bottom: 0,
left: 0,
right: 0
};
var height = 300;
var width = 500;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var data = [{
period: 't1',
fmc1: 2,
fmc2: 5,
fmc3: 6,
fmc4: 9,
fmc5: 10
},
{
period: 't2',
fmc1: 3,
fmc2: 4,
fmc3: 9,
fmc4: 8,
fmc5: 11
},
{
period: 't3',
fmc1: 3,
fmc2: 5,
fmc3: 15,
fmc4: 12,
fmc5: 10
},
];
var groups = d3.map(data, function(d) {
return (d.period)
}).keys();
var subgroups = Object.keys(data[0]).slice(1);
var stackedData = d3.stack()
.keys(subgroups)
.order(d3.stackOrderDescending)
(data);
//console.log(stackedData);
var yScale = d3.scaleLinear()
.domain([0, 60])
.range([height, 0]);
var xScale = d3.scaleBand()
.domain(['t1', 't2', 't3'])
.range([0, width])
.padding([.5]);
var colorScale = d3.scaleOrdinal()
.domain(subgroups)
.range(["#003366", "#366092", "#4f81b9", "#95b3d7", "#b8cce4", "#e7eef8", "#a6a6a6", "#d9d9d9", "#ffffcc", "#f6d18b", "#e4a733", "#b29866", "#a6a6a6", "#d9d9d9", "#e7eef8", "#b8cce4", "#95b3d7", "#4f81b9", "#366092", "#003366"].reverse());
graphGroup.append("g")
.selectAll("g")
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) {
return colorScale(d.key);
})
.selectAll("rect")
.data(function(d) {
return d;
})
.enter().append("rect")
.attr("x", function(d) {
return xScale(d.data.period);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth());
<script src="https://d3js.org/d3.v5.min.js"></script>

d3 selection confusion with exit() and remove()

So I'm working through some d3 tutorials and learning a bit about why things work the way they do, and I've run into a peculiar instance of selection not behaving as expected. So I'm wondering if anyone can explain this to me.
var sales = [
{ product: 'Hoodie', count: 7 },
{ product: 'Jacket', count: 6 },
{ product: 'Snuggie', count: 9 },
];
var rects = svg.selectAll('rect')
.data(sales).enter();
var maxCount = d3.max(sales, function(d, i) {
return d.count;
});
var x = d3.scaleLinear()
.range([0, 300])
.domain([0, maxCount]);
var y = d3.scaleBand()
.rangeRound([0, 75])
.domain(sales.map(function(d, i) {
return d.product;
}));
rects.append('rect')
.attr('x', x(0))
.attr('y', function(d, i) {
return y(d.product);
})
.attr('height', y.bandwidth())
.attr('width', function(d, i) {
return x(d.count);
});
This all works good and fine, generates 3 horizontal bars that correspond to the data in sales, but here's where I'm seeing the ambiguity:
sales.pop();
rects.data(sales).exit().remove();
The last line is supposed to remove the bar that was popped, from the visual but it doesn't work. I think that there must be something going on with the d3 selection that I'm missing, because this does work:
d3.selectAll('rect').data(sales).exit().remove();
Also when I break out the first one that doesn't work, it does appear to be selecting the correct element on exit, but just doesn't seem to be removing it. Anyway if anyone can explain what's going on here that would be very helpful, thanks!
Note: using d3 v4
This is your rects selection:
var rects = svg.selectAll('rect')
.data(sales).enter();
So, when you do this:
rects.data(sales).exit().remove();
You are effectively doing this:
svg.selectAll('rect')
.data(sales)
.enter()
.data(sales)
.exit()
.remove();
Thus, you are binding data, calling enter, binding data again and calling exit on top of all that! Wow!
Solution: just create a regular, old-fashioned "update" selection:
var rects = svg.selectAll('rect')
.data(sales);
Here is a basic demo with your code, calling exit after 2 seconds:
var svg = d3.select("svg")
var sales = [{
product: 'Hoodie',
count: 7
}, {
product: 'Jacket',
count: 6
}, {
product: 'Snuggie',
count: 9
}, ];
draw();
function draw() {
var rects = svg.selectAll('rect')
.data(sales);
var maxCount = d3.max(sales, function(d, i) {
return d.count;
});
var x = d3.scaleLinear()
.range([0, 300])
.domain([0, maxCount]);
var y = d3.scaleBand()
.rangeRound([0, 75])
.domain(sales.map(function(d, i) {
return d.product;
}))
.padding(.2);
var rectsEnter = rects.enter().append('rect')
.attr('x', x(0))
.attr('y', function(d, i) {
return y(d.product);
})
.attr('height', y.bandwidth())
.attr('width', function(d, i) {
return x(d.count);
});
rects.exit().remove();
}
d3.timeout(function() {
sales.pop();
draw()
}, 2000)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
rects is already a d3 selection, so you only need rects.exit().remove()

Plot density function with 2 or 3 colored areas?

I just started learning javascript and d3.js by taking a couple of lynda.com courses. My objective is to create a function that takes an array of numbers and a cutoff and produces a plot like this one:
I was able to write javascript code that generates this:
Alas, I'm having troubles figuring out a way to tell d3.js that the area to the left of -opts.threshold should be read, the area in between -opts.threshold and opts.threshold blue, and the rest green.
This is my javascript code:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
var margin = {left:50,right:50,top:40,bottom:0};
var xMax = opts.x.reduce(function(a, b) {
return Math.max(a, b);
});
var yMax = opts.y.reduce(function(a, b) {
return Math.max(a, b);
});
var xMin = opts.x.reduce(function(a, b) {
return Math.min(a, b);
});
var yMin = opts.y.reduce(function(a, b) {
return Math.min(a, b);
});
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d,i){ return x(opts.x[i]) ;})
.y0(height)
.y1(function(d){ return y(d); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.y));
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
In case this is helpful, I saved all my code on a public github repo.
There are two proposed solutions in this answer, using gradients or using multiple areas. I will propose an alternate solution: Use the area as a clip path for three rectangles that together cover the entire plot area.
Make rectangles by creating a data array that holds the left and right edges of each rectangle. Rectangle height and y attributes can be set to svg height and zero respectively when appending rectangles, and therefore do not need to be included in the array.
The first rectangle will have a left edge at xScale.range()[0], the last rectangle will have an right edge of xScale.range()[1]. Intermediate coordinates can be placed with xScale(1), xScale(-1) etc.
Such an array might look like (using your proposed configuration and x scale name):
var rects = [
[x.range()[0],x(-1)],
[x(-1),x(1)],
[x(1),x.range()[1]]
]
Then place them:
.enter()
.append("rect")
.attr("x", function(d) { return d[0]; })
.attr("width", function(d) { return d[1] - d[0]; })
.attr("y", 0)
.attr("height",height)
Don't forget to set a clip-path attribute for the rectangles:
.attr("clip-path","url(#areaID)"), and to set fill to three different colors.
Now all you have to do is set your area's fill and stroke to none, and append your area to a clip path with the specified id:
svg.append("clipPath)
.attr("id","area")
.append("path")
.attr( // area attributes
...
Here's the concept in action (albeit using v3, which shouldn't affect the rectangles or text paths.
Thanks to #andrew-reid suggestion, I was able to implement the solution that uses multiple areas.
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
console.log("data: ", opts.data);
var margin = {left:50,right:50,top:40,bottom:0};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});

Dynamic tickValues on time.scale in D3

I built a graph in D3, to show spending based on date time and amount of spending, but I can't dynamically set tickValues() on my time.scale to show only ticks for data points.
My scale set up is:
var dateScale = d3.time.scale()
.domain(d3.extent(newData, function(d) { return d.Date; }))
.range([padding, width - padding]);
var amountScale = d3.scale.linear()
.domain([0, d3.max(newData, function(d) { return d.Amount; })])
.range([window.height - padding, padding]);
// Define date Axis
var dateAxis = d3.svg.axis().scale(dateScale)
.tickFormat(d3.time.format('%a %d %m'))
.tickSize(100 - height)
.orient("bottom");
// Draw date Axis
svg.append("g")
.attr({
"class": "axis date-axis",
"transform": "translate(" + [0, height -padding] + ")"
}).call(dateAxis);
// Define amount Axis
var amountAxis = d3.svg.axis().scale(amountScale)
.tickSize(1)
.orient("left");
// Draw amount Axis
svg.append("g")
.attr({
"class": "axis amount-axis",
"transform": "translate(" + padding + ",0)"
}).call(amountAxis);
I've tried to set
dateAxis.tickValues(d3.extent(newData, function(d) { return d.Date; }))
but this only returns min and max value for date axis.
Also tickValues() doesn't accept newData itself - what else I can try here?
I want to achieve this:
to have only ticks highlighted, associated with corresponding data.
I ended up with creating new array of all date points out of my data, to pass it to tickValues() method. My code now is:
var newData = toArray(uniqueBy(data, function(x){return x.Date;}, function(x, y){ x.Amount += y.Amount; return x; }));
// Create array of data date points
var tickValues = newData.map(function(d){return d.Date;});
// Sorting data ascending
newData = newData.sort(sortByDateAscending);
var dateScale = d3.time.scale()
.domain(d3.extent(newData, function(d) { return d.Date; }))
.range([padding, width - padding]);
var amountScale = d3.scale.linear()
.domain([0, d3.max(newData, function(d) { return d.Amount; })])
.range([window.height - padding, padding]);
// Define date Axis
var dateAxis = d3.svg.axis().scale(dateScale)
.tickFormat(d3.time.format('%d %m %Y'))
.tickValues(tickValues)
.orient("bottom");
So now whole graph looks as:
I found this SO (#AmeliaBR) answer explaining tickValues() function, and here is documentation for it too.

Update pattern with path line selections

I am trying to use the enter(), update(), exit() pattern for a line chart, and I'm not getting my lines to appear properly.
A fiddle example. http://jsfiddle.net/wy6h1jcg/
THey do show up in the dom, but have no x or y values (though they are styled)
My svg is already created as follows:
var chart = d3.select("#charts")
.append("svg")
chart
.attr("width", attributes.chartsWidth)
.attr("height", attributes.chartsHeight);
I want to create a new object for my update binding, as follows:
var thechart = chart.selectAll("path.line").data(data, function(d){return d.x_axis} )
thechart.enter()
.append("path")
thechart.transition().duration(100).attr('d', line).attr("class", "line");
But this is no good.
Note, this does work (but can't be used for our update):
chart.append("path")
.datum(data, function(d){return d.x_axis})
.attr("class", "line")
.attr("d", line);
One other note:
I have a separate function that binds data for creating another chart on the svg.
var thechart = chart.selectAll("rect")
.data(data, function(d){return d.x_axis});
thechart.enter()
.append("rect")
.attr("class","bars")
Could these two bindings be interacting?
This is the update logic I ended on, still a closured pattern:
function updateScatterChart(chartUpdate) {
var wxz = (wx * 37) + c;
var x = d3.scale.linear()
.range([c, wxz]);
var y = d3.scale.linear()
.range([h, hTopMargin]);
var line = d3.svg.line()
.x(function(d) { return x(+d.x_axis); })
.y(function(d) { return y(+d.percent); }).interpolate("basis");
if (lastUpdateLine != chartUpdate) {
console.log('updating..')
d3.csv("./data/multiline.csv", function(dataset) {
console.log(chartUpdate);
var data2 = dataset.filter(function(d){return d.type == chartUpdate});
x.domain(d3.extent(data2, function(d) { return +d.x_axis; }));
y.domain([0, d3.max(data2, function(d) { return +d.percent; })]);
var thechart2 = chart.selectAll("path.line").data(data2, function(d){return d.neighborhood;});
thechart2.enter()
.append("svg:path")
.attr("class", "line")
.attr("d", line(data2))
thechart2.transition()
.duration(800)
.attr("d", line(data2))
.style("opacity", (chartUpdate == 'remove') ? 0 : 1 )
thechart2.exit()
.transition()
.duration(400)
.remove();
})
}
lastUpdateLine = chartUpdate;
}

Categories