'use strict'; var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); var boxCalc = require('../box/calc'); var helpers = require('./helpers'); var BADNUM = require('../../constants/numerical').BADNUM; module.exports = function calc(gd, trace) { var cd = boxCalc(gd, trace); if(cd[0].t.empty) return cd; var fullLayout = gd._fullLayout; var valAxis = Axes.getFromId( gd, trace[trace.orientation === 'h' ? 'xaxis' : 'yaxis'] ); var spanMin = Infinity; var spanMax = -Infinity; var maxKDE = 0; var maxCount = 0; for(var i = 0; i < cd.length; i++) { var cdi = cd[i]; var vals = cdi.pts.map(helpers.extractVal); var bandwidth = cdi.bandwidth = calcBandwidth(trace, cdi, vals); var span = cdi.span = calcSpan(trace, cdi, valAxis, bandwidth); if(cdi.min === cdi.max && bandwidth === 0) { // if span is zero and bandwidth is zero, we want a violin with zero width span = cdi.span = [cdi.min, cdi.max]; cdi.density = [{v: 1, t: span[0]}]; cdi.bandwidth = bandwidth; maxKDE = Math.max(maxKDE, 1); } else { // step that well covers the bandwidth and is multiple of span distance var dist = span[1] - span[0]; var n = Math.ceil(dist / (bandwidth / 3)); var step = dist / n; if(!isFinite(step) || !isFinite(n)) { Lib.error('Something went wrong with computing the violin span'); cd[0].t.empty = true; return cd; } var kde = helpers.makeKDE(cdi, trace, vals); cdi.density = new Array(n); for(var k = 0, t = span[0]; t < (span[1] + step / 2); k++, t += step) { var v = kde(t); cdi.density[k] = {v: v, t: t}; maxKDE = Math.max(maxKDE, v); } } maxCount = Math.max(maxCount, vals.length); spanMin = Math.min(spanMin, span[0]); spanMax = Math.max(spanMax, span[1]); } var extremes = Axes.findExtremes(valAxis, [spanMin, spanMax], {padded: true}); trace._extremes[valAxis._id] = extremes; if(trace.width) { cd[0].t.maxKDE = maxKDE; } else { var violinScaleGroupStats = fullLayout._violinScaleGroupStats; var scaleGroup = trace.scalegroup; var groupStats = violinScaleGroupStats[scaleGroup]; if(groupStats) { groupStats.maxKDE = Math.max(groupStats.maxKDE, maxKDE); groupStats.maxCount = Math.max(groupStats.maxCount, maxCount); } else { violinScaleGroupStats[scaleGroup] = { maxKDE: maxKDE, maxCount: maxCount }; } } cd[0].t.labels.kde = Lib._(gd, 'kde:'); return cd; }; // Default to Silveman's rule of thumb // - https://stats.stackexchange.com/a/6671 // - https://en.wikipedia.org/wiki/Kernel_density_estimation#A_rule-of-thumb_bandwidth_estimator // - https://github.com/statsmodels/statsmodels/blob/master/statsmodels/nonparametric/bandwidths.py function silvermanRule(len, ssd, iqr) { var a = Math.min(ssd, iqr / 1.349); return 1.059 * a * Math.pow(len, -0.2); } function calcBandwidth(trace, cdi, vals) { var span = cdi.max - cdi.min; // If span is zero if(!span) { if(trace.bandwidth) { return trace.bandwidth; } else { // if span is zero and no bandwidth is specified // it returns zero bandwidth which is a special case return 0; } } // Limit how small the bandwidth can be. // // Silverman's rule of thumb can be "very" small // when IQR does a poor job at describing the spread // of the distribution. // We also want to limit custom bandwidths // to not blow up kde computations. if(trace.bandwidth) { return Math.max(trace.bandwidth, span / 1e4); } else { var len = vals.length; var ssd = Lib.stdev(vals, len - 1, cdi.mean); return Math.max( silvermanRule(len, ssd, cdi.q3 - cdi.q1), span / 100 ); } } function calcSpan(trace, cdi, valAxis, bandwidth) { var spanmode = trace.spanmode; var spanIn = trace.span || []; var spanTight = [cdi.min, cdi.max]; var spanLoose = [cdi.min - 2 * bandwidth, cdi.max + 2 * bandwidth]; var spanOut; function calcSpanItem(index) { var s = spanIn[index]; var sc = valAxis.type === 'multicategory' ? valAxis.r2c(s) : valAxis.d2c(s, 0, trace[cdi.valLetter + 'calendar']); return sc === BADNUM ? spanLoose[index] : sc; } if(spanmode === 'soft') { spanOut = spanLoose; } else if(spanmode === 'hard') { spanOut = spanTight; } else { spanOut = [calcSpanItem(0), calcSpanItem(1)]; } // to reuse the equal-range-item block var dummyAx = { type: 'linear', range: spanOut }; Axes.setConvert(dummyAx); dummyAx.cleanRange(); return spanOut; }