diff --git a/draftlogs/6187_add.md b/draftlogs/6187_add.md new file mode 100644 index 00000000000..b5bd84735f6 --- /dev/null +++ b/draftlogs/6187_add.md @@ -0,0 +1,2 @@ + - Add "exclusive" and "inclusive" quartile-computing algorithm to `violin` traces + via `quartilemethod` attribute [[#6187](https://github.com/plotly/plotly.js/pull/6187)] diff --git a/src/lib/stats.js b/src/lib/stats.js index 0ccde97802c..2f2350dfcf9 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -77,7 +77,7 @@ exports.median = function(data) { /** * interp() computes a percentile (quantile) for a given distribution. * We interpolate the distribution (to compute quantiles, we follow method #10 here: - * http://www.amstat.org/publications/jse/v14n3/langford.html). + * http://jse.amstat.org/v14n3/langford.html). * Typically the index or rank (n * arr.length) may be non-integer. * For reference: ends are clipped to the extreme values in the array; * For box plots: index you get is half a point too high (see diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 2c0d615a4d5..0d540a40a74 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -275,7 +275,7 @@ module.exports = { '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).', + 'as computed using method #10 (listed on http://jse.amstat.org/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 -', diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js index 9bccbd2ffec..9cb1d9f33af 100644 --- a/src/traces/violin/attributes.js +++ b/src/traces/violin/attributes.js @@ -154,6 +154,8 @@ module.exports = { hovertext: boxAttrs.hovertext, hovertemplate: boxAttrs.hovertemplate, + quartilemethod: boxAttrs.quartilemethod, + box: { visible: { valType: 'boolean', diff --git a/src/traces/violin/defaults.js b/src/traces/violin/defaults.js index 662460663fd..1224fdd9bc8 100644 --- a/src/traces/violin/defaults.js +++ b/src/traces/violin/defaults.js @@ -48,4 +48,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var meanLineWidth = coerce2('meanline.width', lineWidth); var meanLineVisible = coerce('meanline.visible', Boolean(meanLineColor || meanLineWidth)); if(!meanLineVisible) traceOut.meanline = {visible: false}; + + coerce('quartilemethod'); }; diff --git a/test/image/baselines/box_quartile-methods.png b/test/image/baselines/box_quartile-methods.png index 7651df491fc..80bdd6d814a 100644 Binary files a/test/image/baselines/box_quartile-methods.png and b/test/image/baselines/box_quartile-methods.png differ diff --git a/test/image/mocks/box_quartile-methods.json b/test/image/mocks/box_quartile-methods.json index d2c2b2fefaa..871fb6c7550 100644 --- a/test/image/mocks/box_quartile-methods.json +++ b/test/image/mocks/box_quartile-methods.json @@ -13,5 +13,37 @@ "y": [1, 2, 3, 4, 5], "name": "inclusive", "quartilemethod": "inclusive" - }] + }, + + { + "type": "violin", + "yaxis": "y2", + "y": [1, 2, 3, 4, 5], + "name": "linear" + }, { + "type": "violin", + "yaxis": "y2", + "y": [1, 2, 3, 4, 5], + "name": "exclusive", + "quartilemethod": "exclusive" + }, { + "type": "violin", + "yaxis": "y2", + "y": [1, 2, 3, 4, 5], + "name": "inclusive", + "quartilemethod": "inclusive" + }], + "layout": { + "yaxis": { + "domain": [0, 0.45] + }, + "yaxis2": { + "domain": [0.55, 1] + }, + "width": 500, + "height": 500, + "title": { + "text": "box and violin quartile methods" + } + } } diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js index 4b111891583..9487a94605e 100644 --- a/test/jasmine/tests/violin_test.js +++ b/test/jasmine/tests/violin_test.js @@ -223,8 +223,85 @@ describe('Test violin calc:', function() { Plots.doCalcdata(gd); cd = gd.calcdata[0]; fullLayout = gd._fullLayout; + return cd; } + it('should compute q1/q3 depending on *quartilemethod*', function() { + // samples from https://en.wikipedia.org/wiki/Quartile + var specs = { + // N is odd and is spanned by (4n+3) + odd: { + sample: [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49], + methods: { + linear: {q1: 20.25, q3: 42.75}, + exclusive: {q1: 15, q3: 43}, + inclusive: {q1: 25.5, q3: 42.5} + } + }, + // N is odd and is spanned by (4n+1) + odd2: { + sample: [6, 15, 36, 39, 40, 42, 43, 47, 49], + methods: { + linear: {q1: 30.75, q3: 44}, + exclusive: {q1: 25.5, q3: 45}, + inclusive: {q1: 36, q3: 43} + } + }, + // N is even + even: { + sample: [7, 15, 36, 39, 40, 41], + methods: { + linear: {q1: 15, q3: 40}, + exclusive: {q1: 15, q3: 40}, + inclusive: {q1: 15, q3: 40} + } + }, + // samples from http://jse.amstat.org/v14n3/langford.html + s4: { + sample: [1, 2, 3, 4], + methods: { + linear: {q1: 1.5, q3: 3.5}, + exclusive: {q1: 1.5, q3: 3.5}, + inclusive: {q1: 1.5, q3: 3.5} + } + }, + s5: { + sample: [1, 2, 3, 4, 5], + methods: { + linear: {q1: 1.75, q3: 4.25}, + exclusive: {q1: 1.5, q3: 4.5}, + inclusive: {q1: 2, q3: 4} + } + }, + s6: { + sample: [1, 2, 3, 4, 5, 6], + methods: { + linear: {q1: 2, q3: 5}, + exclusive: {q1: 2, q3: 5}, + inclusive: {q1: 2, q3: 5} + } + }, + s7: { + sample: [1, 2, 3, 4, 5, 6, 7], + methods: { + linear: {q1: 2.25, q3: 5.75}, + exclusive: {q1: 2, q3: 6}, + inclusive: {q1: 2.5, q3: 5.5} + } + } + }; + + for(var name in specs) { + var spec = specs[name]; + + for(var m in spec.methods) { + var cd = _calc({y: spec.sample, quartilemethod: m}); + expect(cd[0].q1).toBe(spec.methods[m].q1, ['q1', m, name].join(' | ')); + expect(cd[0].q3).toBe(spec.methods[m].q3, ['q3', m, name].join(' | ')); + } + } + }); + it('should compute bandwidth and span based on the sample and *spanmode*', function() { var y = [1, 1, 2, 2, 3]; diff --git a/test/plot-schema.json b/test/plot-schema.json index 9cf4bbd1950..9e4f43e9025 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -15905,7 +15905,7 @@ "valType": "string" }, "quartilemethod": { - "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.", + "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://jse.amstat.org/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.", "dflt": "linear", "editType": "calc", "valType": "enumerated", @@ -69495,6 +69495,17 @@ false ] }, + "quartilemethod": { + "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://jse.amstat.org/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.", + "dflt": "linear", + "editType": "calc", + "valType": "enumerated", + "values": [ + "linear", + "exclusive", + "inclusive" + ] + }, "scalegroup": { "description": "If there are multiple violins that should be sized according to to some metric (see `scalemode`), link them by providing a non-empty group id here shared by every trace in the same group. If a violin's `width` is undefined, `scalegroup` will default to the trace's name. In this case, violins with the same names will be linked together", "dflt": "",