From cfc87250251467ebb2eba0b85d541d3fb48a604c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 18:11:33 -0400 Subject: [PATCH 01/18] lint (mostly var\n , -> var\n var blocks) --- src/traces/box/attributes.js | 11 ++++++----- src/traces/box/defaults.js | 21 ++++++++++++--------- src/traces/box/plot.js | 35 ++++++++++++++++++----------------- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index d029069cb2a..696ce9e10f2 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -12,9 +12,8 @@ var scatterAttrs = require('../scatter/attributes'); var colorAttrs = require('../../components/color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; -var scatterMarkerAttrs = scatterAttrs.marker, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; - +var scatterMarkerAttrs = scatterAttrs.marker; +var scatterMarkerLineAttrs = scatterMarkerAttrs.line; module.exports = { y: { @@ -159,9 +158,11 @@ module.exports = { {arrayOk: false, editType: 'style'}), line: { color: extendFlat({}, scatterMarkerLineAttrs.color, - {arrayOk: false, dflt: colorAttrs.defaultLine, editType: 'style'}), + {arrayOk: false, dflt: colorAttrs.defaultLine, editType: 'style'} + ), width: extendFlat({}, scatterMarkerLineAttrs.width, - {arrayOk: false, dflt: 0, editType: 'style'}), + {arrayOk: false, dflt: 0, editType: 'style'} + ), outliercolor: { valType: 'color', role: 'style', diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index e913a66d912..1b4c67b181a 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -19,9 +19,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var y = coerce('y'), - x = coerce('x'), - defaultOrientation; + var y = coerce('y'); + var x = coerce('x'); + + var defaultOrientation; if(y && y.length) { defaultOrientation = 'v'; @@ -40,17 +41,19 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('orientation', defaultOrientation); coerce('line.color', (traceIn.marker || {}).color || defaultColor); - coerce('line.width', 2); + coerce('line.width'); coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5)); coerce('whiskerwidth'); coerce('boxmean'); - var outlierColorDflt = Lib.coerce2(traceIn, traceOut, attributes, 'marker.outliercolor'), - lineoutliercolor = coerce('marker.line.outliercolor'), - boxpoints = outlierColorDflt || - lineoutliercolor ? coerce('boxpoints', 'suspectedoutliers') : - coerce('boxpoints'); + var outlierColorDflt = Lib.coerce2(traceIn, traceOut, attributes, 'marker.outliercolor'); + var lineoutliercolor = coerce('marker.line.outliercolor'); + + var boxpoints = coerce( + 'boxpoints', + (outlierColorDflt || lineoutliercolor) ? 'suspectedoutliers' : undefined + ); if(boxpoints) { coerce('jitter', boxpoints === 'all' ? 0.3 : 0); diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index cc959616974..841fc4f7aed 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -13,7 +13,6 @@ var d3 = require('d3'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); - // repeatable pseudorandom generator var randSeed = 2000000000; @@ -31,15 +30,13 @@ function rand() { } // constants for dynamic jitter (ie less jitter for sparser points) -var JITTERCOUNT = 5, // points either side of this to include - JITTERSPREAD = 0.01; // fraction of IQR to count as "dense" - +var JITTERCOUNT = 5; // points either side of this to include +var JITTERSPREAD = 0.01; // fraction of IQR to count as "dense" module.exports = function plot(gd, plotinfo, cdbox) { - var fullLayout = gd._fullLayout, - xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - posAxis, valAxis; + var fullLayout = gd._fullLayout; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; var boxtraces = plotinfo.plot.select('.boxlayer') .selectAll('g.trace.boxes') @@ -48,21 +45,25 @@ module.exports = function plot(gd, plotinfo, cdbox) { .attr('class', 'trace boxes'); boxtraces.each(function(d) { - var t = d[0].t, - trace = d[0].trace, - group = (fullLayout.boxmode === 'group' && gd.numboxes > 1), - // box half width - bdPos = t.dPos * (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) / (group ? gd.numboxes : 1), - // box center offset - bPos = group ? 2 * t.dPos * (-0.5 + (t.boxnum + 0.5) / gd.numboxes) * (1 - fullLayout.boxgap) : 0, - // whisker width - wdPos = bdPos * trace.whiskerwidth; + var cd0 = d[0]; + var t = cd0.t; + var trace = cd0.trace; + + var group = (fullLayout.boxmode === 'group' && gd.numboxes > 1); + // box half width + var bdPos = t.dPos * (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) / (group ? gd.numboxes : 1); + // box center offset + var bPos = group ? 2 * t.dPos * (-0.5 + (t.boxnum + 0.5) / gd.numboxes) * (1 - fullLayout.boxgap) : 0; + // whisker width + var wdPos = bdPos * trace.whiskerwidth; + if(trace.visible !== true || t.emptybox) { d3.select(this).remove(); return; } // set axis via orientation + var posAxis, valAxis; if(trace.orientation === 'h') { posAxis = ya; valAxis = xa; From 53c446bc3be2eeca1efe26aa6866050ae7432686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 18:20:38 -0400 Subject: [PATCH 02/18] revamp Box.calc and Box.plot handling of boxpoints - generate 'pts' array of objects similar to scatter pts inside box calcdata items, instead of simply keep track of all values corresponding to each box. - fill in pt objects fill w/ jitter during Box.plot instead of mapping box 'val' array to d3-esque array of objects. - in preparation for box select and 'points' hover, keep track of original val indices in pt object --- src/traces/box/calc.js | 238 +++++++++++++++++++++++------------------ src/traces/box/plot.js | 41 +++---- 2 files changed, 158 insertions(+), 121 deletions(-) diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index d6a7ca28c14..fbc9fe88c47 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -13,17 +13,17 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); - // outlier definition based on http://www.physics.csbsju.edu/stats/box2.html module.exports = function calc(gd, trace) { - var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - ya = Axes.getFromId(gd, trace.yaxis || 'y'), - orientation = trace.orientation, - cd = [], - valAxis, valLetter, val, valBinned, - posAxis, posLetter, pos, posDistinct, dPos; - - // Set value (val) and position (pos) keys via orientation + var xa = Axes.getFromId(gd, trace.xaxis || 'x'); + var ya = Axes.getFromId(gd, trace.yaxis || 'y'); + var orientation = trace.orientation; + var cd = []; + + var i; + var valAxis, valLetter; + var posAxis, posLetter; + if(orientation === 'h') { valAxis = xa; valLetter = 'x'; @@ -36,112 +36,146 @@ module.exports = function calc(gd, trace) { posLetter = 'x'; } - val = valAxis.makeCalcdata(trace, valLetter); // get val - - // size autorange based on all source points - // position happens afterward when we know all the pos - Axes.expand(valAxis, val, {padded: true}); - - // In vertical (horizontal) box plots: - // if no x (y) data, use x0 (y0), or name - // so if you want one box - // per trace, set x0 (y0) to the x (y) value or category for this trace - // (or set x (y) to a constant array matching y (x)) - function getPos(gd, trace, posLetter, posAxis, val) { - var pos0; - if(posLetter in trace) pos = posAxis.makeCalcdata(trace, posLetter); - else { - if(posLetter + '0' in trace) pos0 = trace[posLetter + '0']; - else if('name' in trace && ( - posAxis.type === 'category' || - (isNumeric(trace.name) && - ['linear', 'log'].indexOf(posAxis.type) !== -1) || - (Lib.isDateTime(trace.name) && - posAxis.type === 'date') - )) { - pos0 = trace.name; - } - else pos0 = gd.numboxes; - pos0 = posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']); - pos = val.map(function() { return pos0; }); - } - return pos; - } - - pos = getPos(gd, trace, posLetter, posAxis, val); + var val = valAxis.makeCalcdata(trace, valLetter); + var pos = getPos(trace, posLetter, posAxis, val, gd.numboxes); - // get distinct positions and min difference var dv = Lib.distinctVals(pos); - posDistinct = dv.vals; - dPos = dv.minDiff / 2; - - function binVal(cd, val, pos, posDistinct, dPos) { - var posDistinctLength = posDistinct.length, - valLength = val.length, - valBinned = [], - bins = [], - i, p, n, v; - - // store distinct pos in cd, find bins, init. valBinned - for(i = 0; i < posDistinctLength; ++i) { - p = posDistinct[i]; - cd[i] = {pos: p}; - bins[i] = p - dPos; - valBinned[i] = []; - } - bins.push(posDistinct[posDistinctLength - 1] + dPos); - - // bin the values - for(i = 0; i < valLength; ++i) { - v = val[i]; - if(!isNumeric(v)) continue; - n = Lib.findBin(pos[i], bins); - if(n >= 0 && n < valLength) valBinned[n].push(v); + var posDistinct = dv.vals; + var dPos = dv.minDiff / 2; + var posBins = makeBins(posDistinct, dPos); + + var vLen = val.length; + var pLen = posDistinct.length; + var ptsPerBin = initNestedArray(pLen); + + // bin pts info per position bins + for(i = 0; i < vLen; i++) { + var v = val[i]; + if(!isNumeric(v)) continue; + + var n = Lib.findBin(pos[i], posBins); + if(n >= 0 && n < pLen) { + var pt = {v: v, i: i}; + ptsPerBin[n].push(pt); } - - return valBinned; } - valBinned = binVal(cd, val, pos, posDistinct, dPos); - - // sort the bins and calculate the stats - function calculateStats(cd, valBinned) { - var v, l, cdi, i; - - for(i = 0; i < valBinned.length; ++i) { - v = valBinned[i].sort(Lib.sorterAsc); - l = v.length; - cdi = cd[i]; - - cdi.val = v; // put all values into calcdata - cdi.min = v[0]; - cdi.max = v[l - 1]; - cdi.mean = Lib.mean(v, l); - cdi.sd = Lib.stdev(v, l, cdi.mean); - cdi.q1 = Lib.interp(v, 0.25); // first quartile - cdi.med = Lib.interp(v, 0.5); // median - cdi.q3 = Lib.interp(v, 0.75); // third quartile + // build calcdata trace items, one item per distinct position + for(i = 0; i < pLen; i++) { + if(ptsPerBin[i].length > 0) { + var pts = ptsPerBin[i].sort(sortByVal); + var boxVals = pts.map(extractVal); + var bvLen = boxVals.length; + + var cdi = { + pos: posDistinct[i], + pts: pts + }; + + cdi.min = boxVals[0]; + cdi.max = boxVals[bvLen - 1]; + cdi.mean = Lib.mean(boxVals, bvLen); + cdi.sd = Lib.stdev(boxVals, bvLen, cdi.mean); + + // first quartile + cdi.q1 = Lib.interp(boxVals, 0.25); + // median + cdi.med = Lib.interp(boxVals, 0.5); + // third quartile + cdi.q3 = Lib.interp(boxVals, 0.75); + // lower and upper fences - last point inside // 1.5 interquartile ranges from quartiles - cdi.lf = Math.min(cdi.q1, v[ - Math.min(Lib.findBin(2.5 * cdi.q1 - 1.5 * cdi.q3, v, true) + 1, l - 1)]); - cdi.uf = Math.max(cdi.q3, v[ - Math.max(Lib.findBin(2.5 * cdi.q3 - 1.5 * cdi.q1, v), 0)]); + cdi.lf = Math.min( + cdi.q1, + boxVals[Math.min( + Lib.findBin(2.5 * cdi.q1 - 1.5 * cdi.q3, boxVals, true) + 1, + bvLen - 1 + )] + ); + cdi.uf = Math.max( + cdi.q3, + boxVals[Math.max( + Lib.findBin(2.5 * cdi.q3 - 1.5 * cdi.q1, boxVals), + 0 + )] + ); + // lower and upper outliers - 3 IQR out (don't clip to max/min, // this is only for discriminating suspected & far outliers) cdi.lo = 4 * cdi.q1 - 3 * cdi.q3; cdi.uo = 4 * cdi.q3 - 3 * cdi.q1; + + cd.push(cdi); } } - calculateStats(cd, valBinned); - - // remove empty bins - cd = cd.filter(function(cdi) { return cdi.val && cdi.val.length; }); - if(!cd.length) return [{t: {emptybox: true}}]; + Axes.expand(valAxis, val, {padded: true}); - // add numboxes and dPos to cd - cd[0].t = {boxnum: gd.numboxes, dPos: dPos}; - gd.numboxes++; - return cd; + if(cd.length > 0) { + cd[0].t = { + boxnum: gd.numboxes, + dPos: dPos + }; + gd.numboxes++; + return cd; + } else { + return [{t: {emptybox: true}}]; + } }; + +// In vertical (horizontal) box plots: +// if no x (y) data, use x0 (y0), or name +// so if you want one box +// per trace, set x0 (y0) to the x (y) value or category for this trace +// (or set x (y) to a constant array matching y (x)) +function getPos(trace, posLetter, posAxis, val, numboxes) { + if(posLetter in trace) { + return posAxis.makeCalcdata(trace, posLetter); + } + + var pos0; + + if(posLetter + '0' in trace) { + pos0 = trace[posLetter + '0']; + } else if('name' in trace && ( + posAxis.type === 'category' || ( + isNumeric(trace.name) && + ['linear', 'log'].indexOf(posAxis.type) !== -1 + ) || ( + Lib.isDateTime(trace.name) && + posAxis.type === 'date' + ) + )) { + pos0 = trace.name; + } else { + pos0 = numboxes; + } + + var pos0c = posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']); + return val.map(function() { return pos0c; }); +} + +function makeBins(x, dx) { + var len = x.length; + var bins = new Array(len + 1); + + for(var i = 0; i < len; i++) { + bins[i] = x[i] - dx; + } + bins[len] = x[len - 1] + dx; + + return bins; +} + +function initNestedArray(len) { + var arr = new Array(len); + for(var i = 0; i < len; i++) { + arr[i] = []; + } + return arr; +} + +function sortByVal(a, b) { return a.v - b.v; } + +function extractVal(o) { return o.v; } diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 841fc4f7aed..793f311f62d 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -132,8 +132,6 @@ module.exports = function plot(gd, plotinfo, cdbox) { .attr('class', 'points') .selectAll('path') .data(function(d) { - var pts = (trace.boxpoints === 'all') ? d.val : - d.val.filter(function(v) { return (v < d.lf || v > d.uf); }), // normally use IQR, but if this is 0 or too small, use max-min typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1), minSpread = typicalSpread * 1e-9, @@ -147,6 +145,10 @@ module.exports = function plot(gd, plotinfo, cdbox) { jitterFactor, newJitter; + var pts = trace.boxpoints === 'all' ? + d.pts : + d.pts.filter(function(pt) { return (pt.v < d.lf || pt.v > d.uf); }); + // dynamic jitter if(trace.jitter) { if(typicalSpread === 0) { @@ -179,31 +181,32 @@ module.exports = function plot(gd, plotinfo, cdbox) { newJitter = trace.jitter * 2 / maxJitterFactor; } - return pts.map(function(v, i) { - var posOffset = trace.pointpos, - p; - if(trace.jitter) { - posOffset += newJitter * jitterFactors[i] * (rand() - 0.5); - } + // 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 ? + bdPos * (newJitter * jitterFactors[i] * (rand() - 0.5)) : + 0; + + var posPx = d.pos + bPos + bdPos * trace.pointpos + jitterOffset; if(trace.orientation === 'h') { - p = { - y: d.pos + posOffset * bdPos + bPos, - x: v - }; + pt.y = posPx; + pt.x = v; } else { - p = { - x: d.pos + posOffset * bdPos + bPos, - y: v - }; + pt.x = posPx; + pt.y = v; } // tag suspected outliers if(trace.boxpoints === 'suspectedoutliers' && v < d.uo && v > d.lo) { - p.so = true; + pt.so = true; } - return p; - }); + } + + return pts; }) .enter().append('path') .classed('point', true) From 581f3fa13c706606ddb6970429e7823a20276756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 22:22:14 -0400 Subject: [PATCH 03/18] add lasso/select dragmode support to box traces - use pts array of object to determine which box points are inside selection polygon - use index field (from Box.calc) to match each selected box point to underlying x/y input value. - dim point node opacity upon selection --- src/traces/box/index.js | 1 + src/traces/box/plot.js | 9 +++-- src/traces/box/select.js | 57 +++++++++++++++++++++++++++++++ test/jasmine/tests/select_test.js | 55 ++++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/traces/box/select.js diff --git a/src/traces/box/index.js b/src/traces/box/index.js index 82ed9d23097..2294107883b 100644 --- a/src/traces/box/index.js +++ b/src/traces/box/index.js @@ -19,6 +19,7 @@ Box.setPositions = require('./set_positions'); Box.plot = require('./plot'); Box.style = require('./style'); Box.hoverPoints = require('./hover'); +Box.selectPoints = require('./select'); Box.moduleType = 'trace'; Box.name = 'box'; diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 793f311f62d..3759e9aac2f 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -48,6 +48,7 @@ module.exports = function plot(gd, plotinfo, cdbox) { var cd0 = d[0]; var t = cd0.t; var trace = cd0.trace; + var sel = cd0.node3 = d3.select(this); var group = (fullLayout.boxmode === 'group' && gd.numboxes > 1); // box half width @@ -80,7 +81,7 @@ module.exports = function plot(gd, plotinfo, cdbox) { seed(); // boxes and whiskers - d3.select(this).selectAll('path.box') + sel.selectAll('path.box') .data(Lib.identity) .enter().append('path') .style('vector-effect', 'non-scaling-stroke') @@ -99,6 +100,7 @@ module.exports = function plot(gd, plotinfo, cdbox) { Math.min(q1, q3) + 1, Math.max(q1, q3) - 1), lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true), uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true); + if(trace.orientation === 'h') { d3.select(this).attr('d', 'M' + m + ',' + pos0 + 'V' + pos1 + // median line @@ -118,7 +120,7 @@ module.exports = function plot(gd, plotinfo, cdbox) { // draw points, if desired if(trace.boxpoints) { - d3.select(this).selectAll('g.points') + sel.selectAll('g.points') // since box plot points get an extra level of nesting, each // box needs the trace styling info .data(function(d) { @@ -212,9 +214,10 @@ module.exports = function plot(gd, plotinfo, cdbox) { .classed('point', true) .call(Drawing.translatePoints, xa, ya); } + // draw mean (and stdev diamond) if desired if(trace.boxmean) { - d3.select(this).selectAll('path.mean') + sel.selectAll('path.mean') .data(Lib.identity) .enter().append('path') .attr('class', 'mean') diff --git a/src/traces/box/select.js b/src/traces/box/select.js new file mode 100644 index 00000000000..b4ebaf3e9f2 --- /dev/null +++ b/src/traces/box/select.js @@ -0,0 +1,57 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; + +module.exports = function selectPoints(searchInfo, polygon) { + var cd = searchInfo.cd; + var xa = searchInfo.xaxis; + var ya = searchInfo.yaxis; + var trace = cd[0].trace; + var node3 = cd[0].node3; + var selection = []; + var i, j; + + if(trace.visible !== true) return []; + + if(polygon === false) { + for(i = 0; i < cd.length; i++) { + for(j = 0; j < (cd[i].pts || []).length; j++) { + // clear selection + cd[i].pts[j].dim = 0; + } + } + } else { + for(i = 0; i < cd.length; i++) { + for(j = 0; j < (cd[i].pts || []).length; j++) { + var pt = cd[i].pts[j]; + var x = xa.c2p(pt.x); + var y = ya.c2p(pt.y); + + if(polygon.contains([x, y])) { + selection.push({ + pointNumber: pt.i, + x: pt.x, + y: pt.y + }); + pt.dim = 0; + } else { + pt.dim = 1; + } + } + } + } + + node3.selectAll('.point').style('opacity', function(d) { + return d.dim ? DESELECTDIM : 1; + }); + + return selection; +}; diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 06f946bcb9b..b15c2038533 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -848,7 +848,60 @@ describe('Test select box and lasso per trace:', function() { ]); assertRanges([[1.66, 3.59], [0.69, 2.17]]); }, - null, BOXEVENTS, 'bar select' + null, BOXEVENTS, 'histogram select' + ); + }) + .catch(fail) + .then(done); + }); + + it('should work for box traces', function(done) { + var assertPoints = makeAssertPoints(['curveNumber', 'y']); + var assertRanges = makeAssertRanges(); + var assertLassoPoints = makeAssertLassoPoints(); + + var fig = Lib.extendDeep({}, require('@mocks/box_grouped')); + fig.data.forEach(function(trace) { + trace.boxpoints = 'all'; + }); + fig.layout.dragmode = 'lasso'; + fig.layout.width = 600; + fig.layout.height = 500; + addInvisible(fig); + + Plotly.plot(gd, fig) + .then(function() { + return _run( + [[200, 200], [400, 200], [400, 350], [200, 350], [200, 200]], + function() { + assertPoints([ + [0, 0.2], [0, 0.3], [0, 0.5], [0, 0.7], + [1, 0.2], [1, 0.5], [1, 0.7], [1, 0.7], + [2, 0.3], [2, 0.6], [2, 0.6] + ]); + assertLassoPoints([ + ['day 1', 'day 2', 'day 2', 'day 1', 'day 1'], + [0.71, 0.71, 0.1875, 0.1875, 0.71] + ]); + }, + null, LASSOEVENTS, 'box lasso' + ); + }) + .then(function() { + return Plotly.relayout(gd, 'dragmode', 'select'); + }) + .then(function() { + return _run( + [[200, 200], [400, 350]], + function() { + assertPoints([ + [0, 0.2], [0, 0.3], [0, 0.5], [0, 0.7], + [1, 0.2], [1, 0.5], [1, 0.7], [1, 0.7], + [2, 0.3], [2, 0.6], [2, 0.6] + ]); + assertRanges([['day 1', 'day 2'], [0.1875, 0.71]]); + }, + null, BOXEVENTS, 'box select' ); }) .catch(fail) From e59f283f38acc1311b02be17ea0b669b9c374365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 22:23:46 -0400 Subject: [PATCH 04/18] flatten box test suite --- test/jasmine/tests/box_test.js | 201 ++++++++++++++++----------------- 1 file changed, 96 insertions(+), 105 deletions(-) diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index c292fc356c9..511b8acae8d 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -1,112 +1,103 @@ var Box = require('@src/traces/box'); +describe('Test boxes supplyDefaults', function() { + var traceIn; + var traceOut; + var defaultColor = '#444'; + var supplyDefaults = Box.supplyDefaults; + + beforeEach(function() { + traceOut = {}; + }); + + it('should set visible to false when x and y are empty', function() { + traceIn = {}; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [], + y: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + }); -describe('Test boxes', function() { - 'use strict'; - - describe('supplyDefaults', function() { - var traceIn, - traceOut; - - var defaultColor = '#444'; - - var supplyDefaults = Box.supplyDefaults; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set visible to false when x and y are empty', function() { - traceIn = {}; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - }); - - it('should set visible to false when x or y is empty', function() { - traceIn = { - x: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [1, 2, 3], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - }); - - it('should set orientation to v by default', function() { - traceIn = { - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.orientation).toBe('v'); - - traceIn = { - x: [1, 1, 1], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.orientation).toBe('v'); - }); - - it('should set orientation to h when only x is supplied', function() { - traceIn = { - x: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.orientation).toBe('h'); - - }); - - it('should inherit layout.calendar', function() { - traceIn = { - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); - - it('should take its own calendars', function() { - traceIn = { - y: [1, 2, 3], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - }); + it('should set visible to false when x or y is empty', function() { + traceIn = { + x: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [], + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { + y: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [1, 2, 3], + y: [] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + }); + it('should set orientation to v by default', function() { + traceIn = { + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.orientation).toBe('v'); + + traceIn = { + x: [1, 1, 1], + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.orientation).toBe('v'); }); + it('should set orientation to h when only x is supplied', function() { + traceIn = { + x: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.orientation).toBe('h'); + + }); + + it('should inherit layout.calendar', function() { + traceIn = { + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('islamic'); + expect(traceOut.ycalendar).toBe('islamic'); + }); + + it('should take its own calendars', function() { + traceIn = { + y: [1, 2, 3], + xcalendar: 'coptic', + ycalendar: 'ethiopian' + }; + supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe('coptic'); + expect(traceOut.ycalendar).toBe('ethiopian'); + }); }); From 5c580491cb8d36564d60b0ba2cedbb5b0b70e447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 22:24:02 -0400 Subject: [PATCH 05/18] add some box hover tests --- test/jasmine/tests/box_test.js | 79 ++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 511b8acae8d..0042be9376d 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -1,5 +1,16 @@ +var Plotly = require('@lib'); +var Lib = require('@src/lib'); + var Box = require('@src/traces/box'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var fail = require('../assets/fail_test'); +var mouseEvent = require('../assets/mouse_event'); + +var customAssertions = require('../assets/custom_assertions'); +var assertHoverLabelContent = customAssertions.assertHoverLabelContent; + describe('Test boxes supplyDefaults', function() { var traceIn; var traceOut; @@ -101,3 +112,71 @@ describe('Test boxes supplyDefaults', function() { expect(traceOut.ycalendar).toBe('ethiopian'); }); }); + +describe('Test box hover:', function() { + var gd; + + afterEach(destroyGraphDiv); + + function run(specs) { + gd = createGraphDiv(); + + var fig = Lib.extendDeep( + {width: 700, height: 500}, + specs.mock || require('@mocks/box_grouped.json') + ); + + if(specs.patch) { + fig = specs.patch(fig); + } + + var pos = specs.pos || [200, 200]; + + return Plotly.plot(gd, fig).then(function() { + mouseEvent('mousemove', pos[0], pos[1]); + assertHoverLabelContent(specs); + }); + } + + [{ + desc: 'base', + nums: ['0.55', '0', '0.3', '0.6', '0.7'], + name: ['radishes', '', '', '', ''], + axis: 'day 1', + + }, { + desc: 'with mean', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxmean = true; + }); + return fig; + }, + nums: ['0.55', '0', '0.3', '0.6', '0.7', '0.45'], + name: ['radishes', '', '', '', '', ''], + axis: 'day 1', + + }, { + desc: 'with sd', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxmean = 'sd'; + }); + return fig; + }, + nums: ['0.55', '0', '0.3', '0.6', '0.7', '0.45 ± 0.2362908'], + name: ['radishes', '', '', '', '', ''], + axis: 'day 1', + }, { + desc: 'with boxpoints fences', + mock: require('@mocks/boxplots_outliercolordflt.json'), + pos: [350, 200], + nums: ['8.15', '0.75', '6.8', '10.25', '23.25', '5.25', '12'], + name: ['', '', '', '', '', '', ''], + axis: 'trace 0', + }].forEach(function(specs) { + it('should generate correct hover labels ' + specs.desc, function(done) { + run(specs).catch(fail).then(done); + }); + }); +}); From aef61ae3f01760a4291eaef4ae4f6b8d39eac494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 22:57:34 -0400 Subject: [PATCH 06/18] introduce box 'hoveron' - flaglist w/ 'boxes' (current + default) and 'points' - use point object x/y values (from Box.plot) to determine closest point(s) - consider jitter offset under hovermode closest, but don't under 'x' or 'y' modes --- src/traces/box/attributes.js | 13 +- src/traces/box/defaults.js | 2 + src/traces/box/hover.js | 225 ++++++++++++++++++++++----------- src/traces/box/plot.js | 11 +- test/jasmine/tests/box_test.js | 42 ++++++ 5 files changed, 212 insertions(+), 81 deletions(-) diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 696ce9e10f2..23130b33516 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -203,5 +203,16 @@ module.exports = { }, editType: 'plot' }, - fillcolor: scatterAttrs.fillcolor + fillcolor: scatterAttrs.fillcolor, + hoveron: { + valType: 'flaglist', + flags: ['boxes', 'points'], + dflt: 'boxes', + role: 'info', + editType: 'style', + description: [ + 'Do the hover effects highlight individual boxes ', + 'or jitter points or both?' + ].join(' ') + } }; diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 1b4c67b181a..9df89592ed6 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -71,4 +71,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('marker.line.outlierwidth'); } } + + coerce('hoveron'); }; diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index c565e9d0d47..9ce35c02a3a 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -14,94 +14,167 @@ var Fx = require('../../components/fx'); var Color = require('../../components/color'); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { - // closest mode: handicap box plots a little relative to others - var cd = pointData.cd, - trace = cd[0].trace, - t = cd[0].t, - xa = pointData.xa, - ya = pointData.ya, - closeData = [], - dx, dy, distfn, boxDelta, - posLetter, posAxis, - val, valLetter, valAxis; - - // adjust inbox w.r.t. to calculate box size - boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos; - - if(trace.orientation === 'h') { - dx = function(di) { - return Fx.inbox(di.min - xval, di.max - xval); - }; - dy = function(di) { - var pos = di.pos + t.bPos - yval; - return Fx.inbox(pos - boxDelta, pos + boxDelta); - }; - posLetter = 'y'; - posAxis = ya; - valLetter = 'x'; - valAxis = xa; - } else { - dx = function(di) { - var pos = di.pos + t.bPos - xval; - return Fx.inbox(pos - boxDelta, pos + boxDelta); - }; - dy = function(di) { - return Fx.inbox(di.min - yval, di.max - yval); - }; - posLetter = 'x'; - posAxis = xa; - valLetter = 'y'; - valAxis = ya; - } + var cd = pointData.cd; + var xa = pointData.xa; + var ya = pointData.ya; + + var trace = cd[0].trace; + var hoveron = trace.hoveron; + var marker = trace.marker || {}; + + // output hover points array + var closeData = []; + // x/y/effective distance functions + var dx, dy, distfn; + // orientation-specific fields + var posLetter, valLetter, posAxis, valAxis; + // calcdata item + var di; + // hover point item extended from pointData + var pointData2; + // loop indices + var i, j; + + if(hoveron.indexOf('boxes') !== -1) { + var t = cd[0].t; + + // closest mode: handicap box plots a little relative to others + // adjust inbox w.r.t. to calculate box size + var boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos; + + if(trace.orientation === 'h') { + dx = function(di) { + return Fx.inbox(di.min - xval, di.max - xval); + }; + dy = function(di) { + var pos = di.pos + t.bPos - yval; + return Fx.inbox(pos - boxDelta, pos + boxDelta); + }; + posLetter = 'y'; + posAxis = ya; + valLetter = 'x'; + valAxis = xa; + } else { + dx = function(di) { + var pos = di.pos + t.bPos - xval; + return Fx.inbox(pos - boxDelta, pos + boxDelta); + }; + dy = function(di) { + return Fx.inbox(di.min - yval, di.max - yval); + }; + posLetter = 'x'; + posAxis = xa; + valLetter = 'y'; + valAxis = ya; + } + + distfn = Fx.getDistanceFunction(hovermode, dx, dy); + Fx.getClosest(cd, distfn, pointData); - distfn = Fx.getDistanceFunction(hovermode, dx, dy); - Fx.getClosest(cd, distfn, pointData); + // skip the rest (for this trace) if we didn't find a close point + // and create the item(s) in closedata for this point + if(pointData.index !== false) { + di = cd[pointData.index]; - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index === false) return; + var lc = trace.line.color; + var mc = marker.color; - // create the item(s) in closedata for this point + if(Color.opacity(lc) && trace.line.width) pointData.color = lc; + else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc; + else pointData.color = trace.fillcolor; - // the closest data point - var di = cd[pointData.index], - lc = trace.line.color, - mc = (trace.marker || {}).color; - if(Color.opacity(lc) && trace.line.width) pointData.color = lc; - else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc; - else pointData.color = trace.fillcolor; + pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true); + pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true); - pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true); - pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true); + Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text; + pointData[posLetter + 'LabelVal'] = di.pos; - Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text; - pointData[posLetter + 'LabelVal'] = di.pos; + // box plots: each "point" gets many labels + var usedVals = {}; + var attrs = ['med', 'min', 'q1', 'q3', 'max']; - // box plots: each "point" gets many labels - var usedVals = {}, - attrs = ['med', 'min', 'q1', 'q3', 'max'], - attr, - pointData2; - if(trace.boxmean) attrs.push('mean'); - if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']); + if(trace.boxmean) attrs.push('mean'); + if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']); - for(var i = 0; i < attrs.length; i++) { - attr = attrs[i]; + for(i = 0; i < attrs.length; i++) { + var attr = attrs[i]; - if(!(attr in di) || (di[attr] in usedVals)) continue; - usedVals[di[attr]] = true; + if(!(attr in di) || (di[attr] in usedVals)) continue; + usedVals[di[attr]] = true; - // copy out to a new object for each value to label - val = valAxis.c2p(di[attr], true); - pointData2 = Lib.extendFlat({}, pointData); - pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val; - pointData2[valLetter + 'LabelVal'] = di[attr]; - pointData2.attr = attr; + // copy out to a new object for each value to label + var val = valAxis.c2p(di[attr], true); + pointData2 = Lib.extendFlat({}, pointData); + pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val; + pointData2[valLetter + 'LabelVal'] = di[attr]; + pointData2.attr = attr; - if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { - pointData2[valLetter + 'err'] = di.sd; + if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { + pointData2[valLetter + 'err'] = di.sd; + } + // only keep name on the first item (median) + pointData.name = ''; + + closeData.push(pointData2); + } } - pointData.name = ''; // only keep name on the first item (median) - closeData.push(pointData2); } + + if(hoveron.indexOf('points') !== -1) { + var xPx = xa.c2p(xval); + var yPx = ya.c2p(yval); + + // do not take jitter into consideration in compare hover modes + var kx, ky; + if(hovermode === 'closest') { + kx = 'x'; + ky = 'y'; + } else { + kx = 'xh'; + ky = 'yh'; + } + + dx = function(di) { + var rad = Math.max(3, di.mrc || 0); + return Math.max(Math.abs(xa.c2p(di[kx]) - xPx) - rad, 1 - 3 / rad); + }; + dy = function(di) { + var rad = Math.max(3, di.mrc || 0); + return Math.max(Math.abs(ya.c2p(di[ky]) - yPx) - rad, 1 - 3 / rad); + }; + distfn = Fx.getDistanceFunction(hovermode, dx, dy); + + for(i = 0; i < cd.length; i++) { + di = cd[i]; + + for(j = 0; j < (di.pts || []).length; j++) { + var pt = di.pts[j]; + + var newDistance = distfn(pt); + if(newDistance <= pointData.distance) { + pointData.distance = newDistance; + + var xc = xa.c2p(pt.x, true); + var yc = ya.c2p(pt.y, true); + var rad = pt.mrc || 1; + + pointData2 = Lib.extendFlat({}, pointData, { + // corresponds to index in x/y input data array + index: pt.i, + color: marker.color, + x0: xc - rad, + x1: xc + rad, + xLabelVal: pt.x, + y0: yc - rad, + y1: yc + rad, + yLabelVal: pt.y + }); + + closeData.push(pointData2); + } + } + } + } + return closeData; }; diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 3759e9aac2f..59f9c70ef93 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -183,7 +183,7 @@ module.exports = function plot(gd, plotinfo, cdbox) { newJitter = trace.jitter * 2 / maxJitterFactor; } - // fills in 'x' and 'y' in calcdata 'pts' item + // fills in 'x' and 'y' (with and w/o jitter offset) in calcdata 'pts' item for(i = 0; i < pts.length; i++) { var pt = pts[i]; var v = pt.v; @@ -192,14 +192,17 @@ module.exports = function plot(gd, plotinfo, cdbox) { bdPos * (newJitter * jitterFactors[i] * (rand() - 0.5)) : 0; - var posPx = d.pos + bPos + bdPos * trace.pointpos + jitterOffset; + var posPxNoJitter = d.pos + bPos + bdPos * trace.pointpos; + var posPx = posPxNoJitter + jitterOffset; if(trace.orientation === 'h') { pt.y = posPx; - pt.x = v; + pt.yh = posPxNoJitter; + pt.x = pt.xh = v; } else { pt.x = posPx; - pt.y = v; + pt.xh = posPxNoJitter; + pt.y = pt.yh = v; } // tag suspected outliers diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 0042be9376d..5922d2a9441 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -174,6 +174,48 @@ describe('Test box hover:', function() { nums: ['8.15', '0.75', '6.8', '10.25', '23.25', '5.25', '12'], name: ['', '', '', '', '', '', ''], axis: 'trace 0', + }, { + desc: 'hoveron points | hovermode closest', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxpoints = 'all'; + trace.hoveron = 'points'; + }); + fig.layout.hovermode = 'closest'; + return fig; + }, + nums: ['(day 1, 0.7)', '(day 1, 0.6)', '(day 1, 0.6)'], + name: ['radishes', 'radishes', 'radishes'] + }, { + desc: 'hoveron points | hovermode x', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxpoints = 'all'; + trace.hoveron = 'points'; + }); + fig.layout.hovermode = 'x'; + return fig; + }, + nums: ['0', '0.3', '0.5', '0.6', '0.6', '0.7'], + name: ['radishes', 'radishes', 'radishes', 'radishes', 'radishes', 'radishes'], + axis: 'day 1' + }, { + desc: 'hoveron boxes+points | hovermode x', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxpoints = 'all'; + trace.hoveron = 'points+boxes'; + }); + fig.layout.hovermode = 'x'; + return fig; + }, + nums: [ + '0', '0.7', '0.6', '0.6', '0.5', '0.3', '0', '0.7', '0.6', '0.3', '0.55' + ], + name: [ + '', '', '', '', '', '', '', '', '', '', 'radishes' + ], + axis: 'day 1' }].forEach(function(specs) { it('should generate correct hover labels ' + specs.desc, function(done) { run(specs).catch(fail).then(done); From d352fa5d839fb428d57cab9f9886dc0370690c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 23:08:15 -0400 Subject: [PATCH 07/18] add 'text' attribute to box traces - associated with each sample pt - only visible on hover (for now) --- src/traces/box/attributes.js | 10 ++++++++++ src/traces/box/calc.js | 13 +++++++++++++ src/traces/box/defaults.js | 1 + src/traces/box/hover.js | 2 ++ test/jasmine/tests/box_test.js | 27 +++++++++++++++++++++++++++ 5 files changed, 53 insertions(+) diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 23130b33516..1fd1420dcc0 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -62,6 +62,16 @@ module.exports = { 'missing and the position axis is categorical' ].join(' ') }, + 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(' ') + }), whiskerwidth: { valType: 'number', min: 0, diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index fbc9fe88c47..601fd6a47b0 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -56,6 +56,7 @@ module.exports = function calc(gd, trace) { var n = Lib.findBin(pos[i], posBins); if(n >= 0 && n < pLen) { var pt = {v: v, i: i}; + arraysToCalcdata(pt, trace, i); ptsPerBin[n].push(pt); } } @@ -176,6 +177,18 @@ function initNestedArray(len) { return arr; } +function arraysToCalcdata(pt, trace, i) { + var trace2calc = { + text: 'tx' + }; + + for(var k in trace2calc) { + if(Array.isArray(trace[k])) { + pt[trace2calc[k]] = trace[k][i]; + } + } +} + function sortByVal(a, b) { return a.v - b.v; } function extractVal(o) { return o.v; } diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 9df89592ed6..3d1260809c7 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -72,5 +72,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } } + coerce('text'); coerce('hoveron'); }; diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index 9ce35c02a3a..c3c8f414b76 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -12,6 +12,7 @@ var Axes = require('../../plots/cartesian/axes'); var Lib = require('../../lib'); var Fx = require('../../components/fx'); var Color = require('../../components/color'); +var fillHoverText = require('../scatter/fill_hover_text'); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var cd = pointData.cd; @@ -170,6 +171,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { yLabelVal: pt.y }); + fillHoverText(pt, trace, pointData2); closeData.push(pointData2); } } diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 5922d2a9441..30f6a3fae60 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -216,6 +216,33 @@ describe('Test box hover:', function() { '', '', '', '', '', '', '', '', '', '', 'radishes' ], axis: 'day 1' + }, { + desc: 'text items on hover', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxpoints = 'all'; + trace.hoveron = 'points'; + trace.text = trace.y.map(function(v) { return 'look:' + v; }); + }); + fig.layout.hovermode = 'closest'; + return fig; + }, + nums: ['(day 1, 0.7)\nlook:0.7', '(day 1, 0.6)\nlook:0.6', '(day 1, 0.6)\nlook:0.6'], + name: ['radishes', 'radishes', 'radishes'] + }, { + desc: 'only text items on hover', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxpoints = 'all'; + trace.hoveron = 'points'; + trace.text = trace.y.map(function(v) { return 'look:' + v; }); + trace.hoverinfo = 'text'; + }); + fig.layout.hovermode = 'closest'; + return fig; + }, + nums: ['look:0.7', 'look:0.6', 'look:0.6'], + name: ['', '', ''] }].forEach(function(specs) { it('should generate correct hover labels ' + specs.desc, function(done) { run(specs).catch(fail).then(done); From 6a878b0eef3e52332bd84eca40d024eec1219264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Mon, 16 Oct 2017 23:08:25 -0400 Subject: [PATCH 08/18] more linting in Box.plot --- src/traces/box/plot.js | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 59f9c70ef93..1b22e09e792 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -134,23 +134,20 @@ module.exports = function plot(gd, plotinfo, cdbox) { .attr('class', 'points') .selectAll('path') .data(function(d) { - // normally use IQR, but if this is 0 or too small, use max-min - typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1), - minSpread = typicalSpread * 1e-9, - spreadLimit = typicalSpread * JITTERSPREAD, - jitterFactors = [], - maxJitterFactor = 0, - i, - i0, i1, - pmin, - pmax, - jitterFactor, - newJitter; + var i; var pts = trace.boxpoints === 'all' ? d.pts : d.pts.filter(function(pt) { return (pt.v < d.lf || pt.v > d.uf); }); + // 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) { @@ -160,20 +157,19 @@ module.exports = function plot(gd, plotinfo, cdbox) { for(i = 0; i < pts.length; i++) { jitterFactors[i] = 1; } - } - else { + } else { for(i = 0; i < pts.length; i++) { - i0 = Math.max(0, i - JITTERCOUNT); - pmin = pts[i0]; - i1 = Math.min(pts.length - 1, i + JITTERCOUNT); - pmax = pts[i1]; + 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(trace.boxpoints !== 'all') { - if(pts[i] < d.lf) pmax = Math.min(pmax, d.lf); + if(pts[i].v < d.lf) pmax = Math.min(pmax, d.lf); else pmin = Math.max(pmin, d.uf); } - jitterFactor = Math.sqrt(spreadLimit * (i1 - i0) / (pmax - pmin + minSpread)) || 0; + var jitterFactor = Math.sqrt(spreadLimit * (i1 - i0) / (pmax - pmin + minSpread)) || 0; jitterFactor = Lib.constrain(Math.abs(jitterFactor), 0, 1); jitterFactors.push(jitterFactor); From a1ea1e83717c99aa8cb0e64a1fa493cae933cc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 18 Oct 2017 10:08:09 -0400 Subject: [PATCH 09/18] fixup box hoveron description --- src/traces/box/attributes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 1fd1420dcc0..bca740c2a39 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -222,7 +222,7 @@ module.exports = { editType: 'style', description: [ 'Do the hover effects highlight individual boxes ', - 'or jitter points or both?' + 'or sample points or both?' ].join(' ') } }; From 6bfcbe0e44a36eb75548f259cb35a808c5671444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 18 Oct 2017 17:25:31 -0400 Subject: [PATCH 10/18] make 'boxes+points' the box hoveron dflt --- src/traces/box/attributes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index bca740c2a39..baa4e056210 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -217,7 +217,7 @@ module.exports = { hoveron: { valType: 'flaglist', flags: ['boxes', 'points'], - dflt: 'boxes', + dflt: 'boxes+points', role: 'info', editType: 'style', description: [ From 9ca410ac82a2c911c44d2fadd215103e157ed8aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 18 Oct 2017 17:27:17 -0400 Subject: [PATCH 11/18] improve `boxpoints: false` default logic - delete 'marker' container for box trace that don't have `boxpoints: false` - do not coerce 'text' when `boxpoints: false` --- src/traces/box/defaults.js | 5 ++++- test/jasmine/tests/box_test.js | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 3d1260809c7..64a926e4af7 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -70,8 +70,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('marker.line.outliercolor', traceOut.marker.color); coerce('marker.line.outlierwidth'); } + + coerce('text'); + } else { + delete traceOut.marker; } - coerce('text'); coerce('hoveron'); }; diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 30f6a3fae60..7ca2db560d9 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -111,6 +111,44 @@ describe('Test boxes supplyDefaults', function() { expect(traceOut.xcalendar).toBe('coptic'); expect(traceOut.ycalendar).toBe('ethiopian'); }); + + it('should not coerce point attributes when boxpoints is false', function() { + traceIn = { + y: [1, 1, 2], + boxpoints: false + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + + expect(traceOut.boxpoints).toBe(false); + expect(traceOut.jitter).toBeUndefined(); + expect(traceOut.pointpos).toBeUndefined(); + expect(traceOut.marker).toBeUndefined(); + expect(traceOut.text).toBeUndefined(); + }); + + it('should default boxpoints to suspectedoutliers when marker.outliercolor is set & valid', function() { + traceIn = { + y: [1, 1, 2], + marker: { + outliercolor: 'blue' + } + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.boxpoints).toBe('suspectedoutliers'); + }); + + it('should default boxpoints to suspectedoutliers when marker.line.outliercolor is set & valid', function() { + traceIn = { + y: [1, 1, 2], + marker: { + line: {outliercolor: 'blue'} + } + }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.boxpoints).toBe('suspectedoutliers'); + expect(traceOut.marker).toBeDefined(); + expect(traceOut.text).toBeDefined(); + }); }); describe('Test box hover:', function() { From 7e44c9cc55a74b847227e589ad56784281c70fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 18 Oct 2017 17:27:53 -0400 Subject: [PATCH 12/18] add `boxmode: 'overlay'` hover test case --- test/jasmine/tests/box_test.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 7ca2db560d9..9ba66e54cf2 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -180,8 +180,7 @@ describe('Test box hover:', function() { desc: 'base', nums: ['0.55', '0', '0.3', '0.6', '0.7'], name: ['radishes', '', '', '', ''], - axis: 'day 1', - + axis: 'day 1' }, { desc: 'with mean', patch: function(fig) { @@ -192,8 +191,7 @@ describe('Test box hover:', function() { }, nums: ['0.55', '0', '0.3', '0.6', '0.7', '0.45'], name: ['radishes', '', '', '', '', ''], - axis: 'day 1', - + axis: 'day 1' }, { desc: 'with sd', patch: function(fig) { @@ -204,14 +202,29 @@ describe('Test box hover:', function() { }, nums: ['0.55', '0', '0.3', '0.6', '0.7', '0.45 ± 0.2362908'], name: ['radishes', '', '', '', '', ''], - axis: 'day 1', + axis: 'day 1' }, { desc: 'with boxpoints fences', mock: require('@mocks/boxplots_outliercolordflt.json'), pos: [350, 200], nums: ['8.15', '0.75', '6.8', '10.25', '23.25', '5.25', '12'], name: ['', '', '', '', '', '', ''], - axis: 'trace 0', + axis: 'trace 0' + }, { + desc: 'with overlaid boxes', + patch: function(fig) { + fig.layout.boxmode = 'overlay'; + return fig; + }, + nums: [ + '0.3', '0.45', '0.6', '1', '0.55', '0.2', + '0.6', '0.7', '0.45', '0.1', '0.6', '0.9' + ], + name: [ + '', 'kale', '', '', 'radishes', '', + '', '', 'carrots', '', '', '' + ], + axis: 'day 1' }, { desc: 'hoveron points | hovermode closest', patch: function(fig) { From f228a5ec6e232d6c1427fdef37b77a252e018eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 18 Oct 2017 17:39:10 -0400 Subject: [PATCH 13/18] aj-proof box hover logic - show only 'best' boxpoint - consider opposite coordinates in compare modes to find that 'best' boxpoint - allow box AND boxpoint hover labels only in compare modes - no need for x/y w/o jitter offset values in 'pts' calc items --- src/components/fx/helpers.js | 6 +-- src/components/fx/index.js | 1 + src/traces/box/hover.js | 93 +++++++++++++++++++--------------- src/traces/box/plot.js | 13 ++--- test/jasmine/tests/box_test.js | 40 +++++++++------ 5 files changed, 87 insertions(+), 66 deletions(-) diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index 9ce5acd160b..22caa8847d0 100644 --- a/src/components/fx/helpers.js +++ b/src/components/fx/helpers.js @@ -34,7 +34,7 @@ exports.p2c = function p2c(axArray, v) { }; exports.getDistanceFunction = function getDistanceFunction(mode, dx, dy, dxy) { - if(mode === 'closest') return dxy || quadrature(dx, dy); + if(mode === 'closest') return dxy || exports.quadrature(dx, dy); return mode === 'x' ? dx : dy; }; @@ -77,13 +77,13 @@ exports.inbox = function inbox(v0, v1) { return Infinity; }; -function quadrature(dx, dy) { +exports.quadrature = function quadrature(dx, dy) { return function(di) { var x = dx(di), y = dy(di); return Math.sqrt(x * x + y * y); }; -} +}; /** Appends values inside array attributes corresponding to given point number * diff --git a/src/components/fx/index.js b/src/components/fx/index.js index 34d7f711855..dd69c0315e3 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -35,6 +35,7 @@ module.exports = { getDistanceFunction: helpers.getDistanceFunction, getClosest: helpers.getClosest, inbox: helpers.inbox, + quadrature: helpers.quadrature, appendArrayPointValue: helpers.appendArrayPointValue, castHoverOption: castHoverOption, diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index c3c8f414b76..61df3e9eac4 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -23,16 +23,15 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var hoveron = trace.hoveron; var marker = trace.marker || {}; - // output hover points array - var closeData = []; + // output hover points components + var closeBoxData = []; + var closePtData; // x/y/effective distance functions var dx, dy, distfn; // orientation-specific fields var posLetter, valLetter, posAxis, valAxis; // calcdata item var di; - // hover point item extended from pointData - var pointData2; // loop indices var i, j; @@ -105,7 +104,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { // copy out to a new object for each value to label var val = valAxis.c2p(di[attr], true); - pointData2 = Lib.extendFlat({}, pointData); + var pointData2 = Lib.extendFlat({}, pointData); + pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val; pointData2[valLetter + 'LabelVal'] = di[attr]; pointData2.attr = attr; @@ -116,7 +116,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { // only keep name on the first item (median) pointData.name = ''; - closeData.push(pointData2); + closeBoxData.push(pointData2); } } } @@ -125,58 +125,71 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var xPx = xa.c2p(xval); var yPx = ya.c2p(yval); - // do not take jitter into consideration in compare hover modes - var kx, ky; - if(hovermode === 'closest') { - kx = 'x'; - ky = 'y'; - } else { - kx = 'xh'; - ky = 'yh'; - } - dx = function(di) { var rad = Math.max(3, di.mrc || 0); - return Math.max(Math.abs(xa.c2p(di[kx]) - xPx) - rad, 1 - 3 / rad); + return Math.max(Math.abs(xa.c2p(di.x) - xPx) - rad, 1 - 3 / rad); }; dy = function(di) { var rad = Math.max(3, di.mrc || 0); - return Math.max(Math.abs(ya.c2p(di[ky]) - yPx) - rad, 1 - 3 / rad); + return Math.max(Math.abs(ya.c2p(di.y) - yPx) - rad, 1 - 3 / rad); }; - distfn = Fx.getDistanceFunction(hovermode, dx, dy); + distfn = Fx.quadrature(dx, dy); + + // show one point per trace + var ijClosest = false; + var pt; for(i = 0; i < cd.length; i++) { di = cd[i]; for(j = 0; j < (di.pts || []).length; j++) { - var pt = di.pts[j]; + pt = di.pts[j]; var newDistance = distfn(pt); if(newDistance <= pointData.distance) { pointData.distance = newDistance; - - var xc = xa.c2p(pt.x, true); - var yc = ya.c2p(pt.y, true); - var rad = pt.mrc || 1; - - pointData2 = Lib.extendFlat({}, pointData, { - // corresponds to index in x/y input data array - index: pt.i, - color: marker.color, - x0: xc - rad, - x1: xc + rad, - xLabelVal: pt.x, - y0: yc - rad, - y1: yc + rad, - yLabelVal: pt.y - }); - - fillHoverText(pt, trace, pointData2); - closeData.push(pointData2); + ijClosest = [i, j]; } } } + + if(ijClosest) { + di = cd[ijClosest[0]]; + pt = di.pts[ijClosest[1]]; + + var xc = xa.c2p(pt.x, true); + var yc = ya.c2p(pt.y, true); + var rad = pt.mrc || 1; + + closePtData = Lib.extendFlat({}, pointData, { + // corresponds to index in x/y input data array + index: pt.i, + color: marker.color, + name: trace.name, + x0: xc - rad, + x1: xc + rad, + xLabelVal: pt.x, + y0: yc - rad, + y1: yc + rad, + yLabelVal: pt.y + }); + fillHoverText(pt, trace, closePtData); + } } - return closeData; + // In closest mode, show only one point or stats for one box, and points have priority + // If there's a point in range and hoveron has points, show the best single point only. + // If hoveron has boxes and there's no point in range (or hoveron doesn't have points), show the box stats. + if(hovermode === 'closest') { + if(closePtData) return [closePtData]; + return closeBoxData; + } + + // Otherwise in compare mode, allow a point AND the box stats to be labeled + // If there are multiple boxes in range (ie boxmode = 'overlay') we'll see stats for all of them. + if(closePtData) { + closeBoxData.push(closePtData); + return closeBoxData; + } + return closeBoxData; }; diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 1b22e09e792..e3fbfd2b90a 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -179,26 +179,23 @@ module.exports = function plot(gd, plotinfo, cdbox) { newJitter = trace.jitter * 2 / maxJitterFactor; } - // fills in 'x' and 'y' (with and w/o jitter offset) in calcdata 'pts' item + // 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 ? - bdPos * (newJitter * jitterFactors[i] * (rand() - 0.5)) : + (newJitter * jitterFactors[i] * (rand() - 0.5)) : 0; - var posPxNoJitter = d.pos + bPos + bdPos * trace.pointpos; - var posPx = posPxNoJitter + jitterOffset; + var posPx = d.pos + bPos + bdPos * (trace.pointpos + jitterOffset); if(trace.orientation === 'h') { pt.y = posPx; - pt.yh = posPxNoJitter; - pt.x = pt.xh = v; + pt.x = v; } else { pt.x = posPx; - pt.xh = posPxNoJitter; - pt.y = pt.yh = v; + pt.y = v; } // tag suspected outliers diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index 9ba66e54cf2..d875af26d03 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -235,8 +235,8 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'closest'; return fig; }, - nums: ['(day 1, 0.7)', '(day 1, 0.6)', '(day 1, 0.6)'], - name: ['radishes', 'radishes', 'radishes'] + nums: '(day 1, 0.7)', + name: 'radishes' }, { desc: 'hoveron points | hovermode x', patch: function(fig) { @@ -247,11 +247,11 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'x'; return fig; }, - nums: ['0', '0.3', '0.5', '0.6', '0.6', '0.7'], - name: ['radishes', 'radishes', 'radishes', 'radishes', 'radishes', 'radishes'], + nums: '0.7', + name: 'radishes', axis: 'day 1' }, { - desc: 'hoveron boxes+points | hovermode x', + desc: 'hoveron boxes+points | hovermode x (hover on box only - same result as base)', patch: function(fig) { fig.data.forEach(function(trace) { trace.boxpoints = 'all'; @@ -260,12 +260,22 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'x'; return fig; }, - nums: [ - '0', '0.7', '0.6', '0.6', '0.5', '0.3', '0', '0.7', '0.6', '0.3', '0.55' - ], - name: [ - '', '', '', '', '', '', '', '', '', '', 'radishes' - ], + nums: ['0.55', '0', '0.3', '0.6', '0.7'], + name: ['radishes', '', '', '', ''], + axis: 'day 1' + }, { + desc: 'hoveron boxes+points | hovermode x (box AND closest point)', + patch: function(fig) { + fig.data.forEach(function(trace) { + trace.boxpoints = 'all'; + trace.hoveron = 'points+boxes'; + trace.pointpos = 0; + }); + fig.layout.hovermode = 'x'; + return fig; + }, + nums: ['0.6', '0.55', '0', '0.3', '0.6', '0.7'], + name: ['radishes', 'radishes', '', '', '', ''], axis: 'day 1' }, { desc: 'text items on hover', @@ -278,8 +288,8 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'closest'; return fig; }, - nums: ['(day 1, 0.7)\nlook:0.7', '(day 1, 0.6)\nlook:0.6', '(day 1, 0.6)\nlook:0.6'], - name: ['radishes', 'radishes', 'radishes'] + nums: '(day 1, 0.7)\nlook:0.7', + name: 'radishes' }, { desc: 'only text items on hover', patch: function(fig) { @@ -292,8 +302,8 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'closest'; return fig; }, - nums: ['look:0.7', 'look:0.6', 'look:0.6'], - name: ['', '', ''] + nums: 'look:0.7', + name: '' }].forEach(function(specs) { it('should generate correct hover labels ' + specs.desc, function(done) { run(specs).catch(fail).then(done); From b4d80449eeb651eb69782540d51902cce18327c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 18 Oct 2017 17:39:58 -0400 Subject: [PATCH 14/18] hide modebar select2d & lasso2d buttons for boxes w/o 'all' boxpoints --- src/components/modebar/manage.js | 4 ++++ test/jasmine/tests/modebar_test.js | 33 +++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 8feee485140..783d1bb7f49 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -179,6 +179,10 @@ function isSelectable(fullData) { if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { selectable = true; } + } else if(Registry.traceIs(trace, 'box')) { + if(trace.boxpoints === 'all') { + selectable = true; + } } // assume that in general if the trace module has selectPoints, // then it's selectable. Scatter is an exception to this because it must diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 3246769b229..1b37fd74b25 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -154,7 +154,6 @@ describe('ModeBar', function() { }); describe('manageModeBar', function() { - function getButtons(list) { for(var i = 0; i < list.length; i++) { for(var j = 0; j < list[i].length; j++) { @@ -174,9 +173,9 @@ describe('ModeBar', function() { }); expect(modeBar.hasButtons(buttons)).toBe(true, 'modeBar.hasButtons'); - expect(countGroups(modeBar)).toEqual(expectedGroupCount, 'correct group count'); - expect(countButtons(modeBar)).toEqual(expectedButtonCount, 'correct button count'); - expect(countLogo(modeBar)).toEqual(1, 'correct logo count'); + expect(countGroups(modeBar)).toBe(expectedGroupCount, 'correct group count'); + expect(countButtons(modeBar)).toBe(expectedButtonCount, 'correct button count'); + expect(countLogo(modeBar)).toBe(1, 'correct logo count'); } it('creates mode bar (unselectable cartesian version)', function() { @@ -197,7 +196,7 @@ describe('ModeBar', function() { checkButtons(modeBar, buttons, 1); }); - it('creates mode bar (selectable cartesian version)', function() { + it('creates mode bar (selectable scatter version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], @@ -221,6 +220,30 @@ describe('ModeBar', function() { checkButtons(modeBar, buttons, 1); }); + it('creates mode bar (selectable box version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], + ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; + gd._fullLayout.xaxis = {fixedrange: false}; + gd._fullData = [{ + type: 'box', + visible: true, + boxpoints: 'all', + _module: {selectPoints: true} + }]; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + it('creates mode bar (cartesian fixed-axes version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], From 9e69900e010b7646c2cd8ddccf3e4d9a1a3cfea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 19 Oct 2017 09:47:43 -0400 Subject: [PATCH 15/18] rm useless 'c' value in scattercarper point event data --- src/traces/scattercarpet/select.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/traces/scattercarpet/select.js b/src/traces/scattercarpet/select.js index 5682b0e1669..ff40c61a271 100644 --- a/src/traces/scattercarpet/select.js +++ b/src/traces/scattercarpet/select.js @@ -24,7 +24,6 @@ module.exports = function selectPoints(searchInfo, polygon) { cdi = cd[pt.pointNumber]; pt.a = cdi.a; pt.b = cdi.b; - pt.c = cdi.c; delete pt.x; delete pt.y; } From 8901528534936bf3d7697db5db6f01f24f256e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 19 Oct 2017 09:48:18 -0400 Subject: [PATCH 16/18] report coords in input-data space in pt event data --- src/traces/bar/select.js | 6 ++- src/traces/box/select.js | 4 +- src/traces/scatter/select.js | 4 +- src/traces/scattergl/select.js | 4 +- test/jasmine/tests/select_test.js | 71 ++++++++++++++++++++++++++++--- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/traces/bar/select.js b/src/traces/bar/select.js index 309016c72be..34a4e23d310 100644 --- a/src/traces/bar/select.js +++ b/src/traces/bar/select.js @@ -12,6 +12,8 @@ var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; module.exports = function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd; + var xa = searchInfo.xaxis; + var ya = searchInfo.yaxis; var selection = []; var trace = cd[0].trace; var node3 = cd[0].node3; @@ -31,8 +33,8 @@ module.exports = function selectPoints(searchInfo, polygon) { if(polygon.contains(di.ct)) { selection.push({ pointNumber: i, - x: di.x, - y: di.y + x: xa.c2d(di.x), + y: ya.c2d(di.y) }); di.dim = 0; } else { diff --git a/src/traces/box/select.js b/src/traces/box/select.js index b4ebaf3e9f2..755246dd20a 100644 --- a/src/traces/box/select.js +++ b/src/traces/box/select.js @@ -38,8 +38,8 @@ module.exports = function selectPoints(searchInfo, polygon) { if(polygon.contains([x, y])) { selection.push({ pointNumber: pt.i, - x: pt.x, - y: pt.y + x: xa.c2d(pt.x), + y: ya.c2d(pt.y) }); pt.dim = 0; } else { diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js index a529a4c21e6..1663848439e 100644 --- a/src/traces/scatter/select.js +++ b/src/traces/scatter/select.js @@ -42,8 +42,8 @@ module.exports = function selectPoints(searchInfo, polygon) { if(polygon.contains([x, y])) { selection.push({ pointNumber: i, - x: di.x, - y: di.y + x: xa.c2d(di.x), + y: ya.c2d(di.y) }); di.dim = 0; } diff --git a/src/traces/scattergl/select.js b/src/traces/scattergl/select.js index e4187fb9eda..22ff3716767 100644 --- a/src/traces/scattergl/select.js +++ b/src/traces/scattergl/select.js @@ -41,8 +41,8 @@ module.exports = function selectPoints(searchInfo, polygon) { if(polygon.contains([x, y])) { selection.push({ pointNumber: i, - x: di.x, - y: di.y + x: xa.c2d(di.x), + y: ya.c2d(di.y) }); di.dim = 0; } diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index b15c2038533..4840f878ba8 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -809,6 +809,63 @@ describe('Test select box and lasso per trace:', function() { .then(done); }); + it('should work for date/category traces', function(done) { + var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']); + + var fig = { + data: [{ + x: ['2017-01-01', '2017-02-01', '2017-03-01'], + y: ['a', 'b', 'c'] + }, { + type: 'bar', + x: ['2017-01-01', '2017-02-02', '2017-03-01'], + y: ['a', 'b', 'c'] + }], + layout: { + dragmode: 'lasso', + width: 400, + height: 400 + } + }; + addInvisible(fig); + + var x0 = 100; + var y0 = 100; + var x1 = 250; + var y1 = 250; + + Plotly.plot(gd, fig) + .then(function() { + return _run( + [[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]], + function() { + assertPoints([ + [0, '2017-02-01', 'b'], + [1, '2017-02-02', 'b'] + ]); + }, + null, LASSOEVENTS, 'date/category lasso' + ); + }) + .then(function() { + return Plotly.relayout(gd, 'dragmode', 'select'); + }) + .then(function() { + return _run( + [[x0, y0], [x1, y1]], + function() { + assertPoints([ + [0, '2017-02-01', 'b'], + [1, '2017-02-02', 'b'] + ]); + }, + null, BOXEVENTS, 'date/category select' + ); + }) + .catch(fail) + .then(done); + }); + it('should work for histogram traces', function(done) { var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']); var assertRanges = makeAssertRanges(); @@ -856,7 +913,7 @@ describe('Test select box and lasso per trace:', function() { }); it('should work for box traces', function(done) { - var assertPoints = makeAssertPoints(['curveNumber', 'y']); + var assertPoints = makeAssertPoints(['curveNumber', 'y', 'x']); var assertRanges = makeAssertRanges(); var assertLassoPoints = makeAssertLassoPoints(); @@ -875,9 +932,9 @@ describe('Test select box and lasso per trace:', function() { [[200, 200], [400, 200], [400, 350], [200, 350], [200, 200]], function() { assertPoints([ - [0, 0.2], [0, 0.3], [0, 0.5], [0, 0.7], - [1, 0.2], [1, 0.5], [1, 0.7], [1, 0.7], - [2, 0.3], [2, 0.6], [2, 0.6] + [0, 0.2, 'day 2'], [0, 0.3, 'day 2'], [0, 0.5, 'day 2'], [0, 0.7, 'day 2'], + [1, 0.2, 'day 2'], [1, 0.5, 'day 2'], [1, 0.7, 'day 2'], [1, 0.7, 'day 2'], + [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'] ]); assertLassoPoints([ ['day 1', 'day 2', 'day 2', 'day 1', 'day 1'], @@ -895,9 +952,9 @@ describe('Test select box and lasso per trace:', function() { [[200, 200], [400, 350]], function() { assertPoints([ - [0, 0.2], [0, 0.3], [0, 0.5], [0, 0.7], - [1, 0.2], [1, 0.5], [1, 0.7], [1, 0.7], - [2, 0.3], [2, 0.6], [2, 0.6] + [0, 0.2, 'day 2'], [0, 0.3, 'day 2'], [0, 0.5, 'day 2'], [0, 0.7, 'day 2'], + [1, 0.2, 'day 2'], [1, 0.5, 'day 2'], [1, 0.7, 'day 2'], [1, 0.7, 'day 2'], + [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'] ]); assertRanges([['day 1', 'day 2'], [0.1875, 0.71]]); }, From b66628e13e9a012016ab3a493171cfc5ab91d27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 19 Oct 2017 13:36:19 -0400 Subject: [PATCH 17/18] introduce Axes.hoverLabelText - dry special log-off-scale hover label text routine - use it in hover.js cleanPoint - allow hoverPoints modules to set their own xLabel and yLabel value --- src/components/fx/hover.js | 26 ++------------------------ src/plots/cartesian/axes.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index bc5866198bd..cc366193eb3 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1065,36 +1065,14 @@ function cleanPoint(d, hovermode) { // and convert the x and y label values into objects // formatted as text, with font info - var logOffScale; if(d.xLabelVal !== undefined) { - logOffScale = (d.xa.type === 'log' && d.xLabelVal <= 0); - var xLabelObj = Axes.tickText(d.xa, - d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal), 'hover'); - if(logOffScale) { - if(d.xLabelVal === 0) d.xLabel = '0'; - else d.xLabel = '-' + xLabelObj.text; - } - // TODO: should we do something special if the axis calendar and - // the data calendar are different? Somehow display both dates with - // their system names? Right now it will just display in the axis calendar - // but users could add the other one as text. - else d.xLabel = xLabelObj.text; + d.xLabel = ('xLabel' in d) ? d.xLabel : Axes.hoverLabelText(d.xa, d.xLabelVal); d.xVal = d.xa.c2d(d.xLabelVal); } - if(d.yLabelVal !== undefined) { - logOffScale = (d.ya.type === 'log' && d.yLabelVal <= 0); - var yLabelObj = Axes.tickText(d.ya, - d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal), 'hover'); - if(logOffScale) { - if(d.yLabelVal === 0) d.yLabel = '0'; - else d.yLabel = '-' + yLabelObj.text; - } - // TODO: see above TODO - else d.yLabel = yLabelObj.text; + d.yLabel = ('yLabel' in d) ? d.yLabel : Axes.hoverLabelText(d.ya, d.yLabelVal); d.yVal = d.ya.c2d(d.yLabelVal); } - if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal); // for box means and error bars, add the range to the label diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 64506c23eb9..e2e7a01e1b7 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1216,6 +1216,21 @@ axes.tickText = function(ax, x, hover) { return out; }; +axes.hoverLabelText = function(ax, val) { + var logOffScale = (ax.type === 'log' && val <= 0); + var tx = axes.tickText(ax, ax.c2l(logOffScale ? -val : val), 'hover').text; + + if(logOffScale) { + return val === 0 ? '0' : '-' + tx; + } + + // TODO: should we do something special if the axis calendar and + // the data calendar are different? Somehow display both dates with + // their system names? Right now it will just display in the axis calendar + // but users could add the other one as text. + return tx; +}; + function tickTextObj(ax, x, text) { var tf = ax.tickfont || {}; From a3ec75a26d44703c78bbba76ed2ed47fdca762a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 19 Oct 2017 13:42:06 -0400 Subject: [PATCH 18/18] add prefix box stats hover labels --- src/traces/box/hover.js | 20 ++++++++++++++------ test/jasmine/tests/box_test.js | 22 ++++++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index 61df3e9eac4..d3668a44575 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -92,9 +92,16 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { // box plots: each "point" gets many labels var usedVals = {}; var attrs = ['med', 'min', 'q1', 'q3', 'max']; + var prefixes = ['median', 'min', 'q1', 'q3', 'max']; - if(trace.boxmean) attrs.push('mean'); - if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']); + if(trace.boxmean) { + attrs.push('mean'); + prefixes.push(trace.boxmean === 'sd' ? 'mean ± σ' : 'mean'); + } + if(trace.boxpoints) { + attrs.push('lf', 'uf'); + prefixes.push('lower fence', 'upper fence'); + } for(i = 0; i < attrs.length; i++) { var attr = attrs[i]; @@ -103,12 +110,13 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { usedVals[di[attr]] = true; // copy out to a new object for each value to label - var val = valAxis.c2p(di[attr], true); + var val = di[attr]; + var valPx = valAxis.c2p(val, true); var pointData2 = Lib.extendFlat({}, pointData); - pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val; - pointData2[valLetter + 'LabelVal'] = di[attr]; - pointData2.attr = attr; + pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = valPx; + pointData2[valLetter + 'LabelVal'] = val; + pointData2[valLetter + 'Label'] = prefixes[i] + ': ' + Axes.hoverLabelText(valAxis, val); if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { pointData2[valLetter + 'err'] = di.sd; diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index d875af26d03..0c56796937f 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -178,7 +178,7 @@ describe('Test box hover:', function() { [{ desc: 'base', - nums: ['0.55', '0', '0.3', '0.6', '0.7'], + nums: ['median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7'], name: ['radishes', '', '', '', ''], axis: 'day 1' }, { @@ -189,7 +189,7 @@ describe('Test box hover:', function() { }); return fig; }, - nums: ['0.55', '0', '0.3', '0.6', '0.7', '0.45'], + nums: ['median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7', 'mean: 0.45'], name: ['radishes', '', '', '', '', ''], axis: 'day 1' }, { @@ -200,14 +200,20 @@ describe('Test box hover:', function() { }); return fig; }, - nums: ['0.55', '0', '0.3', '0.6', '0.7', '0.45 ± 0.2362908'], + nums: [ + 'median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7', + 'mean ± σ: 0.45 ± 0.2362908' + ], name: ['radishes', '', '', '', '', ''], axis: 'day 1' }, { desc: 'with boxpoints fences', mock: require('@mocks/boxplots_outliercolordflt.json'), pos: [350, 200], - nums: ['8.15', '0.75', '6.8', '10.25', '23.25', '5.25', '12'], + nums: [ + 'median: 8.15', 'min: 0.75', 'q1: 6.8', + 'q3: 10.25', 'max: 23.25', 'lower fence: 5.25', 'upper fence: 12' + ], name: ['', '', '', '', '', '', ''], axis: 'trace 0' }, { @@ -217,8 +223,8 @@ describe('Test box hover:', function() { return fig; }, nums: [ - '0.3', '0.45', '0.6', '1', '0.55', '0.2', - '0.6', '0.7', '0.45', '0.1', '0.6', '0.9' + 'q1: 0.3', 'median: 0.45', 'q3: 0.6', 'max: 1', 'median: 0.55', 'min: 0.2', + 'q3: 0.6', 'max: 0.7', 'median: 0.45', 'min: 0.1', 'q3: 0.6', 'max: 0.9' ], name: [ '', 'kale', '', '', 'radishes', '', @@ -260,7 +266,7 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'x'; return fig; }, - nums: ['0.55', '0', '0.3', '0.6', '0.7'], + nums: ['median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7'], name: ['radishes', '', '', '', ''], axis: 'day 1' }, { @@ -274,7 +280,7 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'x'; return fig; }, - nums: ['0.6', '0.55', '0', '0.3', '0.6', '0.7'], + nums: ['0.6', 'median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7'], name: ['radishes', 'radishes', '', '', '', ''], axis: 'day 1' }, {