UNPKG

nvd3-fork

Version:

FORK! of NVD3, a reusable charting library written in d3.js

901 lines (783 loc) 45 kB
nv.models.distroPlot = function() { "use strict"; // IMPROVEMENTS: // - cleanup tooltip to look like candlestick example (don't need color square for everything) // - extend y scale range to min/max data better visually // - tips of violins need to be cut off if very long // - transition from box to violin not great since box only has a few points, and violin has many - need to generate box with as many points as violin // - when providing colorGroup, should color boxes by either parent or child group category (e.g. isolator) //============================================================ // Public Variables with Default Settings //------------------------------------------------------------ var margin = {top: 0, right: 0, bottom: 0, left: 0}, width = 960, height = 500, id = Math.floor(Math.random() * 10000), // Create semi-unique ID in case user doesn't select one xScale = d3.scale.ordinal(), yScale = d3.scale.linear(), getX = function(d) { return d.label }, // Default data model selectors. getY = function(d) { return d.value }, getColor = function(d) { return d.color }, getQ1 = function(d) { return d.values.q1 }, getQ2 = function(d) { return d.values.q2 }, getQ3 = function(d) { return d.values.q3 }, getNl = function(d) { return (centralTendency == 'mean' ? getMean(d) : getQ2(d)) - d.values.notch }, getNu = function(d) { return (centralTendency == 'mean' ? getMean(d) : getQ2(d)) + d.values.notch }, getMean = function(d) { return d.values.mean }, getWl = function(d) { return d.values.wl[whiskerDef] }, getWh = function(d) { return d.values.wu[whiskerDef] }, getMin = function(d) { return d.values.min }, getMax = function(d) { return d.values.max }, getDev = function(d) { return d.values.dev }, getValsObj = function(d) { return d.values.observations; }, getValsArr = function(d) { return d.values.observations.map(function(e) { return e.y }); }, plotType, // type of background: 'box', 'violin', 'none'/false - default: 'box' - 'none' will activate random scatter automatically observationType = false, // type of observations to show: 'random', 'swarm', 'line', 'centered' - default: false (don't show any observations, even if an outlier) whiskerDef = 'iqr', // type of whisker to render: 'iqr', 'minmax', 'stddev' - default: iqr hideWhiskers = false, notchBox = false, // bool whether to notch box colorGroup = false, // if specified, each x-category will be split into groups, each colored centralTendency = false, showOnlyOutliers = true, // show only outliers in box plot jitter = 0.7, // faction of that jitter should take up in 'random' observationType, must be in range [0,1]; see jitterX(), default 0.7 squash = true, // whether to remove the x-axis positions for empty data groups, default is true bandwidth = 'scott', // bandwidth for kde calculation, can be float or str, if str, must be one of scott or silverman clampViolin = true, // whether to clamp the "tails" of the violin; prevents long 0-density area resolution = 50, pointSize = 3, color = nv.utils.defaultColor(), container = null, xDomain, xRange, yDomain, yRange, dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd'), duration = 250, maxBoxWidth = null; //============================================================ // Helper Functions //------------------------------------------------------------ /* Returns the smaller of std(X, ddof=1) or normalized IQR(X) over axis 0. * * @param (list) x - input x formatted as a single list of values * * @return float * * Source: https://github.com/statsmodels/statsmodels/blob/master/statsmodels/nonparametric/bandwidths.py#L9 */ function select_sigma(x) { var sorted = x.sort(d3.ascending); // sort our dat var normalize = 1.349; var IQR = (d3.quantile(sorted, 0.75) - d3.quantile(sorted, 0.25))/normalize; // normalized IQR return d3.min([d3.deviation(sorted), IQR]); } /* Scott's Rule of Thumb Parameters ---------- x : array-like Array for which to get the bandwidth type : string The type of estimate to use, must be one of scott or silverman Returns ------- bw : float The estimate of the bandwidth Notes ----- Returns 1.059 * A * n ** (-1/5.) where :: A = min(std(x, ddof=1), IQR/1.349) IQR = np.subtract.reduce(np.percentile(x, [75,25])) References ---------- Scott, D.W. (1992) Multivariate Density Estimation: Theory, Practice, and Visualization. */ function calcBandwidth(x, type) { if (typeof type === 'undefined') type = 'scott'; // TODO: consider using https://github.com/jasondavies/science.js var A = select_sigma(x); var n = x.length; return type==='scott' ? Math.pow(1.059 * A * n, -0.2) : Math.pow(.9 * A * n, -0.2); } /* * Prep data for use with distroPlot by grouping data * by .x() option set by user and then calculating * count, sum, mean, q1, q2 (median), q3, lower whisker (wl) * upper whisker (wu), iqr, min, max, and standard dev. * * NOTE: preparing this data can be resource intensive, and * is therefore only run once on plot load. It can * manually be run by calling recalcData(). This should * be re-run any time the axis accessors are changed or * when bandwidth/resolution are updated. * * NOTE: this will also setup the individual vertical scales * for the violins. * * @param (list) dat - input data formatted as list of objects, * with an object key that must exist when accessed by getX() * * @return prepared data in the form for box plotType: * [{ * key : YY, * values: { * count: XX, * sum: XX, * mean: XX, * q1: XX, * q2: XX, * q3: XX, * wl: XX, * wu: XX, * iqr: XX, * min: XX, * max: XX, * dev: XX, * observations: [{y:XX,..},..], * key: XX, * kdeDat: XX, * notch: XX, * } * }, * ... * ] * for violin plotType: * [{ * key : YY, * values: { * original: [{y:XX,..},..] * } * }, * ... * ] * where YY are those keys in dat that define the * x-axis and which are defined by .x() */ function prepData(dat) { // helper function to calcuate the various boxplot stats function calcStats(g, xGroup) { // sort data by Y so we can calc quartiles var v = g.map(function(d) { if (colorGroup) allColorGroups.add(colorGroup(d)); // list of all colorGroups; used to set x-axis return getY(d); }).sort(d3.ascending); var q1 = d3.quantile(v, 0.25); var q3 = d3.quantile(v, 0.75); var iqr = q3 - q1; var upper = q3 + 1.5 * iqr; var lower = q1 - 1.5 * iqr; /* whisker definitions: * - iqr: also known as Tukey boxplot, the lowest datum still within 1.5 IQR of the lower quartile, and the highest datum still within 1.5 IQR of the upper quartile * - minmax: the minimum and maximum of all of the data * - sttdev: one standard deviation above and below the mean of the data * Note that the central tendency type (median or mean) does not impact the whisker location */ var wl = {iqr: d3.max([d3.min(v), d3.min(v.filter(function(d) {return d > lower}))]), minmax: d3.min(v), stddev: d3.mean(v) - d3.deviation(v)}; var wu = {iqr: d3.min([d3.max(v), d3.max(v.filter(function(d) {return d < upper}))]), minmax: d3.max(v), stddev: d3.mean(v) + d3.deviation(v)}; var median = d3.median(v); var mean = d3.mean(v); var observations = []; // d3-beeswarm library must be externally loaded if being used // https://github.com/Kcnarf/d3-beeswarm if (typeof d3.beeswarm !== 'undefined') { observations = d3.beeswarm() .data(g.map(function(e) { return getY(e); })) .radius(pointSize+1) .orientation('vertical') .side('symmetric') .distributeOn(function(e) { return yScale(e); }) .arrange() // add group info for tooltip observations.map(function(e,i) { e.key = xGroup; e.object_constancy = g[i].object_constancy; e.isOutlier = (e.datum < wl.iqr || e.datum > wu.iqr) // add isOulier meta for proper class assignment e.isOutlierStdDev = (e.datum < wl.stddev || e.datum > wu.stddev) // add isOulier meta for proper class assignment e.randX = Math.random() * jitter * (Math.floor(Math.random()*2) == 1 ? 1 : -1) // calculate random x-position only once for each point }) } else { v.forEach(function(e,i) { observations.push({ object_constancy: e.object_constancy, datum: e, key: xGroup, isOutlier: (e < wl.iqr || e > wu.iqr), // add isOulier meta for proper class assignment isOutlierStdDev: (e < wl.stddev || e > wu.stddev), // add isOulier meta for proper class assignment randX: Math.random() * jitter * (Math.floor(Math.random()*2) == 1 ? 1 : -1) }) }) } // calculate bandwidth if no number is provided if(isNaN(parseFloat(bandwidth))) { // if not is float var bandwidthCalc; if (['scott','silverman'].indexOf(bandwidth) != -1) { bandwidthCalc = calcBandwidth(v, bandwidth); } else { bandwidthCalc = calcBandwidth(v); // calculate with default 'scott' } } var kde = kernelDensityEstimator(eKernel(bandwidthCalc), yScale.ticks(resolution)); var kdeDat = clampViolin ? clampViolinKDE(kde(v), d3.extent(v)) : kde(v); // make a new vertical scale for each group var tmpScale = d3.scale.linear() .domain([0, d3.max(kdeDat, function (e) { return e.y;})]) .clamp(true); yVScale.push(tmpScale); var reformat = { count: v.length, num_outlier: observations.filter(function (e) { return e.isOutlier; }).length, sum: d3.sum(v), mean: mean, q1: q1, q2: median, q3: q3, wl: wl, wu: wu, iqr: iqr, min: d3.min(v), max: d3.max(v), dev: d3.deviation(v), observations: observations, key: xGroup, kde: kdeDat, notch: 1.57 * iqr / Math.sqrt(v.length), // notch distance from mean/median }; if (colorGroup) {reformatDatFlat.push({key: xGroup, values: reformat});} return reformat; } // assign a unique identifier for each point for object constancy // this makes updating data possible dat.forEach(function(d,i) { d.object_constancy = i + '_' + getY(d) + '_' + getX(d); }) // TODO not DRY // couldn't find a conditional way of doing the key() grouping var formatted; if (!colorGroup) { formatted = d3.nest() .key(function(d) { return getX(d); }) .rollup(function(v,i) { return calcStats(v); }) .entries(dat); } else { allColorGroups = d3.set() // reset var tmp = d3.nest() .key(function(d) { return getX(d); }) .key(function(d) { return colorGroup(d); }) .rollup(function(v) { return calcStats(v, getX(v[0])); }) .entries(dat); // generate a final list of all x & colorGroup combinations // this is used to properly set the x-axis domain allColorGroups = allColorGroups.values(); // convert from d3.set to list var xGroups = tmp.map(function(d) { return d.key; }); var allGroups = []; for (var i = 0; i < xGroups.length; i++) { for (var j = 0; j < allColorGroups.length; j++) { allGroups.push(xGroups[i] + '_' + allColorGroups[j]); } } allColorGroups = allGroups; // flatten the inner most level so that // the plot retains the same DOM structure // to allow for smooth updating between // all groups. formatted = []; tmp.forEach(function(d) { d.values.forEach(function(e) { e.key = d.key +'_'+e.key }) // generate a combo key so that each boxplot has a distinct x-position formatted.push.apply(formatted, d.values) }); } return formatted; } // https://bl.ocks.org/mbostock/4341954 function kernelDensityEstimator(kernel, X) { return function (sample) { return X.map(function(x) { var y = d3.mean(sample, function (v) {return kernel(x - v);}); return {x:x, y:y}; }); }; } /* * Limit whether the density extends past the extreme datapoints * of the violin. * * @param (list) kde - x & y kde cooridinates * @param (list) extent - min/max y-values used for clamping violing */ function clampViolinKDE(kde, extent) { // this handles the case when all the x-values are equal // which means no kde could be properly calculated // just return the kde data so we can continue plotting successfully if (extent[0] === extent[1]) return kde; var clamped = kde.reduce(function(res, d) { if (d.x >= extent[0] && d.x <= extent[1]) res.push(d); return res; },[]); // add the extreme data points back in if (extent[0] < clamped[0].x) clamped.unshift({x:extent[0], y:clamped[0].y}) if (extent[1] > clamped[clamped.length-1].x) clamped.push({x:extent[1], y:clamped[clamped.length-1].y}) return clamped; } // https://bl.ocks.org/mbostock/4341954 function eKernel(scale) { return function (u) { return Math.abs(u /= scale) <= 1 ? .75 * (1 - u * u) / scale : 0; }; } /** * Makes the svg polygon string for a boxplot in either a notched * or square version * * NOTE: this actually only draws the left half of the box, since * the shape is symmetric (and since this is how violins are drawn) * we can simply generate half the box and mirror it. * * @param boxLeft {float} - left position of box * @param notchLeft {float} - left position of notch * @param dat {obj} - box plot data that was run through prepDat, must contain * data for Q1, median, Q2, notch upper and notch lower * @returns {string} A string in the proper format for a svg polygon */ function makeNotchBox(boxLeft, notchLeft, boxCenter, dat) { var boxPoints; var y = centralTendency == 'mean' ? getMean(dat) : getQ2(dat); // if centralTendency is not specified, we still want to notch boxes on 'median' if (notchBox) { boxPoints = [ {x:boxCenter, y:yScale(getQ1(dat))}, {x:boxLeft, y:yScale(getQ1(dat))}, {x:boxLeft, y:yScale(getNl(dat))}, {x:notchLeft, y:yScale(y)}, {x:boxLeft, y:yScale(getNu(dat))}, {x:boxLeft, y:yScale(getQ3(dat))}, {x:boxCenter, y:yScale(getQ3(dat))}, ]; } else { boxPoints = [ {x:boxCenter, y:yScale(getQ1(dat))}, {x:boxLeft, y:yScale(getQ1(dat))}, {x:boxLeft, y:yScale(y)}, // repeated point so that transition between notched/regular more smooth {x:boxLeft, y:yScale(y)}, {x:boxLeft, y:yScale(y)}, // repeated point so that transition between notched/regular more smooth {x:boxLeft, y:yScale(getQ3(dat))}, {x:boxCenter, y:yScale(getQ3(dat))}, ]; } return boxPoints; } /** * Given an x-axis group, return the available color groups within it * provided that colorGroups is set, if not, x-axis group is returned */ function getAvailableColorGroups(x) { if (!colorGroup) return x; var tmp = reformatDat.find(function(d) { return d.key == x }); return tmp.values.map(function(d) { return d.key }).sort(d3.ascending); } // return true if point is an outlier function isOutlier(d) { return (whiskerDef == 'iqr' && d.isOutlier) || (whiskerDef == 'stddev' && d.isOutlierStdDev) } //============================================================ // Private Variables //------------------------------------------------------------ var allColorGroups = d3.set() var yVScale = [], reformatDat, reformatDatFlat = []; var renderWatch = nv.utils.renderWatch(dispatch, duration); var availableWidth, availableHeight; function chart(selection) { renderWatch.reset(); selection.each(function(data) { availableWidth = width - margin.left - margin.right, availableHeight = height - margin.top - margin.bottom; container = d3.select(this); nv.utils.initSVG(container); // Setup y-scale so that beeswarm layout can use it in prepData() yScale.domain(yDomain || d3.extent(data.map(function(d) { return getY(d)}))).nice() .range(yRange || [availableHeight, 0]); if (typeof reformatDat === 'undefined') reformatDat = prepData(data); // this prevents us from recalculating data all the time // Setup x-scale xScale.rangeBands(xRange || [0, availableWidth], 0.1) .domain(xDomain || (colorGroup && !squash) ? allColorGroups : reformatDat.map(function(d) { return d.key })) // Setup containers and skeleton of chart var wrap = container.selectAll('g.nv-wrap').data([reformatDat]); var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap'); wrap.watchTransition(renderWatch, 'nv-wrap: wrap') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); var areaEnter, distroplots = wrap.selectAll('.nv-distroplot-x-group') .data(function(d) { return d; }); // rebind new data // we don't rebuild individual x-axis groups so that we can update transition them // however the data associated with each x-axis group needs to be updated // so we manually update it here distroplots.each(function(d,i) { d3.select(this).selectAll('line.nv-distroplot-middle').datum(d); }) areaEnter = distroplots.enter() .append('g') .attr('class', 'nv-distroplot-x-group') .style('stroke-opacity', 1e-6).style('fill-opacity', 1e-6) .style('fill', function(d,i) { return getColor(d) || color(d,i) }) .style('stroke', function(d,i) { return getColor(d) || color(d,i) }) distroplots.exit().remove(); var rangeBand = function() { return xScale.rangeBand() }; var areaWidth = function() { return d3.min([maxBoxWidth,rangeBand() * 0.9]); }; var areaCenter = function() { return areaWidth()/2; }; var areaLeft = function() { return areaCenter() - areaWidth()/2; }; var areaRight = function() { return areaCenter() + areaWidth()/2; }; var tickLeft = function() { return areaCenter() - areaWidth()/5; }; var tickRight = function() { return areaCenter() + areaWidth()/5; }; areaEnter.attr('transform', function(d) { return 'translate(' + (xScale(d.key) + (rangeBand() - areaWidth()) * 0.5) + ', 0)'; }); distroplots .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots') .style('stroke-opacity', 1) .style('fill-opacity', 0.5) .attr('transform', function(d) { return 'translate(' + (xScale(d.key) + (rangeBand() - areaWidth()) * 0.5) + ', 0)'; }); // set range for violin scale yVScale.map(function(d) { d.range([areaWidth()/2, 0]) }); // ----- add the SVG elements for each plot type ----- // scatter plot type if (!plotType) { showOnlyOutliers = false; // force all observations to be seen if (!observationType) observationType = 'random' } // conditionally append whisker lines areaEnter.each(function(d,i) { var box = d3.select(this); [getWl, getWh].forEach(function (f) { var key = (f === getWl) ? 'low' : 'high'; box.append('line') .style('opacity', function() { return !hideWhiskers ? '0' : '1' }) .attr('class', 'nv-distroplot-whisker nv-distroplot-' + key) box.append('line') .style('opacity', function() { return hideWhiskers ? '0' : '1' }) .attr('class', 'nv-distroplot-tick nv-distroplot-' + key) }); }); // update whisker lines and ticks [getWl, getWh].forEach(function (f) { var key = (f === getWl) ? 'low' : 'high'; var endpoint = (f === getWl) ? getQ1 : getQ3; distroplots.select('line.nv-distroplot-whisker.nv-distroplot-' + key) .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots') .attr('x1', areaCenter()) .attr('y1', function(d) { return plotType!='violin' ? yScale(f(d)) : yScale(getQ2(d)); }) .attr('x2', areaCenter()) .attr('y2', function(d) { return plotType=='box' ? yScale(endpoint(d)) : yScale(getQ2(d)); }) .style('opacity', function() { return hideWhiskers ? '0' : '1' }) distroplots.select('line.nv-distroplot-tick.nv-distroplot-' + key) .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots') .attr('x1', function(d) { return plotType!='violin' ? tickLeft() : areaCenter()} ) .attr('y1', function(d,i) { return plotType!='violin' ? yScale(f(d)) : yScale(getQ2(d)); }) .attr('x2', function(d) { return plotType!='violin' ? tickRight() : areaCenter()} ) .attr('y2', function(d,i) { return plotType!='violin' ? yScale(f(d)) : yScale(getQ2(d)); }) .style('opacity', function() { return hideWhiskers ? '0' : '1' }) }); [getWl, getWh].forEach(function (f) { var key = (f === getWl) ? 'low' : 'high'; areaEnter.selectAll('.nv-distroplot-' + key) .on('mouseover', function(d,i,j) { d3.select(this.parentNode).selectAll('line.nv-distroplot-'+key).classed('hover',true); dispatch.elementMouseover({ value: key == 'low' ? 'Lower whisker' : 'Upper whisker', series: { key: f(d).toFixed(2), color: getColor(d) || color(d,j) }, e: d3.event }); }) .on('mouseout', function(d,i,j) { d3.select(this.parentNode).selectAll('line.nv-distroplot-'+key).classed('hover',false); dispatch.elementMouseout({ value: key == 'low' ? 'Lower whisker' : 'Upper whisker', series: { key: f(d).toFixed(2), color: getColor(d) || color(d,j) }, e: d3.event }); }) .on('mousemove', function(d,i) { dispatch.elementMousemove({e: d3.event}); }); }); // setup boxes as 4 parts: left-area, left-line, right-area, right-line, // this way we can transition to a violin areaEnter.each(function(d,i) { var violin = d3.select(this); ['left','right'].forEach(function(side) { ['line','area'].forEach(function(d) { violin.append('path') .attr('class', 'nv-distribution-' + d + ' nv-distribution-' + side) .attr("transform", "rotate(90,0,0) translate(0," + (side == 'left' ? -areaWidth() : 0) + ")" + (side == 'left' ? '' : ' scale(1,-1)')); // rotate violin }) }) areaEnter.selectAll('.nv-distribution-line') .style('fill','none') areaEnter.selectAll('.nv-distribution-area') .style('stroke','none') .style('opacity',0.7) }); // transitions distroplots.each(function(d,i) { var violin = d3.select(this); var objData = plotType == 'box' ? makeNotchBox(areaLeft(), tickLeft(), areaCenter(), d) : d.values.kde; violin.selectAll('path') .datum(objData) var tmpScale = yVScale[i]; var interp = plotType=='box' ? 'linear' : 'basis'; if (plotType == 'box' || plotType == 'violin') { ['left','right'].forEach(function(side) { // line distroplots.selectAll('.nv-distribution-line.nv-distribution-' + side) //.watchTransition(renderWatch, 'nv-distribution-line: distroplots') // disable transition for now because it's jaring .attr("d", d3.svg.line() .x(function(e) { return plotType=='box' ? e.y : yScale(e.x); }) .y(function(e) { return plotType=='box' ? e.x : tmpScale(e.y) }) .interpolate(interp) ) .attr("transform", "rotate(90,0,0) translate(0," + (side == 'left' ? -areaWidth() : 0) + ")" + (side == 'left' ? '' : ' scale(1,-1)')) // rotate violin .style('opacity', !plotType ? '0' : '1'); // area distroplots.selectAll('.nv-distribution-area.nv-distribution-' + side) //.watchTransition(renderWatch, 'nv-distribution-line: distroplots') // disable transition for now because it's jaring .attr("d", d3.svg.area() .x(function(e) { return plotType=='box' ? e.y : yScale(e.x); }) .y(function(e) { return plotType=='box' ? e.x : tmpScale(e.y) }) .y0(areaWidth()/2) .interpolate(interp) ) .attr("transform", "rotate(90,0,0) translate(0," + (side == 'left' ? -areaWidth() : 0) + ")" + (side == 'left' ? '' : ' scale(1,-1)')) // rotate violin .style('opacity', !plotType ? '0' : '1'); }) } else { // scatter type, hide areas distroplots.selectAll('.nv-distribution-area') .watchTransition(renderWatch, 'nv-distribution-area: distroplots') .style('opacity', !plotType ? '0' : '1'); distroplots.selectAll('.nv-distribution-line') .watchTransition(renderWatch, 'nv-distribution-line: distroplots') .style('opacity', !plotType ? '0' : '1'); } }) // tooltip events distroplots.selectAll('path') .on('mouseover', function(d,i,j) { d = d3.select(this.parentNode).datum(); // grab data from parent g d3.select(this).classed('hover', true); dispatch.elementMouseover({ key: d.key, value: 'Group ' + d.key + ' stats', series: [ { key: 'max', value: getMax(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'Q3', value: getQ3(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'Q2', value: getQ2(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'Q1', value: getQ1(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'min', value: getMin(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'mean', value: getMean(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'std. dev.', value: getDev(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'count', value: d.values.count, color: getColor(d) || color(d,j) }, { key: 'num. outliers', value: d.values.num_outlier, color: getColor(d) || color(d,j) }, ], data: d, index: i, e: d3.event }); }) .on('mouseout', function(d,i,j) { d3.select(this).classed('hover', false); d = d3.select(this.parentNode).datum(); // grab data from parent g dispatch.elementMouseout({ key: d.key, value: 'Group ' + d.key + ' stats', series: [ { key: 'max', value: getMax(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'Q3', value: getQ3(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'Q2', value: getQ2(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'Q1', value: getQ1(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'min', value: getMin(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'mean', value: getMean(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'std. dev.', value: getDev(d).toFixed(2), color: getColor(d) || color(d,j) }, { key: 'count', value: d.values.count, color: getColor(d) || color(d,j) }, { key: 'num. outliers', value: d.values.num_outlier, color: getColor(d) || color(d,j) }, ], data: d, index: i, e: d3.event }); }) .on('mousemove', function(d,i) { dispatch.elementMousemove({e: d3.event}); }); // median/mean line areaEnter.append('line') .attr('class', function(d) { return 'nv-distroplot-middle'}) distroplots.selectAll('line.nv-distroplot-middle') .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots line') .attr('x1', notchBox ? tickLeft : plotType != 'violin' ? areaLeft : tickLeft()) .attr('y1', function(d,i,j) { return centralTendency == 'mean' ? yScale(getMean(d)) : yScale(getQ2(d)); }) .attr('x2', notchBox ? tickRight : plotType != 'violin' ? areaRight : tickRight()) .attr('y2', function(d,i) { return centralTendency == 'mean' ? yScale(getMean(d)) : yScale(getQ2(d)); }) .style('opacity', centralTendency ? '1' : '0'); // tooltip distroplots.selectAll('.nv-distroplot-middle') .on('mouseover', function(d,i,j) { if (d3.select(this).style('opacity') == 0) return; // don't show tooltip for hidden lines var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill d3.select(this).classed('hover', true); dispatch.elementMouseover({ value: centralTendency == 'mean' ? 'Mean' : 'Median', series: { key: centralTendency == 'mean' ? getMean(d).toFixed(2) : getQ2(d).toFixed(2), color: fillColor }, e: d3.event }); }) .on('mouseout', function(d,i,j) { if (d3.select(this).style('opacity') == 0) return; // don't show tooltip for hidden lines d3.select(this).classed('hover', false); var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill dispatch.elementMouseout({ value: centralTendency == 'mean' ? 'Mean' : 'Median', series: { key: centralTendency == 'mean' ? getMean(d).toFixed(2) : getQ2(d).toFixed(2), color: fillColor }, e: d3.event }); }) .on('mousemove', function(d,i) { dispatch.elementMousemove({e: d3.event}); }); // setup observations // create DOMs even if not requested (and hide them), so that // we can do transitions on them var obsWrap = distroplots.selectAll('g.nv-distroplot-observation') .data(function(d) { return getValsObj(d) }, function(d) { return d.object_constancy; }); var obsGroup = obsWrap.enter() .append('g') .attr('class', 'nv-distroplot-observation') obsGroup.append('circle') .style({'opacity': 0}) obsGroup.append('line') .style('stroke-width', 1) .style({'stroke': d3.rgb(85, 85, 85), 'opacity': 0}) obsWrap.exit().remove(); obsWrap.attr('class', function(d) { return 'nv-distroplot-observation ' + (isOutlier(d) && plotType == 'box' ? 'nv-distroplot-outlier' : 'nv-distroplot-non-outlier')}) // transition observations if (observationType == 'line') { distroplots.selectAll('g.nv-distroplot-observation line') .watchTransition(renderWatch, 'nv-distrolot-x-group: nv-distoplot-observation') .attr("x1", tickLeft() + areaWidth()/4) .attr("x2", tickRight() - areaWidth()/4) .attr('y1', function(d) { return yScale(d.datum)}) .attr('y2', function(d) { return yScale(d.datum)}); } else { distroplots.selectAll('g.nv-distroplot-observation circle') .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation') .attr('cy', function(d) { return yScale(d.datum); }) .attr('r', pointSize); // NOTE: this update can be slow when re-sizing window when many point visible // TODO: filter selection down to only visible points, no need to update x-position // of the hidden points distroplots.selectAll('g.nv-distroplot-observation circle') .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation') .attr('cx', function(d) { return observationType == 'swarm' ? d.x + areaWidth()/2 : observationType == 'random' ? areaWidth()/2 + d.randX * areaWidth()/2 : areaWidth()/2; }) } // set opacity on outliers/non-outliers // any circle/line entering has opacity 0 if (observationType !== false) { // observationType is False when hidding all circle/lines if (!showOnlyOutliers) { // show all line/circle distroplots.selectAll(observationType== 'line' ? 'line':'circle') .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation') .style('opacity',1) } else { // show only outliers distroplots.selectAll('.nv-distroplot-outlier '+ (observationType== 'line' ? 'line':'circle')) .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation') .style('opacity',1) distroplots.selectAll('.nv-distroplot-non-outlier '+ (observationType== 'line' ? 'line':'circle')) .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation') .style('opacity',0) } } // hide all other observations distroplots.selectAll('.nv-distroplot-observation' + (observationType=='line'?' circle':' line')) .watchTransition(renderWatch, 'nv-distroplot: nv-distoplot-observation') .style('opacity',0) // tooltip events for observations distroplots.selectAll('.nv-distroplot-observation') .on('mouseover', function(d,i,j) { var pt = d3.select(this); if (showOnlyOutliers && plotType == 'box' && !isOutlier(d)) return; // don't show tooltip for hidden observation var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill pt.classed('hover', true); dispatch.elementMouseover({ value: (plotType == 'box' && isOutlier(d)) ? 'Outlier' : 'Observation', series: { key: d.datum.toFixed(2), color: fillColor }, e: d3.event }); }) .on('mouseout', function(d,i,j) { var pt = d3.select(this); var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill pt.classed('hover', false); dispatch.elementMouseout({ value: (plotType == 'box' && isOutlier(d)) ? 'Outlier' : 'Observation', series: { key: d.datum.toFixed(2), color: fillColor }, e: d3.event }); }) .on('mousemove', function(d,i) { dispatch.elementMousemove({e: d3.event}); }); }); renderWatch.renderEnd('nv-distroplot-x-group immediate'); return chart; } //============================================================ // Expose Public Variables //------------------------------------------------------------ chart.dispatch = dispatch; chart.options = nv.utils.optionsFunc.bind(chart); chart._options = Object.create({}, { // simple options, just get/set the necessary values width: {get: function(){return width;}, set: function(_){width=_;}}, height: {get: function(){return height;}, set: function(_){height=_;}}, maxBoxWidth: {get: function(){return maxBoxWidth;}, set: function(_){maxBoxWidth=_;}}, x: {get: function(){return getX;}, set: function(_){getX=_;}}, y: {get: function(){return getY;}, set: function(_){getY=_;}}, plotType: {get: function(){return plotType;}, set: function(_){plotType=_;}}, // plotType of background: 'box', 'violin' - default: 'box' observationType: {get: function(){return observationType;}, set: function(_){observationType=_;}}, // type of observations to show: 'random', 'swarm', 'line', 'point' - default: false (don't show observations) whiskerDef: {get: function(){return whiskerDef;}, set: function(_){whiskerDef=_;}}, // type of whisker to render: 'iqr', 'minmax', 'stddev' - default: iqr notchBox: {get: function(){return notchBox;}, set: function(_){notchBox=_;}}, // bool whether to notch box hideWhiskers: {get: function(){return hideWhiskers;}, set: function(_){hideWhiskers=_;}}, colorGroup: {get: function(){return colorGroup;}, set: function(_){colorGroup=_;}}, // data key to use to set color group of each x-category - default: don't group centralTendency: {get: function(){return centralTendency;}, set: function(_){centralTendency=_;}}, // add a mean or median line to the data - default: don't show, must be one of 'mean' or 'median' bandwidth: {get: function(){return bandwidth;}, set: function(_){bandwidth=_;}}, // bandwidth for kde calculation, can be float or str, if str, must be one of scott or silverman clampViolin: {get: function(){return clampViolin;}, set: function(_){clampViolin=_;}}, resolution: {get: function(){return resolution;}, set: function(_){resolution=_;}}, // resolution for kde calculation, default 50 xScale: {get: function(){return xScale;}, set: function(_){xScale=_;}}, yScale: {get: function(){return yScale;}, set: function(_){yScale=_;}}, showOnlyOutliers: {get: function(){return showOnlyOutliers;}, set: function(_){showOnlyOutliers=_;}}, // show only outliers in box plot, default true jitter: {get: function(){return jitter;}, set: function(_){jitter=_;}}, // faction of that jitter should take up in 'random' observationType, must be in range [0,1]; see jitterX(), default 0.7 squash: {get: function(){return squash;}, set: function(_){squash=_;}}, // whether to squash sparse distribution of color groups towards middle of x-axis position pointSize: {get: function(){return pointSize;}, set: function(_){pointSize=_;}}, xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}}, yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}}, xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}}, yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}}, recalcData: {get: function() { reformatDat = prepData(container.datum()); } }, itemColor: {get: function(){return getColor;}, set: function(_){getColor=_;}}, id: {get: function(){return id;}, set: function(_){id=_;}}, // options that require extra logic in the setter margin: {get: function(){return margin;}, set: function(_){ margin.top = _.top !== undefined ? _.top : margin.top; margin.right = _.right !== undefined ? _.right : margin.right; margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; margin.left = _.left !== undefined ? _.left : margin.left; }}, color: {get: function(){return color;}, set: function(_){ color = nv.utils.getColor(_); }}, duration: {get: function(){return duration;}, set: function(_){ duration = _; renderWatch.reset(duration); }} }); nv.utils.initOptions(chart); return chart; };