I ended up using Highcharts. I took this example ElementStacks and modified it to handle the intersections. See Negative Area.
$(function() {
var Intersection = function (d1, d2) {
var self = this;
this.init = function () {
this.d1 = this.sortLine(d1);
this.d2 = this.sortLine(d2);
if (this.d1.length != this.d2.length) {
throw 'd1 and d2 expected to be same size';
}
this.dps = _.zip(d1, d2);
hasUnmatchedIndex = _.any(this.dps, function(dp_pair) {
return dp_pair[0][0] != dp_pair[1][0];
});
if (hasUnmatchedIndex)
throw 'd1 and d2 do not have same indices';
};
this.sortLine = function(line) {
return _.sortBy(line, function(dp) { return dp[0]; });
};
this.transitions = function() {
return _.map(this.dps, function(dp_pair) {
a = dp_pair[0];
b = dp_pair[1];
result = null;
if (a[1] < b[1])
result = -1;
else if (a[1] > b[1])
result = 1;
else
result = 0;
return [a[0], result];
});
};
this.dropTransitions = function() {
prev = null;
drops = [];
_.each(this.transitions(), function(curr) {
if (prev && prev[1] != curr[1] && prev[1] != 0 && curr[1] != 0)
drops.push([prev, curr])
prev = curr;
});
return drops;
};
this.data = function() {
//self = this;
_d1 = this.sortLine(this.d1.concat(this.intersections()));
_d2 = this.sortLine(this.d2.concat(this.intersections()));
d1_g = [];
d2_g = [];
d_min = [];
dps = _.zip(_d1, _d2);
_.each(dps, function(dp_pair,i) {
index = dp_pair[0][0];
dpv1 = dp_pair[0][1];
dpv2 = dp_pair[1][1];
if (dpv1 == null || dpv2 == null) {
d1_g.push([index, null]);
d2_g.push([index, null]);
} else {
diff = Math.abs(dpv1 - dpv2);
if (dpv1 > dpv2) {
d1_g.push([index, diff]);
d2_g.push([index, 0]);
} else if (dpv2 > dpv1) {
d1_g.push([index, 0]);
d2_g.push([index, diff]);
} else {
d1_g.push([index, diff]);
d2_g.push([index, diff]);
}
}
d_min.push([index, Math.min(dpv1, dpv2)]);
});
return [d1_g, d2_g, d_min];
};
this.intersections = function() {
//self = this;
return _.map(this.dropTransitions(), function(dt) {
line1 = _.filter(self.d1, function(dp) {
return dp[0] == dt[0][0] || dp[0] == dt[1][0];
});
line2 = _.filter(self.d2, function(dp) {
return dp[0] == dt[0][0] || dp[0] == dt[1][0];
});
return self.findIntersection(line1, line2);
});
};
this.findIntersection = function(line1, line2) {
eq1 = this.lineEquation(line1);
eq2 = this.lineEquation(line2);
m1 = eq1.m;
b1 = eq1.b;
m2 = eq2.m;
b2 = eq2.b;
x = (b2 - b1) / (m1 - m2)
y = (m1 * x) + b1
return [x,y];
};
this.lineEquation = function(line) {
p1 = _.map(line[0], function(n) { return parseFloat(n); });
p2 = _.map(line[1], function(n) { return parseFloat(n); });
x1 = p1[0];
y1 = p1[1];
x2 = p2[0];
y2 = p2[1];
m = (y1 - y2) / (x1 - x2);
b = y1 - (m*x1);
eq = {'m': m, 'b': b};
return eq;
};
this.print = function (obj) { alert(JSON.stringify(obj)); };
this.init(d1, d2);
};
var d1r = [10, 8, 7, 6, 5, 4, 3, 5, 3, 9, 10, 11, 2];
var d2r = [ 5, 6, 7, 8, 9, 10, 9, 8, 12, 3, 2, 1, 20];
var d1 = _.map(d1r, function(e,i) { return [i, e-10]; });
var d2 = _.map(d2r, function(e,i) { return [i, e-10]; });
var t = new Intersection(d1, d2);
var data = t.data();
var values = _.map(data, function(dps) {
return _.map(dps, function(dp) {
return dp[1];
});
});
var minValue = _.min(_.flatten(values));
// Need to find threshold to handle negative stacking values
var threshold = minValue < 0 ? minValue : 0;
var dp1 = t.d1;
var dp2 = t.d2;
var dp1_g = data[0];
var dp2_g = data[1];
var dp_min = data[2];
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'area',
animation: false
},
plotOptions: {
area: {
stacking: true,
lineWidth: 0,
shadow: false,
marker: {
enabled: false
},
enableMouseTracking: false,
showInLegend: false
},
line: {
zIndex: 5
},
series: {
threshold: threshold
}
},
series: [{
type: 'line',
color: 'red',
data: dp1
},{
type: 'line',
color: 'black',
data: dp2
},{
color: 'orange',
data: dp1_g
},{
color: 'grey',
data: dp2_g
},{
id: 'transparent',
color: 'rgba(255,255,255,0.0)',
data: dp_min
}]
}, function(chart){
chart.get('transparent').area.hide();
});
});