'use strict'; var d3 = require('@plotly/d3'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); // constants for dynamic jitter (ie less jitter for sparser points) var JITTERCOUNT = 5; // points either side of this to include var JITTERSPREAD = 0.01; // fraction of IQR to count as "dense" function plot(gd, plotinfo, cdbox, boxLayer) { var isStatic = gd._context.staticPlot; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; Lib.makeTraceGroups(boxLayer, cdbox, 'trace boxes').each(function(cd) { var plotGroup = d3.select(this); var cd0 = cd[0]; var t = cd0.t; var trace = cd0.trace; // whisker width t.wdPos = t.bdPos * trace.whiskerwidth; if(trace.visible !== true || t.empty) { plotGroup.remove(); return; } var posAxis, valAxis; if(trace.orientation === 'h') { posAxis = ya; valAxis = xa; } else { posAxis = xa; valAxis = ya; } plotBoxAndWhiskers(plotGroup, {pos: posAxis, val: valAxis}, trace, t, isStatic); plotPoints(plotGroup, {x: xa, y: ya}, trace, t); plotBoxMean(plotGroup, {pos: posAxis, val: valAxis}, trace, t); }); } function plotBoxAndWhiskers(sel, axes, trace, t, isStatic) { var isHorizontal = trace.orientation === 'h'; var valAxis = axes.val; var posAxis = axes.pos; var posHasRangeBreaks = !!posAxis.rangebreaks; var bPos = t.bPos; var wdPos = t.wdPos || 0; var bPosPxOffset = t.bPosPxOffset || 0; var whiskerWidth = trace.whiskerwidth || 0; var showWhiskers = (trace.showwhiskers !== false); var notched = trace.notched || false; var nw = notched ? 1 - 2 * trace.notchwidth : 1; // to support for one-sided box var bdPos0; var bdPos1; if(Array.isArray(t.bdPos)) { bdPos0 = t.bdPos[0]; bdPos1 = t.bdPos[1]; } else { bdPos0 = t.bdPos; bdPos1 = t.bdPos; } var paths = sel.selectAll('path.box').data(( trace.type !== 'violin' || trace.box.visible ) ? Lib.identity : []); paths.enter().append('path') .style('vector-effect', isStatic ? 'none' : 'non-scaling-stroke') .attr('class', 'box'); paths.exit().remove(); paths.each(function(d) { if(d.empty) return d3.select(this).attr('d', 'M0,0Z'); var lcenter = posAxis.c2l(d.pos + bPos, true); var pos0 = posAxis.l2p(lcenter - bdPos0) + bPosPxOffset; var pos1 = posAxis.l2p(lcenter + bdPos1) + bPosPxOffset; var posc = posHasRangeBreaks ? (pos0 + pos1) / 2 : posAxis.l2p(lcenter) + bPosPxOffset; var r = trace.whiskerwidth; var posw0 = posHasRangeBreaks ? pos0 * r + (1 - r) * posc : posAxis.l2p(lcenter - wdPos) + bPosPxOffset; var posw1 = posHasRangeBreaks ? pos1 * r + (1 - r) * posc : posAxis.l2p(lcenter + wdPos) + bPosPxOffset; var posm0 = posAxis.l2p(lcenter - bdPos0 * nw) + bPosPxOffset; var posm1 = posAxis.l2p(lcenter + bdPos1 * nw) + bPosPxOffset; var sdmode = trace.sizemode === 'sd'; var q1 = valAxis.c2p(sdmode ? d.mean - d.sd : d.q1, true); var q3 = sdmode ? valAxis.c2p(d.mean + d.sd, true) : valAxis.c2p(d.q3, true); // make sure median isn't identical to either of the // quartiles, so we can see it var m = Lib.constrain( sdmode ? valAxis.c2p(d.mean, true) : valAxis.c2p(d.med, true), Math.min(q1, q3) + 1, Math.max(q1, q3) - 1 ); // for compatibility with box, violin, and candlestick // perhaps we should put this into cd0.t instead so it's more explicit, // but what we have now is: // - box always has d.lf, but boxpoints can be anything // - violin has d.lf and should always use it (boxpoints is undefined) // - candlestick has only min/max var useExtremes = (d.lf === undefined) || (trace.boxpoints === false) || sdmode; var lf = valAxis.c2p(useExtremes ? d.min : d.lf, true); var uf = valAxis.c2p(useExtremes ? d.max : d.uf, true); var ln = valAxis.c2p(d.ln, true); var un = valAxis.c2p(d.un, true); if(isHorizontal) { d3.select(this).attr('d', 'M' + m + ',' + posm0 + 'V' + posm1 + // median line 'M' + q1 + ',' + pos0 + 'V' + pos1 + // left edge (notched ? 'H' + ln + 'L' + m + ',' + posm1 + 'L' + un + ',' + pos1 : '' ) + // top notched edge 'H' + q3 + // end of the top edge 'V' + pos0 + // right edge (notched ? 'H' + un + 'L' + m + ',' + posm0 + 'L' + ln + ',' + pos0 : '') + // bottom notched edge 'Z' + // end of the box (showWhiskers ? 'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers (whiskerWidth === 0 ? '' : // whisker caps 'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1 ) : '' ) ); } else { d3.select(this).attr('d', 'M' + posm0 + ',' + m + 'H' + posm1 + // median line 'M' + pos0 + ',' + q1 + 'H' + pos1 + // top of the box (notched ? 'V' + ln + 'L' + posm1 + ',' + m + 'L' + pos1 + ',' + un : '' ) + // notched right edge 'V' + q3 + // end of the right edge 'H' + pos0 + // bottom of the box (notched ? 'V' + un + 'L' + posm0 + ',' + m + 'L' + pos0 + ',' + ln : '' ) + // notched left edge 'Z' + // end of the box (showWhiskers ? 'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers (whiskerWidth === 0 ? '' : // whisker caps 'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1 ) : '' ) ); } }); } function plotPoints(sel, axes, trace, t) { var xa = axes.x; var ya = axes.y; var bdPos = t.bdPos; var bPos = t.bPos; // to support violin points var mode = trace.boxpoints || trace.points; // repeatable pseudo-random number generator Lib.seedPseudoRandom(); // since box plot points get an extra level of nesting, each // box needs the trace styling info var fn = function(d) { d.forEach(function(v) { v.t = t; v.trace = trace; }); return d; }; var gPoints = sel.selectAll('g.points') .data(mode ? fn : []); gPoints.enter().append('g') .attr('class', 'points'); gPoints.exit().remove(); var paths = gPoints.selectAll('path') .data(function(d) { var i; var pts = d.pts2; // normally use IQR, but if this is 0 or too small, use max-min var typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1); var minSpread = typicalSpread * 1e-9; var spreadLimit = typicalSpread * JITTERSPREAD; var jitterFactors = []; var maxJitterFactor = 0; var newJitter; // dynamic jitter if(trace.jitter) { if(typicalSpread === 0) { // edge case of no spread at all: fall back to max jitter maxJitterFactor = 1; jitterFactors = new Array(pts.length); for(i = 0; i < pts.length; i++) { jitterFactors[i] = 1; } } else { for(i = 0; i < pts.length; i++) { var i0 = Math.max(0, i - JITTERCOUNT); var pmin = pts[i0].v; var i1 = Math.min(pts.length - 1, i + JITTERCOUNT); var pmax = pts[i1].v; if(mode !== 'all') { if(pts[i].v < d.lf) pmax = Math.min(pmax, d.lf); else pmin = Math.max(pmin, d.uf); } var jitterFactor = Math.sqrt(spreadLimit * (i1 - i0) / (pmax - pmin + minSpread)) || 0; jitterFactor = Lib.constrain(Math.abs(jitterFactor), 0, 1); jitterFactors.push(jitterFactor); maxJitterFactor = Math.max(jitterFactor, maxJitterFactor); } } newJitter = trace.jitter * 2 / (maxJitterFactor || 1); } // fills in 'x' and 'y' in calcdata 'pts' item for(i = 0; i < pts.length; i++) { var pt = pts[i]; var v = pt.v; var jitterOffset = trace.jitter ? (newJitter * jitterFactors[i] * (Lib.pseudoRandom() - 0.5)) : 0; var posPx = d.pos + bPos + bdPos * (trace.pointpos + jitterOffset); if(trace.orientation === 'h') { pt.y = posPx; pt.x = v; } else { pt.x = posPx; pt.y = v; } // tag suspected outliers if(mode === 'suspectedoutliers' && v < d.uo && v > d.lo) { pt.so = true; } } return pts; }); paths.enter().append('path') .classed('point', true); paths.exit().remove(); paths.call(Drawing.translatePoints, xa, ya); } function plotBoxMean(sel, axes, trace, t) { var valAxis = axes.val; var posAxis = axes.pos; var posHasRangeBreaks = !!posAxis.rangebreaks; var bPos = t.bPos; var bPosPxOffset = t.bPosPxOffset || 0; // to support violin mean lines var mode = trace.boxmean || (trace.meanline || {}).visible; // to support for one-sided box var bdPos0; var bdPos1; if(Array.isArray(t.bdPos)) { bdPos0 = t.bdPos[0]; bdPos1 = t.bdPos[1]; } else { bdPos0 = t.bdPos; bdPos1 = t.bdPos; } var paths = sel.selectAll('path.mean').data(( (trace.type === 'box' && trace.boxmean) || (trace.type === 'violin' && trace.box.visible && trace.meanline.visible) ) ? Lib.identity : []); paths.enter().append('path') .attr('class', 'mean') .style({ fill: 'none', 'vector-effect': 'non-scaling-stroke' }); paths.exit().remove(); paths.each(function(d) { var lcenter = posAxis.c2l(d.pos + bPos, true); var pos0 = posAxis.l2p(lcenter - bdPos0) + bPosPxOffset; var pos1 = posAxis.l2p(lcenter + bdPos1) + bPosPxOffset; var posc = posHasRangeBreaks ? (pos0 + pos1) / 2 : posAxis.l2p(lcenter) + bPosPxOffset; var m = valAxis.c2p(d.mean, true); var sl = valAxis.c2p(d.mean - d.sd, true); var sh = valAxis.c2p(d.mean + d.sd, true); if(trace.orientation === 'h') { d3.select(this).attr('d', 'M' + m + ',' + pos0 + 'V' + pos1 + (mode === 'sd' ? 'm0,0L' + sl + ',' + posc + 'L' + m + ',' + pos0 + 'L' + sh + ',' + posc + 'Z' : '') ); } else { d3.select(this).attr('d', 'M' + pos0 + ',' + m + 'H' + pos1 + (mode === 'sd' ? 'm0,0L' + posc + ',' + sl + 'L' + pos0 + ',' + m + 'L' + posc + ',' + sh + 'Z' : '') ); } }); } module.exports = { plot: plot, plotBoxAndWhiskers: plotBoxAndWhiskers, plotPoints: plotPoints, plotBoxMean: plotBoxMean };