|
function binarySearch(a, key) { |
|
var low = 0, |
|
high = a.length - 1; |
|
while(low <= high) { |
|
var mid = Math.floor((low + high) / 2), |
|
midVal = a[mid]; |
|
if(midVal < key) { |
|
low = mid + 1; |
|
} else if (midVal > key) { |
|
high = mid - 1; |
|
} else { |
|
return mid; |
|
} |
|
} |
|
return low; //returns insertion index if not found |
|
} |
|
|
|
// add boxplot stats. |
|
// m has .sortedVals |
|
function addStats(m) { |
|
var vs = m.sortedVals, |
|
q1 = d3.quantile(vs, 0.25), |
|
q2 = d3.quantile(vs, 0.5), |
|
q3 = d3.quantile(vs, 0.75), |
|
iqr = q3 - q1; |
|
var i = -1, |
|
j = vs.length; |
|
while (vs[++i] < q1 - 1.5 * iqr); |
|
while (vs[--j] > q3 + 1.5 * iqr); |
|
|
|
var wl = vs[i], //whisker low |
|
wh = vs[j], //whisker high |
|
ol = vs.slice(0, i), //outliers low |
|
oh = vs.slice(++j, vs.length); //outliers high |
|
m.min = vs[0]; |
|
m.max = vs[vs.length-1]; |
|
m.q1 = q1; |
|
m.q2 = q2; |
|
m.q3 = q3; |
|
m.wl = wl; |
|
m.wh = wh; |
|
m.ol = ol; |
|
m.oh = oh; |
|
} |
|
|
|
// sets group.reduce |
|
// group => crossfilter group. |
|
function setReduceStats(group, metric) { |
|
function reduceAdd(p, v) { |
|
var i = binarySearch(p.sortedVals,v[metric]); |
|
p.sortedVals.splice(i, 0, v[metric]); |
|
return p; |
|
} |
|
|
|
function reduceRemove(p, v) { |
|
var i = binarySearch(p.sortedVals, v[metric]); |
|
p.sortedVals.splice(i, 1); |
|
return p; |
|
} |
|
|
|
function reduceInitial() { |
|
return {sortedVals:[], min:0, max:0, wl:0, q1:0, q2:0, q3:0, wh:0, ol:[], oh:[]}; |
|
} |
|
return group.reduce(reduceAdd, reduceRemove, reduceInitial); |
|
} |
|
|
|
var rbox = function() { |
|
var width = 960, |
|
height = 500, |
|
innerRadius = 80, |
|
scaleOverride = null, |
|
label = "", |
|
dimension, |
|
group, |
|
axisText = Object, |
|
color = "#3399FF" |
|
; |
|
|
|
// selection => nested data |
|
function my(selection) { |
|
var svg = selection.select("svg g"); |
|
if(svg.empty()) { |
|
selection = selection.append("svg") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.attr('class', 'star-container') |
|
.append("g") |
|
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); |
|
selection.append("g").attr("class", "layers"); |
|
selection.append("g").attr("class", "axes"); |
|
selection.append("g").attr("class", "outliers") |
|
} else { |
|
selection = svg; |
|
} |
|
|
|
var fill = [d3.rgb(color).brighter(), color, color, d3.rgb(color).brighter()]; |
|
|
|
var outerRadius = height / 2 - 10; |
|
var angle = d3.time.scale() |
|
.range([0, 2 * Math.PI]); |
|
|
|
var radius = d3.scale.linear() |
|
.range([innerRadius, outerRadius]); |
|
|
|
var stack = d3.layout.stack() |
|
.offset("zero") |
|
.x(function(d) { return d.key; }) |
|
.y(function(d) { return d.value; }) |
|
; |
|
|
|
var area = d3.svg.area.radial() |
|
.interpolate("cardinal-closed") |
|
.angle(function(d) { return angle(d.key); }) |
|
.innerRadius(function(d) { return radius(d.y0); }) |
|
.outerRadius(function(d) { return radius(d.y0 + d.y); }); |
|
|
|
selection.each(function() { //groups |
|
var selection = d3.select(this), |
|
g = selection.select("g"); |
|
|
|
var gs = group.all(); |
|
gs.forEach(function(g) { addStats(g.value); }); |
|
var ks = ["wl", "q1", "q2", "q3", "wh"]; |
|
var nested = ks.map(function(k, i) { |
|
return gs.map(function(g) { |
|
var tmp = Object.create(g); //clone(ish) |
|
tmp.value = tmp.value[k] - (tmp.value[ks[i - 1]] || 0.0); |
|
return tmp; |
|
}); |
|
}); |
|
var layers = stack(nested); |
|
layers.shift(); //no need to draw first layer |
|
|
|
// Extend the domain slightly to match the range of [0, 2π]. |
|
angle.domain([0, Math.floor(d3.max(gs, function(g) { return g.key; })) + 0.999]); // hour => [0, 24), day => [0, 7) |
|
if(scaleOverride) { |
|
radius.domain(scaleOverride); |
|
} else { |
|
radius.domain([d3.min(gs, function(g) { return g.value.min; }), |
|
d3.max(gs, function(g) { return g.value.max; })]); |
|
} |
|
|
|
var selLabel = selection.selectAll(".label") |
|
.data([label]); |
|
selLabel.enter().append("text") |
|
.attr("class", "label") |
|
.attr("text-anchor", "middle"); |
|
selLabel |
|
.text(function(d) { return d; }) |
|
.style("opacity", 1); |
|
|
|
|
|
var selLayer = selection.select(".layers").selectAll(".layer") |
|
.data(layers); |
|
selLayer.enter().append("path") |
|
.attr("class", "layer") |
|
.attr("d", function(ds) { return area(ds.map(function(d) { return {key:d.key, y0:0, y:0}; })); }) |
|
.style("fill", function(d, i) { return fill[i]; }) |
|
selLayer |
|
.transition().duration(200) |
|
.attr("d", function(d) { return area(d); }) |
|
.style("fill", function(d, i) { return fill[i]; }); |
|
|
|
|
|
var outliersss = ["ol", "oh"].map(function(k) { |
|
return gs.map(function(g) { |
|
var theta = angle(g.key) - 0.5* Math.PI; //0 is at 12 o clock. |
|
return g.value[k].map(function(v) { |
|
var h = radius(v); |
|
var coord = {x: h * Math.cos(theta), |
|
y: h * Math.sin(theta)}; |
|
return coord; |
|
}); |
|
}); |
|
}); |
|
var outlierss = [].concat.apply([], outliersss), |
|
outliers = [].concat.apply([], outlierss); |
|
var selOutlier = selection.select(".outliers").selectAll(".outlier") |
|
.data(outliers); |
|
selOutlier.enter() |
|
.append("circle") |
|
.attr("class", "outlier") |
|
selOutlier |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.attr("r", 2); |
|
selOutlier.exit() |
|
.remove(); |
|
|
|
var selAxis = selection.select(".axes").selectAll(".axis") |
|
.data(d3.range(Math.floor(+angle.domain()[1]) + 1)) |
|
selAxis.enter().append("g") |
|
.attr("class", "axis") |
|
.append("text") |
|
.attr("class", "axisLabel"); |
|
selAxis //.transition().duration(1000) |
|
.attr("transform", function(d) { return "rotate(" + angle(d) * 180 / Math.PI + ")"; }) |
|
.call(d3.svg.axis() |
|
.scale(radius.copy().range([-innerRadius, -outerRadius])) |
|
.orient("left")); |
|
selAxis.selectAll("text.axisLabel") |
|
.text(function(d) { return axisText(d); }) |
|
.attr("y", -innerRadius + 6) |
|
.attr("dy", ".71em") |
|
.attr("text-anchor", "middle"); |
|
selAxis.exit() |
|
.remove(); |
|
}); |
|
} |
|
|
|
/** Getter/Setters **/ |
|
my.width = function(v) { |
|
if (!arguments.length) return width; |
|
width = v; |
|
return my; |
|
} |
|
my.height = function(v) { |
|
if (!arguments.length) return height; |
|
height = v; |
|
return my; |
|
} |
|
my.innerRadius = function(v) { |
|
if (!arguments.length) return innerRadius; |
|
innerRadius = v; |
|
return my; |
|
} |
|
my.outerRadius = function(v) { |
|
if (!arguments.length) return outerRadius; |
|
outerRadius = v; |
|
return my; |
|
} |
|
my.file = function(v) { |
|
if (!arguments.length) return file; |
|
file = v; |
|
return my; |
|
} |
|
my.scaleOverride = function(v) { |
|
if (!arguments.length) return scaleOverride; |
|
scaleOverride = (v[0] == 0 && v[1] == 0) ? null : v; //maybe do something better here? |
|
return my; |
|
} |
|
my.label = function(v) { |
|
if (!arguments.length) return label; |
|
label = v; |
|
return my; |
|
} |
|
my.dimension = function(v) { |
|
if (!arguments.length) return dimension; |
|
dimension = v; |
|
return my; |
|
} |
|
my.group = function(v) { |
|
if (!arguments.length) return group; |
|
group = v; |
|
return my; |
|
} |
|
my.axisText = function(_) { |
|
if (!arguments.length) return axisText; |
|
axisText = _; |
|
return my; |
|
}; |
|
my.color = function(_) { |
|
if (!arguments.length) return color; |
|
color = _; |
|
return my; |
|
}; |
|
return my; |
|
}; |