diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index a70e1c257d3..7bfd9bce3ce 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -171,6 +171,20 @@ module.exports = { 'the vertical (horizontal).' ].join(' ') }, + + width: { + valType: 'number', + min: 0, + role: 'info', + dflt: 0, + editType: 'calc', + description: [ + 'Sets the width of the box in data coordinate', + 'If *0* (default value) the width is automatically selected based on the positions', + 'of other box traces in the same subplot.' + ].join(' ') + }, + marker: { outliercolor: { valType: 'color', @@ -244,7 +258,6 @@ module.exports = { marker: scatterAttrs.unselected.marker, editType: 'style' }, - hoveron: { valType: 'flaglist', flags: ['boxes', 'points'], diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 517c2b3d089..02fa7a15d0e 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -64,6 +64,11 @@ module.exports = function calc(gd, trace) { } } + var cdi; + var ptFilterFn = (trace.boxpoints || trace.points) === 'all' ? + Lib.identity : + function(pt) { return (pt.v < cdi.lf || pt.v > cdi.uf); }; + // build calcdata trace items, one item per distinct position for(i = 0; i < pLen; i++) { if(ptsPerBin[i].length > 0) { @@ -71,10 +76,9 @@ module.exports = function calc(gd, trace) { var boxVals = pts.map(extractVal); var bvLen = boxVals.length; - var cdi = { - pos: posDistinct[i], - pts: pts - }; + cdi = {}; + cdi.pos = posDistinct[i]; + cdi.pts = pts; cdi.min = boxVals[0]; cdi.max = boxVals[bvLen - 1]; @@ -110,13 +114,14 @@ module.exports = function calc(gd, trace) { cdi.lo = 4 * cdi.q1 - 3 * cdi.q3; cdi.uo = 4 * cdi.q3 - 3 * cdi.q1; - // lower and upper notches ~95% Confidence Intervals for median var iqr = cdi.q3 - cdi.q1; var mci = 1.57 * iqr / Math.sqrt(bvLen); cdi.ln = cdi.med - mci; cdi.un = cdi.med + mci; + cdi.pts2 = pts.filter(ptFilterFn); + cd.push(cdi); } } diff --git a/src/traces/box/cross_trace_calc.js b/src/traces/box/cross_trace_calc.js index f7ad3de3561..2d42b0b0c74 100644 --- a/src/traces/box/cross_trace_calc.js +++ b/src/traces/box/cross_trace_calc.js @@ -22,8 +22,6 @@ function crossTraceCalc(gd, plotinfo) { var orientation = orientations[i]; var posAxis = orientation === 'h' ? ya : xa; var boxList = []; - var minPad = 0; - var maxPad = 0; // make list of boxes / candlesticks // For backward compatibility, candlesticks are treated as if they *are* box traces here @@ -40,72 +38,173 @@ function crossTraceCalc(gd, plotinfo) { trace.yaxis === ya._id ) { boxList.push(j); - - if(trace.boxpoints) { - minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1); - maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1); - } } } - setPositionOffset('box', gd, boxList, posAxis, [minPad, maxPad]); + setPositionOffset('box', gd, boxList, posAxis); } } -function setPositionOffset(traceType, gd, boxList, posAxis, pad) { +function setPositionOffset(traceType, gd, boxList, posAxis) { var calcdata = gd.calcdata; var fullLayout = gd._fullLayout; - var pointList = []; + var axId = posAxis._id; + var axLetter = axId.charAt(0); // N.B. reused in violin var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes'; var i, j, calcTrace; + var pointList = []; + var shownPts = 0; // make list of box points for(i = 0; i < boxList.length; i++) { calcTrace = calcdata[boxList[i]]; for(j = 0; j < calcTrace.length; j++) { pointList.push(calcTrace[j].pos); + shownPts += (calcTrace[j].pts2 || []).length; } } if(!pointList.length) return; // box plots - update dPos based on multiple traces - // and then use for posAxis autorange var boxdv = Lib.distinctVals(pointList); - var dPos = boxdv.minDiff / 2; - - // if there's no duplication of x points, - // disable 'group' mode by setting counter to 1 - if(pointList.length === boxdv.vals.length) { - fullLayout[numKey] = 1; - } + var dPos0 = boxdv.minDiff / 2; // check for forced minimum dtick Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true); - var gap = fullLayout[traceType + 'gap']; - var groupgap = fullLayout[traceType + 'groupgap']; - var padfactor = (1 - gap) * (1 - groupgap) * dPos / fullLayout[numKey]; - - // autoscale the x axis - including space for points if they're off the side - // TODO: this will overdo it if the outermost boxes don't have - // their points as far out as the other boxes - var extremes = Axes.findExtremes(posAxis, boxdv.vals, { - vpadminus: dPos + pad[0] * padfactor, - vpadplus: dPos + pad[1] * padfactor - }); + var num = fullLayout[numKey]; + var group = (fullLayout[traceType + 'mode'] === 'group' && num > 1); + var groupFraction = 1 - fullLayout[traceType + 'gap']; + var groupGapFraction = 1 - fullLayout[traceType + 'groupgap']; for(i = 0; i < boxList.length; i++) { calcTrace = calcdata[boxList[i]]; - // set the width of all boxes - calcTrace[0].t.dPos = dPos; - // link extremes to all boxes - calcTrace[0].trace._extremes[posAxis._id] = extremes; - } + var trace = calcTrace[0].trace; + var t = calcTrace[0].t; + var width = trace.width; + var side = trace.side; + + // position coordinate delta + var dPos; + // box half width; + var bdPos; + // box center offset + var bPos; + // half-width within which to accept hover for this box/violin + // always split the distance to the closest box/violin + var wHover; + + if(width) { + dPos = bdPos = wHover = width / 2; + bPos = 0; + } else { + dPos = dPos0; + bdPos = dPos * groupFraction * groupGapFraction / (group ? num : 1); + bPos = group ? 2 * dPos * (-0.5 + (t.num + 0.5) / num) * groupFraction : 0; + wHover = dPos * (group ? groupFraction / num : 1); + } + t.dPos = dPos; + t.bPos = bPos; + t.bdPos = bdPos; + t.wHover = wHover; + + // box/violin-only value-space push value + var pushplus; + var pushminus; + // edge of box/violin + var edge = bPos + bdPos; + var edgeplus; + var edgeminus; + + if(side === 'positive') { + pushplus = dPos * (width ? 1 : 0.5); + edgeplus = edge; + pushminus = edgeplus = bPos; + } else if(side === 'negative') { + pushplus = edgeplus = bPos; + pushminus = dPos * (width ? 1 : 0.5); + edgeminus = edge; + } else { + pushplus = pushminus = dPos; + edgeplus = edgeminus = edge; + } + + // value-space padding + var vpadplus; + var vpadminus; + // pixel-space padding + var ppadplus; + var ppadminus; + // do we add 5% of both sides (for points beyond box/violin) + var padded = false; + // does this trace show points? + var hasPts = (trace.boxpoints || trace.points) && (shownPts > 0); + + if(hasPts) { + var pointpos = trace.pointpos; + var jitter = trace.jitter; + var ms = trace.marker.size / 2; + + var pp = 0; + if((pointpos + jitter) >= 0) { + pp = edge * (pointpos + jitter); + if(pp > pushplus) { + // (++) beyond plus-value, use pp + padded = true; + ppadplus = ms; + vpadplus = pp; + } else if(pp > edgeplus) { + // (+), use push-value (it's bigger), but add px-pad + ppadplus = ms; + vpadplus = pushplus; + } + } + if(pp <= pushplus) { + // (->) fallback to push value + vpadplus = pushplus; + } + + var pm = 0; + if((pointpos - jitter) <= 0) { + pm = -edge * (pointpos - jitter); + if(pm > pushminus) { + // (--) beyond plus-value, use pp + padded = true; + ppadminus = ms; + vpadminus = pm; + } else if(pm > edgeminus) { + // (-), use push-value (it's bigger), but add px-pad + ppadminus = ms; + vpadminus = pushminus; + } + } + if(pm <= pushminus) { + // (<-) fallback to push value + vpadminus = pushminus; + } + } else { + vpadplus = pushplus; + vpadminus = pushminus; + } + + // calcdata[i][j] are in ascending order + var firstPos = calcTrace[0].pos; + var lastPos = calcTrace[calcTrace.length - 1].pos; + + trace._extremes[axId] = Axes.findExtremes(posAxis, [firstPos, lastPos], { + padded: padded, + vpadminus: vpadminus, + vpadplus: vpadplus, + // N.B. SVG px-space positive/negative + ppadminus: {x: ppadminus, y: ppadplus}[axLetter], + ppadplus: {x: ppadplus, y: ppadminus}[axLetter], + }); + } } module.exports = { diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 7e7ec2d642e..b3d6500e5d4 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -28,6 +28,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { coerce('whiskerwidth'); coerce('boxmean'); + coerce('width'); var notched = coerce('notched', traceIn.notchwidth !== undefined); if(notched) coerce('notchwidth'); diff --git a/src/traces/box/layout_attributes.js b/src/traces/box/layout_attributes.js index 2e1ec93dedf..d09052b11be 100644 --- a/src/traces/box/layout_attributes.js +++ b/src/traces/box/layout_attributes.js @@ -22,7 +22,8 @@ module.exports = { 'If *group*, the boxes are plotted next to one another', 'centered around the shared location.', 'If *overlay*, the boxes are plotted over one another,', - 'you might need to set *opacity* to see them multiple boxes.' + 'you might need to set *opacity* to see them multiple boxes.', + 'Has no effect on traces that have *width* set.' ].join(' ') }, boxgap: { @@ -34,7 +35,8 @@ module.exports = { editType: 'calc', description: [ 'Sets the gap (in plot fraction) between boxes of', - 'adjacent location coordinates.' + 'adjacent location coordinates.', + 'Has no effect on traces that have *width* set.' ].join(' ') }, boxgroupgap: { @@ -46,7 +48,8 @@ module.exports = { editType: 'calc', description: [ 'Sets the gap (in plot fraction) between boxes of', - 'the same location coordinate.' + 'the same location coordinate.', + 'Has no effect on traces that have *width* set.' ].join(' ') } }; diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index c04d1d8266b..6d7f03715aa 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -18,12 +18,8 @@ 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 fullLayout = gd._fullLayout; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; - var numBoxes = fullLayout._numBoxes; - var groupFraction = (1 - fullLayout.boxgap); - var group = (fullLayout.boxmode === 'group' && numBoxes > 1); Lib.makeTraceGroups(boxLayer, cdbox, 'trace boxes').each(function(cd) { var plotGroup = d3.select(this); @@ -31,12 +27,9 @@ function plot(gd, plotinfo, cdbox, boxLayer) { var t = cd0.t; var trace = cd0.trace; if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; - // box half width - var bdPos = t.dPos * groupFraction * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1); - // box center offset - var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * groupFraction : 0; + // whisker width - var wdPos = bdPos * trace.whiskerwidth; + t.wdPos = t.bdPos * trace.whiskerwidth; if(trace.visible !== true || t.empty) { plotGroup.remove(); @@ -53,14 +46,6 @@ function plot(gd, plotinfo, cdbox, boxLayer) { valAxis = ya; } - // save the box size and box position for use by hover - t.bPos = bPos; - t.bdPos = bdPos; - t.wdPos = wdPos; - // half-width within which to accept hover for this box - // always split the distance to the closest box - t.wHover = t.dPos * (group ? groupFraction / numBoxes : 1); - plotBoxAndWhiskers(plotGroup, {pos: posAxis, val: valAxis}, trace, t); plotPoints(plotGroup, {x: xa, y: ya}, trace, t); plotBoxMean(plotGroup, {pos: posAxis, val: valAxis}, trace, t); @@ -192,10 +177,7 @@ function plotPoints(sel, axes, trace, t) { var paths = gPoints.selectAll('path') .data(function(d) { var i; - - var pts = mode === 'all' ? - d.pts : - d.pts.filter(function(pt) { return (pt.v < d.lf || pt.v > d.uf); }); + 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); diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js index 1898cb11559..ba71026aadf 100644 --- a/src/traces/violin/attributes.js +++ b/src/traces/violin/attributes.js @@ -135,6 +135,15 @@ module.exports = { 'right (left) for vertical violins and above (below) for horizontal violins.' ].join(' ') }), + + width: extendFlat({}, boxAttrs.width, { + description: [ + 'Sets the width of the violin in data coordinates.', + 'If *0* (default value) the width is automatically selected based on the positions', + 'of other violin traces in the same subplot.', + ].join(' ') + }), + marker: boxAttrs.marker, text: boxAttrs.text, @@ -220,7 +229,7 @@ module.exports = { values: ['both', 'positive', 'negative'], dflt: 'both', role: 'info', - editType: 'plot', + editType: 'calc', description: [ 'Determines on which side of the position value the density function making up', 'one half of a violin is plotted.', diff --git a/src/traces/violin/calc.js b/src/traces/violin/calc.js index bdb2193f0d9..2926a97a5f0 100644 --- a/src/traces/violin/calc.js +++ b/src/traces/violin/calc.js @@ -25,18 +25,10 @@ module.exports = function calc(gd, trace) { trace[trace.orientation === 'h' ? 'xaxis' : 'yaxis'] ); - var violinScaleGroupStats = fullLayout._violinScaleGroupStats; - var scaleGroup = trace.scalegroup; - var groupStats = violinScaleGroupStats[scaleGroup]; - if(!groupStats) { - groupStats = violinScaleGroupStats[scaleGroup] = { - maxWidth: 0, - maxCount: 0 - }; - } - var spanMin = Infinity; var spanMax = -Infinity; + var maxKDE = 0; + var maxCount = 0; for(var i = 0; i < cd.length; i++) { var cdi = cd[i]; @@ -61,12 +53,11 @@ module.exports = function calc(gd, trace) { for(var k = 0, t = span[0]; t < (span[1] + step / 2); k++, t += step) { var v = kde(t); - groupStats.maxWidth = Math.max(groupStats.maxWidth, v); cdi.density[k] = {v: v, t: t}; + maxKDE = Math.max(maxKDE, v); } - groupStats.maxCount = Math.max(groupStats.maxCount, vals.length); - + maxCount = Math.max(maxCount, vals.length); spanMin = Math.min(spanMin, span[0]); spanMax = Math.max(spanMax, span[1]); } @@ -74,6 +65,24 @@ module.exports = function calc(gd, trace) { 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; diff --git a/src/traces/violin/cross_trace_calc.js b/src/traces/violin/cross_trace_calc.js index df04239e697..2463233faf2 100644 --- a/src/traces/violin/cross_trace_calc.js +++ b/src/traces/violin/cross_trace_calc.js @@ -20,8 +20,6 @@ module.exports = function crossTraceCalc(gd, plotinfo) { var orientation = orientations[i]; var posAxis = orientation === 'h' ? ya : xa; var violinList = []; - var minPad = 0; - var maxPad = 0; for(var j = 0; j < calcdata.length; j++) { var cd = calcdata[j]; @@ -35,14 +33,9 @@ module.exports = function crossTraceCalc(gd, plotinfo) { trace.yaxis === ya._id ) { violinList.push(j); - - if(trace.points !== false) { - minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1); - maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1); - } } } - setPositionOffset('violin', gd, violinList, posAxis, [minPad, maxPad]); + setPositionOffset('violin', gd, violinList, posAxis); } }; diff --git a/src/traces/violin/defaults.js b/src/traces/violin/defaults.js index d79d42bac70..0a0fa8d1a73 100644 --- a/src/traces/violin/defaults.js +++ b/src/traces/violin/defaults.js @@ -26,10 +26,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(traceOut.visible === false) return; coerce('bandwidth'); - coerce('scalegroup', traceOut.name); - coerce('scalemode'); coerce('side'); + var width = coerce('width'); + if(!width) { + coerce('scalegroup', traceOut.name); + coerce('scalemode'); + } + var span = coerce('span'); var spanmodeDflt; if(Array.isArray(span)) spanmodeDflt = 'manual'; diff --git a/src/traces/violin/layout_attributes.js b/src/traces/violin/layout_attributes.js index b2800f346e7..9bb04cb19c0 100644 --- a/src/traces/violin/layout_attributes.js +++ b/src/traces/violin/layout_attributes.js @@ -19,19 +19,22 @@ module.exports = { 'If *group*, the violins are plotted next to one another', 'centered around the shared location.', 'If *overlay*, the violins are plotted over one another,', - 'you might need to set *opacity* to see them multiple violins.' + 'you might need to set *opacity* to see them multiple violins.', + 'Has no effect on traces that have *width* set.' ].join(' ') }), violingap: extendFlat({}, boxLayoutAttrs.boxgap, { description: [ 'Sets the gap (in plot fraction) between violins of', - 'adjacent location coordinates.' + 'adjacent location coordinates.', + 'Has no effect on traces that have *width* set.' ].join(' ') }), violingroupgap: extendFlat({}, boxLayoutAttrs.boxgroupgap, { description: [ 'Sets the gap (in plot fraction) between violins of', - 'the same location coordinate.' + 'the same location coordinate.', + 'Has no effect on traces that have *width* set.' ].join(' ') }) }; diff --git a/src/traces/violin/plot.js b/src/traces/violin/plot.js index 4848305dd90..10762814a92 100644 --- a/src/traces/violin/plot.js +++ b/src/traces/violin/plot.js @@ -39,28 +39,19 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) { var t = cd0.t; var trace = cd0.trace; if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; - var numViolins = fullLayout._numViolins; - var group = (fullLayout.violinmode === 'group' && numViolins > 1); - var groupFraction = 1 - fullLayout.violingap; - // violin max half width - var bdPos = t.bdPos = t.dPos * groupFraction * (1 - fullLayout.violingroupgap) / (group ? numViolins : 1); - // violin center offset - var bPos = t.bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numViolins) * groupFraction : 0; - // half-width within which to accept hover for this violin - // always split the distance to the closest violin - t.wHover = t.dPos * (group ? groupFraction / numViolins : 1); if(trace.visible !== true || t.empty) { plotGroup.remove(); return; } + var bPos = t.bPos; + var bdPos = t.bdPos; var valAxis = plotinfo[t.valLetter + 'axis']; var posAxis = plotinfo[t.posLetter + 'axis']; var hasBothSides = trace.side === 'both'; var hasPositiveSide = hasBothSides || trace.side === 'positive'; var hasNegativeSide = hasBothSides || trace.side === 'negative'; - var groupStats = fullLayout._violinScaleGroupStats[trace.scalegroup]; var violins = plotGroup.selectAll('path.violin').data(Lib.identity); @@ -76,15 +67,15 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) { var len = density.length; var posCenter = d.pos + bPos; var posCenterPx = posAxis.c2p(posCenter); - var scale; - switch(trace.scalemode) { - case 'width': - scale = groupStats.maxWidth / bdPos; - break; - case 'count': - scale = (groupStats.maxWidth / bdPos) * (groupStats.maxCount / d.pts.length); - break; + var scale; + if(trace.width) { + scale = t.maxKDE / bdPos; + } else { + var groupStats = fullLayout._violinScaleGroupStats[trace.scalegroup]; + scale = trace.scalemode === 'count' ? + (groupStats.maxKDE / bdPos) * (groupStats.maxCount / d.pts.length) : + groupStats.maxKDE / bdPos; } var pathPos, pathNeg, path; diff --git a/test/image/baselines/10.png b/test/image/baselines/10.png index 475b193bcb5..aaf0100074e 100644 Binary files a/test/image/baselines/10.png and b/test/image/baselines/10.png differ diff --git a/test/image/baselines/31.png b/test/image/baselines/31.png index 1b4de09c01c..e47e81992ed 100644 Binary files a/test/image/baselines/31.png and b/test/image/baselines/31.png differ diff --git a/test/image/baselines/box_grouped.png b/test/image/baselines/box_grouped.png index fc7cf632651..f4b042cbca3 100644 Binary files a/test/image/baselines/box_grouped.png and b/test/image/baselines/box_grouped.png differ diff --git a/test/image/baselines/box_grouped_horz.png b/test/image/baselines/box_grouped_horz.png index 4c55500cd07..8dd083420e4 100644 Binary files a/test/image/baselines/box_grouped_horz.png and b/test/image/baselines/box_grouped_horz.png differ diff --git a/test/image/baselines/box_plot_jitter_edge_cases.png b/test/image/baselines/box_plot_jitter_edge_cases.png index b1fd5f70202..4467b3af805 100644 Binary files a/test/image/baselines/box_plot_jitter_edge_cases.png and b/test/image/baselines/box_plot_jitter_edge_cases.png differ diff --git a/test/image/baselines/box_single-group.png b/test/image/baselines/box_single-group.png new file mode 100644 index 00000000000..1c149acda8f Binary files /dev/null and b/test/image/baselines/box_single-group.png differ diff --git a/test/image/baselines/box_with-empty-1st-trace.png b/test/image/baselines/box_with-empty-1st-trace.png index aaee8d92a53..caf7b7f1883 100644 Binary files a/test/image/baselines/box_with-empty-1st-trace.png and b/test/image/baselines/box_with-empty-1st-trace.png differ diff --git a/test/image/baselines/hist_cum_stacked.png b/test/image/baselines/hist_cum_stacked.png index 29702797e23..53297ad99e4 100644 Binary files a/test/image/baselines/hist_cum_stacked.png and b/test/image/baselines/hist_cum_stacked.png differ diff --git a/test/image/baselines/point-selection2.png b/test/image/baselines/point-selection2.png index 8a898869141..de39ff51820 100644 Binary files a/test/image/baselines/point-selection2.png and b/test/image/baselines/point-selection2.png differ diff --git a/test/image/baselines/violin_box_multiple_widths.png b/test/image/baselines/violin_box_multiple_widths.png new file mode 100644 index 00000000000..64a88204221 Binary files /dev/null and b/test/image/baselines/violin_box_multiple_widths.png differ diff --git a/test/image/baselines/violin_box_overlay.png b/test/image/baselines/violin_box_overlay.png index bf60201ef43..cae0accc9f9 100644 Binary files a/test/image/baselines/violin_box_overlay.png and b/test/image/baselines/violin_box_overlay.png differ diff --git a/test/image/baselines/violin_grouped.png b/test/image/baselines/violin_grouped.png index 30de14adb18..5a7937e41b4 100644 Binary files a/test/image/baselines/violin_grouped.png and b/test/image/baselines/violin_grouped.png differ diff --git a/test/image/baselines/violin_negative_sides_w_points.png b/test/image/baselines/violin_negative_sides_w_points.png new file mode 100644 index 00000000000..87f693f86e7 Binary files /dev/null and b/test/image/baselines/violin_negative_sides_w_points.png differ diff --git a/test/image/baselines/violin_old-faithful.png b/test/image/baselines/violin_old-faithful.png index 26d9b81943a..cc7b2e1040e 100644 Binary files a/test/image/baselines/violin_old-faithful.png and b/test/image/baselines/violin_old-faithful.png differ diff --git a/test/image/baselines/violin_positive_and_negative.png b/test/image/baselines/violin_positive_and_negative.png new file mode 100644 index 00000000000..aee3de868ee Binary files /dev/null and b/test/image/baselines/violin_positive_and_negative.png differ diff --git a/test/image/baselines/violin_positive_sides_w_points.png b/test/image/baselines/violin_positive_sides_w_points.png new file mode 100644 index 00000000000..a5314c831c7 Binary files /dev/null and b/test/image/baselines/violin_positive_sides_w_points.png differ diff --git a/test/image/baselines/violin_side-by-side.png b/test/image/baselines/violin_side-by-side.png index 591ff342f6c..da309be00e9 100644 Binary files a/test/image/baselines/violin_side-by-side.png and b/test/image/baselines/violin_side-by-side.png differ diff --git a/test/image/mocks/box_single-group.json b/test/image/mocks/box_single-group.json new file mode 100644 index 00000000000..199044e9d7d --- /dev/null +++ b/test/image/mocks/box_single-group.json @@ -0,0 +1,16 @@ +{ + "data": [{ + "type": "box", + "x0": 1, + "y": [1, 2, 1, 2, 1, 2, 3, 4, 4] + }, { + "type": "box", + "x0": 2, + "y": [2, 1, 2, 3, 3, 1, 0, 0, 1] + }], + "layout": { + "title": {"text": "single-box groups!"}, + "boxmode": "group", + "showlegend": false + } +} diff --git a/test/image/mocks/violin_box_multiple_widths.json b/test/image/mocks/violin_box_multiple_widths.json new file mode 100644 index 00000000000..60e0934ba2b --- /dev/null +++ b/test/image/mocks/violin_box_multiple_widths.json @@ -0,0 +1,59 @@ +{ + "data": [{ + "type": "violin", + "width": 0.4, + "name": "width: 0.4", + "x": [0, 5, 7, 8], + "side": "positive", + "line": { + "color": "black" + }, + "fillcolor": "#8dd3c7", + "opacity": 0.6, + "y0": 0.0 + }, { + "type": "violin", + "name": "auto", + "x": [0, 5, 7, 8], + "side": "positive", + "line": { + "color": "black" + }, + "fillcolor": "#d3c78d", + "opacity": 0.6, + "y0": 0.1 + }, { + "type": "box", + "width": 0.6, + "name": "width: 0.6", + "x": [0, 5, 7, 8], + "side": "positive", + "line": { + "color": "black" + }, + "fillcolor": "#c78dd3", + "opacity": 0.6, + "y0": 0.2 + }, { + "type": "violin", + "width": 0.4, + "name": "width: 0.4 (solo)", + "x": [0, 5, 7, 8], + "side": "positive", + "line": { + "color": "black" + }, + "fillcolor": "#8dd3c7", + "opacity": 0.6, + "y0": 0.0, + "xaxis": "x2", + "yaxis": "y2" + }], + "layout": { + "grid": {"rows": 1, "columns": 2, "pattern": "independent"}, + "title": {"text" :"Violins/boxes - with multiple widths", "x": 0, "xref": "paper"}, + "legend": {"x": 1, "y": 1, "xanchor": "right", "yanchor": "bottom"}, + "xaxis": {"zeroline": false}, + "yaxis": {"dtick": 0.1, "gridcolor": "black"} + } +} diff --git a/test/image/mocks/violin_negative_sides_w_points.json b/test/image/mocks/violin_negative_sides_w_points.json new file mode 100644 index 00000000000..98b1723aa86 --- /dev/null +++ b/test/image/mocks/violin_negative_sides_w_points.json @@ -0,0 +1,23 @@ +{ + "data": [{ + "type": "violin", + "points": "all", + "pointpos": 1.5, + "marker": {"size": 12}, + "jitter": 0, + "x": [0, 5, 7, 8], + "side": "negative", + "line": { + "color": "black" + }, + "fillcolor": "#d3c78d", + "opacity": 0.6, + "y0": 0.0 + }], + "layout": { + "title": "Violins - negative sided with positive points", + "legend": {"x": 0}, + "xaxis": {"zeroline": false}, + "yaxis": {"dtick": 0.1, "gridcolor": "black"} + } +} diff --git a/test/image/mocks/violin_positive_and_negative.json b/test/image/mocks/violin_positive_and_negative.json new file mode 100644 index 00000000000..722e7303980 --- /dev/null +++ b/test/image/mocks/violin_positive_and_negative.json @@ -0,0 +1,35 @@ +{ + "data": [{ + "type": "violin", + "points": "all", + "pointpos": -0.3, + "jitter": 0, + "x": [0, 5, 7, 8], + "side": "positive", + "line": { + "color": "black" + }, + "fillcolor": "#d3c78d", + "opacity": 0.6, + "y0": 0.0 + },{ + "type": "violin", + "points": "all", + "pointpos": 0, + "jitter": 0, + "x": [20, 25, 27, 28], + "side": "negative", + "line": { + "color": "black" + }, + "fillcolor": "#d3c78d", + "opacity": 0.6, + "y0": 0.0 + }], + "layout": { + "title": "Violins - positive and negative", + "showlegend": false, + "xaxis": {"zeroline": false}, + "yaxis": {"dtick": 0.1, "gridcolor": "black"} + } +} diff --git a/test/image/mocks/violin_positive_sides_w_points.json b/test/image/mocks/violin_positive_sides_w_points.json new file mode 100644 index 00000000000..79a90b0477e --- /dev/null +++ b/test/image/mocks/violin_positive_sides_w_points.json @@ -0,0 +1,35 @@ +{ + "data": [{ + "type": "violin", + "points": "all", + "pointpos": -0.4, + "jitter": 0, + "x": [0, 5, 7, 8], + "side": "positive", + "line": { + "color": "black" + }, + "fillcolor": "#8dd3c7", + "opacity": 0.6, + "y0": 0.0 + }, { + "type": "violin", + "points": "all", + "pointpos": -0.1, + "jitter": 0, + "x": [20, 25, 27, 28], + "side": "positive", + "line": { + "color": "black" + }, + "fillcolor": "#d3c78d", + "opacity": 0.6, + "y0": 0.0 + }], + "layout": { + "title": "Violins - only positive sided", + "legend": {"x": 0}, + "xaxis": {"zeroline": false}, + "yaxis": {"dtick": 0.1, "gridcolor": "black"} + } +} diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 5d15f42e0de..abe59d33d4a 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -240,6 +240,7 @@ describe('Test box hover:', function() { trace.hoveron = 'points'; }); fig.layout.hovermode = 'closest'; + fig.layout.xaxis = {range: [-0.565, 1.5]}; return fig; }, nums: '(day 1, 0.7)', @@ -252,6 +253,7 @@ describe('Test box hover:', function() { trace.hoveron = 'points'; }); fig.layout.hovermode = 'x'; + fig.layout.xaxis = {range: [-0.565, 1.5]}; return fig; }, nums: '0.7', @@ -265,6 +267,7 @@ describe('Test box hover:', function() { trace.hoveron = 'points+boxes'; }); fig.layout.hovermode = 'x'; + fig.layout.xaxis = {range: [-0.565, 1.5]}; return fig; }, pos: [215, 200], @@ -294,6 +297,7 @@ describe('Test box hover:', function() { trace.text = trace.y.map(function(v) { return 'look:' + v; }); }); fig.layout.hovermode = 'closest'; + fig.layout.xaxis = {range: [-0.565, 1.5]}; return fig; }, nums: '(day 1, 0.7)\nlook:0.7', @@ -308,6 +312,7 @@ describe('Test box hover:', function() { trace.hoverinfo = 'text'; }); fig.layout.hovermode = 'closest'; + fig.layout.xaxis = {range: [-0.565, 1.5]}; return fig; }, nums: 'look:0.7', @@ -449,7 +454,7 @@ describe('Test box restyle:', function() { }); }) .then(function() { - _assert('auto rng / all boxpoints', [-0.695, 0.5], [-0.555, 10.555]); + _assert('auto rng / all boxpoints', [-0.5055, 0.5], [-0.555, 10.555]); return Plotly.restyle(gd, 'boxpoints', false); }) .then(function() { @@ -458,4 +463,36 @@ describe('Test box restyle:', function() { .catch(failTest) .then(done); }); + + it('should be able to change axis range when the number of distinct positions changes', function(done) { + function _assert(msg, xrng, yrng) { + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng'); + expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng'); + } + + Plotly.plot(gd, [{ + type: 'box', + width: 0.4, + y: [0, 5, 7, 8], + y0: 0 + }, { + type: 'box', + y: [0, 5, 7, 8], + y0: 0.1 + }]) + .then(function() { + _assert('base', [-0.2, 1.5], [-0.444, 8.444]); + return Plotly.restyle(gd, 'visible', [true, 'legendonly']); + }) + .then(function() { + _assert('only trace0 visible', [-0.2, 0.2], [-0.444, 8.444]); + return Plotly.restyle(gd, 'visible', ['legendonly', true]); + }) + .then(function() { + _assert('only trace1 visible', [-0.5, 0.5], [-0.444, 8.444]); + }) + .catch(failTest) + .then(done); + }); }); diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index b124e9cc2fb..aeca9bf68a0 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -77,10 +77,10 @@ describe('main plot pan', function() { var mock = require('@mocks/10.json'); var precision = 5; - var originalX = [-0.6225, 5.5]; + var originalX = [-0.5251046025104602, 5.5]; var originalY = [-1.6340975059013805, 7.166241526218911]; - var newX = [-2.0255729166666665, 4.096927083333333]; + var newX = [-1.905857740585774, 4.119246861924687]; var newY = [-0.3769062155984817, 8.42343281652181]; function _drag(x0, y0, x1, y1) { diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 2a903e20c1c..3f165f7cfc7 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -2294,6 +2294,7 @@ describe('Test select box and lasso per trace:', function() { fig.layout.dragmode = 'lasso'; fig.layout.width = 600; fig.layout.height = 500; + fig.layout.xaxis = {range: [-0.565, 1.5]}; addInvisible(fig); Plotly.plot(gd, fig) diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js index d4149927eb4..454ceb50c95 100644 --- a/test/jasmine/tests/violin_test.js +++ b/test/jasmine/tests/violin_test.js @@ -142,6 +142,23 @@ describe('Test violin defaults', function() { expect(traceOut.meanline.color).toBe('blue'); expect(traceOut.meanline.width).toBe(10); }); + + it('should not coerce *scalegroup* and *scalemode* when *width* is set', function() { + _supply({ + y: [1, 2, 1], + width: 1 + }); + expect(traceOut.scalemode).toBeUndefined(); + expect(traceOut.scalegroup).toBeUndefined(); + + _supply({ + y: [1, 2, 1], + // width=0 is ignored during calc + width: 0 + }); + expect(traceOut.scalemode).toBe('width'); + expect(traceOut.scalegroup).toBe(''); + }); }); describe('Test violin calc:', function() { @@ -236,7 +253,7 @@ describe('Test violin calc:', function() { name: 'one', y: [0, 0, 0, 0, 10, 10, 10, 10] }); - expect(fullLayout._violinScaleGroupStats.one.maxWidth).toBeCloseTo(0.055); + expect(fullLayout._violinScaleGroupStats.one.maxKDE).toBeCloseTo(0.055); expect(fullLayout._violinScaleGroupStats.one.maxCount).toBe(8); }); @@ -482,7 +499,10 @@ describe('Test violin hover:', function() { patch: function(fig) { fig.data[0].x = fig.data[0].y; delete fig.data[0].y; - fig.layout = {hovermode: 'closest'}; + fig.layout = { + hovermode: 'closest', + yaxis: {range: [-0.696, 0.5]} + }; return fig; }, pos: [539, 293], @@ -567,7 +587,7 @@ describe('Test violin hover:', function() { Plotly.plot(gd, fig).then(function() { mouseEvent('mousemove', 300, 250); - assertViolinHoverLine([299.35, 250, 250, 250]); + assertViolinHoverLine([277.3609, 250, 80, 250]); }) .catch(failTest) .then(done); @@ -578,7 +598,7 @@ describe('Test violin hover:', function() { Plotly.plot(gd, fig).then(function() { mouseEvent('mousemove', 200, 250); - assertViolinHoverLine([200.65, 250, 250, 250]); + assertViolinHoverLine([222.6391, 250, 420, 250]); }) .catch(failTest) .then(done);