Skip to content

Box pre-computed q1/median/q3 input signature + more quartile-computing methods #4432

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 2, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/lib/index.js
Original file line number Diff line number Diff line change
@@ -584,7 +584,9 @@ lib.tagSelected = function(calcTrace, trace, ptNumber2cdIndex) {
for(var i = 0; i < selectedpoints.length; i++) {
var ptIndex = selectedpoints[i];

if(lib.isIndex(ptIndex)) {
if(lib.isIndex(ptIndex) ||
(lib.isArrayOrTypedArray(ptIndex) && lib.isIndex(ptIndex[0]) && lib.isIndex(ptIndex[1]))
) {
var ptNumber = ptIndex2ptNumber ? ptIndex2ptNumber[ptIndex] : ptIndex;
var cdIndex = ptNumber2cdIndex ? ptNumber2cdIndex[ptNumber] : ptNumber;

12 changes: 10 additions & 2 deletions src/plots/cartesian/type_defaults.js
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ function setAutoType(ax, data) {

var id = ax._id;
var axLetter = id.charAt(0);
var i;

// support 3d
if(id.indexOf('scene') !== -1) id = axLetter;
@@ -50,15 +51,22 @@ function setAutoType(ax, data) {
// first check for histograms, as the count direction
// should always default to a linear axis
if(d0.type === 'histogram' &&
axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']) {
axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']
) {
ax.type = 'linear';
return;
}

var calAttr = axLetter + 'calendar';
var calendar = d0[calAttr];
var opts = {noMultiCategory: !traceIs(d0, 'cartesian') || traceIs(d0, 'noMultiCategory')};
var i;

// To not confuse 2D x/y used for per-box sample points for multicategory coordinates
if(d0.type === 'box' && d0._hasPreCompStats &&
axLetter === {h: 'x', v: 'y'}[d0.orientation || 'v']
) {
opts.noMultiCategory = true;
}

// check all boxes on this x axis to see
// if they're dates, numbers, or categories
241 changes: 199 additions & 42 deletions src/traces/box/attributes.js
Original file line number Diff line number Diff line change
@@ -39,7 +39,9 @@ module.exports = {
role: 'info',
editType: 'calc+clearAxisTypes',
description: [
'Sets the x coordinate of the box.',
'Sets the x coordinate for single-box traces',
'or the starting coordinate for multi-box traces',
'set using q1/median/q3.',
'See overview for more info.'
].join(' ')
},
@@ -48,10 +50,32 @@ module.exports = {
role: 'info',
editType: 'calc+clearAxisTypes',
description: [
'Sets the y coordinate of the box.',
'Sets the y coordinate for single-box traces',
'or the starting coordinate for multi-box traces',
'set using q1/median/q3.',
'See overview for more info.'
].join(' ')
},

dx: {
valType: 'number',
role: 'info',
editType: 'calc',
description: [
'Sets the x coordinate step for multi-box traces',
'set using q1/median/q3.'
].join(' ')
},
dy: {
valType: 'number',
role: 'info',
editType: 'calc',
description: [
'Sets the y coordinate step for multi-box traces',
'set using q1/median/q3.'
].join(' ')
},

name: {
valType: 'string',
role: 'info',
@@ -64,48 +88,71 @@ module.exports = {
'missing and the position axis is categorical'
].join(' ')
},
text: extendFlat({}, scatterAttrs.text, {

q1: {
valType: 'data_array',
role: 'info',
editType: 'calc+clearAxisTypes',
description: [
'Sets the text elements associated with each sample value.',
'If a single string, the same string appears over',
'all the data points.',
'If an array of string, the items are mapped in order to the',
'this trace\'s (x,y) coordinates.',
'To be seen, trace `hoverinfo` must contain a *text* flag.'
'Sets the Quartile 1 values.',
'There should be as many items as the number of boxes desired.',
].join(' ')
}),
hovertext: extendFlat({}, scatterAttrs.hovertext, {
description: 'Same as `text`.'
}),
hovertemplate: hovertemplateAttrs({
},
median: {
valType: 'data_array',
role: 'info',
editType: 'calc+clearAxisTypes',
description: [
'N.B. This only has an effect when hovering on points.'
'Sets the median values.',
'There should be as many items as the number of boxes desired.',
].join(' ')
}),
whiskerwidth: {
valType: 'number',
min: 0,
max: 1,
dflt: 0.5,
role: 'style',
},
q3: {
valType: 'data_array',
role: 'info',
editType: 'calc+clearAxisTypes',
description: [
'Sets the Quartile 3 values.',
'There should be as many items as the number of boxes desired.',
].join(' ')
},
lowerfence: {
valType: 'data_array',
role: 'info',
editType: 'calc',
description: [
'Sets the width of the whiskers relative to',
'the box\' width.',
'For example, with 1, the whiskers are as wide as the box(es).'
'Sets the lower fence values.',
'There should be as many items as the number of boxes desired.',
'This attribute has effect only under the q1/median/q3 signature.',
'If `lowerfence` is not provided but a sample (in `y` or `x`) is set,',
'we compute the lower as the last sample point below 1.5 times the IQR.'
].join(' ')
},
upperfence: {
valType: 'data_array',
role: 'info',
editType: 'calc',
description: [
'Sets the upper fence values.',
'There should be as many items as the number of boxes desired.',
'This attribute has effect only under the q1/median/q3 signature.',
'If `upperfence` is not provided but a sample (in `y` or `x`) is set,',
'we compute the lower as the last sample point above 1.5 times the IQR.'
].join(' ')
},

notched: {
valType: 'boolean',
role: 'style',
role: 'info',
editType: 'calc',
description: [
'Determines whether or not notches are drawn.',
'Notches displays a confidence interval around the median.',
'We compute the confidence interval as median +/- 1.57 / IQR * sqrt(N),',
'We compute the confidence interval as median +/- 1.57 * IQR / sqrt(N),',
'where IQR is the interquartile range and N is the sample size.',
'If two boxes\' notches do not overlap there is 95% confidence their medians differ.',
'See https://sites.google.com/site/davidsstatistics/home/notched-box-plots for more info.'
'See https://sites.google.com/site/davidsstatistics/home/notched-box-plots for more info.',
'Defaults to *false* unless `notchwidth` or `notchspan` is set.'
].join(' ')
},
notchwidth: {
@@ -121,10 +168,28 @@ module.exports = {
'For example, with 0, the notches are as wide as the box(es).'
].join(' ')
},
notchspan: {
valType: 'data_array',
role: 'info',
editType: 'calc',
description: [
'Sets the notch span from the boxes\' `median` values.',
'There should be as many items as the number of boxes desired.',
'This attribute has effect only under the q1/median/q3 signature.',
'If `notchspan` is not provided but a sample (in `y` or `x`) is set,',
'we compute it as 1.57 * IQR / sqrt(N),',
'where N is the sample size.'
].join(' ')
},

// TODO
// maybe add
// - loweroutlierbound / upperoutlierbound
// - lowersuspectedoutlierbound / uppersuspectedoutlierbound

boxpoints: {
valType: 'enumerated',
values: ['all', 'outliers', 'suspectedoutliers', false],
dflt: 'outliers',
role: 'style',
editType: 'calc',
description: [
@@ -134,19 +199,11 @@ module.exports = {
'points either less than 4*Q1-3*Q3 or greater than 4*Q3-3*Q1',
'are highlighted (see `outliercolor`)',
'If *all*, all sample points are shown',
'If *false*, only the box(es) are shown with no sample points'
].join(' ')
},
boxmean: {
valType: 'enumerated',
values: [true, 'sd', false],
dflt: false,
role: 'style',
editType: 'calc',
description: [
'If *true*, the mean of the box(es)\' underlying distribution is',
'drawn as a dashed line inside the box(es).',
'If *sd* the standard deviation is also drawn.'
'If *false*, only the box(es) are shown with no sample points',
'Defaults to *suspectedoutliers* when `marker.outliercolor` or',
'`marker.line.outliercolor` is set.',
'Defaults to *all* under the q1/median/q3 signature.',
'Otherwise defaults to *outliers*.',
].join(' ')
},
jitter: {
@@ -175,6 +232,46 @@ module.exports = {
'right (left) for vertical boxes and above (below) for horizontal boxes'
].join(' ')
},

boxmean: {
valType: 'enumerated',
values: [true, 'sd', false],
role: 'style',
editType: 'calc',
description: [
'If *true*, the mean of the box(es)\' underlying distribution is',
'drawn as a dashed line inside the box(es).',
'If *sd* the standard deviation is also drawn.',
'Defaults to *true* when `mean` is set.',
'Defaults to *sd* when `sd` is set',
'Otherwise defaults to *false*.'
].join(' ')
},
mean: {
valType: 'data_array',
role: 'info',
editType: 'calc',
description: [
'Sets the mean values.',
'There should be as many items as the number of boxes desired.',
'This attribute has effect only under the q1/median/q3 signature.',
'If `mean` is not provided but a sample (in `y` or `x`) is set,',
'we compute the mean for each box using the sample values.'
].join(' ')
},
sd: {
valType: 'data_array',
role: 'info',
editType: 'calc',
description: [
'Sets the standard deviation values.',
'There should be as many items as the number of boxes desired.',
'This attribute has effect only under the q1/median/q3 signature.',
'If `sd` is not provided but a sample (in `y` or `x`) is set,',
'we compute the standard deviation for each box using the sample values.'
].join(' ')
},

orientation: {
valType: 'enumerated',
values: ['v', 'h'],
@@ -187,6 +284,30 @@ module.exports = {
].join(' ')
},

quartilemethod: {
valType: 'enumerated',
values: ['linear', 'exclusive', 'inclusive'],
dflt: 'linear',
role: 'info',
editType: 'calc',
description: [
'Sets the method used to compute the sample\'s Q1 and Q3 quartiles.',

'The *linear* method uses the 25th percentile for Q1 and 75th percentile for Q3',
'as computed using method #10 (listed on http://www.amstat.org/publications/jse/v14n3/langford.html).',

'The *exclusive* method uses the median to divide the ordered dataset into two halves',
'if the sample is odd, it does not include the median in either half -',
'Q1 is then the median of the lower half and',
'Q3 the median of the upper half.',

'The *inclusive* method also uses the median to divide the ordered dataset into two halves',
'but if the sample is odd, it includes the median in both halves -',
'Q1 is then the median of the lower half and',
'Q3 the median of the upper half.'
].join(' ')
},

width: {
valType: 'number',
min: 0,
@@ -246,6 +367,7 @@ module.exports = {
},
editType: 'plot'
},

line: {
color: {
valType: 'color',
@@ -263,8 +385,23 @@ module.exports = {
},
editType: 'plot'
},

fillcolor: scatterAttrs.fillcolor,

whiskerwidth: {
valType: 'number',
min: 0,
max: 1,
dflt: 0.5,
role: 'style',
editType: 'calc',
description: [
'Sets the width of the whiskers relative to',
'the box\' width.',
'For example, with 1, the whiskers are as wide as the box(es).'
].join(' ')
},

offsetgroup: barAttrs.offsetgroup,
alignmentgroup: barAttrs.alignmentgroup,

@@ -276,6 +413,26 @@ module.exports = {
marker: scatterAttrs.unselected.marker,
editType: 'style'
},

text: extendFlat({}, scatterAttrs.text, {
description: [
'Sets the text elements associated with each sample value.',
'If a single string, the same string appears over',
'all the data points.',
'If an array of string, the items are mapped in order to the',
'this trace\'s (x,y) coordinates.',
'To be seen, trace `hoverinfo` must contain a *text* flag.'
].join(' ')
}),
hovertext: extendFlat({}, scatterAttrs.hovertext, {
description: 'Same as `text`.'
}),
hovertemplate: hovertemplateAttrs({
description: [
'N.B. This only has an effect when hovering on points.'
].join(' ')
}),

hoveron: {
valType: 'flaglist',
flags: ['boxes', 'points'],
382 changes: 284 additions & 98 deletions src/traces/box/calc.js

Large diffs are not rendered by default.

206 changes: 185 additions & 21 deletions src/traces/box/defaults.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ var Lib = require('../../lib');
var Registry = require('../../registry');
var Color = require('../../components/color');
var handleGroupingDefaults = require('../bar/defaults').handleGroupingDefaults;
var autoType = require('../../plots/cartesian/axis_autotype');
var attributes = require('./attributes');

function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
@@ -22,49 +23,208 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
handleSampleDefaults(traceIn, traceOut, coerce, layout);
if(traceOut.visible === false) return;

var hasPreCompStats = traceOut._hasPreCompStats;

if(hasPreCompStats) {
coerce('lowerfence');
coerce('upperfence');
}

coerce('line.color', (traceIn.marker || {}).color || defaultColor);
coerce('line.width');
coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));

var boxmeanDflt = false;
if(hasPreCompStats) {
var mean = coerce('mean');
var sd = coerce('sd');
if(mean && mean.length) {
boxmeanDflt = true;
if(sd && sd.length) boxmeanDflt = 'sd';
}
}
coerce('boxmean', boxmeanDflt);

coerce('whiskerwidth');
coerce('boxmean');
coerce('width');
coerce('quartilemethod');

var notched = coerce('notched', traceIn.notchwidth !== undefined);
var notchedDflt = false;
if(hasPreCompStats) {
var notchspan = coerce('notchspan');
if(notchspan && notchspan.length) {
notchedDflt = true;
}
} else if(Lib.validate(traceIn.notchwidth, attributes.notchwidth)) {
notchedDflt = true;
}
var notched = coerce('notched', notchedDflt);
if(notched) coerce('notchwidth');

handlePointsDefaults(traceIn, traceOut, coerce, {prefix: 'box'});
}

function handleSampleDefaults(traceIn, traceOut, coerce, layout) {
function getDims(arr) {
var dims = 0;
if(arr && arr.length) {
dims += 1;
if(Lib.isArrayOrTypedArray(arr[0]) && arr[0].length) {
dims += 1;
}
}
return dims;
}

function valid(astr) {
return Lib.validate(traceIn[astr], attributes[astr]);
}

var y = coerce('y');
var x = coerce('x');
var hasX = x && x.length;

var sLen;
if(traceOut.type === 'box') {
var q1 = coerce('q1');
var median = coerce('median');
var q3 = coerce('q3');

traceOut._hasPreCompStats = (
q1 && q1.length &&
median && median.length &&
q3 && q3.length
);
sLen = Math.min(
Lib.minRowLength(q1),
Lib.minRowLength(median),
Lib.minRowLength(q3)
);
}

var yDims = getDims(y);
var xDims = getDims(x);
var yLen = yDims && Lib.minRowLength(y);
var xLen = xDims && Lib.minRowLength(x);

var defaultOrientation, len;
if(traceOut._hasPreCompStats) {
switch(String(xDims) + String(yDims)) {
// no x / no y
case '00':
var setInX = valid('x0') || valid('dx');
var setInY = valid('y0') || valid('dy');

if(y && y.length) {
if(setInY && !setInX) {
defaultOrientation = 'h';
} else {
defaultOrientation = 'v';
}

len = sLen;
break;
// just x
case '10':
defaultOrientation = 'v';
len = Math.min(sLen, xLen);
break;
case '20':
defaultOrientation = 'h';
len = Math.min(sLen, x.length);
break;
// just y
case '01':
defaultOrientation = 'h';
len = Math.min(sLen, yLen);
break;
case '02':
defaultOrientation = 'v';
len = Math.min(sLen, y.length);
break;
// both
case '12':
defaultOrientation = 'v';
len = Math.min(sLen, xLen, y.length);
break;
case '21':
defaultOrientation = 'h';
len = Math.min(sLen, x.length, yLen);
break;
case '11':
// this one is ill-defined
len = 0;
break;
case '22':
var hasCategories = false;
var i;
for(i = 0; i < x.length; i++) {
if(autoType(x[i]) === 'category') {
hasCategories = true;
break;
}
}

if(hasCategories) {
defaultOrientation = 'v';
len = Math.min(sLen, xLen, y.length);
} else {
for(i = 0; i < y.length; i++) {
if(autoType(y[i]) === 'category') {
hasCategories = true;
break;
}
}

if(hasCategories) {
defaultOrientation = 'h';
len = Math.min(sLen, x.length, yLen);
} else {
defaultOrientation = 'v';
len = Math.min(sLen, xLen, y.length);
}
}
break;
}
} else if(yDims > 0) {
defaultOrientation = 'v';
if(hasX) {
len = Math.min(Lib.minRowLength(x), Lib.minRowLength(y));
if(xDims > 0) {
len = Math.min(xLen, yLen);
} else {
coerce('x0');
len = Lib.minRowLength(y);
len = Math.min(yLen);
}
} else if(hasX) {
} else if(xDims > 0) {
defaultOrientation = 'h';
coerce('y0');
len = Lib.minRowLength(x);
len = Math.min(xLen);
} else {
len = 0;
}

if(!len) {
traceOut.visible = false;
return;
}
traceOut._length = len;

var orientation = coerce('orientation', defaultOrientation);

// these are just used for positioning, they never define the sample
if(traceOut._hasPreCompStats) {
if(orientation === 'v' && xDims === 0) {
coerce('x0', 0);
coerce('dx', 1);
} else if(orientation === 'h' && yDims === 0) {
coerce('y0', 0);
coerce('dy', 1);
}
} else {
if(orientation === 'v' && xDims === 0) {
coerce('x0');
} else if(orientation === 'h' && yDims === 0) {
coerce('y0');
}
}

var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout);

coerce('orientation', defaultOrientation);
}

function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
@@ -73,14 +233,18 @@ function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
var outlierColorDflt = Lib.coerce2(traceIn, traceOut, attributes, 'marker.outliercolor');
var lineoutliercolor = coerce('marker.line.outliercolor');

var points = coerce(
prefix + 'points',
(outlierColorDflt || lineoutliercolor) ? 'suspectedoutliers' : undefined
);
var modeDflt = 'outliers';
if(traceOut._hasPreCompStats) {
modeDflt = 'all';
} else if(outlierColorDflt || lineoutliercolor) {
modeDflt = 'suspectedoutliers';
}

var mode = coerce(prefix + 'points', modeDflt);

if(points) {
coerce('jitter', points === 'all' ? 0.3 : 0);
coerce('pointpos', points === 'all' ? -1.5 : 0);
if(mode) {
coerce('jitter', mode === 'all' ? 0.3 : 0);
coerce('pointpos', mode === 'all' ? -1.5 : 0);

coerce('marker.symbol');
coerce('marker.opacity');
@@ -89,7 +253,7 @@ function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
coerce('marker.line.color');
coerce('marker.line.width');

if(points === 'suspectedoutliers') {
if(mode === 'suspectedoutliers') {
coerce('marker.line.outliercolor', traceOut.marker.color);
coerce('marker.line.outlierwidth');
}
35 changes: 24 additions & 11 deletions src/traces/box/index.js
Original file line number Diff line number Diff line change
@@ -29,18 +29,31 @@ module.exports = {
categories: ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'boxLayout', 'zoomScale'],
meta: {
description: [
'In vertical (horizontal) box plots,',
'statistics are computed using `y` (`x`) values.',
'By supplying an `x` (`y`) array, one box per distinct x (y) value',
'is drawn',
'If no `x` (`y`) {array} is provided, a single box is drawn.',
'That box position is then positioned with',
'with `name` or with `x0` (`y0`) if provided.',
'Each box spans from quartile 1 (Q1) to quartile 3 (Q3).',
'The second quartile (Q2) is marked by a line inside the box.',
'By default, the whiskers correspond to the box\' edges',
'+/- 1.5 times the interquartile range (IQR: Q3-Q1),',
'see *boxpoints* for other options.'
'The second quartile (Q2, i.e. the median) is marked by a line inside the box.',
'The fences grow outward from the boxes\' edges,',
'by default they span +/- 1.5 times the interquartile range (IQR: Q3-Q1),',
'The sample mean and standard deviation as well as notches and',
'the sample, outlier and suspected outliers points can be optionally',
'added to the box plot.',

'The values and positions corresponding to each boxes can be input',
'using two signatures.',

'The first signature expects users to supply the sample values in the `y`',
'data array for vertical boxes (`x` for horizontal boxes).',
'By supplying an `x` (`y`) array, one box per distinct `x` (`y`) value is drawn',
'If no `x` (`y`) {array} is provided, a single box is drawn.',
'In this case, the box is positioned with the trace `name` or with `x0` (`y0`) if provided.',

'The second signature expects users to supply the boxes corresponding Q1, median and Q3',
'statistics in the `q1`, `median` and `q3` data arrays respectively.',
'Other box features relying on statistics namely `lowerfence`, `upperfence`, `notchspan`',
'can be set directly by the users.',
'To have plotly compute them or to show sample points besides the boxes,',
'users can set the `y` data array for vertical boxes (`x` for horizontal boxes)',
'to a 2D array with the outer length corresponding',
'to the number of boxes in the traces and the inner length corresponding the sample size.'
].join(' ')
}
};
5 changes: 4 additions & 1 deletion src/traces/violin/attributes.js
Original file line number Diff line number Diff line change
@@ -128,7 +128,10 @@ module.exports = {
'points either less than 4*Q1-3*Q3 or greater than 4*Q3-3*Q1',
'are highlighted (see `outliercolor`)',
'If *all*, all sample points are shown',
'If *false*, only the violins are shown with no sample points'
'If *false*, only the violins are shown with no sample points.',
'Defaults to *suspectedoutliers* when `marker.outliercolor` or',
'`marker.line.outliercolor` is set,',
'otherwise defaults to *outliers*.'
].join(' ')
}),
jitter: extendFlat({}, boxAttrs.jitter, {
Binary file added test/image/baselines/box_precomputed-stats.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/box_quartile-methods.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
211 changes: 211 additions & 0 deletions test/image/mocks/box_precomputed-stats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
{
"data": [
{
"type": "box",
"name": "[V] just q1/median/q3",
"offsetgroup": "1",
"q1": [ 1, "2", 1 ],
"median": [ 2, 3, "2" ],
"q3": [ "3", 4, 3 ]
},
{
"type": "box",
"name": "[V] q1/median/q3/lowerfence/upperfence",
"offsetgroup": "2",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"lowerfence": [ 0, "1", 0 ],
"upperfence": [ 4, 5, "4" ]
},
{
"type": "box",
"name": "[V] all pre-computed stats",
"offsetgroup": "3",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"lowerfence": [ 0, 1, 0 ],
"upperfence": [ 4, 5, 4 ],
"mean": [ 2.2, 2.8, "2.2" ],
"sd": [ "0.4", 0.4, 0.4 ],
"notchspan": [ 0.2, "0.1", 0.2 ]
},
{
"type": "box",
"name": "[V] set q1/median/q3 computed lowerfence/upperfence",
"offsetgroup": "1",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"y": [
[ 0, 1, 2, 3, 4 ],
[ 1, 2, 3, 4, 5 ],
[ 0, 1, 2, 3, 4 ]
],
"xaxis": "x2",
"yaxis": "y2"
},
{
"type": "box",
"name": "[V] set q1/median/q3/lowerfence/upperfence computed mean",
"offsetgroup": "2",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"lowerfence": [ -1, 0, -1 ],
"upperfence": [ 5, 6, 5 ],
"boxmean": true,
"y": [
[ 0, 1, 2, 3, 4 ],
[ 1, 2, 3, 4, 5, 8 ],
[ 0, 1, 2, 3, 4 ]
],
"xaxis": "x2",
"yaxis": "y2"
},
{
"type": "box",
"name": "[V] set q1/median/q3 computed lowerfence/upperfence/mean/sd/notches",
"offsetgroup": "3",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"y": [
[ 0, 1, 2, 3, 4 ],
[ 1, 2, 3, 4, 5 ],
[ 0, 1, 2, 3, 4 ]
],
"boxmean": "sd",
"notched": true,
"xaxis": "x2",
"yaxis": "y2"
},
{
"type": "box",
"name": "[H] just q1/median/q3",
"offsetgroup": "1",
"y0": 1,
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"xaxis": "x3",
"yaxis": "y3"
},
{
"type": "box",
"name": "[H] q1/median/q3/lowerfence/upperfence",
"offsetgroup": "2",
"y0": 1,
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"lowerfence": [ 0, 1, 0 ],
"upperfence": [ 4, 5, 4 ],
"xaxis": "x3",
"yaxis": "y3"
},
{
"type": "box",
"name": "[H] all pre-computed stats",
"offsetgroup": "3",
"y0": 1,
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"lowerfence": [ 0, 1, 0 ],
"upperfence": [ 4, 5, 4 ],
"mean": [ 2.2, 2.8, 2.2 ],
"sd": [ 0.4, 0.4, 0.4 ],
"notchspan": [ 0.2, 0.1, 0.2 ],
"xaxis": "x3",
"yaxis": "y3"
},
{
"type": "box",
"name": "[H] set q1/median/q3 computed lowerfence/upperfence",
"offsetgroup": "1",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"x": [
[ 0, 1, 2, 3, 4 ],
[ 1, 2, 3, 4, 5 ],
[ 0, 1, 2, 3, 4 ]
],
"xaxis": "x4",
"yaxis": "y4"
},
{
"type": "box",
"name": "[H] set q1/median/q3/lowerfence/upperfence computed mean",
"offsetgroup": "2",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"lowerfence": [ -1, 0, -1 ],
"upperfence": [ 5, 6, 5 ],
"boxmean": true,
"x": [
[ 0, 1, 2, 3, 4 ],
[ 1, 2, 3, 4, 5, 8 ],
[ 0, 1, 2, 3, 4 ]
],
"xaxis": "x4",
"yaxis": "y4"
},
{
"type": "box",
"name": "[H] set q1/median/q3 computed lowerfence/upperfence/mean/sd/notches",
"offsetgroup": "3",
"q1": [ 1, 2, 1 ],
"median": [ 2, 3, 2 ],
"q3": [ 3, 4, 3 ],
"x": [
[ 0, 1, 2, 3, 4 ],
[ 1, 2, 3, 4, 5 ],
[ 0, 1, 2, 3, 4 ]
],
"boxmean": "sd",
"notched": true,
"xaxis": "x4",
"yaxis": "y4"
}
],
"layout": {
"showlegend": false,
"boxmode": "group",
"yaxis": {
"domain": [ 0.78, 1 ]
},
"xaxis2": {
"anchor": "y2"
},
"yaxis2": {
"domain": [ 0.51, 0.72 ],
"anchor": "x2"
},
"xaxis3": {
"domain": [ 0, 0.5 ],
"anchor": "y3"
},
"yaxis3": {
"domain": [ 0, 0.48 ],
"anchor": "x3"
},
"xaxis4": {
"domain": [ 0.5, 1 ],
"anchor": "y4"
},
"yaxis4": {
"domain": [ 0, 0.48 ],
"anchor": "x4"
},
"margin": { "l": 20, "t": 40, "b": 20, "r": 20 },
"title": {
"text": "box traces with pre-computed stats",
"x": 0
},
"hovermode": "closest"
}
}
17 changes: 17 additions & 0 deletions test/image/mocks/box_quartile-methods.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"data": [{
"type": "box",
"y": [1, 2, 3, 4, 5],
"name": "linear"
}, {
"type": "box",
"y": [1, 2, 3, 4, 5],
"name": "exclusive",
"quartilemethod": "exclusive"
}, {
"type": "box",
"y": [1, 2, 3, 4, 5],
"name": "inclusive",
"quartilemethod": "inclusive"
}]
}
924 changes: 924 additions & 0 deletions test/jasmine/tests/box_test.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions test/jasmine/tests/select_test.js
Original file line number Diff line number Diff line change
@@ -2692,6 +2692,55 @@ describe('Test select box and lasso per trace:', function() {
.then(done);
});

it('@flaky should work for box traces (q1/median/q3 case)', function(done) {
var assertPoints = makeAssertPoints(['curveNumber', 'y', 'x']);
var assertSelectedPoints = makeAssertSelectedPoints();

var fig = {
data: [{
type: 'box',
x0: 'A',
q1: [1],
median: [2],
q3: [3],
y: [[0, 1, 2, 3, 4]],
pointpos: 0,
}],
layout: {
width: 500,
height: 500,
dragmode: 'lasso'
}
};

Plotly.plot(gd, fig)
.then(function() {
return _run(
[[200, 200], [400, 200], [400, 350], [200, 350], [200, 200]],
function() {
assertPoints([ [0, 1, undefined], [0, 2, undefined] ]);
assertSelectedPoints({ 0: [[0, 1], [0, 2]] });
},
null, LASSOEVENTS, 'box lasso'
);
})
.then(function() {
return Plotly.relayout(gd, 'dragmode', 'select');
})
.then(function() {
return _run(
[[200, 200], [400, 300]],
function() {
assertPoints([ [0, 2, undefined] ]);
assertSelectedPoints({ 0: [[0, 2]] });
},
null, BOXEVENTS, 'box select'
);
})
.catch(failTest)
.then(done);
});

it('@flaky should work for violin traces', function(done) {
var assertPoints = makeAssertPoints(['curveNumber', 'y', 'x']);
var assertSelectedPoints = makeAssertSelectedPoints();