var svg_dx = 600,
svg_dy = 350,
sd_plot_dx = 300,
sd_plot_dy = 350,
margin_sd_plot = {
top: sd_plot_dy * 0.08,
bottom: sd_plot_dy * 0.22,
left: sd_plot_dx * 0.10,
right: sd_plot_dx * 0.10
};
// vertical midline of jitterplot for x positioning
var jitter_plot_x_midline = sd_plot_dx + 50;
// track whether jitterplot is visible
var is_jitter_plot_visible = false;
// random number generator for jitter
var xJitter = d3.randomUniform(-jitter_plot_x_midline * 0.10,
jitter_plot_x_midline * 0.10);
// sd distance to be outlier
var sd_mult = 2.0;
var svg = d3.select("#block")
.append("svg")
.attr("height", svg_dy)
.attr("width", svg_dx);
var sd_plot = svg.append("g")
.attr("id", "sd_plot");
var tooltip = d3.select("body")
.append("div")
.attr("id", "tooltip")
.style("opacity", 0);
d3.csv("/data/mtcars_melted.csv", d => {
// group melted data by variable
var d_grouped = d3.nest()
.key(d => d.variable)
.entries(d);
// stats by variable
calcStatsByVar(d_grouped);
// sort variables by max SD
sortBySd(d_grouped);
// initially all variables in SD plot and not expanded to jitterplot
d_grouped.forEach(v => v._jittered = false);
// max and min SD dist for x-axis
var sd_dist = {
max : d3.max(d_grouped, d => d._extentSD[1]),
min : d3.min(d_grouped, d => d._extentSD[0])
};
// SD plot x scale and axis
var xScale = d3.scaleLinear()
.domain([sd_dist.min, sd_dist.max])
.range([margin_sd_plot.left, sd_plot_dx - margin_sd_plot.right]);
var xAxis = d3.axisBottom(xScale);
// SD plot y scale
var yScale = d3.scalePoint()
.domain(d_grouped.map(d => d.key))
.range([margin_sd_plot.top, sd_plot_dy - margin_sd_plot.bottom]);
// group elements by variable
var vars = sd_plot.selectAll("g")
.data(d_grouped)
.enter()
.append("g")
.attr("class", "variable");
// for each variable, plot rects for each datum
// note: arrow function does not bind 'this'
vars.each(function(v) {
d3.select(this)
.selectAll("rect")
.data(v.values)
.enter()
.append("rect")
.attr("x", d => xScale(+d._sdMult))
.attr("y", () => yScale(v.key))
.attr("width", 5)
.attr("height", yScale.step() * 0.75)
.attr("class", d => d._outlier ? "outlier rect_data" : "non-outlier rect_data")
.on("mouseover", d => displayTooltip(d.model))
.on("mouseout", hideTooltip);
});
// overlay grouping rect
// note: rect needed because in Chrome event listeners to g element
// are bound to constitutive elements and not entire g rect
vars.append("rect")
.attr("class", "rect_g")
.attr("x", margin_sd_plot.left)
.attr("y", v => yScale(v.key))
.attr("width", sd_plot_dx - margin_sd_plot.right - margin_sd_plot.left)
.attr("height", yScale.step() * 0.75)
.on("mouseover", v => displayTooltip(v.key))
.on("mousemove", v => displayTooltip(v.key))
.on("mouseout", hideTooltip)
.on("click", function(v) {
if (!is_jitter_plot_visible && v._jittered == false) {
expandToJitterplot(this, v);
v._jittered = true;
is_jitter_plot_visible = true;
} else if (is_jitter_plot_visible && v._jittered == true) {
collapseToSdPlot(this, v, xScale, yScale);
v._jittered = false;
is_jitter_plot_visible = false;
}
});
// plot x-axis
sd_plot.append("g")
.attr("id", "z_score_axis")
.call(xAxis);
// x-axis label
d3.select("#z_score_axis")
.append("text")
.attr("id", "z_score_label")
.text("z-score")
.attr("transform", "translate(20, 15)");
// plot SD line
sd_plot.append("g")
.attr("id", "SD_annotation")
.attr("transform", "translate(" + xScale(sd_mult) + ",0)")
.call(addLine, "SD_line", "SD_label", sd_mult + " SD");
// plot mean line
sd_plot.append("g")
.attr("id", "mean_annotation")
.attr("transform", "translate(" + xScale(0) + ",0)")
.call(addLine, "mean_line", "mean_label", "mean");
});
function collapseToSdPlot(rect_g, d, xScale, yScale) {
// remove toolTip
hideTooltip();
// revert data rects
d3.select(rect_g.parentNode)
.selectAll(".rect_data")
.classed("jittered_d", false)
.transition()
.duration(500)
.attr("x", d => xScale(+d._sdMult))
.attr("y", () => yScale(d.key))
.attr("width", 5)
.attr("height", yScale.step() * 0.75)
.attr("rx", 0)
.attr("ry", 0);
// revert grouping rect and re-apply tooltip
d3.select(rect_g)
.attr("x", margin_sd_plot.left)
.attr("y", v => yScale(v.key))
.attr("width", sd_plot_dx - margin_sd_plot.right - margin_sd_plot.left)
.attr("height", yScale.step() * 0.75)
.on("mouseover", v => displayTooltip(v.key))
.on("mousemove", v => displayTooltip(v.key))
.on("mouseout", hideTooltip);
// raise rect to mask circle mouseover events
d3.select(rect_g)
.raise();
// remove x-axis of jitterplot
d3.select("#jitter_plot_axis")
.remove();
// remove jitterplot title
d3.select("#jitter_plot_label")
.remove();
}
function expandToJitterplot(rect_g, d) {
// min and max of variable values
var d_extent = d3.extent(d.values, d => +d.value);
// extend range for aesthetics
var d_extent_plus = {
min : d_extent[0] - (d_extent[0] * 0.05),
max : d_extent[1] + (d_extent[1] * 0.05),
};
// y scale for jitterplot
var yScaleJittered = d3.scaleLinear()
.domain([d_extent_plus.min, d_extent_plus.max])
.range([svg_dy - margin_sd_plot.bottom, margin_sd_plot.top]);
// y axis for jitterplot
var yAxisJittered = d3.axisLeft(yScaleJittered);
// remove tooltip of variable names
hideTooltip();
// circle dimensions
var dim = 8;
// jitterplot transition
d3.select(rect_g.parentNode)
.selectAll(".rect_data")
.classed("jittered_d", true)
.transition()
.duration(500)
.attr("height", dim)
.attr("width", dim)
.attr("rx", dim)
.attr("ry", dim)
.attr("x", () => sd_plot_dx + 100 + xJitter())
.attr("y", d => yScaleJittered(+d.value))
.attr("transform", "translate(0," + -(dim / 2) + ")"); // centers rect
// transition grouping rect and remove its event listeners
d3.select(rect_g)
.classed("jittered", true)
.attr("x", sd_plot_dx)
.attr("y", 0)
.attr("width", svg_dx - sd_plot_dx)
.attr("height", svg_dy)
.on("mouseover", null)
.on("mousemove", null)
.on("mouseout", null);
// lower group rect to display circle mouseover events
d3.select(rect_g)
.lower();
// add y-axis
yAxisJitter = d3.select("svg")
.append("g")
.attr("id", "jitter_plot_axis")
.call(yAxisJittered);
yAxisJitter.attr("transform", "translate(" + (sd_plot_dx + 50) + ", 0)");
// add jitterplot title
d3.select(rect_g.parentNode)
.append("text")
.attr("id", "jitter_plot_label")
.text(d.key)
.attr("transform", "translate(" + jitter_plot_x_midline + "," + 17 + ")");
}
function displayTooltip(d) {
tooltip.html(d)
.style("left", (d3.event.pageX + 8) + "px")
.style("top", (d3.event.pageY - 20) + "px")
.style("opacity", 0.9);
}
function hideTooltip() {
tooltip.style("opacity", 0);
}
function addLine(selection, line_ID, text_ID, text) {
selection.append("line")
.attr("class", "annotate_line")
.attr("id", line_ID)
.attr("x1", 0)
.attr("y1", margin_sd_plot.top)
.attr("x2", 0)
.attr("y2", sd_plot_dy);
selection.append("text")
.text(text)
.attr("class", "annotate_text")
.attr("id", text_ID)
.attr("transform", "translate(-4," + sd_plot_dy + ") rotate(270)");
}
function isOutlier(value, group) {
var is_gt_eq_thes = value >= group._mean + (sd_mult * group._sd),
is_lt_eq_thres = value <= group._mean - (sd_mult * group._sd);
if (is_gt_eq_thes | is_lt_eq_thres) {
return true;
} else {
return false;
}
}
function calcStatsByVar(d_grouped) {
// for each variable
d_grouped.forEach(v => {
// standard deviation
v._sd = d3.deviation(v.values, d => +d.value);
// mean
v._mean = d3.mean(v.values, d => +d.value);
// flag outliers
v.values.forEach(d => isOutlier(+d.value, v) ? d._outlier = true : d._outlier = false);
// flag variables having outlier
v._hasOutlier = v.values.map(d => d._outlier).includes(true);
// multiple of SD from mean (+ / -)
v.values.forEach(d => d._sdMult = (d.value - v._mean) / v._sd)
// min and max of SDs
v._extentSD = d3.extent(v.values, d => +d._sdMult);
});
}
function sortBySd(d_grouped) {
// sort groups by max SD multiple distance, in descending order
d_grouped.sort((a, b) => d3.descending(a._extentSD[1], b._extentSD[1]));
}