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();