diff --git a/BUILDING.md b/BUILDING.md
index 816ecb20daa..c2aea8a659e 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -40,13 +40,6 @@ then simply run,
browserify index.js > bundle.js
```
-to trim meta information (and thus save a few bytes), run:
-
-
-```
-browserify -t path/to/plotly.js/tasks/util/compress_attributes.js index.js > bundle.js
-```
-
## Angular CLI
Currently Angular CLI uses Webpack under the hood to bundle and build your Angular application.
diff --git a/package-lock.json b/package-lock.json
index 8b17ba63fc3..a17da6327a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,29 +35,6 @@
"commander": "^2.15.1"
}
},
- "@etpinard/gl-text": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@etpinard/gl-text/-/gl-text-1.1.6.tgz",
- "integrity": "sha512-sN007FwlqdSKJt2/cnGZu3jsAN7G4R/wxk/D6ZivPuQtrwJ42B68iuAqysJPgFepUTAsDRtGAOd1U7tZxfDJwA==",
- "requires": {
- "color-normalize": "^1.1.0",
- "css-font": "^1.2.0",
- "detect-kerning": "^2.1.2",
- "es6-weak-map": "^2.0.2",
- "flatten-vertex-data": "^1.0.2",
- "font-atlas": "^2.1.0",
- "font-measure": "^1.2.2",
- "gl-util": "^3.0.7",
- "is-plain-obj": "^1.1.0",
- "object-assign": "^4.1.1",
- "parse-rect": "^1.2.0",
- "parse-unit": "^1.0.1",
- "pick-by-alias": "^1.2.0",
- "regl": "^1.3.6",
- "to-px": "^1.0.1",
- "typedarray-pool": "^1.1.0"
- }
- },
"@mapbox/geojson-area": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz",
@@ -4889,6 +4866,30 @@
"typedarray-pool": "^1.0.0"
}
},
+ "gl-text": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.1.6.tgz",
+ "integrity": "sha512-OB+Nc5JKO1gyYYqBOJrYvCvRXIecfVpIKP7AviQNY63jrWPM9hUFSwZG7sH/paVnR1yCZBVirqOPfiFeF1Qo4g==",
+ "requires": {
+ "bit-twiddle": "^1.0.2",
+ "color-normalize": "^1.1.0",
+ "css-font": "^1.2.0",
+ "detect-kerning": "^2.1.2",
+ "es6-weak-map": "^2.0.2",
+ "flatten-vertex-data": "^1.0.2",
+ "font-atlas": "^2.1.0",
+ "font-measure": "^1.2.2",
+ "gl-util": "^3.0.7",
+ "is-plain-obj": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "parse-rect": "^1.2.0",
+ "parse-unit": "^1.0.1",
+ "pick-by-alias": "^1.2.0",
+ "regl": "^1.3.6",
+ "to-px": "^1.0.1",
+ "typedarray-pool": "^1.1.0"
+ }
+ },
"gl-texture2d": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/gl-texture2d/-/gl-texture2d-2.1.0.tgz",
@@ -5171,9 +5172,9 @@
}
},
"glslify": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/glslify/-/glslify-6.3.0.tgz",
- "integrity": "sha512-9VWypdkvL907Jn37QcCZXIHpLbmqs+fjnmjNszSFc+5ztmsGFzcknjCgeF887+xxfx32oGrgN7xfkSwa1D0khA==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-6.3.1.tgz",
+ "integrity": "sha512-3Hy85N8NmpDprwAxZaOC9k+DBXEwblVZ+yHIyt1QYg5dIrYaiGorz2WWBRxdUzapjDsxdhQ1ad9GSlIebxeBmA==",
"requires": {
"bl": "^1.0.0",
"concat-stream": "^1.5.2",
@@ -8860,9 +8861,9 @@
}
},
"regl-line2d": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.9.tgz",
- "integrity": "sha512-D3ASXgofHVcdxi6qfQRJ7YsAVHkK0i7rkKx9qwDLYoZ96eRyyFMDb1zA3ulrmarPnb/U2G7EfsYQDU3V96EP4Q==",
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.11.tgz",
+ "integrity": "sha512-nf0Ftpf6boR0oJ24Gs77J8pQE0wet59T1TkrK1f0TWKJgWgRXByxRHDD92m/KZ2dpl+XTvCORk2NRqitSJGwWw==",
"requires": {
"array-bounds": "^1.0.0",
"array-normalize": "^1.1.3",
@@ -8871,7 +8872,7 @@
"earcut": "^2.1.1",
"es6-weak-map": "^2.0.2",
"flatten-vertex-data": "^1.0.0",
- "glslify": "^6.1.0",
+ "glslify": "^6.3.1",
"object-assign": "^4.1.1",
"parse-rect": "^1.2.0",
"pick-by-alias": "^1.1.0",
@@ -8901,9 +8902,9 @@
}
},
"regl-splom": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.3.tgz",
- "integrity": "sha512-3oJT26xm91p303Jb3jMI7PptHYMSbR2/ZnTLolYGnC42jVp/e+xbbik1pTNFyeS5WiaE0M+Ssl3tUC6zgQ8nOw==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.4.tgz",
+ "integrity": "sha512-+iq/RJAJdHCp48wPbEGQ5qw29OXFVF/m7CzcuLZxwptjdkB/FHGKiMuyqclOSNQcEKFxQTvRRJMJJ6brd8VvrA==",
"requires": {
"array-bounds": "^1.0.1",
"array-range": "^1.0.1",
@@ -8916,7 +8917,7 @@
"pick-by-alias": "^1.2.0",
"point-cluster": "^1.0.2",
"raf": "^3.4.0",
- "regl-scatter2d": "^3.0.0"
+ "regl-scatter2d": "^3.0.6"
},
"dependencies": {
"binary-search-bounds": {
diff --git a/package.json b/package.json
index e0f42fb32e0..0219f2bc703 100644
--- a/package.json
+++ b/package.json
@@ -51,12 +51,12 @@
},
"browserify": {
"transform": [
- "glslify"
+ "glslify",
+ "./tasks/compress_attributes.js"
]
},
"dependencies": {
"3d-view": "^2.0.0",
- "@etpinard/gl-text": "^1.1.6",
"@plotly/d3-sankey": "^0.5.0",
"alpha-shape": "^1.0.0",
"array-range": "^1.0.1",
@@ -85,7 +85,8 @@
"gl-spikes2d": "^1.0.1",
"gl-streamtube3d": "^1.0.0",
"gl-surface3d": "^1.3.5",
- "glslify": "^6.2.1",
+ "gl-text": "^1.1.6",
+ "glslify": "^6.3.1",
"has-hover": "^1.0.1",
"has-passive-events": "^1.0.0",
"mapbox-gl": "0.45.0",
@@ -101,9 +102,9 @@
"polybooljs": "^1.2.0",
"regl": "^1.3.7",
"regl-error2d": "^2.0.5",
- "regl-line2d": "^3.0.9",
+ "regl-line2d": "^3.0.11",
"regl-scatter2d": "^3.0.6",
- "regl-splom": "^1.0.3",
+ "regl-splom": "^1.0.4",
"right-now": "^1.0.0",
"robust-orientation": "^1.1.3",
"sane-topojson": "^2.0.0",
diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js
index 4c35c2b4086..2429a5541f5 100644
--- a/src/components/fx/layout_attributes.js
+++ b/src/components/fx/layout_attributes.js
@@ -18,6 +18,29 @@ fontAttrs.family.dflt = constants.HOVERFONT;
fontAttrs.size.dflt = constants.HOVERFONTSIZE;
module.exports = {
+ clickmode: {
+ valType: 'flaglist',
+ role: 'info',
+ flags: ['event', 'select'],
+ dflt: 'event',
+ editType: 'plot',
+ extras: ['none'],
+ description: [
+ 'Determines the mode of single click interactions.',
+ '*event* is the default value and emits the `plotly_click`',
+ 'event. In addition this mode emits the `plotly_selected` event',
+ 'in drag modes *lasso* and *select*, but with no event data attached',
+ '(kept for compatibility reasons).',
+ 'The *select* flag enables selecting single',
+ 'data points via click. This mode also supports persistent selections,',
+ 'meaning that pressing Shift while clicking, adds to / subtracts from an',
+ 'existing selection. *select* with `hovermode`: *x* can be confusing, consider',
+ 'explicitly setting `hovermode`: *closest* when using this feature.',
+ 'Selection events are sent accordingly as long as *event* flag is set as well.',
+ 'When the *event* flag is missing, `plotly_click` and `plotly_selected`',
+ 'events are not fired.'
+ ].join(' ')
+ },
dragmode: {
valType: 'enumerated',
role: 'info',
@@ -36,7 +59,16 @@ module.exports = {
role: 'info',
values: ['x', 'y', 'closest', false],
editType: 'modebar',
- description: 'Determines the mode of hover interactions.'
+ description: [
+ 'Determines the mode of hover interactions.',
+ 'If `clickmode` includes the *select* flag,',
+ '`hovermode` defaults to *closest*.',
+ 'If `clickmode` lacks the *select* flag,',
+ 'it defaults to *x* or *y* (depending on the trace\'s',
+ '`orientation` value) for plots based on',
+ 'cartesian coordinates. For anything else the default',
+ 'value is *closest*.',
+ ].join(' ')
},
hoverdistance: {
valType: 'integer',
diff --git a/src/components/fx/layout_defaults.js b/src/components/fx/layout_defaults.js
index 742c6eb1621..5f7372d6edf 100644
--- a/src/components/fx/layout_defaults.js
+++ b/src/components/fx/layout_defaults.js
@@ -16,15 +16,21 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
}
+ var clickmode = coerce('clickmode');
+
var dragMode = coerce('dragmode');
if(dragMode === 'select') coerce('selectdirection');
var hovermodeDflt;
if(layoutOut._has('cartesian')) {
- // flag for 'horizontal' plots:
- // determines the state of the mode bar 'compare' hovermode button
- layoutOut._isHoriz = isHoriz(fullData);
- hovermodeDflt = layoutOut._isHoriz ? 'y' : 'x';
+ if(clickmode.indexOf('select') > -1) {
+ hovermodeDflt = 'closest';
+ } else {
+ // flag for 'horizontal' plots:
+ // determines the state of the mode bar 'compare' hovermode button
+ layoutOut._isHoriz = isHoriz(fullData);
+ hovermodeDflt = layoutOut._isHoriz ? 'y' : 'x';
+ }
}
else hovermodeDflt = 'closest';
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index fd68381fcdd..21afb93c07f 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -208,7 +208,9 @@ module.exports = function style(s, gd) {
var pts = ptgroup.selectAll('path.scatterpts')
.data(showMarkers ? dMod : []);
- pts.enter().append('path').classed('scatterpts', true)
+ // make sure marker is on the bottom, in case it enters after text
+ pts.enter().insert('path', ':first-child')
+ .classed('scatterpts', true)
.attr('transform', 'translate(20,0)');
pts.exit().remove();
pts.call(Drawing.pointStyle, tMod, gd);
diff --git a/src/lib/polygon.js b/src/lib/polygon.js
index d84583e696e..fb07a677af6 100644
--- a/src/lib/polygon.js
+++ b/src/lib/polygon.js
@@ -31,8 +31,6 @@ var polygon = module.exports = {};
* returns boolean: is pt inside the polygon (including on its edges)
*/
polygon.tester = function tester(ptsIn) {
- if(Array.isArray(ptsIn[0][0])) return polygon.multitester(ptsIn);
-
var pts = ptsIn.slice(),
xmin = pts[0][0],
xmax = xmin,
@@ -174,50 +172,6 @@ polygon.tester = function tester(ptsIn) {
};
};
-/**
- * Test multiple polygons
- */
-polygon.multitester = function multitester(list) {
- var testers = [],
- xmin = list[0][0][0],
- xmax = xmin,
- ymin = list[0][0][1],
- ymax = ymin;
-
- for(var i = 0; i < list.length; i++) {
- var tester = polygon.tester(list[i]);
- tester.subtract = list[i].subtract;
- testers.push(tester);
- xmin = Math.min(xmin, tester.xmin);
- xmax = Math.max(xmax, tester.xmax);
- ymin = Math.min(ymin, tester.ymin);
- ymax = Math.max(ymax, tester.ymax);
- }
-
- function contains(pt, arg) {
- var yes = false;
- for(var i = 0; i < testers.length; i++) {
- if(testers[i].contains(pt, arg)) {
- // if contained by subtract polygon - exclude the point
- yes = testers[i].subtract === false;
- }
- }
-
- return yes;
- }
-
- return {
- xmin: xmin,
- xmax: xmax,
- ymin: ymin,
- ymax: ymax,
- pts: [],
- contains: contains,
- isRect: false,
- degenerate: false
- };
-};
-
/**
* Test if a segment of a points array is bent or straight
*
diff --git a/src/lib/prepare_regl.js b/src/lib/prepare_regl.js
index 13931e4eaa4..d262a510aa9 100644
--- a/src/lib/prepare_regl.js
+++ b/src/lib/prepare_regl.js
@@ -48,6 +48,17 @@ module.exports = function prepareRegl(gd, extensions) {
} catch(e) {
success = false;
}
+
+ if(success) {
+ this.addEventListener('webglcontextlost', function(event) {
+ if(gd && gd.emit) {
+ gd.emit('plotly_webglcontextlost', {
+ event: event,
+ layer: d.key
+ });
+ }
+ }, false);
+ }
});
if(!success) {
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index 8d66a4cb4a5..bdd180b17c4 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -30,6 +30,7 @@ var doTicksSingle = require('./axes').doTicksSingle;
var getFromId = require('./axis_ids').getFromId;
var prepSelect = require('./select').prepSelect;
var clearSelect = require('./select').clearSelect;
+var selectOnClick = require('./select').selectOnClick;
var scaleZoom = require('./scale_zoom');
var constants = require('./constants');
@@ -148,7 +149,11 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
};
dragOptions.prepFn = function(e, startX, startY) {
+ var dragModePrev = dragOptions.dragmode;
var dragModeNow = gd._fullLayout.dragmode;
+ if(dragModeNow !== dragModePrev) {
+ dragOptions.dragmode = dragModeNow;
+ }
recomputeAxisLists();
@@ -178,7 +183,19 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
prepSelect(e, startX, startY, dragOptions, dragModeNow);
} else {
dragOptions.clickFn = clickFn;
- clearAndResetSelect();
+ if(isSelectOrLasso(dragModePrev)) {
+ // TODO Fix potential bug
+ // Note: clearing / resetting selection state only happens, when user
+ // triggers at least one interaction in pan/zoom mode. Otherwise, the
+ // select/lasso outlines are deleted (in plots.js.cleanPlot) but the selection
+ // cache isn't cleared. So when the user switches back to select/lasso and
+ // 'adds to a selection' with Shift, the "old", seemingly removed outlines
+ // are redrawn again because the selection cache still holds their coordinates.
+ // However, this isn't easily solved, since plots.js would need
+ // to have a reference to the dragOptions object (which holds the
+ // selection cache).
+ clearAndResetSelect();
+ }
if(!allFixedRanges) {
if(dragModeNow === 'zoom') {
@@ -207,12 +224,20 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
}
function clickFn(numClicks, evt) {
+ var clickmode = gd._fullLayout.clickmode;
+
removeZoombox(gd);
if(numClicks === 2 && !singleEnd) doubleClick();
if(isMainDrag) {
- Fx.click(gd, evt, plotinfo.id);
+ if(clickmode.indexOf('select') > -1) {
+ selectOnClick(evt, gd, xaxes, yaxes, plotinfo.id, dragOptions);
+ }
+
+ if(clickmode.indexOf('event') > -1) {
+ Fx.click(gd, evt, plotinfo.id);
+ }
}
else if(numClicks === 1 && singleEnd) {
var ax = ns ? ya0 : xa0,
diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js
index 498dfff60d2..fdc869f8690 100644
--- a/src/plots/cartesian/select.js
+++ b/src/plots/cartesian/select.js
@@ -26,7 +26,6 @@ var MINSELECT = constants.MINSELECT;
var filteredPolygon = polygon.filter;
var polygonTester = polygon.tester;
-var multipolygonTester = polygon.multitester;
function getAxId(ax) { return ax._id; }
@@ -45,43 +44,13 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
var path0 = 'M' + x0 + ',' + y0;
var pw = dragOptions.xaxes[0]._length;
var ph = dragOptions.yaxes[0]._length;
- var xAxisIds = dragOptions.xaxes.map(getAxId);
- var yAxisIds = dragOptions.yaxes.map(getAxId);
var allAxes = dragOptions.xaxes.concat(dragOptions.yaxes);
var subtract = e.altKey;
- var filterPoly, testPoly, mergedPolygons, currentPolygon;
- var i, cd, trace, searchInfo, eventData;
+ var filterPoly, selectionTester, mergedPolygons, currentPolygon;
+ var i, searchInfo, eventData;
- var selectingOnSameSubplot = (
- fullLayout._lastSelectedSubplot &&
- fullLayout._lastSelectedSubplot === plotinfo.id
- );
-
- if(
- selectingOnSameSubplot &&
- (e.shiftKey || e.altKey) &&
- (plotinfo.selection && plotinfo.selection.polygons) &&
- !dragOptions.polygons
- ) {
- // take over selection polygons from prev mode, if any
- dragOptions.polygons = plotinfo.selection.polygons;
- dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons;
- } else if(
- (!e.shiftKey && !e.altKey) ||
- ((e.shiftKey || e.altKey) && !plotinfo.selection)
- ) {
- // create new polygons, if shift mode or selecting across different subplots
- plotinfo.selection = {};
- plotinfo.selection.polygons = dragOptions.polygons = [];
- plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = [];
- }
-
- // clear selection outline when selecting a different subplot
- if(!selectingOnSameSubplot) {
- clearSelect(zoomLayer);
- fullLayout._lastSelectedSubplot = plotinfo.id;
- }
+ coerceSelectionsCache(e, gd, dragOptions);
if(mode === 'lasso') {
filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX);
@@ -106,52 +75,12 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
.attr('d', 'M0,0Z');
- // find the traces to search for selection points
- var searchTraces = [];
var throttleID = fullLayout._uid + constants.SELECTID;
var selection = [];
- for(i = 0; i < gd.calcdata.length; i++) {
- cd = gd.calcdata[i];
- trace = cd[0].trace;
-
- if(trace.visible !== true || !trace._module || !trace._module.selectPoints) continue;
-
- if(dragOptions.subplot) {
- if(
- trace.subplot === dragOptions.subplot ||
- trace.geo === dragOptions.subplot
- ) {
- searchTraces.push({
- _module: trace._module,
- cd: cd,
- xaxis: dragOptions.xaxes[0],
- yaxis: dragOptions.yaxes[0]
- });
- }
- } else if(
- trace.type === 'splom' &&
- // FIXME: make sure we don't have more than single axis for splom
- trace._xaxes[xAxisIds[0]] && trace._yaxes[yAxisIds[0]]
- ) {
- searchTraces.push({
- _module: trace._module,
- cd: cd,
- xaxis: dragOptions.xaxes[0],
- yaxis: dragOptions.yaxes[0]
- });
- } else {
- if(xAxisIds.indexOf(trace.xaxis) === -1) continue;
- if(yAxisIds.indexOf(trace.yaxis) === -1) continue;
-
- searchTraces.push({
- _module: trace._module,
- cd: cd,
- xaxis: getFromId(gd, trace.xaxis),
- yaxis: getFromId(gd, trace.yaxis)
- });
- }
- }
+ // find the traces to search for selection points
+ var searchTraces = determineSearchTraces(gd, dragOptions.xaxes,
+ dragOptions.yaxes, dragOptions.subplot);
function axValue(ax) {
var index = (ax._id.charAt(0) === 'y') ? 1 : 0;
@@ -253,24 +182,19 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
}
// create outline & tester
- if(dragOptions.polygons && dragOptions.polygons.length) {
+ if(dragOptions.selectionDefs && dragOptions.selectionDefs.length) {
mergedPolygons = mergePolygons(dragOptions.mergedPolygons, currentPolygon, subtract);
currentPolygon.subtract = subtract;
- testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon]));
+ selectionTester = multiTester(dragOptions.selectionDefs.concat([currentPolygon]));
}
else {
mergedPolygons = [currentPolygon];
- testPoly = polygonTester(currentPolygon);
+ selectionTester = polygonTester(currentPolygon);
}
// draw selection
- var paths = [];
- for(i = 0; i < mergedPolygons.length; i++) {
- var ppts = mergedPolygons[i];
- paths.push(ppts.join('L') + 'L' + ppts[0]);
- }
- outlines
- .attr('d', 'M' + paths.join('M') + 'Z');
+ drawSelection(mergedPolygons, outlines);
+
throttle.throttle(
throttleID,
@@ -282,7 +206,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
for(i = 0; i < searchTraces.length; i++) {
searchInfo = searchTraces[i];
- traceSelection = searchInfo._module.selectPoints(searchInfo, testPoly);
+ traceSelection = searchInfo._module.selectPoints(searchInfo, selectionTester);
traceSelections.push(traceSelection);
thisSelection = fillSelectionItem(traceSelection, searchInfo);
@@ -304,6 +228,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
};
dragOptions.clickFn = function(numClicks, evt) {
+ var clickmode = fullLayout.clickmode;
+
corners.remove();
throttle.done(throttleID).then(function() {
@@ -317,12 +243,23 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
}
updateSelectedState(gd, searchTraces);
+
+ clearSelectionsCache(dragOptions);
+
gd.emit('plotly_deselect', null);
- }
- else {
- // TODO: remove in v2 - this was probably never intended to work as it does,
- // but in case anyone depends on it we don't want to break it now.
- gd.emit('plotly_selected', undefined);
+ } else {
+ if(clickmode.indexOf('select') > -1) {
+ selectOnClick(evt, gd, dragOptions.xaxes, dragOptions.yaxes,
+ dragOptions.subplot, dragOptions, outlines);
+ }
+
+ if(clickmode === 'event') {
+ // TODO: remove in v2 - this was probably never intended to work as it does,
+ // but in case anyone depends on it we don't want to break it now.
+ // Note that click-to-select introduced pre v2 also emitts proper
+ // event data when clickmode is having 'select' in its flag list.
+ gd.emit('plotly_selected', undefined);
+ }
}
Fx.click(gd, evt);
@@ -336,10 +273,10 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
throttle.clear(throttleID);
dragOptions.gd.emit('plotly_selected', eventData);
- if(currentPolygon && dragOptions.polygons) {
+ if(currentPolygon && dragOptions.selectionDefs) {
// save last polygons
currentPolygon.subtract = subtract;
- dragOptions.polygons.push(currentPolygon);
+ dragOptions.selectionDefs.push(currentPolygon);
// we have to keep reference to arrays container
dragOptions.mergedPolygons.length = 0;
@@ -349,6 +286,380 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
};
}
+function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutlines) {
+ var hoverData = gd._hoverdata;
+ var clickmode = gd._fullLayout.clickmode;
+ var sendEvents = clickmode.indexOf('event') > -1;
+ var selection = [];
+ var searchTraces, searchInfo, currentSelectionDef, selectionTester, traceSelection;
+ var thisTracesSelection, pointOrBinSelected, subtract, eventData, i;
+
+ if(isHoverDataSet(hoverData)) {
+ coerceSelectionsCache(evt, gd, dragOptions);
+ searchTraces = determineSearchTraces(gd, xAxes, yAxes, subplot);
+ var clickedPtInfo = extractClickedPtInfo(hoverData, searchTraces);
+ var isBinnedTrace = clickedPtInfo.pointNumbers.length > 0;
+
+
+ // Note: potentially costly operation isPointOrBinSelected is
+ // called as late as possible through the use of an assignment
+ // in an if condition.
+ if(isBinnedTrace ?
+ isOnlyThisBinSelected(searchTraces, clickedPtInfo) :
+ isOnlyOnePointSelected(searchTraces) &&
+ (pointOrBinSelected = isPointOrBinSelected(clickedPtInfo)))
+ {
+ if(polygonOutlines) polygonOutlines.remove();
+ for(i = 0; i < searchTraces.length; i++) {
+ searchInfo = searchTraces[i];
+ searchInfo._module.selectPoints(searchInfo, false);
+ }
+
+ updateSelectedState(gd, searchTraces);
+
+ clearSelectionsCache(dragOptions);
+
+ if(sendEvents) {
+ gd.emit('plotly_deselect', null);
+ }
+ } else {
+ subtract = evt.shiftKey &&
+ (pointOrBinSelected !== undefined ?
+ pointOrBinSelected :
+ isPointOrBinSelected(clickedPtInfo));
+ currentSelectionDef = newPointSelectionDef(clickedPtInfo.pointNumber, clickedPtInfo.searchInfo, subtract);
+
+ var allSelectionDefs = dragOptions.selectionDefs.concat([currentSelectionDef]);
+ selectionTester = multiTester(allSelectionDefs);
+
+ for(i = 0; i < searchTraces.length; i++) {
+ traceSelection = searchTraces[i]._module.selectPoints(searchTraces[i], selectionTester);
+ thisTracesSelection = fillSelectionItem(traceSelection, searchTraces[i]);
+
+ if(selection.length) {
+ for(var j = 0; j < thisTracesSelection.length; j++) {
+ selection.push(thisTracesSelection[j]);
+ }
+ }
+ else selection = thisTracesSelection;
+ }
+
+ eventData = {points: selection};
+ updateSelectedState(gd, searchTraces, eventData);
+
+ if(currentSelectionDef && dragOptions) {
+ dragOptions.selectionDefs.push(currentSelectionDef);
+ }
+
+ if(polygonOutlines) drawSelection(dragOptions.mergedPolygons, polygonOutlines);
+
+ if(sendEvents) {
+ gd.emit('plotly_selected', eventData);
+ }
+ }
+ }
+}
+
+/**
+ * Constructs a new point selection definition object.
+ */
+function newPointSelectionDef(pointNumber, searchInfo, subtract) {
+ return {
+ pointNumber: pointNumber,
+ searchInfo: searchInfo,
+ subtract: subtract
+ };
+}
+
+function isPointSelectionDef(o) {
+ return 'pointNumber' in o && 'searchInfo' in o;
+}
+
+/*
+ * Constructs a new point number tester.
+ */
+function newPointNumTester(pointSelectionDef) {
+ return {
+ xmin: 0,
+ xmax: 0,
+ ymin: 0,
+ ymax: 0,
+ pts: [],
+ contains: function(pt, omitFirstEdge, pointNumber, searchInfo) {
+ var idxWantedTrace = pointSelectionDef.searchInfo.cd[0].trace._expandedIndex;
+ var idxActualTrace = searchInfo.cd[0].trace._expandedIndex;
+ return idxActualTrace === idxWantedTrace &&
+ pointNumber === pointSelectionDef.pointNumber;
+ },
+ isRect: false,
+ degenerate: false,
+ subtract: pointSelectionDef.subtract
+ };
+}
+
+/**
+ * Wraps multiple selection testers.
+ *
+ * @param {Array} list - An array of selection testers.
+ *
+ * @return a selection tester object with a contains function
+ * that can be called to evaluate a point against all wrapped
+ * selection testers that were passed in list.
+ */
+function multiTester(list) {
+ var testers = [];
+ var xmin = isPointSelectionDef(list[0]) ? 0 : list[0][0][0];
+ var xmax = xmin;
+ var ymin = isPointSelectionDef(list[0]) ? 0 : list[0][0][1];
+ var ymax = ymin;
+
+ for(var i = 0; i < list.length; i++) {
+ if(isPointSelectionDef(list[i])) {
+ testers.push(newPointNumTester(list[i]));
+ } else {
+ var tester = polygon.tester(list[i]);
+ tester.subtract = list[i].subtract;
+ testers.push(tester);
+ xmin = Math.min(xmin, tester.xmin);
+ xmax = Math.max(xmax, tester.xmax);
+ ymin = Math.min(ymin, tester.ymin);
+ ymax = Math.max(ymax, tester.ymax);
+ }
+ }
+
+ /**
+ * Tests if the given point is within this tester.
+ *
+ * @param {Array} pt - [0] is the x coordinate, [1] is the y coordinate of the point.
+ * @param {*} arg - An optional parameter to pass down to wrapped testers.
+ * @param {number} pointNumber - The point number of the point within the underlying data array.
+ * @param {number} searchInfo - An object identifying the trace the point is contained in.
+ *
+ * @return {boolean} true if point is considered to be selected, false otherwise.
+ */
+ function contains(pt, arg, pointNumber, searchInfo) {
+ var contained = false;
+ for(var i = 0; i < testers.length; i++) {
+ if(testers[i].contains(pt, arg, pointNumber, searchInfo)) {
+ // if contained by subtract tester - exclude the point
+ contained = testers[i].subtract === false;
+ }
+ }
+
+ return contained;
+ }
+
+ return {
+ xmin: xmin,
+ xmax: xmax,
+ ymin: ymin,
+ ymax: ymax,
+ pts: [],
+ contains: contains,
+ isRect: false,
+ degenerate: false
+ };
+}
+
+function coerceSelectionsCache(evt, gd, dragOptions) {
+ var fullLayout = gd._fullLayout;
+ var zoomLayer = fullLayout._zoomlayer;
+ var plotinfo = dragOptions.plotinfo;
+
+ var selectingOnSameSubplot = (
+ fullLayout._lastSelectedSubplot &&
+ fullLayout._lastSelectedSubplot === plotinfo.id
+ );
+ var hasModifierKey = evt.shiftKey || evt.altKey;
+ if(selectingOnSameSubplot && hasModifierKey &&
+ (plotinfo.selection && plotinfo.selection.selectionDefs) && !dragOptions.selectionDefs) {
+ // take over selection definitions from prev mode, if any
+ dragOptions.selectionDefs = plotinfo.selection.selectionDefs;
+ dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons;
+ } else if(!hasModifierKey || !plotinfo.selection) {
+ clearSelectionsCache(dragOptions);
+ }
+
+ // clear selection outline when selecting a different subplot
+ if(!selectingOnSameSubplot) {
+ clearSelect(zoomLayer);
+ fullLayout._lastSelectedSubplot = plotinfo.id;
+ }
+}
+
+function clearSelectionsCache(dragOptions) {
+ var plotinfo = dragOptions.plotinfo;
+
+ plotinfo.selection = {};
+ plotinfo.selection.selectionDefs = dragOptions.selectionDefs = [];
+ plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = [];
+}
+
+function determineSearchTraces(gd, xAxes, yAxes, subplot) {
+ var searchTraces = [];
+ var xAxisIds = xAxes.map(getAxId);
+ var yAxisIds = yAxes.map(getAxId);
+ var cd, trace, i;
+
+ for(i = 0; i < gd.calcdata.length; i++) {
+ cd = gd.calcdata[i];
+ trace = cd[0].trace;
+
+ if(trace.visible !== true || !trace._module || !trace._module.selectPoints) continue;
+
+ if(subplot && (trace.subplot === subplot || trace.geo === subplot)) {
+ searchTraces.push(createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]));
+ } else if(
+ trace.type === 'splom' &&
+ // FIXME: make sure we don't have more than single axis for splom
+ trace._xaxes[xAxisIds[0]] && trace._yaxes[yAxisIds[0]]
+ ) {
+ searchTraces.push(createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]));
+ } else {
+ if(xAxisIds.indexOf(trace.xaxis) === -1) continue;
+ if(yAxisIds.indexOf(trace.yaxis) === -1) continue;
+
+ searchTraces.push(createSearchInfo(trace._module, cd,
+ getFromId(gd, trace.xaxis), getFromId(gd, trace.yaxis)));
+ }
+ }
+
+ return searchTraces;
+
+ function createSearchInfo(module, calcData, xaxis, yaxis) {
+ return {
+ _module: module,
+ cd: calcData,
+ xaxis: xaxis,
+ yaxis: yaxis
+ };
+ }
+}
+
+function drawSelection(polygons, outlines) {
+ var paths = [];
+ var i, d;
+
+ for(i = 0; i < polygons.length; i++) {
+ var ppts = polygons[i];
+ paths.push(ppts.join('L') + 'L' + ppts[0]);
+ }
+
+ d = polygons.length > 0 ?
+ 'M' + paths.join('M') + 'Z' :
+ 'M0,0Z';
+ outlines.attr('d', d);
+}
+
+function isHoverDataSet(hoverData) {
+ return hoverData &&
+ Array.isArray(hoverData) &&
+ hoverData[0].hoverOnBox !== true;
+}
+
+function extractClickedPtInfo(hoverData, searchTraces) {
+ var hoverDatum = hoverData[0];
+ var pointNumber = -1;
+ var pointNumbers = [];
+ var searchInfo, i;
+
+ for(i = 0; i < searchTraces.length; i++) {
+ searchInfo = searchTraces[i];
+ if(hoverDatum.fullData._expandedIndex === searchInfo.cd[0].trace._expandedIndex) {
+
+ // Special case for box (and violin)
+ if(hoverDatum.hoverOnBox === true) {
+ break;
+ }
+
+ // Hint: in some traces like histogram, one graphical element
+ // doesn't correspond to one particular data point, but to
+ // bins of data points. Thus, hoverDatum can have a binNumber
+ // property instead of pointNumber.
+ if(hoverDatum.pointNumber !== undefined) {
+ pointNumber = hoverDatum.pointNumber;
+ } else if(hoverDatum.binNumber !== undefined) {
+ pointNumber = hoverDatum.binNumber;
+ pointNumbers = hoverDatum.pointNumbers;
+ }
+
+ break;
+ }
+ }
+
+ return {
+ pointNumber: pointNumber,
+ pointNumbers: pointNumbers,
+ searchInfo: searchInfo
+ };
+}
+
+function isPointOrBinSelected(clickedPtInfo) {
+ var trace = clickedPtInfo.searchInfo.cd[0].trace;
+ var ptNum = clickedPtInfo.pointNumber;
+ var ptNums = clickedPtInfo.pointNumbers;
+ var ptNumsSet = ptNums.length > 0;
+
+ // When pointsNumbers is set (e.g. histogram's binning),
+ // it is assumed that when the first point of
+ // a bin is selected, all others are as well
+ var ptNumToTest = ptNumsSet ? ptNums[0] : ptNum;
+
+ // TODO potential performance improvement
+ // Primarily we need this function to determine if a click adds
+ // or subtracts from a selection.
+ // In cases `trace.selectedpoints` is a huge array, indexOf
+ // might be slow. One remedy would be to introduce a hash somewhere.
+ return trace.selectedpoints ? trace.selectedpoints.indexOf(ptNumToTest) > -1 : false;
+}
+
+function isOnlyThisBinSelected(searchTraces, clickedPtInfo) {
+ var tracesWithSelectedPts = [];
+ var searchInfo, trace, isSameTrace, i;
+
+ for(i = 0; i < searchTraces.length; i++) {
+ searchInfo = searchTraces[i];
+ if(searchInfo.cd[0].trace.selectedpoints && searchInfo.cd[0].trace.selectedpoints.length > 0) {
+ tracesWithSelectedPts.push(searchInfo);
+ }
+ }
+
+ if(tracesWithSelectedPts.length === 1) {
+ isSameTrace = tracesWithSelectedPts[0] === clickedPtInfo.searchInfo;
+ if(isSameTrace) {
+ trace = clickedPtInfo.searchInfo.cd[0].trace;
+ if(trace.selectedpoints.length === clickedPtInfo.pointNumbers.length) {
+ for(i = 0; i < clickedPtInfo.pointNumbers.length; i++) {
+ if(trace.selectedpoints.indexOf(clickedPtInfo.pointNumbers[i]) < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function isOnlyOnePointSelected(searchTraces) {
+ var len = 0;
+ var searchInfo, trace, i;
+
+ for(i = 0; i < searchTraces.length; i++) {
+ searchInfo = searchTraces[i];
+ trace = searchInfo.cd[0].trace;
+ if(trace.selectedpoints) {
+ if(trace.selectedpoints.length > 1) return false;
+
+ len += trace.selectedpoints.length;
+ if(len > 1) return false;
+ }
+ }
+
+ return len === 1;
+}
+
function updateSelectedState(gd, searchTraces, eventData) {
var i, j, searchInfo, trace;
@@ -471,5 +782,6 @@ function clearSelect(zoomlayer) {
module.exports = {
prepSelect: prepSelect,
- clearSelect: clearSelect
+ clearSelect: clearSelect,
+ selectOnClick: selectOnClick
};
diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js
index e6d419503b5..83153e0dfe3 100644
--- a/src/plots/geo/geo.js
+++ b/src/plots/geo/geo.js
@@ -21,6 +21,7 @@ var Plots = require('../plots');
var Axes = require('../cartesian/axes');
var dragElement = require('../../components/dragelement');
var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
var createGeoZoom = require('./zoom');
var constants = require('./constants');
@@ -354,6 +355,7 @@ proto.updateFx = function(fullLayout, geoLayout) {
var gd = _this.graphDiv;
var bgRect = _this.bgRect;
var dragMode = fullLayout.dragmode;
+ var clickMode = fullLayout.clickmode;
if(_this.isStatic) return;
@@ -376,6 +378,44 @@ proto.updateFx = function(fullLayout, geoLayout) {
]);
}
+ var fillRangeItems;
+
+ if(dragMode === 'select') {
+ fillRangeItems = function(eventData, poly) {
+ var ranges = eventData.range = {};
+ ranges[_this.id] = [
+ invert([poly.xmin, poly.ymin]),
+ invert([poly.xmax, poly.ymax])
+ ];
+ };
+ } else if(dragMode === 'lasso') {
+ fillRangeItems = function(eventData, poly, pts) {
+ var dataPts = eventData.lassoPoints = {};
+ dataPts[_this.id] = pts.filtered.map(invert);
+ };
+ }
+
+ // Note: dragOptions is needed to be declared for all dragmodes because
+ // it's the object that holds persistent selection state.
+ var dragOptions = {
+ element: _this.bgRect.node(),
+ gd: gd,
+ plotinfo: {
+ id: _this.id,
+ xaxis: _this.xaxis,
+ yaxis: _this.yaxis,
+ fillRangeItems: fillRangeItems
+ },
+ xaxes: [_this.xaxis],
+ yaxes: [_this.yaxis],
+ subplot: _this.id,
+ clickFn: function(numClicks) {
+ if(numClicks === 2) {
+ fullLayout._zoomlayer.selectAll('.select-outline').remove();
+ }
+ }
+ };
+
if(dragMode === 'pan') {
bgRect.node().onmousedown = null;
bgRect.call(createGeoZoom(_this, geoLayout));
@@ -384,41 +424,6 @@ proto.updateFx = function(fullLayout, geoLayout) {
else if(dragMode === 'select' || dragMode === 'lasso') {
bgRect.on('.zoom', null);
- var fillRangeItems;
-
- if(dragMode === 'select') {
- fillRangeItems = function(eventData, poly) {
- var ranges = eventData.range = {};
- ranges[_this.id] = [
- invert([poly.xmin, poly.ymin]),
- invert([poly.xmax, poly.ymax])
- ];
- };
- } else if(dragMode === 'lasso') {
- fillRangeItems = function(eventData, poly, pts) {
- var dataPts = eventData.lassoPoints = {};
- dataPts[_this.id] = pts.filtered.map(invert);
- };
- }
-
- var dragOptions = {
- element: _this.bgRect.node(),
- gd: gd,
- plotinfo: {
- xaxis: _this.xaxis,
- yaxis: _this.yaxis,
- fillRangeItems: fillRangeItems
- },
- xaxes: [_this.xaxis],
- yaxes: [_this.yaxis],
- subplot: _this.id,
- clickFn: function(numClicks) {
- if(numClicks === 2) {
- fullLayout._zoomlayer.selectAll('.select-outline').remove();
- }
- }
- };
-
dragOptions.prepFn = function(e, startX, startY) {
prepSelect(e, startX, startY, dragOptions, dragMode);
};
@@ -440,15 +445,26 @@ proto.updateFx = function(fullLayout, geoLayout) {
});
bgRect.on('mouseout', function() {
+ if(gd._dragging) return;
dragElement.unhover(gd, d3.event);
});
bgRect.on('click', function() {
- // TODO: like pie and mapbox, this doesn't support right-click
- // actually this one is worse, as right-click starts a pan, or leaves
- // select in a weird state.
- // Also, only tangentially related, we should cancel hover during pan
- Fx.click(gd, d3.event);
+ // For select and lasso the dragElement is handling clicks
+ if(dragMode !== 'select' && dragMode !== 'lasso') {
+ if(clickMode.indexOf('select') > -1) {
+ selectOnClick(d3.event, gd, [_this.xaxis], [_this.yaxis],
+ _this.id, dragOptions);
+ }
+
+ if(clickMode.indexOf('event') > -1) {
+ // TODO: like pie and mapbox, this doesn't support right-click
+ // actually this one is worse, as right-click starts a pan, or leaves
+ // select in a weird state.
+ // Also, only tangentially related, we should cancel hover during pan
+ Fx.click(gd, d3.event);
+ }
+ }
});
};
diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js
index 2a3c9a9d489..99dc45152ec 100644
--- a/src/plots/gl3d/scene.js
+++ b/src/plots/gl3d/scene.js
@@ -170,6 +170,8 @@ function render(scene) {
}
function initializeGLPlot(scene, fullLayout, canvas, gl) {
+ var gd = scene.graphDiv;
+
var glplotOptions = {
canvas: canvas,
gl: gl,
@@ -220,7 +222,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) {
var update = {};
update[scene.id + '.camera'] = getLayoutCamera(scene.camera);
- scene.saveCamera(scene.graphDiv.layout);
+ scene.saveCamera(gd.layout);
scene.graphDiv.emit('plotly_relayout', update);
};
@@ -228,10 +230,14 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) {
scene.glplot.canvas.addEventListener('wheel', relayoutCallback.bind(null, scene), passiveSupported ? {passive: false} : false);
if(!scene.staticMode) {
- scene.glplot.canvas.addEventListener('webglcontextlost', function(ev) {
- Lib.warn('Lost WebGL context.');
- ev.preventDefault();
- });
+ scene.glplot.canvas.addEventListener('webglcontextlost', function(event) {
+ if(gd && gd.emit) {
+ gd.emit('plotly_webglcontextlost', {
+ event: event,
+ layer: scene.id
+ });
+ }
+ }, false);
}
if(!scene.camera) {
diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js
index c5332d05fd8..77da3871017 100644
--- a/src/plots/mapbox/mapbox.js
+++ b/src/plots/mapbox/mapbox.js
@@ -15,6 +15,7 @@ var Fx = require('../../components/fx');
var Lib = require('../../lib');
var dragElement = require('../../components/dragelement');
var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
var constants = require('./constants');
var layoutAttributes = require('./layout_attributes');
var createMapboxLayer = require('./layers');
@@ -176,15 +177,6 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
Fx.hover(gd, evt, self.id);
});
- map.on('click', function(evt) {
- // TODO: this does not support right-click. If we want to support it, we
- // would likely need to change mapbox to use dragElement instead of straight
- // mapbox event binding. Or perhaps better, make a simple wrapper with the
- // right mousedown, mousemove, and mouseup handlers just for a left/right click
- // pie would use this too.
- Fx.click(gd, evt.originalEvent);
- });
-
function unhover() {
Fx.loneUnhover(fullLayout._toppaper);
}
@@ -221,11 +213,34 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
gd.emit('plotly_relayout', evtData);
}
- // define clear select on map creation, to keep one ref per map,
+ // define event handlers on map creation, to keep one ref per map,
// so that map.on / map.off in updateFx works as expected
self.clearSelect = function() {
gd._fullLayout._zoomlayer.selectAll('.select-outline').remove();
};
+
+ /**
+ * Returns a click handler function that is supposed
+ * to handle clicks in pan mode.
+ */
+ self.onClickInPanFn = function(dragOptions) {
+ return function(evt) {
+ var clickMode = gd._fullLayout.clickmode;
+
+ if(clickMode.indexOf('select') > -1) {
+ selectOnClick(evt.originalEvent, gd, [self.xaxis], [self.yaxis], self.id, dragOptions);
+ }
+
+ if(clickMode.indexOf('event') > -1) {
+ // TODO: this does not support right-click. If we want to support it, we
+ // would likely need to change mapbox to use dragElement instead of straight
+ // mapbox event binding. Or perhaps better, make a simple wrapper with the
+ // right mousedown, mousemove, and mouseup handlers just for a left/right click
+ // pie would use this too.
+ Fx.click(gd, evt.originalEvent);
+ }
+ };
+ };
};
proto.updateMap = function(calcData, fullLayout, resolve, reject) {
@@ -382,32 +397,50 @@ proto.updateFx = function(fullLayout) {
};
}
+ // Note: dragOptions is needed to be declared for all dragmodes because
+ // it's the object that holds persistent selection state.
+ // Merge old dragOptions with new to keep possibly initialized
+ // persistent selection state.
+ var oldDragOptions = self.dragOptions;
+ self.dragOptions = Lib.extendDeep(oldDragOptions || {}, {
+ element: self.div,
+ gd: gd,
+ plotinfo: {
+ id: self.id,
+ xaxis: self.xaxis,
+ yaxis: self.yaxis,
+ fillRangeItems: fillRangeItems
+ },
+ xaxes: [self.xaxis],
+ yaxes: [self.yaxis],
+ subplot: self.id
+ });
+
+ // Unregister the old handler before potentially registering
+ // a new one. Otherwise multiple click handlers might
+ // be registered resulting in unwanted behavior.
+ map.off('click', self.onClickInPanHandler);
if(dragMode === 'select' || dragMode === 'lasso') {
map.dragPan.disable();
map.on('zoomstart', self.clearSelect);
- var dragOptions = {
- element: self.div,
- gd: gd,
- plotinfo: {
- xaxis: self.xaxis,
- yaxis: self.yaxis,
- fillRangeItems: fillRangeItems
- },
- xaxes: [self.xaxis],
- yaxes: [self.yaxis],
- subplot: self.id
+ self.dragOptions.prepFn = function(e, startX, startY) {
+ prepSelect(e, startX, startY, self.dragOptions, dragMode);
};
- dragOptions.prepFn = function(e, startX, startY) {
- prepSelect(e, startX, startY, dragOptions, dragMode);
- };
-
- dragElement.init(dragOptions);
+ dragElement.init(self.dragOptions);
} else {
map.dragPan.enable();
map.off('zoomstart', self.clearSelect);
self.div.onmousedown = null;
+
+ // TODO: this does not support right-click. If we want to support it, we
+ // would likely need to change mapbox to use dragElement instead of straight
+ // mapbox event binding. Or perhaps better, make a simple wrapper with the
+ // right mousedown, mousemove, and mouseup handlers just for a left/right click
+ // pie would use this too.
+ self.onClickInPanHandler = self.onClickInPanFn(self.dragOptions);
+ map.on('click', self.onClickInPanHandler);
}
};
diff --git a/src/plots/plots.js b/src/plots/plots.js
index da202ed8005..4cc227fb3cb 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -1894,6 +1894,10 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) {
return d.map(stripObj);
}
+ if(Lib.isTypedArray(d)) {
+ return Lib.simpleMap(d, Lib.identity);
+ }
+
// convert native dates to date strings...
// mostly for external users exporting to plotly
if(Lib.isJSDate(d)) return Lib.ms2DateTimeLocal(+d);
diff --git a/src/plots/polar/constants.js b/src/plots/polar/constants.js
index ca0c0734803..71b196f422c 100644
--- a/src/plots/polar/constants.js
+++ b/src/plots/polar/constants.js
@@ -22,10 +22,10 @@ module.exports = {
'angular-grid',
'radial-grid',
'frontplot',
- 'angular-axis',
- 'radial-axis',
'angular-line',
- 'radial-line'
+ 'radial-line',
+ 'angular-axis',
+ 'radial-axis'
],
radialDragBoxSize: 50,
diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js
index ab2c880daeb..d7fa1d6c31f 100644
--- a/src/plots/polar/layout_attributes.js
+++ b/src/plots/polar/layout_attributes.js
@@ -112,16 +112,6 @@ var radialAxisAttrs = {
hoverformat: axesAttrs.hoverformat,
- // More attributes:
-
- // We'll need some attribute that determines the span
- // to draw donut-like charts
- // e.g. https://github.com/matplotlib/matplotlib/issues/4217
- //
- // maybe something like 'span' or 'hole' (like pie, but pie set it in data coords?)
- // span: {},
- // hole: 1
-
editType: 'calc'
};
@@ -256,6 +246,17 @@ module.exports = {
'with *0* corresponding to rightmost limit of the polar subplot.'
].join(' ')
},
+ hole: {
+ valType: 'number',
+ min: 0,
+ max: 1,
+ dflt: 0,
+ editType: 'plot',
+ role: 'info',
+ description: [
+ 'Sets the fraction of the radius to cut out of the polar subplot.'
+ ].join(' ')
+ },
bgcolor: {
valType: 'color',
diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js
index f8bb7a9103b..462916536ac 100644
--- a/src/plots/polar/layout_defaults.js
+++ b/src/plots/polar/layout_defaults.js
@@ -30,6 +30,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
opts.bgColor = Color.combine(bgColor, opts.paper_bgcolor);
var sector = coerce('sector');
+ coerce('hole');
// could optimize, subplotData is not always needed!
var subplotData = getSubplotData(opts.fullData, constants.name, opts.id);
diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js
index e550b52b325..6c78215d339 100644
--- a/src/plots/polar/polar.js
+++ b/src/plots/polar/polar.js
@@ -25,6 +25,7 @@ var dragElement = require('../../components/dragelement');
var Fx = require('../../components/fx');
var Titles = require('../../components/titles');
var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
var clearSelect = require('../cartesian/select').clearSelect;
var setCursor = require('../../lib/setcursor');
@@ -89,7 +90,7 @@ proto.plot = function(polarCalcData, fullLayout) {
_this.updateLayers(fullLayout, polarLayout);
_this.updateLayout(fullLayout, polarLayout);
Plots.generalUpdatePerTraceModule(_this.gd, _this, polarCalcData, polarLayout);
- _this.updateFx(fullLayout);
+ _this.updateFx(fullLayout, polarLayout);
};
proto.updateLayers = function(fullLayout, polarLayout) {
@@ -104,17 +105,17 @@ proto.updateLayers = function(fullLayout, polarLayout) {
var isAngularAxisBelowTraces = angularLayout.layer === 'below traces';
var isRadialAxisBelowTraces = radialLayout.layer === 'below traces';
- if(isAngularAxisBelowTraces) layerData.push('angular-axis');
- if(isRadialAxisBelowTraces) layerData.push('radial-axis');
if(isAngularAxisBelowTraces) layerData.push('angular-line');
if(isRadialAxisBelowTraces) layerData.push('radial-line');
+ if(isAngularAxisBelowTraces) layerData.push('angular-axis');
+ if(isRadialAxisBelowTraces) layerData.push('radial-axis');
layerData.push('frontplot');
- if(!isAngularAxisBelowTraces) layerData.push('angular-axis');
- if(!isRadialAxisBelowTraces) layerData.push('radial-axis');
if(!isAngularAxisBelowTraces) layerData.push('angular-line');
if(!isRadialAxisBelowTraces) layerData.push('radial-line');
+ if(!isAngularAxisBelowTraces) layerData.push('angular-axis');
+ if(!isRadialAxisBelowTraces) layerData.push('radial-axis');
var join = _this.framework.selectAll('.polarsublayer')
.data(layerData, String);
@@ -126,9 +127,9 @@ proto.updateLayers = function(fullLayout, polarLayout) {
switch(d) {
case 'frontplot':
- sel.append('g').classed('scatterlayer', true);
// TODO add option to place in 'backplot' layer??
sel.append('g').classed('barlayer', true);
+ sel.append('g').classed('scatterlayer', true);
break;
case 'backplot':
sel.append('g').classed('maplayer', true);
@@ -234,6 +235,8 @@ proto.updateLayout = function(fullLayout, polarLayout) {
var yOffset2 = _this.yOffset2 = gs.t + gs.h * (1 - yDomain2[1]);
// circle radius in px
var radius = _this.radius = xLength2 / dxSectorBBox;
+ // 'inner' radius in px (when polar.hole is set)
+ var innerRadius = _this.innerRadius = polarLayout.hole * radius;
// circle center position in px
var cx = _this.cx = xOffset2 - radius * sectorBBox[0];
var cy = _this.cy = yOffset2 + radius * sectorBBox[3];
@@ -252,7 +255,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
clockwise: 'bottom'
}[radialLayout.side],
// spans length 1 radius
- domain: [0, radius / gs.w]
+ domain: [innerRadius / gs.w, radius / gs.w]
});
_this.angularAxis = _this.mockAxis(fullLayout, polarLayout, angularLayout, {
@@ -282,7 +285,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
domain: yDomain2
});
- var dPath = _this.pathSector();
+ var dPath = _this.pathSubplot();
_this.clipPaths.forTraces.select('path')
.attr('d', dPath)
@@ -333,9 +336,9 @@ proto.mockCartesianAxis = function(fullLayout, polarLayout, opts) {
ax.setRange = function() {
var sectorBBox = _this.sectorBBox;
- var rl = _this.radialAxis._rl;
- var drl = rl[1] - rl[0];
var ind = bboxIndices[axId];
+ var rl = _this.radialAxis._rl;
+ var drl = (rl[1] - rl[0]) / (1 - polarLayout.hole);
ax.range = [sectorBBox[ind[0]] * drl, sectorBBox[ind[1]] * drl];
};
@@ -371,6 +374,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
var gd = _this.gd;
var layers = _this.layers;
var radius = _this.radius;
+ var innerRadius = _this.innerRadius;
var cx = _this.cx;
var cy = _this.cy;
var radialLayout = polarLayout.radialaxis;
@@ -392,12 +396,12 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
// easier to set rotate angle with custom translate function
ax._transfn = function(d) {
- return 'translate(' + ax.l2p(d.x) + ',0)';
+ return 'translate(' + (ax.l2p(d.x) + innerRadius) + ',0)';
};
// set special grid path function
ax._gridpath = function(d) {
- return _this.pathArc(ax.r2p(d.x));
+ return _this.pathArc(ax.r2p(d.x) + innerRadius);
};
var newTickLayout = strTickLayout(radialLayout);
@@ -428,7 +432,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
.selectAll('path').attr('transform', null);
updateElement(layers['radial-line'].select('line'), radialLayout.showline, {
- x1: 0,
+ x1: innerRadius,
y1: 0,
x2: radius,
y2: 0,
@@ -479,6 +483,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
var gd = _this.gd;
var layers = _this.layers;
var radius = _this.radius;
+ var innerRadius = _this.innerRadius;
var cx = _this.cx;
var cy = _this.cy;
var angularLayout = polarLayout.angularaxis;
@@ -491,11 +496,6 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
// 't'ick to 'g'eometric radians is used all over the place here
var t2g = function(d) { return ax.t2g(d.x); };
- // (x,y) at max radius
- function rad2xy(rad) {
- return [radius * Math.cos(rad), radius * Math.sin(rad)];
- }
-
// run rad2deg on tick0 and ditck for thetaunit: 'radians' axes
if(ax.type === 'linear' && ax.thetaunit === 'radians') {
ax.tick0 = rad2deg(ax.tick0);
@@ -512,13 +512,17 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
}
ax._transfn = function(d) {
+ var sel = d3.select(this);
+ var hasElement = sel && sel.node();
+
+ // don't translate grid lines
+ if(hasElement && sel.classed('angularaxisgrid')) return '';
+
var rad = t2g(d);
- var xy = rad2xy(rad);
- var out = strTranslate(cx + xy[0], cy - xy[1]);
+ var out = strTranslate(cx + radius * Math.cos(rad), cy - radius * Math.sin(rad));
- // must also rotate ticks, but don't rotate labels and grid lines
- var sel = d3.select(this);
- if(sel && sel.node() && sel.classed('ticks')) {
+ // must also rotate ticks, but don't rotate labels
+ if(hasElement && sel.classed('ticks')) {
out += strRotate(-rad2deg(rad));
}
@@ -527,8 +531,10 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
ax._gridpath = function(d) {
var rad = t2g(d);
- var xy = rad2xy(rad);
- return 'M0,0L' + (-xy[0]) + ',' + xy[1];
+ var cosRad = Math.cos(rad);
+ var sinRad = Math.sin(rad);
+ return 'M' + [cx + innerRadius * cosRad, cy - innerRadius * sinRad] +
+ 'L' + [cx + radius * cosRad, cy - radius * sinRad];
};
var offset4fontsize = (angularLayout.ticks !== 'outside' ? 0.7 : 0.5);
@@ -590,18 +596,22 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
}
_this.vangles = vangles;
+ // TODO maybe two arcs is better here?
+ // maybe split style attributes between inner and outer angular axes?
+
updateElement(layers['angular-line'].select('path'), angularLayout.showline, {
- d: _this.pathSector(),
+ d: _this.pathSubplot(),
transform: strTranslate(cx, cy)
})
.attr('stroke-width', angularLayout.linewidth)
.call(Color.stroke, angularLayout.linecolor);
};
-proto.updateFx = function(fullLayout) {
+proto.updateFx = function(fullLayout, polarLayout) {
if(!this.gd._context.staticPlot) {
this.updateAngularDrag(fullLayout);
- this.updateRadialDrag(fullLayout);
+ this.updateRadialDrag(fullLayout, polarLayout, 0);
+ this.updateRadialDrag(fullLayout, polarLayout, 1);
this.updateMainDrag(fullLayout);
}
};
@@ -614,12 +624,14 @@ proto.updateMainDrag = function(fullLayout) {
var MINZOOM = constants.MINZOOM;
var OFFEDGE = constants.OFFEDGE;
var radius = _this.radius;
+ var innerRadius = _this.innerRadius;
var cx = _this.cx;
var cy = _this.cy;
var cxx = _this.cxx;
var cyy = _this.cyy;
var sectorInRad = _this.sectorInRad;
var vangles = _this.vangles;
+ var radialAxis = _this.radialAxis;
var clampTiny = helpers.clampTiny;
var findXYatLength = helpers.findXYatLength;
var findEnclosingVertexAngles = helpers.findEnclosingVertexAngles;
@@ -629,7 +641,7 @@ proto.updateMainDrag = function(fullLayout) {
var mainDrag = dragBox.makeDragger(layers, 'path', 'maindrag', 'crosshair');
d3.select(mainDrag)
- .attr('d', _this.pathSector())
+ .attr('d', _this.pathSubplot())
.attr('transform', strTranslate(cx, cy));
var dragOpts = {
@@ -637,6 +649,7 @@ proto.updateMainDrag = function(fullLayout) {
gd: gd,
subplot: _this.id,
plotinfo: {
+ id: _this.id,
xaxis: _this.xaxis,
yaxis: _this.yaxis
},
@@ -727,7 +740,7 @@ proto.updateMainDrag = function(fullLayout) {
function zoomPrep() {
r0 = null;
r1 = null;
- path0 = _this.pathSector();
+ path0 = _this.pathSubplot();
dimmed = false;
var polarLayoutNow = gd._fullLayout[_this.id];
@@ -742,7 +755,7 @@ proto.updateMainDrag = function(fullLayout) {
// N.B. this sets scoped 'r0' and 'r1'
// return true if 'valid' zoom distance, false otherwise
function clampAndSetR0R1(rr0, rr1) {
- rr1 = Math.min(rr1, radius);
+ rr1 = Math.max(Math.min(rr1, radius), innerRadius);
// starting or ending drag near center (outer edge),
// clamps radial distance at origin (at r=radius)
@@ -831,16 +844,38 @@ proto.updateMainDrag = function(fullLayout) {
dragBox.showDoubleClickNotifier(gd);
- var radialAxis = _this.radialAxis;
var rl = radialAxis._rl;
- var drl = rl[1] - rl[0];
- var updateObj = {};
- updateObj[_this.id + '.radialaxis.range'] = [
- rl[0] + r0 * drl / radius,
- rl[0] + r1 * drl / radius
+ var m = (rl[1] - rl[0]) / (1 - innerRadius / radius) / radius;
+ var newRng = [
+ rl[0] + (r0 - innerRadius) * m,
+ rl[0] + (r1 - innerRadius) * m
];
+ Registry.call('relayout', gd, _this.id + '.radialaxis.range', newRng);
+ }
- Registry.call('relayout', gd, updateObj);
+ function zoomClick(numClicks, evt) {
+ var clickMode = gd._fullLayout.clickmode;
+
+ dragBox.removeZoombox(gd);
+
+ // TODO double once vs twice logic (autorange vs fixed range)
+ if(numClicks === 2) {
+ var updateObj = {};
+ for(var k in _this.viewInitial) {
+ updateObj[_this.id + '.' + k] = _this.viewInitial[k];
+ }
+
+ gd.emit('plotly_doubleclick', null);
+ Registry.call('relayout', gd, updateObj);
+ }
+
+ if(clickMode.indexOf('select') > -1 && numClicks === 1) {
+ selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, dragOpts);
+ }
+
+ if(clickMode.indexOf('event') > -1) {
+ Fx.click(gd, evt, _this.id);
+ }
}
dragOpts.prepFn = function(evt, startX, startY) {
@@ -865,6 +900,7 @@ proto.updateMainDrag = function(fullLayout) {
} else {
dragOpts.moveFn = zoomMove;
}
+ dragOpts.clickFn = zoomClick;
dragOpts.doneFn = zoomDone;
zoomPrep(evt, startX, startY);
break;
@@ -875,23 +911,6 @@ proto.updateMainDrag = function(fullLayout) {
}
};
- dragOpts.clickFn = function(numClicks, evt) {
- dragBox.removeZoombox(gd);
-
- // TODO double once vs twice logic (autorange vs fixed range)
- if(numClicks === 2) {
- var updateObj = {};
- for(var k in _this.viewInitial) {
- updateObj[_this.id + '.' + k] = _this.viewInitial[k];
- }
-
- gd.emit('plotly_doubleclick', null);
- Registry.call('relayout', gd, updateObj);
- }
-
- Fx.click(gd, evt, _this.id);
- };
-
mainDrag.onmousemove = function(evt) {
Fx.hover(gd, evt, _this.id);
gd._fullLayout._lasthover = mainDrag;
@@ -906,38 +925,52 @@ proto.updateMainDrag = function(fullLayout) {
dragElement.init(dragOpts);
};
-proto.updateRadialDrag = function(fullLayout) {
+proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) {
var _this = this;
var gd = _this.gd;
var layers = _this.layers;
var radius = _this.radius;
+ var innerRadius = _this.innerRadius;
var cx = _this.cx;
var cy = _this.cy;
var radialAxis = _this.radialAxis;
+ var bl = constants.radialDragBoxSize;
+ var bl2 = bl / 2;
if(!radialAxis.visible) return;
var angle0 = deg2rad(_this.radialAxisAngle);
- var rl0 = radialAxis._rl[0];
- var rl1 = radialAxis._rl[1];
- var drl = rl1 - rl0;
+ var rl = radialAxis._rl;
+ var rl0 = rl[0];
+ var rl1 = rl[1];
+ var rbase = rl[rngIndex];
+ var m = 0.75 * (rl[1] - rl[0]) / (1 - polarLayout.hole) / radius;
+
+ var tx, ty, className;
+ if(rngIndex) {
+ tx = cx + (radius + bl2) * Math.cos(angle0);
+ ty = cy - (radius + bl2) * Math.sin(angle0);
+ className = 'radialdrag';
+ } else {
+ // the 'inner' box can get called:
+ // - when polar.hole>0
+ // - when polar.sector isn't a full circle
+ // otherwise it is hidden behind the main drag.
+ tx = cx + (innerRadius - bl2) * Math.cos(angle0);
+ ty = cy - (innerRadius - bl2) * Math.sin(angle0);
+ className = 'radialdrag-inner';
+ }
- var bl = constants.radialDragBoxSize;
- var bl2 = bl / 2;
- var radialDrag = dragBox.makeRectDragger(layers, 'radialdrag', 'crosshair', -bl2, -bl2, bl, bl);
+ var radialDrag = dragBox.makeRectDragger(layers, className, 'crosshair', -bl2, -bl2, bl, bl);
var dragOpts = {element: radialDrag, gd: gd};
- var tx = cx + (radius + bl2) * Math.cos(angle0);
- var ty = cy - (radius + bl2) * Math.sin(angle0);
-
- d3.select(radialDrag)
- .attr('transform', strTranslate(tx, ty));
+ d3.select(radialDrag).attr('transform', strTranslate(tx, ty));
// move function (either rotate or re-range flavor)
var moveFn2;
// rotate angle on done
var angle1;
- // re-range range[1] on done
- var rng1;
+ // re-range range[1] (or range[0]) on done
+ var rprime;
function moveFn(dx, dy) {
if(moveFn2) {
@@ -958,12 +991,15 @@ proto.updateRadialDrag = function(fullLayout) {
function doneFn() {
if(angle1 !== null) {
Registry.call('relayout', gd, _this.id + '.radialaxis.angle', angle1);
- } else if(rng1 !== null) {
- Registry.call('relayout', gd, _this.id + '.radialaxis.range[1]', rng1);
+ } else if(rprime !== null) {
+ Registry.call('relayout', gd, _this.id + '.radialaxis.range[' + rngIndex + ']', rprime);
}
}
function rotateMove(dx, dy) {
+ // disable for inner drag boxes
+ if(rngIndex === 0) return;
+
var x1 = tx + dx;
var y1 = ty + dy;
@@ -983,14 +1019,17 @@ proto.updateRadialDrag = function(fullLayout) {
function rerangeMove(dx, dy) {
// project (dx, dy) unto unit radial axis vector
var dr = Lib.dot([dx, -dy], [Math.cos(angle0), Math.sin(angle0)]);
- rng1 = rl1 - drl * dr / radius * 0.75;
+ rprime = rbase - m * dr;
- // make sure new range[1] does not change the range[0] -> range[1] sign
- if((drl > 0) !== (rng1 > rl0)) return;
+ // make sure rprime does not change the range[0] -> range[1] sign
+ if((m > 0) !== (rngIndex ? rprime > rl0 : rprime < rl1)) {
+ rprime = null;
+ return;
+ }
// update radial range -> update c2g -> update _m,_b
- radialAxis.range[1] = rng1;
- radialAxis._rl[1] = rng1;
+ radialAxis.range[rngIndex] = rprime;
+ radialAxis._rl[rngIndex] = rprime;
radialAxis.setGeometry();
radialAxis.setScale();
@@ -1018,7 +1057,7 @@ proto.updateRadialDrag = function(fullLayout) {
dragOpts.prepFn = function() {
moveFn2 = null;
angle1 = null;
- rng1 = null;
+ rprime = null;
dragOpts.moveFn = moveFn;
dragOpts.doneFn = doneFn;
@@ -1193,7 +1232,6 @@ proto.isPtInside = function(d) {
};
proto.pathArc = function(r) {
- r = r || this.radius;
var sectorInRad = this.sectorInRad;
var vangles = this.vangles;
var fn = vangles ? helpers.pathPolygon : Lib.pathArc;
@@ -1201,7 +1239,6 @@ proto.pathArc = function(r) {
};
proto.pathSector = function(r) {
- r = r || this.radius;
var sectorInRad = this.sectorInRad;
var vangles = this.vangles;
var fn = vangles ? helpers.pathPolygon : Lib.pathSector;
@@ -1215,6 +1252,12 @@ proto.pathAnnulus = function(r0, r1) {
return fn(r0, r1, sectorInRad[0], sectorInRad[1], vangles);
};
+proto.pathSubplot = function() {
+ var r0 = this.innerRadius;
+ var r1 = this.radius;
+ return r0 ? this.pathAnnulus(r0, r1) : this.pathSector(r1);
+};
+
proto.fillViewInitialKey = function(key, val) {
if(!(key in this.viewInitial)) {
this.viewInitial[key] = val;
diff --git a/src/plots/polar/set_convert.js b/src/plots/polar/set_convert.js
index 7eb8af9b852..684a8224cc8 100644
--- a/src/plots/polar/set_convert.js
+++ b/src/plots/polar/set_convert.js
@@ -34,7 +34,7 @@ var rad2deg = Lib.rad2deg;
*
* Radial axis coordinate systems:
* - d, c and l: same as for cartesian axes
- * - g: like calcdata but translated about `radialaxis.range[0]`
+ * - g: like calcdata but translated about `radialaxis.range[0]` & `polar.hole`
*
* Angular axis coordinate systems:
* - d: data, in whatever form it's provided
@@ -61,25 +61,29 @@ module.exports = function setConvert(ax, polarLayout, fullLayout) {
};
function setConvertRadial(ax, polarLayout) {
+ var subplot = polarLayout._subplot;
+
ax.setGeometry = function() {
var rl0 = ax._rl[0];
var rl1 = ax._rl[1];
+ var b = subplot.innerRadius;
+ var m = (subplot.radius - b) / (rl1 - rl0);
+ var b2 = b / m;
+
var rFilter = rl0 > rl1 ?
function(v) { return v <= 0; } :
function(v) { return v >= 0; };
ax.c2g = function(v) {
var r = ax.c2l(v) - rl0;
- return rFilter(r) ? r : 0;
+ return (rFilter(r) ? r : 0) + b2;
};
ax.g2c = function(v) {
- return ax.l2c(v + rl0);
+ return ax.l2c(v + rl0 - b2);
};
- var m = polarLayout._subplot.radius / (rl1 - rl0);
-
ax.g2p = function(v) { return v * m; };
ax.c2p = function(v) { return ax.g2p(ax.c2g(v)); };
};
diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js
index fa5a265cb01..fd119f5f336 100644
--- a/src/plots/ternary/index.js
+++ b/src/plots/ternary/index.js
@@ -17,17 +17,29 @@ var TERNARY = 'ternary';
exports.name = TERNARY;
-exports.attr = 'subplot';
+var attr = exports.attr = 'subplot';
exports.idRoot = TERNARY;
exports.idRegex = exports.attrRegex = counterRegex(TERNARY);
-exports.attributes = require('./layout/attributes');
+var attributes = exports.attributes = {};
+attributes[attr] = {
+ valType: 'subplotid',
+ role: 'info',
+ dflt: 'ternary',
+ editType: 'calc',
+ description: [
+ 'Sets a reference between this trace\'s data coordinates and',
+ 'a ternary subplot.',
+ 'If *ternary* (the default value), the data refer to `layout.ternary`.',
+ 'If *ternary2*, the data refer to `layout.ternary2`, and so on.'
+ ].join(' ')
+};
-exports.layoutAttributes = require('./layout/layout_attributes');
+exports.layoutAttributes = require('./layout_attributes');
-exports.supplyLayoutDefaults = require('./layout/defaults');
+exports.supplyLayoutDefaults = require('./layout_defaults');
exports.plot = function plotTernary(gd) {
var fullLayout = gd._fullLayout;
diff --git a/src/plots/ternary/layout/attributes.js b/src/plots/ternary/layout/attributes.js
deleted file mode 100644
index 585b109cb10..00000000000
--- a/src/plots/ternary/layout/attributes.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
-* Copyright 2012-2018, 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';
-
-
-module.exports = {
- subplot: {
- valType: 'subplotid',
- role: 'info',
- dflt: 'ternary',
- editType: 'calc',
- description: [
- 'Sets a reference between this trace\'s data coordinates and',
- 'a ternary subplot.',
- 'If *ternary* (the default value), the data refer to `layout.ternary`.',
- 'If *ternary2*, the data refer to `layout.ternary2`, and so on.'
- ].join(' ')
- }
-};
diff --git a/src/plots/ternary/layout/axis_defaults.js b/src/plots/ternary/layout/axis_defaults.js
deleted file mode 100644
index aa4f984c22a..00000000000
--- a/src/plots/ternary/layout/axis_defaults.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
-* Copyright 2012-2018, 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 Lib = require('../../../lib');
-var layoutAttributes = require('./axis_attributes');
-var handleTickLabelDefaults = require('../../cartesian/tick_label_defaults');
-var handleTickMarkDefaults = require('../../cartesian/tick_mark_defaults');
-var handleTickValueDefaults = require('../../cartesian/tick_value_defaults');
-var handleLineGridDefaults = require('../../cartesian/line_grid_defaults');
-
-module.exports = function supplyLayoutDefaults(containerIn, containerOut, options) {
- function coerce(attr, dflt) {
- return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt);
- }
-
- containerOut.type = 'linear'; // no other types allowed for ternary
-
- var dfltColor = coerce('color');
- // if axis.color was provided, use it for fonts too; otherwise,
- // inherit from global font color in case that was provided.
- var dfltFontColor = (dfltColor !== layoutAttributes.color.dflt) ? dfltColor : options.font.color;
-
- var axName = containerOut._name,
- letterUpper = axName.charAt(0).toUpperCase(),
- dfltTitle = 'Component ' + letterUpper;
-
- var title = coerce('title', dfltTitle);
- containerOut._hovertitle = title === dfltTitle ? title : letterUpper;
-
- Lib.coerceFont(coerce, 'titlefont', {
- family: options.font.family,
- size: Math.round(options.font.size * 1.2),
- color: dfltFontColor
- });
-
- // range is just set by 'min' - max is determined by the other axes mins
- coerce('min');
-
- handleTickValueDefaults(containerIn, containerOut, coerce, 'linear');
- handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear', {});
- handleTickMarkDefaults(containerIn, containerOut, coerce,
- { outerTicks: true });
-
- var showTickLabels = coerce('showticklabels');
- if(showTickLabels) {
- Lib.coerceFont(coerce, 'tickfont', {
- family: options.font.family,
- size: options.font.size,
- color: dfltFontColor
- });
- coerce('tickangle');
- coerce('tickformat');
- }
-
- handleLineGridDefaults(containerIn, containerOut, coerce, {
- dfltColor: dfltColor,
- bgColor: options.bgColor,
- // default grid color is darker here (60%, vs cartesian default ~91%)
- // because the grid is not square so the eye needs heavier cues to follow
- blend: 60,
- showLine: true,
- showGrid: true,
- noZeroLine: true,
- attributes: layoutAttributes
- });
-
- coerce('hoverformat');
- coerce('layer');
-};
diff --git a/src/plots/ternary/layout/defaults.js b/src/plots/ternary/layout/defaults.js
deleted file mode 100644
index 12bcdf22499..00000000000
--- a/src/plots/ternary/layout/defaults.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
-* Copyright 2012-2018, 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 Color = require('../../../components/color');
-var Template = require('../../../plot_api/plot_template');
-
-var handleSubplotDefaults = require('../../subplot_defaults');
-var layoutAttributes = require('./layout_attributes');
-var handleAxisDefaults = require('./axis_defaults');
-
-var axesNames = ['aaxis', 'baxis', 'caxis'];
-
-module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
- handleSubplotDefaults(layoutIn, layoutOut, fullData, {
- type: 'ternary',
- attributes: layoutAttributes,
- handleDefaults: handleTernaryDefaults,
- font: layoutOut.font,
- paper_bgcolor: layoutOut.paper_bgcolor
- });
-};
-
-function handleTernaryDefaults(ternaryLayoutIn, ternaryLayoutOut, coerce, options) {
- var bgColor = coerce('bgcolor');
- var sum = coerce('sum');
- options.bgColor = Color.combine(bgColor, options.paper_bgcolor);
- var axName, containerIn, containerOut;
-
- // TODO: allow most (if not all) axis attributes to be set
- // in the outer container and used as defaults in the individual axes?
-
- for(var j = 0; j < axesNames.length; j++) {
- axName = axesNames[j];
- containerIn = ternaryLayoutIn[axName] || {};
- containerOut = Template.newContainer(ternaryLayoutOut, axName);
- containerOut._name = axName;
-
- handleAxisDefaults(containerIn, containerOut, options);
- }
-
- // if the min values contradict each other, set them all to default (0)
- // and delete *all* the inputs so the user doesn't get confused later by
- // changing one and having them all change.
- var aaxis = ternaryLayoutOut.aaxis,
- baxis = ternaryLayoutOut.baxis,
- caxis = ternaryLayoutOut.caxis;
- if(aaxis.min + baxis.min + caxis.min >= sum) {
- aaxis.min = 0;
- baxis.min = 0;
- caxis.min = 0;
- if(ternaryLayoutIn.aaxis) delete ternaryLayoutIn.aaxis.min;
- if(ternaryLayoutIn.baxis) delete ternaryLayoutIn.baxis.min;
- if(ternaryLayoutIn.caxis) delete ternaryLayoutIn.caxis.min;
- }
-}
diff --git a/src/plots/ternary/layout/layout_attributes.js b/src/plots/ternary/layout/layout_attributes.js
deleted file mode 100644
index 77c06326974..00000000000
--- a/src/plots/ternary/layout/layout_attributes.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
-* Copyright 2012-2018, 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 colorAttrs = require('../../../components/color/attributes');
-var domainAttrs = require('../../domain').attributes;
-var ternaryAxesAttrs = require('./axis_attributes');
-var overrideAll = require('../../../plot_api/edit_types').overrideAll;
-
-module.exports = overrideAll({
- domain: domainAttrs({name: 'ternary'}),
-
- bgcolor: {
- valType: 'color',
- role: 'style',
- dflt: colorAttrs.background,
- description: 'Set the background color of the subplot'
- },
- sum: {
- valType: 'number',
- role: 'info',
- dflt: 1,
- min: 0,
- description: [
- 'The number each triplet should sum to,',
- 'and the maximum range of each axis'
- ].join(' ')
- },
- aaxis: ternaryAxesAttrs,
- baxis: ternaryAxesAttrs,
- caxis: ternaryAxesAttrs
-}, 'plot', 'from-root');
diff --git a/src/plots/ternary/layout/axis_attributes.js b/src/plots/ternary/layout_attributes.js
similarity index 67%
rename from src/plots/ternary/layout/axis_attributes.js
rename to src/plots/ternary/layout_attributes.js
index 2ed0bcc74e2..72397f0f876 100644
--- a/src/plots/ternary/layout/axis_attributes.js
+++ b/src/plots/ternary/layout_attributes.js
@@ -8,12 +8,14 @@
'use strict';
+var colorAttrs = require('../../components/color/attributes');
+var domainAttrs = require('../domain').attributes;
+var axesAttrs = require('../cartesian/layout_attributes');
-var axesAttrs = require('../../cartesian/layout_attributes');
-var extendFlat = require('../../../lib/extend').extendFlat;
+var overrideAll = require('../../plot_api/edit_types').overrideAll;
+var extendFlat = require('../../lib/extend').extendFlat;
-
-module.exports = {
+var ternaryAxesAttrs = {
title: axesAttrs.title,
titlefont: axesAttrs.titlefont,
color: axesAttrs.color,
@@ -63,3 +65,27 @@ module.exports = {
].join(' ')
}
};
+
+module.exports = overrideAll({
+ domain: domainAttrs({name: 'ternary'}),
+
+ bgcolor: {
+ valType: 'color',
+ role: 'style',
+ dflt: colorAttrs.background,
+ description: 'Set the background color of the subplot'
+ },
+ sum: {
+ valType: 'number',
+ role: 'info',
+ dflt: 1,
+ min: 0,
+ description: [
+ 'The number each triplet should sum to,',
+ 'and the maximum range of each axis'
+ ].join(' ')
+ },
+ aaxis: ternaryAxesAttrs,
+ baxis: ternaryAxesAttrs,
+ caxis: ternaryAxesAttrs
+}, 'plot', 'from-root');
diff --git a/src/plots/ternary/layout_defaults.js b/src/plots/ternary/layout_defaults.js
new file mode 100644
index 00000000000..7952a4559a5
--- /dev/null
+++ b/src/plots/ternary/layout_defaults.js
@@ -0,0 +1,128 @@
+/**
+* Copyright 2012-2018, 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 Color = require('../../components/color');
+var Template = require('../../plot_api/plot_template');
+var Lib = require('../../lib');
+
+var handleSubplotDefaults = require('../subplot_defaults');
+var handleTickLabelDefaults = require('../cartesian/tick_label_defaults');
+var handleTickMarkDefaults = require('../cartesian/tick_mark_defaults');
+var handleTickValueDefaults = require('../cartesian/tick_value_defaults');
+var handleLineGridDefaults = require('../cartesian/line_grid_defaults');
+var layoutAttributes = require('./layout_attributes');
+
+var axesNames = ['aaxis', 'baxis', 'caxis'];
+
+module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
+ handleSubplotDefaults(layoutIn, layoutOut, fullData, {
+ type: 'ternary',
+ attributes: layoutAttributes,
+ handleDefaults: handleTernaryDefaults,
+ font: layoutOut.font,
+ paper_bgcolor: layoutOut.paper_bgcolor
+ });
+};
+
+function handleTernaryDefaults(ternaryLayoutIn, ternaryLayoutOut, coerce, options) {
+ var bgColor = coerce('bgcolor');
+ var sum = coerce('sum');
+ options.bgColor = Color.combine(bgColor, options.paper_bgcolor);
+ var axName, containerIn, containerOut;
+
+ // TODO: allow most (if not all) axis attributes to be set
+ // in the outer container and used as defaults in the individual axes?
+
+ for(var j = 0; j < axesNames.length; j++) {
+ axName = axesNames[j];
+ containerIn = ternaryLayoutIn[axName] || {};
+ containerOut = Template.newContainer(ternaryLayoutOut, axName);
+ containerOut._name = axName;
+
+ handleAxisDefaults(containerIn, containerOut, options);
+ }
+
+ // if the min values contradict each other, set them all to default (0)
+ // and delete *all* the inputs so the user doesn't get confused later by
+ // changing one and having them all change.
+ var aaxis = ternaryLayoutOut.aaxis,
+ baxis = ternaryLayoutOut.baxis,
+ caxis = ternaryLayoutOut.caxis;
+ if(aaxis.min + baxis.min + caxis.min >= sum) {
+ aaxis.min = 0;
+ baxis.min = 0;
+ caxis.min = 0;
+ if(ternaryLayoutIn.aaxis) delete ternaryLayoutIn.aaxis.min;
+ if(ternaryLayoutIn.baxis) delete ternaryLayoutIn.baxis.min;
+ if(ternaryLayoutIn.caxis) delete ternaryLayoutIn.caxis.min;
+ }
+}
+
+function handleAxisDefaults(containerIn, containerOut, options) {
+ var axAttrs = layoutAttributes[containerOut._name];
+
+ function coerce(attr, dflt) {
+ return Lib.coerce(containerIn, containerOut, axAttrs, attr, dflt);
+ }
+
+ containerOut.type = 'linear'; // no other types allowed for ternary
+
+ var dfltColor = coerce('color');
+ // if axis.color was provided, use it for fonts too; otherwise,
+ // inherit from global font color in case that was provided.
+ var dfltFontColor = (dfltColor !== axAttrs.color.dflt) ? dfltColor : options.font.color;
+
+ var axName = containerOut._name,
+ letterUpper = axName.charAt(0).toUpperCase(),
+ dfltTitle = 'Component ' + letterUpper;
+
+ var title = coerce('title', dfltTitle);
+ containerOut._hovertitle = title === dfltTitle ? title : letterUpper;
+
+ Lib.coerceFont(coerce, 'titlefont', {
+ family: options.font.family,
+ size: Math.round(options.font.size * 1.2),
+ color: dfltFontColor
+ });
+
+ // range is just set by 'min' - max is determined by the other axes mins
+ coerce('min');
+
+ handleTickValueDefaults(containerIn, containerOut, coerce, 'linear');
+ handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear', {});
+ handleTickMarkDefaults(containerIn, containerOut, coerce,
+ { outerTicks: true });
+
+ var showTickLabels = coerce('showticklabels');
+ if(showTickLabels) {
+ Lib.coerceFont(coerce, 'tickfont', {
+ family: options.font.family,
+ size: options.font.size,
+ color: dfltFontColor
+ });
+ coerce('tickangle');
+ coerce('tickformat');
+ }
+
+ handleLineGridDefaults(containerIn, containerOut, coerce, {
+ dfltColor: dfltColor,
+ bgColor: options.bgColor,
+ // default grid color is darker here (60%, vs cartesian default ~91%)
+ // because the grid is not square so the eye needs heavier cues to follow
+ blend: 60,
+ showLine: true,
+ showGrid: true,
+ noZeroLine: true,
+ attributes: axAttrs
+ });
+
+ coerce('hoverformat');
+ coerce('layer');
+}
diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js
index 436397bc5aa..a4ec4a7427c 100644
--- a/src/plots/ternary/ternary.js
+++ b/src/plots/ternary/ternary.js
@@ -25,6 +25,7 @@ var dragElement = require('../../components/dragelement');
var Fx = require('../../components/fx');
var Titles = require('../../components/titles');
var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
var clearSelect = require('../cartesian/select').clearSelect;
var constants = require('../cartesian/constants');
@@ -33,6 +34,12 @@ function Ternary(options, fullLayout) {
this.graphDiv = options.graphDiv;
this.init(fullLayout);
this.makeFramework(fullLayout);
+
+ // unfortunately, we have to keep track of some axis tick settings
+ // as ternary subplots do not implement the 'ticks' editType
+ this.aTickLayout = null;
+ this.bTickLayout = null;
+ this.cTickLayout = null;
}
module.exports = Ternary;
@@ -253,6 +260,8 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h],
_axislayer: _this.layers.aaxis,
_gridlayer: _this.layers.agrid,
+ anchor: 'free',
+ position: 0,
_pos: 0, // _this.xaxis.domain[0] * graphSize.w,
_id: 'y',
_length: w,
@@ -273,6 +282,8 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
_axislayer: _this.layers.baxis,
_gridlayer: _this.layers.bgrid,
_counteraxis: _this.aaxis,
+ anchor: 'free',
+ position: 0,
_pos: 0, // (1 - yDomain0) * graphSize.h,
_id: 'x',
_length: w,
@@ -295,6 +306,8 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
_axislayer: _this.layers.caxis,
_gridlayer: _this.layers.cgrid,
_counteraxis: _this.baxis,
+ anchor: 'free',
+ position: 0,
_pos: 0, // _this.xaxis.domain[1] * graphSize.w,
_id: 'y',
_length: w,
@@ -368,12 +381,33 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
};
proto.drawAxes = function(doTitles) {
- var _this = this,
- gd = _this.graphDiv,
- titlesuffix = _this.id.substr(7) + 'title',
- aaxis = _this.aaxis,
- baxis = _this.baxis,
- caxis = _this.caxis;
+ var _this = this;
+ var gd = _this.graphDiv;
+ var titlesuffix = _this.id.substr(7) + 'title';
+ var layers = _this.layers;
+ var aaxis = _this.aaxis;
+ var baxis = _this.baxis;
+ var caxis = _this.caxis;
+ var newTickLayout;
+
+ newTickLayout = strTickLayout(aaxis);
+ if(_this.aTickLayout !== newTickLayout) {
+ layers.aaxis.selectAll('.ytick').remove();
+ _this.aTickLayout = newTickLayout;
+ }
+
+ newTickLayout = strTickLayout(baxis);
+ if(_this.bTickLayout !== newTickLayout) {
+ layers.baxis.selectAll('.xtick').remove();
+ _this.bTickLayout = newTickLayout;
+ }
+
+ newTickLayout = strTickLayout(caxis);
+ if(_this.cTickLayout !== newTickLayout) {
+ layers.caxis.selectAll('.ytick').remove();
+ _this.cTickLayout = newTickLayout;
+ }
+
// 3rd arg true below skips titles, so we can configure them
// correctly later on.
Axes.doTicksSingle(gd, aaxis, true);
@@ -423,6 +457,11 @@ proto.drawAxes = function(doTitles) {
}
};
+function strTickLayout(axLayout) {
+ return axLayout.ticks + String(axLayout.ticklen) + String(axLayout.showticklabels);
+}
+
+
// hard coded paths for zoom corners
// uses the same sizing as cartesian, length is MINZOOM/2, width is 3px
var CLEN = constants.MINZOOM / 2 + 0.87;
@@ -452,6 +491,7 @@ proto.initInteractions = function() {
element: dragger,
gd: gd,
plotinfo: {
+ id: _this.id,
xaxis: _this.xaxis,
yaxis: _this.yaxis
},
@@ -462,21 +502,19 @@ proto.initInteractions = function() {
dragOptions.xaxes = [_this.xaxis];
dragOptions.yaxes = [_this.yaxis];
var dragModeNow = gd._fullLayout.dragmode;
- if(e.shiftKey) {
- if(dragModeNow === 'pan') dragModeNow = 'zoom';
- else dragModeNow = 'pan';
- }
if(dragModeNow === 'lasso') dragOptions.minDrag = 1;
else dragOptions.minDrag = undefined;
if(dragModeNow === 'zoom') {
dragOptions.moveFn = zoomMove;
+ dragOptions.clickFn = clickZoomPan;
dragOptions.doneFn = zoomDone;
zoomPrep(e, startX, startY);
}
else if(dragModeNow === 'pan') {
dragOptions.moveFn = plotDrag;
+ dragOptions.clickFn = clickZoomPan;
dragOptions.doneFn = dragDone;
panPrep();
clearSelect(zoomContainer);
@@ -484,24 +522,34 @@ proto.initInteractions = function() {
else if(dragModeNow === 'select' || dragModeNow === 'lasso') {
prepSelect(e, startX, startY, dragOptions, dragModeNow);
}
- },
- clickFn: function(numClicks, evt) {
- removeZoombox(gd);
-
- if(numClicks === 2) {
- var attrs = {};
- attrs[_this.id + '.aaxis.min'] = 0;
- attrs[_this.id + '.baxis.min'] = 0;
- attrs[_this.id + '.caxis.min'] = 0;
- gd.emit('plotly_doubleclick', null);
- Registry.call('relayout', gd, attrs);
- }
- Fx.click(gd, evt, _this.id);
}
};
var x0, y0, mins0, span0, mins, lum, path0, dimmed, zb, corners;
+ function clickZoomPan(numClicks, evt) {
+ var clickMode = gd._fullLayout.clickmode;
+
+ removeZoombox(gd);
+
+ if(numClicks === 2) {
+ var attrs = {};
+ attrs[_this.id + '.aaxis.min'] = 0;
+ attrs[_this.id + '.baxis.min'] = 0;
+ attrs[_this.id + '.caxis.min'] = 0;
+ gd.emit('plotly_doubleclick', null);
+ Registry.call('relayout', gd, attrs);
+ }
+
+ if(clickMode.indexOf('select') > -1 && numClicks === 1) {
+ selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, dragOptions);
+ }
+
+ if(clickMode.indexOf('event') > -1) {
+ Fx.click(gd, evt, _this.id);
+ }
+ }
+
function zoomPrep(e, startX, startY) {
var dragBBox = dragger.getBoundingClientRect();
x0 = startX - dragBBox.left;
diff --git a/src/traces/bar/select.js b/src/traces/bar/select.js
index 04ede09356c..4d80b7b4836 100644
--- a/src/traces/bar/select.js
+++ b/src/traces/bar/select.js
@@ -8,14 +8,14 @@
'use strict';
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var xa = searchInfo.xaxis;
var ya = searchInfo.yaxis;
var selection = [];
var i;
- if(polygon === false) {
+ if(selectionTester === false) {
// clear selection
for(i = 0; i < cd.length; i++) {
cd[i].selected = 0;
@@ -24,7 +24,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
for(i = 0; i < cd.length; i++) {
var di = cd[i];
- if(polygon.contains(di.ct)) {
+ if(selectionTester.contains(di.ct, false, i, searchInfo)) {
selection.push({
pointNumber: i,
x: xa.c2d(di.x),
diff --git a/src/traces/box/event_data.js b/src/traces/box/event_data.js
new file mode 100644
index 00000000000..a12ee8eb67a
--- /dev/null
+++ b/src/traces/box/event_data.js
@@ -0,0 +1,24 @@
+/**
+* Copyright 2012-2018, 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';
+
+module.exports = function eventData(out, pt) {
+
+ // Note: hoverOnBox property is needed for click-to-select
+ // to ignore when a box was clicked. This is the reason box
+ // implements this custom eventData function.
+ if(pt.hoverOnBox) out.hoverOnBox = pt.hoverOnBox;
+
+ if('xVal' in pt) out.x = pt.xVal;
+ if('yVal' in pt) out.y = pt.yVal;
+ if(pt.xa) out.xaxis = pt.xa;
+ if(pt.ya) out.yaxis = pt.ya;
+
+ return out;
+};
diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js
index 79e24509360..b4729279e1c 100644
--- a/src/traces/box/hover.js
+++ b/src/traces/box/hover.js
@@ -169,6 +169,10 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) {
pointData2[vLetter + 'LabelVal'] = val;
pointData2[vLetter + 'Label'] = (t.labels ? t.labels[attr] + ' ' : '') + Axes.hoverLabelText(vAxis, val);
+ // Note: introduced to be able to distinguish a
+ // clicked point from a box during click-to-select
+ pointData2.hoverOnBox = true;
+
if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
pointData2[vLetter + 'err'] = di.sd;
}
diff --git a/src/traces/box/index.js b/src/traces/box/index.js
index 3ad049e1701..17931ec782d 100644
--- a/src/traces/box/index.js
+++ b/src/traces/box/index.js
@@ -20,6 +20,7 @@ Box.plot = require('./plot').plot;
Box.style = require('./style').style;
Box.styleOnSelect = require('./style').styleOnSelect;
Box.hoverPoints = require('./hover').hoverPoints;
+Box.eventData = require('./event_data');
Box.selectPoints = require('./select');
Box.moduleType = 'trace';
diff --git a/src/traces/box/select.js b/src/traces/box/select.js
index 9ec9ed03e3f..069b9b1896f 100644
--- a/src/traces/box/select.js
+++ b/src/traces/box/select.js
@@ -8,14 +8,14 @@
'use strict';
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var xa = searchInfo.xaxis;
var ya = searchInfo.yaxis;
var selection = [];
var i, j;
- if(polygon === false) {
+ if(selectionTester === false) {
for(i = 0; i < cd.length; i++) {
for(j = 0; j < (cd[i].pts || []).length; j++) {
// clear selection
@@ -29,7 +29,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
var x = xa.c2p(pt.x);
var y = ya.c2p(pt.y);
- if(polygon.contains([x, y])) {
+ if(selectionTester.contains([x, y], null, pt.i, searchInfo)) {
selection.push({
pointNumber: pt.i,
x: xa.c2d(pt.x),
diff --git a/src/traces/choropleth/select.js b/src/traces/choropleth/select.js
index c3a8f332c4d..9052c06a74e 100644
--- a/src/traces/choropleth/select.js
+++ b/src/traces/choropleth/select.js
@@ -8,7 +8,7 @@
'use strict';
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var xa = searchInfo.xaxis;
var ya = searchInfo.yaxis;
@@ -16,7 +16,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
var i, di, ct, x, y;
- if(polygon === false) {
+ if(selectionTester === false) {
for(i = 0; i < cd.length; i++) {
cd[i].selected = 0;
}
@@ -30,7 +30,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
x = xa.c2p(ct);
y = ya.c2p(ct);
- if(polygon.contains([x, y])) {
+ if(selectionTester.contains([x, y], null, i, searchInfo)) {
selection.push({
pointNumber: i,
lon: ct[0],
diff --git a/src/traces/ohlc/select.js b/src/traces/ohlc/select.js
index 29bed35028f..a588e2ac164 100644
--- a/src/traces/ohlc/select.js
+++ b/src/traces/ohlc/select.js
@@ -8,7 +8,7 @@
'use strict';
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var xa = searchInfo.xaxis;
var ya = searchInfo.yaxis;
@@ -17,7 +17,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
// for (potentially grouped) candlesticks
var posOffset = cd[0].t.bPos || 0;
- if(polygon === false) {
+ if(selectionTester === false) {
// clear selection
for(i = 0; i < cd.length; i++) {
cd[i].selected = 0;
@@ -26,7 +26,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
for(i = 0; i < cd.length; i++) {
var di = cd[i];
- if(polygon.contains([xa.c2p(di.pos + posOffset), ya.c2p(di.yc)])) {
+ if(selectionTester.contains([xa.c2p(di.pos + posOffset), ya.c2p(di.yc)], null, di.i, searchInfo)) {
selection.push({
pointNumber: di.i,
x: xa.c2d(di.pos),
diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js
index 56980cdcf22..f881a5f6223 100644
--- a/src/traces/scatter/plot.js
+++ b/src/traces/scatter/plot.js
@@ -13,6 +13,8 @@ var d3 = require('d3');
var Registry = require('../../registry');
var Lib = require('../../lib');
+var ensureSingle = Lib.ensureSingle;
+var identity = Lib.identity;
var Drawing = require('../../components/drawing');
var subTypes = require('./subtypes');
@@ -43,7 +45,7 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
// the z-order of fill layers is correct.
linkTraces(gd, plotinfo, cdscatter);
- createFills(gd, scatterLayer, plotinfo);
+ createFills(gd, join, plotinfo);
// Sort the traces, once created, so that the ordering is preserved even when traces
// are shown and hidden. This is needed since we're not just wiping everything out
@@ -52,10 +54,10 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
uids[cdscatter[i][0].trace.uid] = i;
}
- scatterLayer.selectAll('g.trace').sort(function(a, b) {
+ join.sort(function(a, b) {
var idx1 = uids[a[0].trace.uid];
var idx2 = uids[b[0].trace.uid];
- return idx1 > idx2 ? 1 : -1;
+ return idx1 - idx2;
});
if(hasTransition) {
@@ -97,51 +99,45 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
scatterLayer.selectAll('path:not([d])').remove();
};
-function createFills(gd, scatterLayer, plotinfo) {
- var trace;
+function createFills(gd, traceJoin, plotinfo) {
+ traceJoin.each(function(d) {
+ var fills = ensureSingle(d3.select(this), 'g', 'fills');
+ Drawing.setClipUrl(fills, plotinfo.layerClipId);
- scatterLayer.selectAll('g.trace').each(function(d) {
- var tr = d3.select(this);
-
- // Loop only over the traces being redrawn:
- trace = d[0].trace;
+ var trace = d[0].trace;
- // make the fill-to-next path now for the NEXT trace, so it shows
- // behind both lines.
+ var fillData = [];
+ if(trace.fill && (trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' ||
+ (trace.fill.substr(0, 2) === 'to' && !trace._prevtrace))
+ ) {
+ fillData = ['_ownFill'];
+ }
if(trace._nexttrace) {
- trace._nextFill = tr.select('.js-fill.js-tonext');
- if(!trace._nextFill.size()) {
+ // make the fill-to-next path now for the NEXT trace, so it shows
+ // behind both lines.
+ fillData.push('_nextFill');
+ }
- // If there is an existing tozero fill, we must insert this *after* that fill:
- var loc = ':first-child';
- if(tr.select('.js-fill.js-tozero').size()) {
- loc += ' + *';
- }
+ var fillJoin = fills.selectAll('g')
+ .data(fillData, identity);
- trace._nextFill = tr.insert('path', loc).attr('class', 'js-fill js-tonext');
- }
- } else {
- tr.selectAll('.js-fill.js-tonext').remove();
- trace._nextFill = null;
- }
+ fillJoin.enter().append('g');
- if(trace.fill && (trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' ||
- (trace.fill.substr(0, 2) === 'to' && !trace._prevtrace))) {
- trace._ownFill = tr.select('.js-fill.js-tozero');
- if(!trace._ownFill.size()) {
- trace._ownFill = tr.insert('path', ':first-child').attr('class', 'js-fill js-tozero');
- }
- } else {
- tr.selectAll('.js-fill.js-tozero').remove();
- trace._ownFill = null;
- }
+ fillJoin.exit()
+ .each(function(d) { trace[d] = null; })
+ .remove();
- tr.selectAll('.js-fill').call(Drawing.setClipUrl, plotinfo.layerClipId);
+ fillJoin.order().each(function(d) {
+ // make a path element inside the fill group, just so
+ // we can give it its own data later on and the group can
+ // keep its simple '_*Fill' data
+ trace[d] = ensureSingle(d3.select(this), 'path', 'js-fill');
+ });
});
}
function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transitionOpts) {
- var join, i;
+ var i;
// Since this has been reorganized and we're executing this on individual traces,
// we need to pass it the full list of cdscatter as well as this trace's index (idx)
@@ -157,12 +153,17 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
var xa = plotinfo.xaxis,
ya = plotinfo.yaxis;
- var trace = cdscatter[0].trace,
- line = trace.line,
- tr = d3.select(element);
+ var trace = cdscatter[0].trace;
+ var line = trace.line;
+ var tr = d3.select(element);
+
+ var errorBarGroup = ensureSingle(tr, 'g', 'errorbars');
+ var lines = ensureSingle(tr, 'g', 'lines');
+ var points = ensureSingle(tr, 'g', 'points');
+ var text = ensureSingle(tr, 'g', 'text');
// error bars are at the bottom
- Registry.getComponentMethod('errorbars', 'plot')(tr, plotinfo, transitionOpts);
+ Registry.getComponentMethod('errorbars', 'plot')(errorBarGroup, plotinfo, transitionOpts);
if(trace.visible !== true) return;
@@ -303,7 +304,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
};
}
- var lineJoin = tr.selectAll('.js-line').data(segments);
+ var lineJoin = lines.selectAll('.js-line').data(segments);
transition(lineJoin.exit())
.style('opacity', 0)
@@ -325,6 +326,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
if(segments.length) {
if(ownFillEl3) {
+ ownFillEl3.datum(cdscatter);
if(pt0 && pt1) {
if(ownFillDir) {
if(ownFillDir === 'y') {
@@ -412,11 +414,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
return false;
}
- function makePoints(d) {
+ function makePoints(points, text, cdscatter) {
var join, selection, hasNode;
- var trace = d[0].trace;
- var s = d3.select(this);
+ var trace = cdscatter[0].trace;
var showMarkers = subTypes.hasMarkers(trace);
var showText = subTypes.hasText(trace);
@@ -425,7 +426,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
var textFilter = hideFilter;
if(showMarkers || showText) {
- var showFilter = Lib.identity;
+ var showFilter = identity;
// if we're stacking, "infer zero" gap mode gets markers in the
// gap points - because we've inferred a zero there - but other
// modes (currently "interpolate", later "interrupt" hopefully)
@@ -446,7 +447,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
// marker points
- selection = s.selectAll('path.point');
+ selection = points.selectAll('path.point');
join = selection.data(markerFilter, keyFunc);
@@ -498,7 +499,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
}
// text points
- selection = s.selectAll('g');
+ selection = text.selectAll('g');
join = selection.data(textFilter, keyFunc);
// each text needs to go in its own 'g' in case
@@ -537,28 +538,16 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
join.exit().remove();
}
- // NB: selectAll is evaluated on instantiation:
- var pointSelection = tr.selectAll('.points');
-
- // Join with new data
- join = pointSelection.data([cdscatter]);
-
- // Transition existing, but don't defer this to an async .transition since
- // there's no timing involved:
- pointSelection.each(makePoints);
-
- join.enter().append('g')
- .classed('points', true)
- .each(makePoints);
-
- join.exit().remove();
+ points.datum(cdscatter);
+ text.datum(cdscatter);
+ makePoints(points, text, cdscatter);
// lastly, clip points groups of `cliponaxis !== false` traces
// on `plotinfo._hasClipOnAxisFalse === true` subplots
- join.each(function(d) {
- var hasClipOnAxisFalse = d[0].trace.cliponaxis === false;
- Drawing.setClipUrl(d3.select(this), hasClipOnAxisFalse ? null : plotinfo.layerClipId);
- });
+ var hasClipOnAxisFalse = trace.cliponaxis === false;
+ var clipUrl = hasClipOnAxisFalse ? null : plotinfo.layerClipId;
+ Drawing.setClipUrl(points, clipUrl);
+ Drawing.setClipUrl(text, clipUrl);
}
function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) {
diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js
index e980a6d7400..79e1a689e41 100644
--- a/src/traces/scatter/select.js
+++ b/src/traces/scatter/select.js
@@ -11,7 +11,7 @@
var subtypes = require('./subtypes');
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd,
xa = searchInfo.xaxis,
ya = searchInfo.yaxis,
@@ -25,7 +25,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace));
if(hasOnlyLines) return [];
- if(polygon === false) { // clear selection
+ if(selectionTester === false) { // clear selection
for(i = 0; i < cd.length; i++) {
cd[i].selected = 0;
}
@@ -36,7 +36,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
x = xa.c2p(di.x);
y = ya.c2p(di.y);
- if((di.i !== null) && polygon.contains([x, y])) {
+ if((di.i !== null) && selectionTester.contains([x, y], false, i, searchInfo)) {
selection.push({
pointNumber: di.i,
x: xa.c2d(di.x),
diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js
index b9090261e93..4ce72bdf388 100644
--- a/src/traces/scatter/style.js
+++ b/src/traces/scatter/style.js
@@ -26,6 +26,12 @@ function style(gd, cd) {
stylePoints(sel, trace, gd);
});
+ s.selectAll('g.text').each(function(d) {
+ var sel = d3.select(this);
+ var trace = d.trace || d[0].trace;
+ styleText(sel, trace, gd);
+ });
+
s.selectAll('g.trace path.js-line')
.call(Drawing.lineGroupStyle);
@@ -37,6 +43,9 @@ function style(gd, cd) {
function stylePoints(sel, trace, gd) {
Drawing.pointStyle(sel.selectAll('path.point'), trace, gd);
+}
+
+function styleText(sel, trace, gd) {
Drawing.textPointStyle(sel.selectAll('text'), trace, gd);
}
@@ -49,11 +58,13 @@ function styleOnSelect(gd, cd) {
Drawing.selectedTextStyle(s.selectAll('text'), trace);
} else {
stylePoints(s, trace, gd);
+ styleText(s, trace, gd);
}
}
module.exports = {
style: style,
stylePoints: stylePoints,
+ styleText: styleText,
styleOnSelect: styleOnSelect
};
diff --git a/src/traces/scattergeo/select.js b/src/traces/scattergeo/select.js
index 4c11e6c3196..b6b9fe2b212 100644
--- a/src/traces/scattergeo/select.js
+++ b/src/traces/scattergeo/select.js
@@ -11,7 +11,7 @@
var subtypes = require('../scatter/subtypes');
var BADNUM = require('../../constants/numerical').BADNUM;
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var xa = searchInfo.xaxis;
var ya = searchInfo.yaxis;
@@ -23,7 +23,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace));
if(hasOnlyLines) return [];
- if(polygon === false) {
+ if(selectionTester === false) {
for(i = 0; i < cd.length; i++) {
cd[i].selected = 0;
}
@@ -38,7 +38,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
x = xa.c2p(lonlat);
y = ya.c2p(lonlat);
- if(polygon.contains([x, y])) {
+ if(selectionTester.contains([x, y], null, i, searchInfo)) {
selection.push({
pointNumber: i,
lon: lonlat[0],
diff --git a/src/traces/scattergeo/style.js b/src/traces/scattergeo/style.js
index c4d9a644321..6d89407e62f 100644
--- a/src/traces/scattergeo/style.js
+++ b/src/traces/scattergeo/style.js
@@ -12,7 +12,9 @@ var d3 = require('d3');
var Drawing = require('../../components/drawing');
var Color = require('../../components/color');
-var stylePoints = require('../scatter/style').stylePoints;
+var scatterStyle = require('../scatter/style');
+var stylePoints = scatterStyle.stylePoints;
+var styleText = scatterStyle.styleText;
module.exports = function style(gd, calcTrace) {
if(calcTrace) styleTrace(gd, calcTrace);
@@ -25,6 +27,7 @@ function styleTrace(gd, calcTrace) {
s.style('opacity', calcTrace[0].trace.opacity);
stylePoints(s, trace, gd);
+ styleText(s, trace, gd);
// this part is incompatible with Drawing.lineGroupStyle
s.selectAll('path.js-line')
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index 0bb6dabe015..14f8b27d021 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -13,7 +13,7 @@ var createLine = require('regl-line2d');
var createError = require('regl-error2d');
var cluster = require('point-cluster');
var arrayRange = require('array-range');
-var Text = require('@etpinard/gl-text');
+var Text = require('gl-text');
var Registry = require('../../registry');
var Lib = require('../../lib');
@@ -431,7 +431,7 @@ function plot(gd, subplot, cdata) {
if(scene.fill2d) {
scene.fillOptions = scene.fillOptions.map(function(fillOptions, i) {
var cdscatter = cdata[i];
- if(!fillOptions || !cdscatter || !cdscatter[0] || !cdscatter[0].trace) return null;
+ if(!fillOptions || !cdscatter || !cdscatter[0] || !cdscatter[0].trace) return;
var cd = cdscatter[0];
var trace = cd.trace;
var stash = cd.t;
@@ -524,6 +524,7 @@ function plot(gd, subplot, cdata) {
scene.unselectBatch = null;
var dragmode = fullLayout.dragmode;
var selectMode = dragmode === 'lasso' || dragmode === 'select';
+ var clickSelectEnabled = fullLayout.clickmode.indexOf('select') > -1;
for(i = 0; i < cdata.length; i++) {
var cd0 = cdata[i][0];
@@ -533,7 +534,7 @@ function plot(gd, subplot, cdata) {
var x = stash.x;
var y = stash.y;
- if(trace.selectedpoints || selectMode) {
+ if(trace.selectedpoints || selectMode || clickSelectEnabled) {
if(!selectMode) selectMode = true;
if(!scene.selectBatch) {
@@ -822,7 +823,7 @@ function calcHover(pointData, x, y, trace) {
}
-function selectPoints(searchInfo, polygon) {
+function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var selection = [];
var trace = cd[0].trace;
@@ -844,10 +845,10 @@ function selectPoints(searchInfo, polygon) {
var unels = null;
// FIXME: clearing selection does not work here
var i;
- if(polygon !== false && !polygon.degenerate) {
+ if(selectionTester !== false && !selectionTester.degenerate) {
els = [], unels = [];
for(i = 0; i < stash.count; i++) {
- if(polygon.contains([stash.xpx[i], stash.ypx[i]])) {
+ if(selectionTester.contains([stash.xpx[i], stash.ypx[i]], false, i, searchInfo)) {
els.push(i);
selection.push({
pointNumber: i,
diff --git a/src/traces/scattermapbox/select.js b/src/traces/scattermapbox/select.js
index dd6f3536903..34bbeedf0e6 100644
--- a/src/traces/scattermapbox/select.js
+++ b/src/traces/scattermapbox/select.js
@@ -12,7 +12,7 @@ var Lib = require('../../lib');
var subtypes = require('../scatter/subtypes');
var BADNUM = require('../../constants/numerical').BADNUM;
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var xa = searchInfo.xaxis;
var ya = searchInfo.yaxis;
@@ -22,7 +22,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
if(!subtypes.hasMarkers(trace)) return [];
- if(polygon === false) {
+ if(selectionTester === false) {
for(i = 0; i < cd.length; i++) {
cd[i].selected = 0;
}
@@ -35,7 +35,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
var lonlat2 = [Lib.modHalf(lonlat[0], 360), lonlat[1]];
var xy = [xa.c2p(lonlat2), ya.c2p(lonlat2)];
- if(polygon.contains(xy)) {
+ if(selectionTester.contains(xy, null, i, searchInfo)) {
selection.push({
pointNumber: i,
lon: lonlat[0],
diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js
index 902646cb17a..cbbade4c81b 100644
--- a/src/traces/splom/index.js
+++ b/src/traces/splom/index.js
@@ -229,7 +229,9 @@ function plotOne(gd, cd0) {
scene.matrix = createMatrix(regl);
}
- var selectMode = dragmode === 'lasso' || dragmode === 'select' || !!trace.selectedpoints;
+ var clickSelectEnabled = fullLayout.clickmode.indexOf('select') > -1;
+ var selectMode = dragmode === 'lasso' || dragmode === 'select' ||
+ !!trace.selectedpoints || clickSelectEnabled;
scene.selectBatch = null;
scene.unselectBatch = null;
@@ -346,7 +348,7 @@ function hoverPoints(pointData, xval, yval) {
return [pointData];
}
-function selectPoints(searchInfo, polygon) {
+function selectPoints(searchInfo, selectionTester) {
var cd = searchInfo.cd;
var trace = cd[0].trace;
var stash = cd[0].t;
@@ -375,10 +377,10 @@ function selectPoints(searchInfo, polygon) {
// filter out points by visible scatter ones
var els = null;
var unels = null;
- if(polygon !== false && !polygon.degenerate) {
+ if(selectionTester !== false && !selectionTester.degenerate) {
els = [], unels = [];
for(i = 0; i < x.length; i++) {
- if(polygon.contains([xpx[i], ypx[i]])) {
+ if(selectionTester.contains([xpx[i], ypx[i]], null, i, searchInfo)) {
els.push(i);
selection.push({
pointNumber: i,
diff --git a/tasks/bundle.js b/tasks/bundle.js
index 7c6586c1c14..a4f0fce1516 100644
--- a/tasks/bundle.js
+++ b/tasks/bundle.js
@@ -50,7 +50,6 @@ tasks.push(function(cb) {
_bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyDist, {
standalone: 'Plotly',
debug: DEV,
- compressAttrs: true,
pathToMinBundle: constants.pathToPlotlyDistMin
}, cb);
});
@@ -62,11 +61,12 @@ tasks.push(function(cb) {
}, cb);
});
-// Browserify the plotly.js with meta
+// Browserify plotly.js with meta and output plot-schema JSON
tasks.push(function(cb) {
_bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyDistWithMeta, {
standalone: 'Plotly',
debug: DEV,
+ noCompress: true
}, function() {
makeSchema(constants.pathToPlotlyDistWithMeta, constants.pathToSchema)();
cb();
@@ -79,7 +79,6 @@ constants.partialBundlePaths.forEach(function(pathObj) {
_bundle(pathObj.index, pathObj.dist, {
standalone: 'Plotly',
debug: DEV,
- compressAttrs: true,
pathToMinBundle: pathObj.distMin
}, cb);
});
diff --git a/tasks/cibundle.js b/tasks/cibundle.js
index 5a2310e2295..7b53995b81e 100644
--- a/tasks/cibundle.js
+++ b/tasks/cibundle.js
@@ -11,12 +11,10 @@ var _bundle = require('./util/browserify_wrapper');
* - plotly.min.js bundle in dist/ (for requirejs test)
*/
-
// Browserify plotly.js and plotly.min.js
_bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyBuild, {
standalone: 'Plotly',
debug: true,
- compressAttrs: true,
pathToMinBundle: constants.pathToPlotlyDistMin
});
diff --git a/tasks/util/compress_attributes.js b/tasks/compress_attributes.js
similarity index 100%
rename from tasks/util/compress_attributes.js
rename to tasks/compress_attributes.js
diff --git a/tasks/util/browserify_wrapper.js b/tasks/util/browserify_wrapper.js
index 362a6121097..7a4e49113b4 100644
--- a/tasks/util/browserify_wrapper.js
+++ b/tasks/util/browserify_wrapper.js
@@ -7,7 +7,6 @@ var derequire = require('derequire');
var through = require('through2');
var constants = require('./constants');
-var compressAttributes = require('./compress_attributes');
var strictD3 = require('./strict_d3');
/** Convenience browserify wrapper
@@ -20,7 +19,7 @@ var strictD3 = require('./strict_d3');
* - debug {boolean} [optional]
* Additional option:
* - pathToMinBundle {string} path to destination minified bundle
- * - compressAttrs {boolean} do we compress attribute meta?
+ * - noCompress {boolean} skip attribute meta compression?
* @param {function} cb callback
*
* Outputs one bundle (un-minified) file if opts.pathToMinBundle is omitted
@@ -38,10 +37,11 @@ module.exports = function _bundle(pathToIndex, pathToBundle, opts, cb) {
browserifyOpts.standalone = opts.standalone;
browserifyOpts.debug = opts.debug;
- browserifyOpts.transform = [];
- if(opts.compressAttrs) {
- browserifyOpts.transform.push(compressAttributes);
+ if(opts.noCompress) {
+ browserifyOpts.ignoreTransform = './tasks/compress_attributes.js';
}
+
+ browserifyOpts.transform = [];
if(opts.debug) {
browserifyOpts.transform.push(strictD3);
}
diff --git a/tasks/util/watchified_bundle.js b/tasks/util/watchified_bundle.js
index 864716dca6f..05fb07c9526 100644
--- a/tasks/util/watchified_bundle.js
+++ b/tasks/util/watchified_bundle.js
@@ -6,7 +6,6 @@ var prettySize = require('prettysize');
var constants = require('./constants');
var common = require('./common');
-var compressAttributes = require('./compress_attributes');
var strictD3 = require('./strict_d3');
/**
@@ -23,7 +22,7 @@ module.exports = function makeWatchifiedBundle(onFirstBundleCallback) {
var b = browserify(constants.pathToPlotlyIndex, {
debug: true,
standalone: 'Plotly',
- transform: [strictD3, compressAttributes],
+ transform: [strictD3],
cache: {},
packageCache: {},
plugin: [watchify]
diff --git a/test/image/baselines/gl2d_fill-ordering.png b/test/image/baselines/gl2d_fill-ordering.png
new file mode 100644
index 00000000000..7b71d7d2dcb
Binary files /dev/null and b/test/image/baselines/gl2d_fill-ordering.png differ
diff --git a/test/image/baselines/gl2d_text_chart_basic.png b/test/image/baselines/gl2d_text_chart_basic.png
index 93488811117..afece63c14c 100644
Binary files a/test/image/baselines/gl2d_text_chart_basic.png and b/test/image/baselines/gl2d_text_chart_basic.png differ
diff --git a/test/image/baselines/gl2d_text_chart_single-string.png b/test/image/baselines/gl2d_text_chart_single-string.png
index 733e3584811..bebed9f57f3 100644
Binary files a/test/image/baselines/gl2d_text_chart_single-string.png and b/test/image/baselines/gl2d_text_chart_single-string.png differ
diff --git a/test/image/baselines/glpolar_scatter.png b/test/image/baselines/glpolar_scatter.png
index d278a1bd29e..d631ae1d118 100644
Binary files a/test/image/baselines/glpolar_scatter.png and b/test/image/baselines/glpolar_scatter.png differ
diff --git a/test/image/baselines/glpolar_style.png b/test/image/baselines/glpolar_style.png
index 31c2b18b375..8e27ce4e66c 100644
Binary files a/test/image/baselines/glpolar_style.png and b/test/image/baselines/glpolar_style.png differ
diff --git a/test/image/baselines/glpolar_subplots.png b/test/image/baselines/glpolar_subplots.png
index ed807c1ab15..7fd76151d3f 100644
Binary files a/test/image/baselines/glpolar_subplots.png and b/test/image/baselines/glpolar_subplots.png differ
diff --git a/test/image/baselines/polar_bar-overlay.png b/test/image/baselines/polar_bar-overlay.png
index 54e29302826..c10f093cd84 100644
Binary files a/test/image/baselines/polar_bar-overlay.png and b/test/image/baselines/polar_bar-overlay.png differ
diff --git a/test/image/baselines/polar_bar-stacked.png b/test/image/baselines/polar_bar-stacked.png
index c232d2b3c0a..e317800d5fb 100644
Binary files a/test/image/baselines/polar_bar-stacked.png and b/test/image/baselines/polar_bar-stacked.png differ
diff --git a/test/image/baselines/polar_bar-width-base-offset.png b/test/image/baselines/polar_bar-width-base-offset.png
index 760c06c857c..eac19c97c6c 100644
Binary files a/test/image/baselines/polar_bar-width-base-offset.png and b/test/image/baselines/polar_bar-width-base-offset.png differ
diff --git a/test/image/baselines/polar_blank.png b/test/image/baselines/polar_blank.png
index d5c9b75dd63..ac57f2fcf44 100644
Binary files a/test/image/baselines/polar_blank.png and b/test/image/baselines/polar_blank.png differ
diff --git a/test/image/baselines/polar_categories.png b/test/image/baselines/polar_categories.png
index 36e92602dc3..67e45a62d91 100644
Binary files a/test/image/baselines/polar_categories.png and b/test/image/baselines/polar_categories.png differ
diff --git a/test/image/baselines/polar_dates.png b/test/image/baselines/polar_dates.png
index 6127f9afd71..b102e5ea70c 100644
Binary files a/test/image/baselines/polar_dates.png and b/test/image/baselines/polar_dates.png differ
diff --git a/test/image/baselines/polar_direction.png b/test/image/baselines/polar_direction.png
index 967aead4abc..4b57bf4a130 100644
Binary files a/test/image/baselines/polar_direction.png and b/test/image/baselines/polar_direction.png differ
diff --git a/test/image/baselines/polar_fills.png b/test/image/baselines/polar_fills.png
index efd3df8448b..fd34fb02040 100644
Binary files a/test/image/baselines/polar_fills.png and b/test/image/baselines/polar_fills.png differ
diff --git a/test/image/baselines/polar_funky-bars.png b/test/image/baselines/polar_funky-bars.png
index 721da8c124b..f986930a388 100644
Binary files a/test/image/baselines/polar_funky-bars.png and b/test/image/baselines/polar_funky-bars.png differ
diff --git a/test/image/baselines/polar_hole.png b/test/image/baselines/polar_hole.png
new file mode 100644
index 00000000000..b701371fe93
Binary files /dev/null and b/test/image/baselines/polar_hole.png differ
diff --git a/test/image/baselines/polar_line.png b/test/image/baselines/polar_line.png
index 8b5849a3303..bd00514df84 100644
Binary files a/test/image/baselines/polar_line.png and b/test/image/baselines/polar_line.png differ
diff --git a/test/image/baselines/polar_polygon-bars.png b/test/image/baselines/polar_polygon-bars.png
index a6dabee586c..892e5dd4ac5 100644
Binary files a/test/image/baselines/polar_polygon-bars.png and b/test/image/baselines/polar_polygon-bars.png differ
diff --git a/test/image/baselines/polar_polygon-grids.png b/test/image/baselines/polar_polygon-grids.png
index 8a6e41ffdef..dea0454b4d3 100644
Binary files a/test/image/baselines/polar_polygon-grids.png and b/test/image/baselines/polar_polygon-grids.png differ
diff --git a/test/image/baselines/polar_r0dr-theta0dtheta.png b/test/image/baselines/polar_r0dr-theta0dtheta.png
index e5b78152117..b11d35e2c42 100644
Binary files a/test/image/baselines/polar_r0dr-theta0dtheta.png and b/test/image/baselines/polar_r0dr-theta0dtheta.png differ
diff --git a/test/image/baselines/polar_radial-range.png b/test/image/baselines/polar_radial-range.png
index e79a88f3b3d..8e6e570dc3e 100644
Binary files a/test/image/baselines/polar_radial-range.png and b/test/image/baselines/polar_radial-range.png differ
diff --git a/test/image/baselines/polar_scatter.png b/test/image/baselines/polar_scatter.png
index 2a94c6851c4..c5fffaf3d30 100644
Binary files a/test/image/baselines/polar_scatter.png and b/test/image/baselines/polar_scatter.png differ
diff --git a/test/image/baselines/polar_sector.png b/test/image/baselines/polar_sector.png
index 78e5585892a..d81ffb8f7c9 100644
Binary files a/test/image/baselines/polar_sector.png and b/test/image/baselines/polar_sector.png differ
diff --git a/test/image/baselines/polar_subplots.png b/test/image/baselines/polar_subplots.png
index 140e0aac811..e441914242d 100644
Binary files a/test/image/baselines/polar_subplots.png and b/test/image/baselines/polar_subplots.png differ
diff --git a/test/image/baselines/polar_ticks.png b/test/image/baselines/polar_ticks.png
index 2cb629b084a..6a4eb5b642b 100644
Binary files a/test/image/baselines/polar_ticks.png and b/test/image/baselines/polar_ticks.png differ
diff --git a/test/image/baselines/polar_transforms.png b/test/image/baselines/polar_transforms.png
index c7c0a5eb218..b120caaafdc 100644
Binary files a/test/image/baselines/polar_transforms.png and b/test/image/baselines/polar_transforms.png differ
diff --git a/test/image/baselines/polar_wind-rose.png b/test/image/baselines/polar_wind-rose.png
index d1050119a3f..951dfe8f833 100644
Binary files a/test/image/baselines/polar_wind-rose.png and b/test/image/baselines/polar_wind-rose.png differ
diff --git a/test/image/baselines/ternary_noticks.png b/test/image/baselines/ternary_noticks.png
new file mode 100644
index 00000000000..e643f587845
Binary files /dev/null and b/test/image/baselines/ternary_noticks.png differ
diff --git a/test/image/mocks/gl2d_fill-ordering.json b/test/image/mocks/gl2d_fill-ordering.json
new file mode 100644
index 00000000000..49d491f95f8
--- /dev/null
+++ b/test/image/mocks/gl2d_fill-ordering.json
@@ -0,0 +1,40 @@
+{
+ "data": [
+ {
+ "x": [1, 2, 3, 4, 5, 6],
+ "y": [100, 100, 0, 0, 0, 0],
+ "fill": "tozeroy",
+ "type": "scattergl"
+ },
+ {
+ "x": [1, 2, 3, 4, 5, 6],
+ "y": [0, 0, 0, 100, 100, 0],
+ "fill": "tozeroy",
+ "type": "scattergl",
+ "mode": "none"
+ },
+ {
+ "x": [1, 2, 3, 4, 5, 6],
+ "y": [99, 99, 99, 100, 100, 100],
+ "type": "scattergl",
+ "mode": "lines+markers"
+ },
+ {
+ "x": [1, 2, 3, 4, 5, 6],
+ "y": [0, 0, 0, null, 50, 50],
+ "fill": "tozeroy",
+ "type": "scattergl",
+ "mode": "none"
+ },
+ {
+ "x": [1, 2, 3, 4, 5, 6],
+ "y": [100, 0, 0, 0, 0, 100],
+ "type": "scattergl",
+ "mode": "lines+markers"
+ }
+ ],
+ "layout": {
+ "margin": {"l": 40, "r": 50, "b": 80, "t": 40},
+ "legend": {"orientation": "h", "x": "0.5", "xanchor": "center"}
+ }
+}
diff --git a/test/image/mocks/polar_hole.json b/test/image/mocks/polar_hole.json
new file mode 100644
index 00000000000..4eb7d7dd016
--- /dev/null
+++ b/test/image/mocks/polar_hole.json
@@ -0,0 +1,101 @@
+{
+ "data": [
+ {
+ "type": "barpolar",
+ "r": [10, 12, 15]
+ },
+ {
+ "type": "scatterpolar",
+ "r": [10, 12, 15],
+ "theta0": 90
+ },
+
+ {
+ "type": "scatterpolar",
+ "subplot": "polar2",
+ "r": [100, 50, 200],
+ "marker": {"size": 20}
+ },
+ {
+ "type": "barpolar",
+ "subplot": "polar2",
+ "r": [100, 50, 200]
+ },
+
+ {
+ "type": "barpolar",
+ "subplot": "polar3",
+ "theta": ["a", "b", "c", "d", "b", "f", "a", "a"]
+ },
+ {
+ "type": "scatterpolar",
+ "subplot": "polar3",
+ "theta": ["a", "b", "c", "d", "b", "f", "a", "a"]
+ },
+
+ {
+ "type": "barpolar",
+ "subplot": "polar4",
+ "r": [10, 12, 15]
+ },
+ {
+ "type": "scatterpolar",
+ "subplot": "polar4",
+ "r": [10, 12, 5],
+ "theta": [0, 90, -90],
+ "cliponaxis": true,
+ "marker": {"size": 20}
+ },
+
+ {
+ "type": "barpolar",
+ "subplot": "polar5",
+ "r": [10, 12, 15]
+ },
+ {
+ "type": "scatterpolar",
+ "subplot": "polar5",
+ "r": [10, 12, 15]
+ }
+ ],
+ "layout": {
+ "width": 600,
+ "height": 800,
+ "margin": {"l": 40, "r": 40, "b": 40, "t": 40, "pad": 0},
+ "grid": {
+ "rows": 3,
+ "columns": 2,
+ "ygap": 0.2
+ },
+ "polar": {
+ "hole": 0.1,
+ "domain": {"row": 0, "column": 0}
+ },
+ "polar2": {
+ "hole": 0.4,
+ "domain": {"row": 0, "column": 1},
+ "angularaxis": {"layer": "below traces"},
+ "radialaxis": {"type": "log", "range": [1.65, 2.35]}
+ },
+ "polar3": {
+ "hole": 0.2,
+ "gridshape": "linear",
+ "angularaxis": {"direction": "clockwise"},
+ "domain": {"row": 1, "column": 0}
+ },
+ "polar4": {
+ "hole": 0.4,
+ "domain": {"row": 1, "column": 1},
+ "sector": [0, 180],
+ "angularaxis": {"direction": "clockwise"},
+ "radialaxis": {"angle": 90, "side": "counterclockwise", "range": [5, 15]}
+ },
+ "polar5": {
+ "hole": 0.5,
+ "domain": {"row": 2, "column": 0},
+ "radialaxis": {"range": [20, 0], "tickfont": {"color": "red", "size": 20}},
+ "angularaxis": {"linewidth": 3}
+ },
+ "showlegend": false
+ }
+}
diff --git a/test/image/mocks/ternary_noticks.json b/test/image/mocks/ternary_noticks.json
new file mode 100644
index 00000000000..bb0f110a3ab
--- /dev/null
+++ b/test/image/mocks/ternary_noticks.json
@@ -0,0 +1,31 @@
+{
+ "data": [
+ {
+ "a": [2, 1, 1],
+ "b": [1, 2, 1],
+ "c": [1, 1, 2.12345],
+ "type": "scatterternary"
+ }
+ ],
+ "layout": {
+ "ternary": {
+ "aaxis": {
+ "showticklabels": false,
+ "showline": false,
+ "title": "no labels / no line"
+ },
+ "baxis": {
+ "ticks": "",
+ "showticklabels": false,
+ "title": "no ticks / no labels"
+ },
+ "caxis": {
+ "showticklabels": false,
+ "showgrid": false,
+ "title": "no grid / no labels"
+ }
+ },
+ "height": 450,
+ "width": 700
+ }
+}
diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js
index aab46b67f5e..4cdee1c84f7 100644
--- a/test/jasmine/assets/custom_assertions.js
+++ b/test/jasmine/assets/custom_assertions.js
@@ -312,6 +312,22 @@ exports.assertNodeOrder = function(selectorBehind, selectorInFront, msg) {
}
};
+/**
+ * Ordering test for any number of nodes - calls assertNodeOrder n-1 times.
+ * Note that we only take the first matching node for each selector, and it's
+ * not necessary that the nodes be siblings or at the same level of nesting.
+ *
+ * @param {Array[string]} selectorArray: css selectors in the order they should
+ * appear in the document, from back to front.
+ * @param {string} msg: context for debugging
+ */
+exports.assertMultiNodeOrder = function(selectorArray, msg) {
+ for(var i = 0; i < selectorArray.length - 1; i++) {
+ var msgi = (msg ? msg + ' - ' : '') + 'entries ' + i + ' and ' + (i + 1);
+ exports.assertNodeOrder(selectorArray[i], selectorArray[i + 1], msgi);
+ }
+};
+
function getParents(node) {
var parent = node.parentNode;
if(parent) return getParents(parent).concat(node);
@@ -324,3 +340,9 @@ function collectionToArray(collection) {
for(var i = 0; i < len; i++) a[i] = collection[i];
return a;
}
+
+exports.assertD3Data = function(selection, expectedData) {
+ var data = [];
+ selection.each(function(d) { data.push(d); });
+ expect(data).toEqual(expectedData);
+};
diff --git a/test/jasmine/assets/double_click.js b/test/jasmine/assets/double_click.js
index 73d444d0782..222ecd7e5a4 100644
--- a/test/jasmine/assets/double_click.js
+++ b/test/jasmine/assets/double_click.js
@@ -3,22 +3,25 @@ var getNodeCoords = require('./get_node_coords');
var DBLCLICKDELAY = require('../../../src/constants/interactions').DBLCLICKDELAY;
/*
- * double click on a point.
- * you can either specify x,y as pixels, or
+ * Double click on a point.
+ * You can either specify x,y as pixels, or
* you can specify node and optionally an edge ('n', 'se', 'w' etc)
- * to grab it by an edge or corner (otherwise the middle is used)
+ * to grab it by an edge or corner (otherwise the middle is used).
+ * You can also pass options for the underlying click, e.g.
+ * to specify modifier keys. See `click` function
+ * for more info.
*/
-module.exports = function doubleClick(x, y) {
+module.exports = function doubleClick(x, y, clickOpts) {
if(typeof x === 'object') {
var coords = getNodeCoords(x, y);
x = coords.x;
y = coords.y;
}
return new Promise(function(resolve) {
- click(x, y);
+ click(x, y, clickOpts);
setTimeout(function() {
- click(x, y);
+ click(x, y, clickOpts);
setTimeout(function() { resolve(); }, DBLCLICKDELAY / 2);
}, DBLCLICKDELAY / 2);
});
diff --git a/test/jasmine/assets/transitions.js b/test/jasmine/assets/transitions.js
new file mode 100644
index 00000000000..cf1be579d51
--- /dev/null
+++ b/test/jasmine/assets/transitions.js
@@ -0,0 +1,35 @@
+'use strict';
+
+/**
+ * Given n states (denoted by their indices 0..n-1) this routine produces
+ * a sequence of indices such that you efficiently execute each transition
+ * from any state to any other state.
+ */
+module.exports = function transitions(n) {
+ var out = [0];
+ var nextStates = [];
+ var i;
+ for(i = 0; i < n; i++) nextStates[i] = (i + 1) % n;
+ var finishedStates = 0;
+ var thisState = 0;
+ var nextState;
+ while(finishedStates < n) {
+ nextState = nextStates[thisState];
+ if(nextState === thisState) {
+ // I don't actually know how to prove that this algorithm works,
+ // but I've never seen it fail for n>1
+ // For prime n it's the same sequence as the one I started with
+ // (n transitions of +1 index, then n transitions +2 etc...)
+ // but this one works for non-prime n as well.
+ throw new Error('your transitions algo failed.');
+ }
+ nextStates[thisState] = (nextStates[thisState] + 1) % n;
+ if(nextStates[thisState] === thisState) finishedStates++;
+ out.push(nextState);
+ thisState = nextState;
+ }
+ if(out.length !== n * (n - 1) + 1) {
+ throw new Error('your transitions algo failed.');
+ }
+ return out;
+};
diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/bundle_tests/plotschema_test.js
similarity index 100%
rename from test/jasmine/tests/plotschema_test.js
rename to test/jasmine/bundle_tests/plotschema_test.js
diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js
index 66bce39e73c..4050c8ccc5e 100644
--- a/test/jasmine/karma.conf.js
+++ b/test/jasmine/karma.conf.js
@@ -274,6 +274,10 @@ if(isBundleTest) {
func.defaultConfig.files.push(pathToIE9mock);
func.defaultConfig.preprocessors[testFileGlob] = ['browserify'];
break;
+ case 'plotschema':
+ func.defaultConfig.browserify.ignoreTransform = './tasks/compress_attributes.js';
+ func.defaultConfig.preprocessors[testFileGlob] = ['browserify'];
+ break;
default:
func.defaultConfig.preprocessors[testFileGlob] = ['browserify'];
break;
diff --git a/test/jasmine/tests/barpolar_test.js b/test/jasmine/tests/barpolar_test.js
index a6aa3ef7f5f..b5314a8f330 100644
--- a/test/jasmine/tests/barpolar_test.js
+++ b/test/jasmine/tests/barpolar_test.js
@@ -274,6 +274,24 @@ describe('Test barpolar hover:', function() {
extraText: 'r: 12
θ: 120°',
color: '#1f77b4'
}
+ }, {
+ desc: 'works on a subplot with hole>0',
+ traces: [{
+ r: [1, 2, 3],
+ theta: [0, 90, 180]
+ }],
+ layout: {
+ polar: {hole: 0.2}
+ },
+ xval: 1,
+ yval: 0,
+ exp: {
+ index: 0,
+ x: 290.67,
+ y: 200,
+ extraText: 'r: 1
θ: 0°',
+ color: '#1f77b4'
+ }
}, {
desc: 'on overlapping bars of same size, the narrower wins',
traces: [{
diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js
index 21a018d7ff6..6f5c26b203b 100644
--- a/test/jasmine/tests/cartesian_test.js
+++ b/test/jasmine/tests/cartesian_test.js
@@ -7,6 +7,7 @@ var Drawing = require('@src/components/drawing');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var failTest = require('../assets/fail_test');
+var assertD3Data = require('../assets/custom_assertions').assertD3Data;
describe('restyle', function() {
describe('scatter traces', function() {
@@ -21,37 +22,35 @@ describe('restyle', function() {
it('reuses SVG fills', function(done) {
var fills, firstToZero, secondToZero, firstToNext, secondToNext;
var mock = Lib.extendDeep({}, require('@mocks/basic_area.json'));
+ function getFills() {
+ return d3.selectAll('g.trace.scatter .fills>g');
+ }
Plotly.plot(gd, mock.data, mock.layout).then(function() {
- // Assert there are two fills:
- fills = d3.selectAll('g.trace.scatter .js-fill')[0];
+ fills = getFills();
- // First is tozero, second is tonext:
- expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2);
- expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
- expect(fills[1]).toBeClassed(['js-fill', 'js-tonext']);
+ // Assert there are two fills, first is tozero, second is tonext
+ assertD3Data(fills, ['_ownFill', '_nextFill']);
+
+ firstToZero = fills[0][0];
+ firstToNext = fills[0][1];
- firstToZero = fills[0];
- firstToNext = fills[1];
- }).then(function() {
return Plotly.restyle(gd, {visible: [false]}, [1]);
}).then(function() {
+ fills = getFills();
// Trace 1 hidden leaves only trace zero's tozero fill:
- expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(1);
- expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
- }).then(function() {
+ assertD3Data(fills, ['_ownFill']);
+
return Plotly.restyle(gd, {visible: [true]}, [1]);
}).then(function() {
- // Reshow means two fills again AND order is preserved:
- fills = d3.selectAll('g.trace.scatter .js-fill')[0];
+ fills = getFills();
+ // Reshow means two fills again AND order is preserved
// First is tozero, second is tonext:
- expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2);
- expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
- expect(fills[1]).toBeClassed(['js-fill', 'js-tonext']);
+ assertD3Data(fills, ['_ownFill', '_nextFill']);
- secondToZero = fills[0];
- secondToNext = fills[1];
+ secondToZero = fills[0][0];
+ secondToNext = fills[0][1];
// The identity of the first is retained:
expect(firstToZero).toBe(secondToZero);
@@ -61,8 +60,7 @@ describe('restyle', function() {
return Plotly.restyle(gd, 'visible', false);
}).then(function() {
- expect(d3.selectAll('g.trace.scatter').size()).toEqual(0);
-
+ expect(d3.selectAll('g.trace.scatter').size()).toBe(0);
})
.catch(failTest)
.then(done);
diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js
index 2982a653529..ecc46e7968b 100644
--- a/test/jasmine/tests/gl2d_plot_interact_test.js
+++ b/test/jasmine/tests/gl2d_plot_interact_test.js
@@ -214,6 +214,53 @@ describe('Test gl plot side effects', function() {
.catch(failTest)
.then(done);
});
+
+ it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) {
+ var _mock = Lib.extendDeep({}, require('@mocks/gl2d_12.json'));
+
+ function _trigger(name) {
+ var ev = new window.WebGLContextEvent('webglcontextlost');
+ var canvas = gd.querySelector('.gl-canvas-' + name);
+ canvas.dispatchEvent(ev);
+ }
+
+ Plotly.plot(gd, _mock).then(function() {
+ return new Promise(function(resolve, reject) {
+ gd.once('plotly_webglcontextlost', resolve);
+ setTimeout(reject, 10);
+ _trigger('context');
+ });
+ })
+ .then(function(eventData) {
+ expect((eventData || {}).event).toBeDefined();
+ expect((eventData || {}).layer).toBe('contextLayer');
+ })
+ .then(function() {
+ return new Promise(function(resolve, reject) {
+ gd.once('plotly_webglcontextlost', resolve);
+ setTimeout(reject, 10);
+ _trigger('focus');
+ });
+ })
+ .then(function(eventData) {
+ expect((eventData || {}).event).toBeDefined();
+ expect((eventData || {}).layer).toBe('focusLayer');
+ })
+ .then(function() {
+ return new Promise(function(resolve, reject) {
+ gd.once('plotly_webglcontextlost', reject);
+ setTimeout(resolve, 10);
+ _trigger('pick');
+ });
+ })
+ .then(function(eventData) {
+ // should add event listener on pick canvas which
+ // isn't used for scattergl traces
+ expect(eventData).toBeUndefined();
+ })
+ .catch(failTest)
+ .then(done);
+ });
});
describe('Test gl2d plots', function() {
diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js
index 64b22e447bc..89d00c3ac11 100644
--- a/test/jasmine/tests/gl3d_plot_interact_test.js
+++ b/test/jasmine/tests/gl3d_plot_interact_test.js
@@ -1425,4 +1425,25 @@ describe('Test removal of gl contexts', function() {
})
.then(done);
});
+
+ it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) {
+ var _mock = Lib.extendDeep({}, require('@mocks/gl3d_marker-arrays.json'));
+
+ Plotly.plot(gd, _mock).then(function() {
+ return new Promise(function(resolve, reject) {
+ gd.on('plotly_webglcontextlost', resolve);
+ setTimeout(reject, 10);
+
+ var ev = new window.WebGLContextEvent('webglcontextlost');
+ var canvas = gd.querySelector('div#scene > canvas');
+ canvas.dispatchEvent(ev);
+ });
+ })
+ .then(function(eventData) {
+ expect((eventData || {}).event).toBeDefined();
+ expect((eventData || {}).layer).toBe('scene');
+ })
+ .catch(failTest)
+ .then(done);
+ });
});
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index aa0981c5ac3..437c47392d1 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -2454,3 +2454,52 @@ describe('hover distance', function() {
});
});
});
+
+describe('hovermode defaults to', function() {
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ it('\'closest\' for cartesian plots if clickmode includes \'select\'', function(done) {
+ Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6] }], { clickmode: 'event+select' })
+ .then(function() {
+ expect(gd._fullLayout.hovermode).toBe('closest');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('\'x\' for horizontal cartesian plots if clickmode lacks \'select\'', function(done) {
+ Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6], type: 'bar', orientation: 'h' }], { clickmode: 'event' })
+ .then(function() {
+ expect(gd._fullLayout.hovermode).toBe('y');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('\'y\' for vertical cartesian plots if clickmode lacks \'select\'', function(done) {
+ Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6], type: 'bar', orientation: 'v' }], { clickmode: 'event' })
+ .then(function() {
+ expect(gd._fullLayout.hovermode).toBe('x');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('\'closest\' for a non-cartesian plot', function(done) {
+ var mock = require('@mocks/polar_scatter.json');
+ expect(mock.layout.hovermode).toBeUndefined();
+
+ Plotly.plot(gd, mock.data, mock.layout)
+ .then(function() {
+ expect(gd._fullLayout.hovermode).toBe('closest');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+});
diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js
index fe05a10bf67..d1dea69c844 100644
--- a/test/jasmine/tests/parcoords_test.js
+++ b/test/jasmine/tests/parcoords_test.js
@@ -1052,6 +1052,36 @@ describe('parcoords basic use', function() {
.catch(failTest)
.then(done);
});
+
+ it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function() {
+ var eventData;
+ var cnt = 0;
+ gd.on('plotly_webglcontextlost', function(d) {
+ eventData = d;
+ cnt++;
+ });
+
+ function trigger(name) {
+ var ev = new window.WebGLContextEvent('webglcontextlost');
+ var canvas = gd.querySelector('.gl-canvas-' + name);
+ canvas.dispatchEvent(ev);
+ }
+
+ function _assert(d, c) {
+ expect((eventData || {}).event).toBeDefined();
+ expect((eventData || {}).layer).toBe(d);
+ expect(cnt).toBe(c);
+ }
+
+ trigger('context');
+ _assert('contextLayer', 1);
+
+ trigger('focus');
+ _assert('focusLayer', 2);
+
+ trigger('pick');
+ _assert('pickLayer', 3);
+ });
});
describe('@noCI parcoords constraint interactions', function() {
diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index eb1e735387c..9b2469b630a 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -2962,7 +2962,7 @@ describe('Test plot api', function() {
Plotly.newPlot(gd, data, layout)
.then(countPlots)
.then(function() {
- expect(d3.select(gd).selectAll('.drag').size()).toBe(3);
+ expect(d3.select(gd).selectAll('.drag').size()).toBe(4);
return Plotly.react(gd, data, layout, {staticPlot: true});
})
@@ -2972,7 +2972,7 @@ describe('Test plot api', function() {
return Plotly.react(gd, data, layout, {});
})
.then(function() {
- expect(d3.select(gd).selectAll('.drag').size()).toBe(3);
+ expect(d3.select(gd).selectAll('.drag').size()).toBe(4);
})
.catch(failTest)
.then(done);
diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js
index 4b5139bf818..ede60e8b130 100644
--- a/test/jasmine/tests/plots_test.js
+++ b/test/jasmine/tests/plots_test.js
@@ -500,6 +500,13 @@ describe('Test Plots', function() {
});
describe('Plots.graphJson', function() {
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
it('should serialize data, layout and frames', function(done) {
var mock = {
@@ -533,7 +540,7 @@ describe('Test Plots', function() {
}]
};
- Plotly.plot(createGraphDiv(), mock).then(function(gd) {
+ Plotly.plot(gd, mock).then(function() {
var str = Plots.graphJson(gd, false, 'keepdata');
var obj = JSON.parse(str);
@@ -547,10 +554,38 @@ describe('Test Plots', function() {
name: 'garbage'
});
})
- .then(function() {
- destroyGraphDiv();
- done();
- });
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should convert typed arrays to regular arrays', function(done) {
+ var trace = {
+ x: new Float32Array([1, 2, 3]),
+ y: new Float32Array([1, 2, 1]),
+ marker: {
+ size: new Float32Array([20, 30, 10]),
+ color: new Float32Array([10, 30, 20]),
+ cmin: 10,
+ cmax: 30,
+ colorscale: [
+ [0, 'rgb(255, 0, 0)'],
+ [0.5, 'rgb(0, 255, 0)'],
+ [1, 'rgb(0, 0, 255)']
+ ]
+ }
+ };
+
+ Plotly.plot(gd, [trace]).then(function() {
+ var str = Plots.graphJson(gd, false, 'keepdata');
+ var obj = JSON.parse(str);
+
+ expect(obj.data[0].x).toEqual([1, 2, 3]);
+ expect(obj.data[0].y).toEqual([1, 2, 1]);
+ expect(obj.data[0].marker.size).toEqual([20, 30, 10]);
+ expect(obj.data[0].marker.color).toEqual([10, 30, 20]);
+ })
+ .catch(failTest)
+ .then(done);
});
});
diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js
index c7ffc6a685c..84565b145d7 100644
--- a/test/jasmine/tests/polar_test.js
+++ b/test/jasmine/tests/polar_test.js
@@ -236,19 +236,19 @@ describe('Test relayout on polar subplots:', function() {
.then(function() {
_assert([
'draglayer', 'plotbg', 'backplot', 'angular-grid', 'radial-grid',
- 'radial-axis', 'radial-line',
+ 'radial-line', 'radial-axis',
'frontplot',
- 'angular-axis', 'angular-line'
+ 'angular-line', 'angular-axis'
]);
return Plotly.relayout(gd, 'polar.angularaxis.layer', 'below traces');
})
.then(function() {
_assert([
'draglayer', 'plotbg', 'backplot', 'angular-grid', 'radial-grid',
- 'angular-axis',
- 'radial-axis',
'angular-line',
'radial-line',
+ 'angular-axis',
+ 'radial-axis',
'frontplot'
]);
return Plotly.relayout(gd, 'polar.radialaxis.layer', 'above traces');
@@ -256,9 +256,9 @@ describe('Test relayout on polar subplots:', function() {
.then(function() {
_assert([
'draglayer', 'plotbg', 'backplot', 'angular-grid', 'radial-grid',
- 'angular-axis', 'angular-line',
+ 'angular-line', 'angular-axis',
'frontplot',
- 'radial-axis', 'radial-line'
+ 'radial-line', 'radial-axis'
]);
return Plotly.relayout(gd, 'polar.angularaxis.layer', null);
})
@@ -381,75 +381,62 @@ describe('Test relayout on polar subplots:', function() {
}
function toggle(astr, vals, exps, selector, fn) {
- return Plotly.relayout(gd, astr, vals[0]).then(function() {
- fn(selector, exps[0], astr + ' ' + vals[0]);
- return Plotly.relayout(gd, astr, vals[1]);
- })
- .then(function() {
- fn(selector, exps[1], astr + ' ' + vals[1]);
- return Plotly.relayout(gd, astr, vals[0]);
- })
- .then(function() {
- fn(selector, exps[0], astr + ' ' + vals[0]);
- });
+ return function() {
+ return Plotly.relayout(gd, astr, vals[0]).then(function() {
+ fn(selector, exps[0], astr + ' ' + vals[0]);
+ return Plotly.relayout(gd, astr, vals[1]);
+ })
+ .then(function() {
+ fn(selector, exps[1], astr + ' ' + vals[1]);
+ return Plotly.relayout(gd, astr, vals[0]);
+ })
+ .then(function() {
+ fn(selector, exps[0], astr + ' ' + vals[0]);
+ });
+ };
}
- Plotly.plot(gd, fig).then(function() {
- return toggle(
- 'polar.radialaxis.showline',
- [true, false], [null, 'none'],
- '.radial-line > line', assertDisplay
- );
- })
- .then(function() {
- return toggle(
- 'polar.radialaxis.showgrid',
- [true, false], [null, 'none'],
- '.radial-grid', assertDisplay
- );
- })
- .then(function() {
- return toggle(
- 'polar.radialaxis.showticklabels',
- [true, false], [6, 0],
- '.radial-axis > .xtick > text', assertCnt
- );
- })
- .then(function() {
- return toggle(
- 'polar.radialaxis.ticks',
- ['outside', ''], [6, 0],
- '.radial-axis > path.xtick', assertCnt
- );
- })
- .then(function() {
- return toggle(
- 'polar.angularaxis.showline',
- [true, false], [null, 'none'],
- '.angular-line > path', assertDisplay
- );
- })
- .then(function() {
- return toggle(
- 'polar.angularaxis.showgrid',
- [true, false], [8, 0],
- '.angular-grid > .angularaxis > path', assertCnt
- );
- })
- .then(function() {
- return toggle(
- 'polar.angularaxis.showticklabels',
- [true, false], [8, 0],
- '.angular-axis > .angularaxistick > text', assertCnt
- );
- })
- .then(function() {
- return toggle(
- 'polar.angularaxis.ticks',
- ['outside', ''], [8, 0],
- '.angular-axis > path.angularaxistick', assertCnt
- );
- })
+ Plotly.plot(gd, fig)
+ .then(toggle(
+ 'polar.radialaxis.showline',
+ [true, false], [null, 'none'],
+ '.radial-line > line', assertDisplay
+ ))
+ .then(toggle(
+ 'polar.radialaxis.showgrid',
+ [true, false], [null, 'none'],
+ '.radial-grid', assertDisplay
+ ))
+ .then(toggle(
+ 'polar.radialaxis.showticklabels',
+ [true, false], [6, 0],
+ '.radial-axis > .xtick > text', assertCnt
+ ))
+ .then(toggle(
+ 'polar.radialaxis.ticks',
+ ['outside', ''], [6, 0],
+ '.radial-axis > path.xtick', assertCnt
+ ))
+ .then(toggle(
+ 'polar.angularaxis.showline',
+ [true, false], [null, 'none'],
+ '.angular-line > path', assertDisplay
+ ))
+ .then(toggle(
+ 'polar.angularaxis.showgrid',
+ [true, false], [8, 0],
+ '.angular-grid > .angularaxis > path', assertCnt
+ ))
+ .then(toggle(
+ 'polar.angularaxis.showticklabels',
+ [true, false], [8, 0],
+ '.angular-axis > .angularaxistick > text', assertCnt
+ ))
+ .then(toggle(
+ 'polar.angularaxis.ticks',
+ ['outside', ''], [8, 0],
+ '.angular-axis > path.angularaxistick', assertCnt
+ ))
.catch(failTest)
.then(done);
});
@@ -924,6 +911,13 @@ describe('Test polar interactions:', function() {
expect(eventCnts.plotly_relayout)
.toBe(relayoutNumber, 'no new relayout events after *not far enough* cases');
})
+ .then(_reset)
+ .then(function() { return Plotly.relayout(gd, 'polar.hole', 0.2); })
+ .then(function() { relayoutNumber++; })
+ .then(function() { return _drag([mid[0] + 30, mid[0] - 30], [50, -50]); })
+ .then(function() {
+ _assertDrag([1.15, 7.70], 'with polar.hole>0, from quadrant #1 move top-right');
+ })
.catch(failTest)
.then(done);
});
@@ -1012,6 +1006,45 @@ describe('Test polar interactions:', function() {
.then(done);
});
+ it('should response to drag interactions on inner radial drag area', function(done) {
+ var fig = Lib.extendDeep({}, require('@mocks/polar_scatter.json'));
+ fig.layout.polar.hole = 0.2;
+ // to avoid dragging on hover labels
+ fig.layout.hovermode = false;
+ // adjust margins so that middle of plot area is at 300x300
+ // with its middle at [200,200]
+ fig.layout.width = 400;
+ fig.layout.height = 400;
+ fig.layout.margin = {l: 50, t: 50, b: 50, r: 50};
+
+ var dragPos0 = [200, 200];
+
+ // use 'special' drag method - as we need two mousemove events
+ // to activate the radial drag mode
+ function _drag(p0, dp) {
+ var node = d3.select('.polar > .draglayer > .radialdrag-inner').node();
+ return drag(node, dp[0], dp[1], null, p0[0], p0[1], 2);
+ }
+
+ function _assert(rng, msg) {
+ expect(gd._fullLayout.polar.radialaxis.range)
+ .toBeCloseToArray(rng, 1, msg + ' - range');
+ }
+
+ _plot(fig)
+ .then(function() { return _drag(dragPos0, [-50, 0]); })
+ .then(function() {
+ _assert([3.55, 11.36], 'move inward');
+ })
+ .then(function() { return Plotly.relayout(gd, 'polar.radialaxis.autorange', true); })
+ .then(function() { return _drag(dragPos0, [50, 0]); })
+ .then(function() {
+ _assert([-3.55, 11.36], 'move outward');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
it('should response to drag interactions on angular drag area', function(done) {
var fig = Lib.extendDeep({}, require('@mocks/polar_scatter.json'));
diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js
index 74817411abf..a49b8a69c3e 100644
--- a/test/jasmine/tests/scatter_test.js
+++ b/test/jasmine/tests/scatter_test.js
@@ -9,9 +9,11 @@ var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var customAssertions = require('../assets/custom_assertions');
var failTest = require('../assets/fail_test');
+var transitions = require('../assets/transitions');
var assertClip = customAssertions.assertClip;
var assertNodeDisplay = customAssertions.assertNodeDisplay;
+var assertMultiNodeOrder = customAssertions.assertMultiNodeOrder;
var getOpacity = function(node) { return Number(node.style.opacity); };
var getFillOpacity = function(node) { return Number(node.style['fill-opacity']); };
@@ -629,6 +631,91 @@ describe('end-to-end scatter tests', function() {
.then(done);
});
+ it('should keep layering correct as mode & fill change', function(done) {
+ var fillCase = {name: 'fill', edit: {mode: 'none', fill: 'tonexty'}};
+ var i, j;
+
+ var cases = [fillCase];
+ var modeParts = ['lines', 'markers', 'text'];
+ for(i = 0; i < modeParts.length; i++) {
+ var modePart = modeParts[i];
+ var prevCasesLength = cases.length;
+
+ cases.push({name: modePart, edit: {mode: modePart, fill: 'none'}});
+ for(j = 0; j < prevCasesLength; j++) {
+ var prevCase = cases[j];
+ cases.push({
+ name: prevCase.name + '_' + modePart,
+ edit: {
+ mode: (prevCase.edit.mode === 'none' ? '' : (prevCase.edit.mode + '+')) + modePart,
+ fill: prevCase.edit.fill
+ }
+ });
+ }
+ }
+
+ // visit each case N times, in an order that covers each *transition*
+ // from any case to any other case.
+ var indices = transitions(cases.length);
+
+ var p = Plotly.plot(gd, [
+ {y: [1, 2], text: 'a'},
+ {y: [2, 3], text: 'b'},
+ {y: [3, 4], text: 'c'}
+ ]);
+
+ function setMode(i) { return function() {
+ return Plotly.restyle(gd, cases[indices[i]].edit);
+ }; }
+
+ function testOrdering(i) { return function() {
+ var name = cases[indices[i]].name;
+ var hasFills = name.indexOf('fill') !== -1;
+ var hasLines = name.indexOf('lines') !== -1;
+ var hasMarkers = name.indexOf('markers') !== -1;
+ var hasText = name.indexOf('text') !== -1;
+ var tracei, prefix;
+
+ // construct the expected ordering based on case name
+ var selectorArray = [];
+ for(tracei = 0; tracei < 3; tracei++) {
+ prefix = '.xy .trace:nth-child(' + (tracei + 1) + ') ';
+
+ // two fills are attached to the first trace, one to the second
+ if(hasFills) {
+ if(tracei === 0) {
+ selectorArray.push(
+ prefix + 'g:first-child>.js-fill',
+ prefix + 'g:last-child>.js-fill');
+ }
+ else if(tracei === 1) selectorArray.push(prefix + 'g:last-child>.js-fill');
+ }
+ if(hasLines) selectorArray.push(prefix + '.js-line');
+ if(hasMarkers) selectorArray.push(prefix + '.point');
+ if(hasText) selectorArray.push(prefix + '.textpoint');
+ }
+
+ // ordering in the legend
+ for(tracei = 0; tracei < 3; tracei++) {
+ prefix = '.legend .traces:nth-child(' + (tracei + 1) + ') ';
+ if(hasFills) selectorArray.push(prefix + '.js-fill');
+ if(hasLines) selectorArray.push(prefix + '.js-line');
+ if(hasMarkers) selectorArray.push(prefix + '.scatterpts');
+ if(hasText) selectorArray.push(prefix + '.pointtext');
+ }
+
+ var msg = i ? ('from ' + cases[indices[i - 1]].name + ' to ') : 'from default to ';
+ msg += name;
+ assertMultiNodeOrder(selectorArray, msg);
+ }; }
+
+ for(i = 0; i < indices.length; i++) {
+ p = p.then(setMode(i)).then(testOrdering(i));
+ }
+
+ p.catch(failTest).then(done);
+ });
+
function _assertNodes(ptStyle, txContent) {
var pts = d3.selectAll('.point');
var txs = d3.selectAll('.textpoint');
diff --git a/test/jasmine/tests/scatterpolar_test.js b/test/jasmine/tests/scatterpolar_test.js
index 14d2ea0a954..27584eb48db 100644
--- a/test/jasmine/tests/scatterpolar_test.js
+++ b/test/jasmine/tests/scatterpolar_test.js
@@ -150,6 +150,14 @@ describe('Test scatterpolar hover:', function() {
pos: [465, 90],
nums: 'r: 4\nθ: d',
name: 'angular cate...'
+ }, {
+ desc: 'on a subplot with hole>0',
+ patch: function(fig) {
+ fig.layout.polar.hole = 0.2;
+ return fig;
+ },
+ nums: 'r: 1.108937\nθ: 115.4969°',
+ name: 'Trial 3'
}]
.forEach(function(specs) {
it('should generate correct hover labels ' + specs.desc, function(done) {
diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 821c7380e96..750071a2973 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -2,7 +2,9 @@ var d3 = require('d3');
var Plotly = require('@lib/index');
var Lib = require('@src/lib');
+var click = require('../assets/click');
var doubleClick = require('../assets/double_click');
+var DBLCLICKDELAY = require('../../../src/constants/interactions').DBLCLICKDELAY;
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
@@ -52,7 +54,7 @@ function assertSelectionNodes(cornerCnt, outlineCnt, _msg) {
}
var selectingCnt, selectingData, selectedCnt, selectedData, deselectCnt, doubleClickData;
-var selectedPromise, deselectPromise;
+var selectedPromise, deselectPromise, clickedPromise;
function resetEvents(gd) {
selectingCnt = 0;
@@ -75,7 +77,13 @@ function resetEvents(gd) {
});
gd.on('plotly_selected', function(data) {
- assertSelectionNodes(0, 2);
+ // With click-to-select supported, selection nodes are only
+ // in the DOM in certain circumstances.
+ if(data &&
+ gd._fullLayout.dragmode.indexOf('select') > -1 &&
+ gd._fullLayout.dragmode.indexOf('lasso') > -1) {
+ assertSelectionNodes(0, 2);
+ }
selectedCnt++;
selectedData = data;
resolve();
@@ -90,6 +98,12 @@ function resetEvents(gd) {
resolve();
});
});
+
+ clickedPromise = new Promise(function(resolve) {
+ gd.on('plotly_click', function() {
+ resolve();
+ });
+ });
}
function assertEventCounts(selecting, selected, deselect, msg) {
@@ -109,6 +123,659 @@ var BOXEVENTS = [1, 2, 1];
// assumes 5 points in the lasso path
var LASSOEVENTS = [4, 2, 1];
+var SELECT_PATH = [[93, 193], [143, 193]];
+var LASSO_PATH = [[316, 171], [318, 239], [335, 243], [328, 169]];
+
+describe('Click-to-select', function() {
+ var mock14Pts = {
+ '1': { x: 134, y: 116 },
+ '7': { x: 270, y: 160 },
+ '10': { x: 324, y: 198 },
+ '35': { x: 685, y: 341 }
+ };
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+
+ afterEach(destroyGraphDiv);
+
+ function plotMock14(layoutOpts) {
+ var mock = require('@mocks/14.json');
+ var defaultLayoutOpts = {
+ layout: {
+ clickmode: 'event+select',
+ dragmode: 'select',
+ hovermode: 'closest'
+ }
+ };
+ var mockCopy = Lib.extendDeep(
+ {},
+ mock,
+ defaultLayoutOpts,
+ { layout: layoutOpts });
+
+ return Plotly.plot(gd, mockCopy.data, mockCopy.layout);
+ }
+
+ /**
+ * Executes a click and before resets selection event handlers.
+ * By default, click is executed with a delay to prevent unwanted double clicks.
+ * Returns the `selectedPromise` promise for convenience.
+ */
+ function _click(x, y, clickOpts, immediate) {
+ resetEvents(gd);
+
+ // Too fast subsequent calls of `click` would
+ // produce an unwanted double click, thus we need
+ // to delay the click.
+ if(immediate) {
+ click(x, y, clickOpts);
+ } else {
+ setTimeout(function() {
+ click(x, y, clickOpts);
+ }, DBLCLICKDELAY * 1.01);
+ }
+
+ return selectedPromise;
+ }
+
+ function _clickPt(coords, clickOpts, immediate) {
+ expect(coords).toBeDefined('coords needs to be defined');
+ expect(coords.x).toBeDefined('coords.x needs to be defined');
+ expect(coords.y).toBeDefined('coords.y needs to be defined');
+
+ return _click(coords.x, coords.y, clickOpts, immediate);
+ }
+
+ /**
+ * Convenient helper to execute a click immediately.
+ */
+ function _immediateClickPt(coords, clickOpts) {
+ return _clickPt(coords, clickOpts, true);
+ }
+
+ /**
+ * Asserting selected points.
+ *
+ * @param expected can be a point number, an array
+ * of point numbers (for a single trace) or an array of point number
+ * arrays in case of multiple traces. undefined in an array of arrays
+ * is also allowed, e.g. useful when not all traces support selection.
+ */
+ function assertSelectedPoints(expected) {
+ var expectedPtsPerTrace = toArrayOfArrays(expected);
+ var expectedPts, traceNum;
+
+ for(traceNum = 0; traceNum < expectedPtsPerTrace.length; traceNum++) {
+ expectedPts = expectedPtsPerTrace[traceNum];
+ expect(gd._fullData[traceNum].selectedpoints).toEqual(expectedPts);
+ expect(gd.data[traceNum].selectedpoints).toEqual(expectedPts);
+ }
+
+ function toArrayOfArrays(expected) {
+ var isArrayInArray, i;
+
+ if(Array.isArray(expected)) {
+ isArrayInArray = false;
+ for(i = 0; i < expected.length; i++) {
+ if(Array.isArray(expected[i])) {
+ isArrayInArray = true;
+ break;
+ }
+ }
+
+ return isArrayInArray ? expected : [expected];
+ } else {
+ return [[expected]];
+ }
+ }
+ }
+
+ function assertSelectionCleared() {
+ gd._fullData.forEach(function(fullDataItem) {
+ expect(fullDataItem.selectedpoints).toBeUndefined();
+ });
+ }
+
+ it('selects a single data point when being clicked', function(done) {
+ plotMock14()
+ .then(function() { return _immediateClickPt(mock14Pts[7]); })
+ .then(function() { assertSelectedPoints(7); })
+ .catch(failTest)
+ .then(done);
+ });
+
+ describe('clears entire selection when the last selected data point', function() {
+ [{
+ desc: 'is clicked',
+ clickOpts: {}
+ }, {
+ desc: 'is clicked while add/subtract modifier keys are active',
+ clickOpts: { shiftKey: true }
+ }].forEach(function(testData) {
+ it('@flaky ' + testData.desc, function(done) {
+ plotMock14()
+ .then(function() { return _immediateClickPt(mock14Pts[7]); })
+ .then(function() {
+ assertSelectedPoints(7);
+ _clickPt(mock14Pts[7], testData.clickOpts);
+ return deselectPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ return _clickPt(mock14Pts[35], testData.clickOpts);
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
+ });
+
+ it('@flaky cleanly clears and starts selections although add/subtract mode on', function(done) {
+ plotMock14()
+ .then(function() {
+ return _immediateClickPt(mock14Pts[7]);
+ })
+ .then(function() {
+ assertSelectedPoints(7);
+ _clickPt(mock14Pts[7], { shiftKey: true });
+ return deselectPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ return _clickPt(mock14Pts[35], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@flaky supports adding to an existing selection', function(done) {
+ plotMock14()
+ .then(function() { return _immediateClickPt(mock14Pts[7]); })
+ .then(function() {
+ assertSelectedPoints(7);
+ return _clickPt(mock14Pts[35], { shiftKey: true });
+ })
+ .then(function() { assertSelectedPoints([7, 35]); })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@flaky supports subtracting from an existing selection', function(done) {
+ plotMock14()
+ .then(function() { return _immediateClickPt(mock14Pts[7]); })
+ .then(function() {
+ assertSelectedPoints(7);
+ return _clickPt(mock14Pts[35], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([7, 35]);
+ return _clickPt(mock14Pts[7], { shiftKey: true });
+ })
+ .then(function() { assertSelectedPoints(35); })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@flaky can be used interchangeably with lasso/box select', function(done) {
+ plotMock14()
+ .then(function() {
+ return _immediateClickPt(mock14Pts[35]);
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ drag(SELECT_PATH, { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([0, 1, 35]);
+ return _immediateClickPt(mock14Pts[7], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([0, 1, 7, 35]);
+ return _clickPt(mock14Pts[1], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([0, 7, 35]);
+ return Plotly.relayout(gd, 'dragmode', 'lasso');
+ })
+ .then(function() {
+ assertSelectedPoints([0, 7, 35]);
+ drag(LASSO_PATH, { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([0, 7, 10, 35]);
+ return _clickPt(mock14Pts[10], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([0, 7, 35]);
+ drag([[670, 330], [695, 330], [695, 350], [670, 350]],
+ { shiftKey: true, altKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([0, 7]);
+ return _clickPt(mock14Pts[35], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([0, 7, 35]);
+ return _clickPt(mock14Pts[7]);
+ })
+ .then(function() {
+ assertSelectedPoints([7]);
+ return doubleClick(650, 100);
+ })
+ .then(function() {
+ assertSelectionCleared();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@gl works in a multi-trace plot', function(done) {
+ Plotly.plot(gd, [
+ {
+ x: [1, 3, 5, 4, 10, 12, 12, 7],
+ y: [2, 7, 6, 1, 0, 13, 6, 12],
+ type: 'scatter',
+ mode: 'markers',
+ marker: { size: 20 }
+ }, {
+ x: [1, 7, 6, 2],
+ y: [2, 3, 5, 4],
+ type: 'bar'
+ }, {
+ x: [7, 8, 9, 10],
+ y: [7, 9, 13, 21],
+ type: 'scattergl',
+ mode: 'markers',
+ marker: { size: 20 }
+ }
+ ], {
+ width: 400,
+ height: 600,
+ hovermode: 'closest',
+ dragmode: 'select',
+ clickmode: 'event+select'
+ })
+ .then(function() {
+ return _click(136, 369, {}, true); })
+ .then(function() {
+ assertSelectedPoints([[1], [], []]);
+ return _click(245, 136, { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([[1], [], [3]]);
+ return _click(183, 470, { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([[1], [2], [3]]);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@flaky is supported in pan/zoom mode', function(done) {
+ plotMock14({ dragmode: 'zoom' })
+ .then(function() {
+ return _immediateClickPt(mock14Pts[35]);
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ return _clickPt(mock14Pts[7], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([7, 35]);
+ return _clickPt(mock14Pts[7], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ drag(LASSO_PATH);
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ _clickPt(mock14Pts[35], { shiftKey: true });
+ return deselectPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@flaky retains selected points when switching between pan and zoom mode', function(done) {
+ plotMock14({ dragmode: 'zoom' })
+ .then(function() {
+ return _immediateClickPt(mock14Pts[35]);
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ return Plotly.relayout(gd, 'dragmode', 'pan');
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ return _clickPt(mock14Pts[7], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints([7, 35]);
+ return Plotly.relayout(gd, 'dragmode', 'zoom');
+ })
+ .then(function() {
+ assertSelectedPoints([7, 35]);
+ return _clickPt(mock14Pts[7], { shiftKey: true });
+ })
+ .then(function() {
+ assertSelectedPoints(35);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@gl is supported by scattergl in pan/zoom mode', function(done) {
+ Plotly.plot(gd, [
+ {
+ x: [7, 8, 9, 10],
+ y: [7, 9, 13, 21],
+ type: 'scattergl',
+ mode: 'markers',
+ marker: { size: 20 }
+ }
+ ], {
+ width: 400,
+ height: 600,
+ hovermode: 'closest',
+ dragmode: 'zoom',
+ clickmode: 'event+select'
+ })
+ .then(function() {
+ return _click(230, 340, {}, true);
+ })
+ .then(function() {
+ assertSelectedPoints(2);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('@flaky deals correctly with histogram\'s binning in the persistent selection case', function(done) {
+ var mock = require('@mocks/histogram_colorscale.json');
+ var firstBinPts = [0];
+ var secondBinPts = [1, 2];
+ var thirdBinPts = [3, 4, 5];
+
+ mock.layout.clickmode = 'event+select';
+ Plotly.plot(gd, mock.data, mock.layout)
+ .then(function() {
+ return clickFirstBinImmediately();
+ })
+ .then(function() {
+ assertSelectedPoints(firstBinPts);
+ return shiftClickSecondBin();
+ })
+ .then(function() {
+ assertSelectedPoints([].concat(firstBinPts, secondBinPts));
+ return shiftClickThirdBin();
+ })
+ .then(function() {
+ assertSelectedPoints([].concat(firstBinPts, secondBinPts, thirdBinPts));
+ return clickFirstBin();
+ })
+ .then(function() {
+ assertSelectedPoints([].concat(firstBinPts));
+ clickFirstBin();
+ return deselectPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ })
+ .catch(failTest)
+ .then(done);
+
+ function clickFirstBinImmediately() { return _immediateClickPt({ x: 141, y: 358 }); }
+ function clickFirstBin() { return _click(141, 358); }
+ function shiftClickSecondBin() { return _click(239, 330, { shiftKey: true }); }
+ function shiftClickThirdBin() { return _click(351, 347, { shiftKey: true }); }
+ });
+
+ it('@flaky ignores clicks on boxes in a box trace type', function(done) {
+ var mock = Lib.extendDeep({}, require('@mocks/box_grouped_horz.json'));
+
+ mock.layout.clickmode = 'event+select';
+ mock.layout.width = 1100;
+ mock.layout.height = 450;
+
+ Plotly.plot(gd, mock.data, mock.layout)
+ .then(function() {
+ return clickPtImmediately();
+ })
+ .then(function() {
+ assertSelectedPoints(2);
+ clickPt();
+ return deselectPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ clickBox();
+ return clickedPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ })
+ .catch(failTest)
+ .then(done);
+
+ function clickPtImmediately() { return _immediateClickPt({ x: 610, y: 342 }); }
+ function clickPt() { return _clickPt({ x: 610, y: 342 }); }
+ function clickBox() { return _clickPt({ x: 565, y: 329 }); }
+ });
+
+ describe('is disabled when clickmode does not include \'select\'', function() {
+ ['select', 'lasso']
+ .forEach(function(dragmode) {
+ it('@flaky and dragmode is ' + dragmode, function(done) {
+ plotMock14({ clickmode: 'event', dragmode: dragmode })
+ .then(function() {
+ // Still, the plotly_selected event should be thrown,
+ // so return promise here
+ return _immediateClickPt(mock14Pts[1]);
+ })
+ .then(function() {
+ assertSelectionCleared();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
+ });
+
+ describe('is disabled when clickmode does not include \'select\'', function() {
+ ['pan', 'zoom']
+ .forEach(function(dragmode) {
+ it('@flaky and dragmode is ' + dragmode, function(done) {
+ plotMock14({ clickmode: 'event', dragmode: dragmode })
+ .then(function() {
+ _immediateClickPt(mock14Pts[1]);
+ return clickedPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
+ });
+
+ describe('is supported by', function() {
+ // On loading mocks:
+ // - Note, that `require` function calls are resolved at compile time
+ // and thus dynamically concatenated mock paths won't work.
+ // - Some mocks don't specify a width and height, so this needs
+ // to be set explicitly to ensure click coordinates fit.
+
+ // The non-gl traces: use @flaky CI annotation
+ [
+ testCase('histrogram', require('@mocks/histogram_colorscale.json'), 355, 301, [3, 4, 5]),
+ testCase('box', require('@mocks/box_grouped_horz.json'), 610, 342, [[2], [], []],
+ { width: 1100, height: 450 }),
+ testCase('violin', require('@mocks/violin_grouped.json'), 166, 187, [[3], [], []],
+ { width: 1100, height: 450 }),
+ testCase('ohlc', require('@mocks/ohlc_first.json'), 669, 165, [9]),
+ testCase('candlestick', require('@mocks/finance_style.json'), 331, 162, [[], [5]]),
+ testCase('choropleth', require('@mocks/geo_choropleth-text.json'), 440, 163, [6]),
+ testCase('scattergeo', require('@mocks/geo_scattergeo-locations.json'), 285, 240, [1]),
+ testCase('scatterternary', require('@mocks/ternary_markers.json'), 485, 335, [7]),
+
+ // Note that first trace (carpet) in mock doesn't support selection,
+ // thus undefined is expected
+ testCase('scattercarpet', require('@mocks/scattercarpet.json'), 532, 178,
+ [undefined, [], [], [], [], [], [2]], { width: 1100, height: 450 }),
+
+ // scatterpolar and scatterpolargl do not support pan (the default),
+ // so set dragmode to zoom
+ testCase('scatterpolar', require('@mocks/polar_scatter.json'), 130, 290,
+ [[], [], [], [19], [], []], { dragmode: 'zoom' }),
+ ]
+ .forEach(function(testCase) {
+ it('@flaky trace type ' + testCase.label, function(done) {
+ _run(testCase, done);
+ });
+ });
+
+ // The gl traces: use @gl CI annotation
+ [
+ testCase('scatterpolargl', require('@mocks/glpolar_scatter.json'), 130, 290,
+ [[], [], [], [19], [], []], { dragmode: 'zoom' }),
+ testCase('splom', require('@mocks/splom_lower.json'), 427, 400, [[], [7], []])
+ ]
+ .forEach(function(testCase) {
+ it('@gl trace type ' + testCase.label, function(done) {
+ _run(testCase, done);
+ });
+ });
+
+ // The mapbox traces: use @noCI annotation cause they are usually too resource-intensive
+ [
+ testCase('scattermapbox', require('@mocks/mapbox_0.json'), 650, 195, [[2], []], {},
+ { mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN })
+ ]
+ .forEach(function(testCase) {
+ it('@noCI trace type ' + testCase.label, function(done) {
+ _run(testCase, done);
+ });
+ });
+
+ function _run(testCase, doneFn) {
+ Plotly.plot(gd, testCase.mock.data, testCase.mock.layout, testCase.mock.config)
+ .then(function() {
+ return _immediateClickPt(testCase);
+ })
+ .then(function() {
+ assertSelectedPoints(testCase.expectedPts);
+ return Plotly.relayout(gd, 'dragmode', 'lasso');
+ })
+ .then(function() {
+ _clickPt(testCase);
+ return deselectPromise;
+ })
+ .then(function() {
+ assertSelectionCleared();
+ return _clickPt(testCase);
+ })
+ .then(function() {
+ assertSelectedPoints(testCase.expectedPts);
+ })
+ .catch(failTest)
+ .then(doneFn);
+ }
+ });
+
+ describe('triggers \'plotly_selected\' before \'plotly_click\'', function() {
+ [
+ testCase('cartesian', require('@mocks/14.json'), 270, 160, [7]),
+ testCase('geo', require('@mocks/geo_scattergeo-locations.json'), 285, 240, [1]),
+ testCase('ternary', require('@mocks/ternary_markers.json'), 485, 335, [7]),
+ testCase('polar', require('@mocks/polar_scatter.json'), 130, 290,
+ [[], [], [], [19], [], []], { dragmode: 'zoom' })
+ ].forEach(function(testCase) {
+ it('@flaky for base plot ' + testCase.label, function(done) {
+ _run(testCase, done);
+ });
+ });
+
+ // The mapbox traces: use @noCI annotation cause they are usually too resource-intensive
+ [
+ testCase('mapbox', require('@mocks/mapbox_0.json'), 650, 195, [[2], []], {},
+ { mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN })
+ ].forEach(function(testCase) {
+ it('@noCI for base plot ' + testCase.label, function(done) {
+ _run(testCase, done);
+ });
+ });
+
+ function _run(testCase, doneFn) {
+ Plotly.plot(gd, testCase.mock.data, testCase.mock.layout, testCase.mock.config)
+ .then(function() {
+ var clickHandlerCalled = false;
+ var selectedHandlerCalled = false;
+
+ gd.on('plotly_selected', function() {
+ expect(clickHandlerCalled).toBe(false);
+ selectedHandlerCalled = true;
+ });
+ gd.on('plotly_click', function() {
+ clickHandlerCalled = true;
+ expect(selectedHandlerCalled).toBe(true);
+ doneFn();
+ });
+
+ return click(testCase.x, testCase.y);
+ })
+ .catch(failTest)
+ .then(doneFn);
+ }
+ });
+
+ function testCase(label, mock, x, y, expectedPts, layoutOptions, configOptions) {
+ var defaultLayoutOpts = {
+ layout: {
+ clickmode: 'event+select',
+ dragmode: 'pan',
+ hovermode: 'closest'
+ }
+ };
+ var customLayoutOptions = {
+ layout: layoutOptions
+ };
+ var customConfigOptions = {
+ config: configOptions
+ };
+ var mockCopy = Lib.extendDeep(
+ {},
+ mock,
+ defaultLayoutOpts,
+ customLayoutOptions,
+ customConfigOptions);
+
+ return {
+ label: label,
+ mock: mockCopy,
+ layoutOptions: layoutOptions,
+ x: x,
+ y: y,
+ expectedPts: expectedPts,
+ configOptions: configOptions
+ };
+ }
+});
+
describe('Test select box and lasso in general:', function() {
var mock = require('@mocks/14.json');
var selectPath = [[93, 193], [143, 193]];
@@ -143,6 +810,7 @@ describe('Test select box and lasso in general:', function() {
describe('select events', function() {
var mockCopy = Lib.extendDeep({}, mock);
mockCopy.layout.dragmode = 'select';
+ mockCopy.layout.hovermode = 'closest';
mockCopy.data[0].ids = mockCopy.data[0].x
.map(function(v) { return 'id-' + v; });
mockCopy.data[0].customdata = mockCopy.data[0].y
@@ -293,6 +961,7 @@ describe('Test select box and lasso in general:', function() {
describe('lasso events', function() {
var mockCopy = Lib.extendDeep({}, mock);
mockCopy.layout.dragmode = 'lasso';
+ mockCopy.layout.hovermode = 'closest';
addInvisible(mockCopy);
var gd;
@@ -627,6 +1296,43 @@ describe('Test select box and lasso in general:', function() {
.then(done);
});
+ it('should cleanly clear and restart selections on double click when add/subtract mode on', function(done) {
+ var gd = createGraphDiv();
+ var fig = Lib.extendDeep({}, require('@mocks/0.json'));
+
+ fig.layout.dragmode = 'select';
+ Plotly.plot(gd, fig)
+ .then(function() {
+ return drag([[350, 100], [400, 400]]);
+ })
+ .then(function() {
+ _assertSelectedPoints([49, 50, 51, 52, 53, 54, 55, 56, 57]);
+
+ // Note: although Shift has no behavioral effect on clearing a selection
+ // with a double click, users might hold the Shift key by accident.
+ // This test ensures selection is cleared as expected although
+ // the Shift key is held and no selection state is retained in any way.
+ return doubleClick(500, 200, { shiftKey: true });
+ })
+ .then(function() {
+ _assertSelectedPoints(null);
+ return drag([[450, 100], [500, 400]], { shiftKey: true });
+ })
+ .then(function() {
+ _assertSelectedPoints([67, 68, 69, 70, 71, 72, 73, 74]);
+ })
+ .catch(failTest)
+ .then(done);
+
+ function _assertSelectedPoints(selPts) {
+ if(selPts) {
+ expect(gd.data[0].selectedpoints).toEqual(selPts);
+ } else {
+ expect('selectedpoints' in gd.data[0]).toBe(false);
+ }
+ }
+ });
+
it('@flaky should clear selected points on double click only on pan/lasso modes', function(done) {
var gd = createGraphDiv();
var fig = Lib.extendDeep({}, require('@mocks/0.json'));
@@ -635,6 +1341,7 @@ describe('Test select box and lasso in general:', function() {
fig.layout.xaxis.range = [2, 8];
fig.layout.yaxis.autorange = false;
fig.layout.yaxis.range = [0, 3];
+ fig.layout.hovermode = 'closest';
function _assert(msg, exp) {
expect(gd.layout.xaxis.range)
@@ -1394,7 +2101,7 @@ describe('Test select box and lasso per trace:', function() {
})
.then(function() {
return _run(
- [[370, 120], [500, 200]], null, [280, 190], NOEVENTS, 'choropleth pan'
+ [[370, 120], [500, 200]], null, [200, 180], NOEVENTS, 'choropleth pan'
);
})
.catch(failTest)
@@ -1833,14 +2540,14 @@ describe('Test select box and lasso per trace:', function() {
it('@flaky should work on scatter/bar traces with text nodes', function(done) {
var assertSelectedPoints = makeAssertSelectedPoints();
- function assertFillOpacity(exp) {
+ function assertFillOpacity(exp, msg) {
var txtPts = d3.select(gd).select('g.plot').selectAll('text');
- expect(txtPts.size()).toBe(exp.length, '# of text nodes');
+ expect(txtPts.size()).toBe(exp.length, '# of text nodes: ' + msg);
txtPts.each(function(_, i) {
var act = Number(this.style['fill-opacity']);
- expect(act).toBe(exp[i], 'node ' + i + ' fill opacity');
+ expect(act).toBe(exp[i], 'node ' + i + ' fill opacity: ' + msg);
});
}
@@ -1857,6 +2564,7 @@ describe('Test select box and lasso per trace:', function() {
textposition: 'outside'
}], {
dragmode: 'select',
+ hovermode: 'closest',
showlegend: false,
width: 400,
height: 400,
@@ -1867,13 +2575,13 @@ describe('Test select box and lasso per trace:', function() {
[[10, 10], [100, 300]],
function() {
assertSelectedPoints({0: [0], 1: [0]});
- assertFillOpacity([1, 0.2, 0.2, 1, 0.2, 0.2]);
+ assertFillOpacity([1, 0.2, 0.2, 1, 0.2, 0.2], '_run');
},
- null, BOXEVENTS, 'selecting first scatter/bar text nodes'
+ [10, 10], BOXEVENTS, 'selecting first scatter/bar text nodes'
);
})
.then(function() {
- assertFillOpacity([1, 1, 1, 1, 1, 1]);
+ assertFillOpacity([1, 1, 1, 1, 1, 1], 'final');
})
.catch(failTest)
.then(done);
diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js
index 8bd9847049a..f202749ed3b 100644
--- a/test/jasmine/tests/ternary_test.js
+++ b/test/jasmine/tests/ternary_test.js
@@ -1,7 +1,7 @@
var Plotly = require('@lib');
var Lib = require('@src/lib');
-var supplyLayoutDefaults = require('@src/plots/ternary/layout/defaults');
+var supplyLayoutDefaults = require('@src/plots/ternary/layout_defaults');
var d3 = require('d3');
var createGraphDiv = require('../assets/create_graph_div');
@@ -382,6 +382,60 @@ describe('ternary plots', function() {
.then(done);
});
+ it('should be able to hide/show ticks and tick labels', function(done) {
+ var gd = createGraphDiv();
+ var fig = Lib.extendDeep({}, require('@mocks/ternary_simple.json'));
+
+ function assertCnt(selector, expected, msg) {
+ var sel = d3.select(gd).selectAll(selector);
+ expect(sel.size()).toBe(expected, msg);
+ }
+
+ function toggle(selector, astr, vals, exps) {
+ return function() {
+ return Plotly.relayout(gd, astr, vals[0]).then(function() {
+ assertCnt(selector, exps[0], astr + ' ' + vals[0]);
+ return Plotly.relayout(gd, astr, vals[1]);
+ })
+ .then(function() {
+ assertCnt(selector, exps[1], astr + ' ' + vals[1]);
+ return Plotly.relayout(gd, astr, vals[0]);
+ })
+ .then(function() {
+ assertCnt(selector, exps[0], astr + ' ' + vals[0]);
+ });
+ };
+ }
+
+ Plotly.plot(gd, fig)
+ .then(toggle(
+ '.aaxis > .ytick > text', 'ternary.aaxis.showticklabels',
+ [true, false], [4, 0]
+ ))
+ .then(toggle(
+ '.baxis > .xtick > text', 'ternary.baxis.showticklabels',
+ [true, false], [5, 0]
+ ))
+ .then(toggle(
+ '.caxis > .ytick > text', 'ternary.caxis.showticklabels',
+ [true, false], [4, 0]
+ ))
+ .then(toggle(
+ '.aaxis > path.ytick', 'ternary.aaxis.ticks',
+ ['outside', ''], [4, 0]
+ ))
+ .then(toggle(
+ '.baxis > path.xtick', 'ternary.baxis.ticks',
+ ['outside', ''], [5, 0]
+ ))
+ .then(toggle(
+ '.caxis > path.ytick', 'ternary.caxis.ticks',
+ ['outside', ''], [4, 0]
+ ))
+ .catch(failTest)
+ .then(done);
+ });
+
it('should render a-axis and c-axis with negative offsets', function(done) {
var gd = createGraphDiv();