'use strict'; var polybool = require('polybooljs'); var pointInPolygon = require('point-in-polygon/nested'); // could we use contains lib/polygon instead? var Registry = require('../../registry'); var dashStyle = require('../drawing').dashStyle; var Color = require('../color'); var Fx = require('../fx'); var makeEventData = require('../fx/helpers').makeEventData; var dragHelpers = require('../dragelement/helpers'); var freeMode = dragHelpers.freeMode; var rectMode = dragHelpers.rectMode; var drawMode = dragHelpers.drawMode; var openMode = dragHelpers.openMode; var selectMode = dragHelpers.selectMode; var shapeHelpers = require('../shapes/helpers'); var shapeConstants = require('../shapes/constants'); var displayOutlines = require('../shapes/display_outlines'); var clearOutline = require('../shapes/handle_outline').clearOutline; var newShapeHelpers = require('../shapes/draw_newshape/helpers'); var handleEllipse = newShapeHelpers.handleEllipse; var readPaths = newShapeHelpers.readPaths; var newShapes = require('../shapes/draw_newshape/newshapes').newShapes; var newSelections = require('./draw_newselection/newselections'); var activateLastSelection = require('./draw').activateLastSelection; var Lib = require('../../lib'); var ascending = Lib.sorterAsc; var libPolygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); var getFromId = require('../../plots/cartesian/axis_ids').getFromId; var clearGlCanvases = require('../../lib/clear_gl_canvases'); var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var constants = require('./constants'); var MINSELECT = constants.MINSELECT; var filteredPolygon = libPolygon.filter; var polygonTester = libPolygon.tester; var helpers = require('./helpers'); var p2r = helpers.p2r; var axValue = helpers.axValue; var getTransform = helpers.getTransform; function hasSubplot(dragOptions) { // N.B. subplot may be falsy e.g zero sankey index! return dragOptions.subplot !== undefined; } function prepSelect(evt, startX, startY, dragOptions, mode) { var isCartesian = !hasSubplot(dragOptions); var isFreeMode = freeMode(mode); var isRectMode = rectMode(mode); var isOpenMode = openMode(mode); var isDrawMode = drawMode(mode); var isSelectMode = selectMode(mode); var isLine = mode === 'drawline'; var isEllipse = mode === 'drawcircle'; var isLineOrEllipse = isLine || isEllipse; // cases with two start & end positions var gd = dragOptions.gd; var fullLayout = gd._fullLayout; var immediateSelect = isSelectMode && fullLayout.newselection.mode === 'immediate' && isCartesian; // N.B. only cartesian subplots have persistent selection var zoomLayer = fullLayout._zoomlayer; var dragBBox = dragOptions.element.getBoundingClientRect(); var plotinfo = dragOptions.plotinfo; var transform = getTransform(plotinfo); var x0 = startX - dragBBox.left; var y0 = startY - dragBBox.top; fullLayout._calcInverseTransform(gd); var transformedCoords = Lib.apply3DTransform(fullLayout._invTransform)(x0, y0); x0 = transformedCoords[0]; y0 = transformedCoords[1]; var scaleX = fullLayout._invScaleX; var scaleY = fullLayout._invScaleY; var x1 = x0; var y1 = y0; var path0 = 'M' + x0 + ',' + y0; var xAxis = dragOptions.xaxes[0]; var yAxis = dragOptions.yaxes[0]; var pw = xAxis._length; var ph = yAxis._length; var subtract = evt.altKey && !(drawMode(mode) && isOpenMode); var filterPoly, selectionTesters, mergedPolygons, currentPolygon; var i, searchInfo, eventData; coerceSelectionsCache(evt, gd, dragOptions); if(isFreeMode) { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); } var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data([1]); var newStyle = isDrawMode ? fullLayout.newshape : fullLayout.newselection; if(isDrawMode) { dragOptions.hasText = newStyle.label.text || newStyle.label.texttemplate; } var fillC = (isDrawMode && !isOpenMode) ? newStyle.fillcolor : 'rgba(0,0,0,0)'; var strokeC = newStyle.line.color || ( isCartesian ? Color.contrast(gd._fullLayout.plot_bgcolor) : '#7f7f7f' // non-cartesian subplot ); outlines.enter() .append('path') .attr('class', 'select-outline select-outline-' + plotinfo.id) .style({ opacity: isDrawMode ? newStyle.opacity / 2 : 1, 'stroke-dasharray': dashStyle(newStyle.line.dash, newStyle.line.width), 'stroke-width': newStyle.line.width + 'px', 'shape-rendering': 'crispEdges' }) .call(Color.stroke, strokeC) .call(Color.fill, fillC) .attr('fill-rule', 'evenodd') .classed('cursor-move', isDrawMode ? true : false) .attr('transform', transform) .attr('d', path0 + 'Z'); var corners = zoomLayer.append('path') .attr('class', 'zoombox-corners') .style({ fill: Color.background, stroke: Color.defaultLine, 'stroke-width': 1 }) .attr('transform', transform) .attr('d', 'M0,0Z'); // create & style group for text label if(isDrawMode && dragOptions.hasText) { var shapeGroup = zoomLayer.select('.label-temp'); if(shapeGroup.empty()) { shapeGroup = zoomLayer.append('g') .classed('label-temp', true) .classed('select-outline', true) .style({ opacity: 0.8 }); } } var throttleID = fullLayout._uid + constants.SELECTID; var selection = []; // find the traces to search for selection points var searchTraces = determineSearchTraces(gd, dragOptions.xaxes, dragOptions.yaxes, dragOptions.subplot); if(immediateSelect && !evt.shiftKey) { dragOptions._clearSubplotSelections = function() { if(!isCartesian) return; var xRef = xAxis._id; var yRef = yAxis._id; deselectSubplot(gd, xRef, yRef, searchTraces); var selections = (gd.layout || {}).selections || []; var list = []; var selectionErased = false; for(var q = 0; q < selections.length; q++) { var s = fullLayout.selections[q]; if( s.xref !== xRef || s.yref !== yRef ) { list.push(selections[q]); } else { selectionErased = true; } } if(selectionErased) { gd._fullLayout._noEmitSelectedAtStart = true; Registry.call('_guiRelayout', gd, { selections: list }); } }; } var fillRangeItems = getFillRangeItems(dragOptions); dragOptions.moveFn = function(dx0, dy0) { if(dragOptions._clearSubplotSelections) { dragOptions._clearSubplotSelections(); dragOptions._clearSubplotSelections = undefined; } x1 = Math.max(0, Math.min(pw, scaleX * dx0 + x0)); y1 = Math.max(0, Math.min(ph, scaleY * dy0 + y0)); var dx = Math.abs(x1 - x0); var dy = Math.abs(y1 - y0); if(isRectMode) { var direction; var start, end; if(isSelectMode) { var q = fullLayout.selectdirection; if(q === 'any') { if(dy < Math.min(dx * 0.6, MINSELECT)) { direction = 'h'; } else if(dx < Math.min(dy * 0.6, MINSELECT)) { direction = 'v'; } else { direction = 'd'; } } else { direction = q; } switch(direction) { case 'h': start = isEllipse ? ph / 2 : 0; end = ph; break; case 'v': start = isEllipse ? pw / 2 : 0; end = pw; break; } } if(isDrawMode) { switch(fullLayout.newshape.drawdirection) { case 'vertical': direction = 'h'; start = isEllipse ? ph / 2 : 0; end = ph; break; case 'horizontal': direction = 'v'; start = isEllipse ? pw / 2 : 0; end = pw; break; case 'ortho': if(dx < dy) { direction = 'h'; start = y0; end = y1; } else { direction = 'v'; start = x0; end = x1; } break; default: // i.e. case of 'diagonal' direction = 'd'; } } if(direction === 'h') { // horizontal motion currentPolygon = isLineOrEllipse ? handleEllipse(isEllipse, [x1, start], [x1, end]) : // using x1 instead of x0 allows adjusting the line while drawing [[x0, start], [x0, end], [x1, end], [x1, start]]; // make a vertical box currentPolygon.xmin = isLineOrEllipse ? x1 : Math.min(x0, x1); currentPolygon.xmax = isLineOrEllipse ? x1 : Math.max(x0, x1); currentPolygon.ymin = Math.min(start, end); currentPolygon.ymax = Math.max(start, end); // extras to guide users in keeping a straight selection corners.attr('d', 'M' + currentPolygon.xmin + ',' + (y0 - MINSELECT) + 'h-4v' + (2 * MINSELECT) + 'h4Z' + 'M' + (currentPolygon.xmax - 1) + ',' + (y0 - MINSELECT) + 'h4v' + (2 * MINSELECT) + 'h-4Z'); } else if(direction === 'v') { // vertical motion currentPolygon = isLineOrEllipse ? handleEllipse(isEllipse, [start, y1], [end, y1]) : // using y1 instead of y0 allows adjusting the line while drawing [[start, y0], [start, y1], [end, y1], [end, y0]]; // make a horizontal box currentPolygon.xmin = Math.min(start, end); currentPolygon.xmax = Math.max(start, end); currentPolygon.ymin = isLineOrEllipse ? y1 : Math.min(y0, y1); currentPolygon.ymax = isLineOrEllipse ? y1 : Math.max(y0, y1); corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + currentPolygon.ymin + 'v-4h' + (2 * MINSELECT) + 'v4Z' + 'M' + (x0 - MINSELECT) + ',' + (currentPolygon.ymax - 1) + 'v4h' + (2 * MINSELECT) + 'v-4Z'); } else if(direction === 'd') { // diagonal motion currentPolygon = isLineOrEllipse ? handleEllipse(isEllipse, [x0, y0], [x1, y1]) : [[x0, y0], [x0, y1], [x1, y1], [x1, y0]]; currentPolygon.xmin = Math.min(x0, x1); currentPolygon.xmax = Math.max(x0, x1); currentPolygon.ymin = Math.min(y0, y1); currentPolygon.ymax = Math.max(y0, y1); corners.attr('d', 'M0,0Z'); } } else if(isFreeMode) { filterPoly.addPt([x1, y1]); currentPolygon = filterPoly.filtered; } // create outline & tester if(dragOptions.selectionDefs && dragOptions.selectionDefs.length) { mergedPolygons = mergePolygons(dragOptions.mergedPolygons, currentPolygon, subtract); currentPolygon.subtract = subtract; selectionTesters = multiTester(dragOptions.selectionDefs.concat([currentPolygon])); } else { mergedPolygons = [currentPolygon]; selectionTesters = polygonTester(currentPolygon); } // display polygons on the screen displayOutlines(convertPoly(mergedPolygons, isOpenMode), outlines, dragOptions); if(isSelectMode) { var _res = reselect(gd, false); var extraPoints = _res.eventData ? _res.eventData.points.slice() : []; _res = reselect(gd, false, selectionTesters, searchTraces, dragOptions); selectionTesters = _res.selectionTesters; eventData = _res.eventData; var poly; if(filterPoly) { poly = filterPoly.filtered; } else { poly = castMultiPolygon(mergedPolygons); } throttle.throttle( throttleID, constants.SELECTDELAY, function() { selection = _doSelect(selectionTesters, searchTraces); var newPoints = selection.slice(); for(var w = 0; w < extraPoints.length; w++) { var p = extraPoints[w]; var found = false; for(var u = 0; u < newPoints.length; u++) { if( newPoints[u].curveNumber === p.curveNumber && newPoints[u].pointNumber === p.pointNumber ) { found = true; break; } } if(!found) newPoints.push(p); } if(newPoints.length) { if(!eventData) eventData = {}; eventData.points = newPoints; } fillRangeItems(eventData, poly); emitSelecting(gd, eventData); } ); } }; dragOptions.clickFn = function(numClicks, evt) { corners.remove(); if(gd._fullLayout._activeShapeIndex >= 0) { gd._fullLayout._deactivateShape(gd); return; } if(isDrawMode) return; var clickmode = fullLayout.clickmode; throttle.done(throttleID).then(function() { throttle.clear(throttleID); if(numClicks === 2) { // clear selection on doubleclick outlines.remove(); for(i = 0; i < searchTraces.length; i++) { searchInfo = searchTraces[i]; searchInfo._module.selectPoints(searchInfo, false); } updateSelectedState(gd, searchTraces); clearSelectionsCache(dragOptions); emitDeselect(gd); if(searchTraces.length) { var clickedXaxis = searchTraces[0].xaxis; var clickedYaxis = searchTraces[0].yaxis; if(clickedXaxis && clickedYaxis) { // drop selections in the clicked subplot var subSelections = []; var allSelections = gd._fullLayout.selections; for(var k = 0; k < allSelections.length; k++) { var s = allSelections[k]; if(!s) continue; // also drop null selections if any if( s.xref !== clickedXaxis._id || s.yref !== clickedYaxis._id ) { subSelections.push(s); } } if(subSelections.length < allSelections.length) { gd._fullLayout._noEmitSelectedAtStart = true; Registry.call('_guiRelayout', gd, { selections: subSelections }); } } } } else { if(clickmode.indexOf('select') > -1) { selectOnClick(evt, gd, dragOptions.xaxes, dragOptions.yaxes, dragOptions.subplot, dragOptions, outlines); } if(clickmode === 'event') { // TODO: remove in v3 - 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 v3 also emitts proper // event data when clickmode is having 'select' in its flag list. emitSelected(gd, undefined); } } Fx.click(gd, evt, plotinfo.id); }).catch(Lib.error); }; dragOptions.doneFn = function() { corners.remove(); throttle.done(throttleID).then(function() { throttle.clear(throttleID); if(!immediateSelect && currentPolygon && dragOptions.selectionDefs) { // save last polygons currentPolygon.subtract = subtract; dragOptions.selectionDefs.push(currentPolygon); // we have to keep reference to arrays container dragOptions.mergedPolygons.length = 0; [].push.apply(dragOptions.mergedPolygons, mergedPolygons); } if(immediateSelect || isDrawMode) { clearSelectionsCache(dragOptions, immediateSelect); } if(dragOptions.doneFnCompleted) { dragOptions.doneFnCompleted(selection); } if(isSelectMode) { emitSelected(gd, eventData); } }).catch(Lib.error); }; } function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutlines) { var hoverData = gd._hoverdata; var fullLayout = gd._fullLayout; var clickmode = fullLayout.clickmode; var sendEvents = clickmode.indexOf('event') > -1; var selection = []; var searchTraces, searchInfo, currentSelectionDef, selectionTesters, 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) { emitDeselect(gd); } } else { subtract = evt.shiftKey && (pointOrBinSelected !== undefined ? pointOrBinSelected : isPointOrBinSelected(clickedPtInfo)); currentSelectionDef = newPointSelectionDef(clickedPtInfo.pointNumber, clickedPtInfo.searchInfo, subtract); var allSelectionDefs = dragOptions.selectionDefs.concat([currentSelectionDef]); selectionTesters = multiTester(allSelectionDefs, selectionTesters); for(i = 0; i < searchTraces.length; i++) { traceSelection = searchTraces[i]._module.selectPoints(searchTraces[i], selectionTesters); 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) { var polygons = dragOptions.mergedPolygons; var isOpenMode = openMode(dragOptions.dragmode); // display polygons on the screen displayOutlines(convertPoly(polygons, isOpenMode), polygonOutlines, dragOptions); } if(sendEvents) { emitSelected(gd, 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) { if(!list.length) return; 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 = polygonTester(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; } } 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 plotinfo = dragOptions.plotinfo; var dragmode = dragOptions.dragmode; var selectingOnSameSubplot = ( fullLayout._lastSelectedSubplot && fullLayout._lastSelectedSubplot === plotinfo.id ); var hasModifierKey = (evt.shiftKey || evt.altKey) && !(drawMode(dragmode) && openMode(dragmode)); 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) { clearOutline(gd); fullLayout._lastSelectedSubplot = plotinfo.id; } } function hasActiveShape(gd) { return gd._fullLayout._activeShapeIndex >= 0; } function hasActiveSelection(gd) { return gd._fullLayout._activeSelectionIndex >= 0; } function clearSelectionsCache(dragOptions, immediateSelect) { var dragmode = dragOptions.dragmode; var plotinfo = dragOptions.plotinfo; var gd = dragOptions.gd; if(hasActiveShape(gd)) { gd._fullLayout._deactivateShape(gd); } if(hasActiveSelection(gd)) { gd._fullLayout._deactivateSelection(gd); } var fullLayout = gd._fullLayout; var zoomLayer = fullLayout._zoomlayer; var isDrawMode = drawMode(dragmode); var isSelectMode = selectMode(dragmode); if(isDrawMode || isSelectMode) { var outlines = zoomLayer.selectAll('.select-outline-' + plotinfo.id); if(outlines && gd._fullLayout._outlining) { // add shape var shapes; if(isDrawMode) { shapes = newShapes(outlines, dragOptions); } if(shapes) { Registry.call('_guiRelayout', gd, { shapes: shapes }); } // add selection var selections; if( isSelectMode && !hasSubplot(dragOptions) // only allow cartesian - no maps for now ) { selections = newSelections(outlines, dragOptions); } if(selections) { gd._fullLayout._noEmitSelectedAtStart = true; Registry.call('_guiRelayout', gd, { selections: selections }).then(function() { if(immediateSelect) { activateLastSelection(gd); } }); } gd._fullLayout._outlining = false; } } plotinfo.selection = {}; plotinfo.selection.selectionDefs = dragOptions.selectionDefs = []; plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = []; } function getAxId(ax) { return ax._id; } function determineSearchTraces(gd, xAxes, yAxes, subplot) { if(!gd.calcdata) return []; 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( hasSubplot({subplot: 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 if(trace._xaxes[xAxisIds[0]] && trace._yaxes[yAxisIds[0]]) { var info = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]); info.scene = gd._fullLayout._splomScenes[trace.uid]; searchTraces.push(info); } } else if(trace.type === 'sankey') { var sankeyInfo = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]); searchTraces.push(sankeyInfo); } else { if(xAxisIds.indexOf(trace.xaxis) === -1 && (!trace._xA || !trace._xA.overlaying)) continue; if(yAxisIds.indexOf(trace.yaxis) === -1 && (!trace._yA || !trace._yA.overlaying)) 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 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; // before anything else, update preGUI if necessary for(i = 0; i < searchTraces.length; i++) { var fullInputTrace = searchTraces[i].cd[0].trace._fullInput; var tracePreGUI = gd._fullLayout._tracePreGUI[fullInputTrace.uid] || {}; if(tracePreGUI.selectedpoints === undefined) { tracePreGUI.selectedpoints = fullInputTrace._input.selectedpoints || null; } } var trace; if(eventData) { var pts = eventData.points || []; for(i = 0; i < searchTraces.length; i++) { trace = searchTraces[i].cd[0].trace; trace._input.selectedpoints = trace._fullInput.selectedpoints = []; if(trace._fullInput !== trace) trace.selectedpoints = []; } for(var k = 0; k < pts.length; k++) { var pt = pts[k]; var data = pt.data; var fullData = pt.fullData; var pointIndex = pt.pointIndex; var pointIndices = pt.pointIndices; if(pointIndices) { [].push.apply(data.selectedpoints, pointIndices); if(trace._fullInput !== trace) { [].push.apply(fullData.selectedpoints, pointIndices); } } else { data.selectedpoints.push(pointIndex); if(trace._fullInput !== trace) { fullData.selectedpoints.push(pointIndex); } } } } else { for(i = 0; i < searchTraces.length; i++) { trace = searchTraces[i].cd[0].trace; delete trace.selectedpoints; delete trace._input.selectedpoints; if(trace._fullInput !== trace) { delete trace._fullInput.selectedpoints; } } } updateReglSelectedState(gd, searchTraces); } function updateReglSelectedState(gd, searchTraces) { var hasRegl = false; for(var i = 0; i < searchTraces.length; i++) { var searchInfo = searchTraces[i]; var cd = searchInfo.cd; if(Registry.traceIs(cd[0].trace, 'regl')) { hasRegl = true; } var _module = searchInfo._module; var fn = _module.styleOnSelect || _module.style; if(fn) { fn(gd, cd, cd[0].node3); if(cd[0].nodeRangePlot3) fn(gd, cd, cd[0].nodeRangePlot3); } } if(hasRegl) { clearGlCanvases(gd); redrawReglTraces(gd); } } function mergePolygons(list, poly, subtract) { var fn = subtract ? polybool.difference : polybool.union; var res = fn({ regions: list }, { regions: [poly] }); var allPolygons = res.regions.reverse(); for(var i = 0; i < allPolygons.length; i++) { var polygon = allPolygons[i]; polygon.subtract = getSubtract(polygon, allPolygons.slice(0, i)); } return allPolygons; } function fillSelectionItem(selection, searchInfo) { if(Array.isArray(selection)) { var cd = searchInfo.cd; var trace = searchInfo.cd[0].trace; for(var i = 0; i < selection.length; i++) { selection[i] = makeEventData(selection[i], trace, cd); } } return selection; } function convertPoly(polygonsIn, isOpenMode) { // add M and L command to draft positions var polygonsOut = []; for(var i = 0; i < polygonsIn.length; i++) { polygonsOut[i] = []; for(var j = 0; j < polygonsIn[i].length; j++) { polygonsOut[i][j] = []; polygonsOut[i][j][0] = j ? 'L' : 'M'; for(var k = 0; k < polygonsIn[i][j].length; k++) { polygonsOut[i][j].push( polygonsIn[i][j][k] ); } } if(!isOpenMode) { polygonsOut[i].push([ 'Z', polygonsOut[i][0][1], // initial x polygonsOut[i][0][2] // initial y ]); } } return polygonsOut; } function _doSelect(selectionTesters, searchTraces) { var allSelections = []; var thisSelection; var traceSelections = []; var traceSelection; for(var i = 0; i < searchTraces.length; i++) { var searchInfo = searchTraces[i]; traceSelection = searchInfo._module.selectPoints(searchInfo, selectionTesters); traceSelections.push(traceSelection); thisSelection = fillSelectionItem(traceSelection, searchInfo); allSelections = allSelections.concat(thisSelection); } return allSelections; } function reselect(gd, mayEmitSelected, selectionTesters, searchTraces, dragOptions) { var hadSearchTraces = !!searchTraces; var plotinfo, xRef, yRef; if(dragOptions) { plotinfo = dragOptions.plotinfo; xRef = dragOptions.xaxes[0]._id; yRef = dragOptions.yaxes[0]._id; } var allSelections = []; var allSearchTraces = []; // select layout.selection polygons var layoutPolygons = getLayoutPolygons(gd); // add draft outline polygons to layoutPolygons var fullLayout = gd._fullLayout; if(plotinfo) { var zoomLayer = fullLayout._zoomlayer; var mode = fullLayout.dragmode; var isDrawMode = drawMode(mode); var isSelectMode = selectMode(mode); if(isDrawMode || isSelectMode) { var xaxis = getFromId(gd, xRef, 'x'); var yaxis = getFromId(gd, yRef, 'y'); if(xaxis && yaxis) { var outlines = zoomLayer.selectAll('.select-outline-' + plotinfo.id); if(outlines && gd._fullLayout._outlining) { if(outlines.length) { var e = outlines[0][0]; // pick first var d = e.getAttribute('d'); var outlinePolys = readPaths(d, gd, plotinfo); var draftPolygons = []; for(var u = 0; u < outlinePolys.length; u++) { var p = outlinePolys[u]; var polygon = []; for(var t = 0; t < p.length; t++) { polygon.push([ convert(xaxis, p[t][1]), convert(yaxis, p[t][2]) ]); } polygon.xref = xRef; polygon.yref = yRef; polygon.subtract = getSubtract(polygon, draftPolygons); draftPolygons.push(polygon); } layoutPolygons = layoutPolygons.concat(draftPolygons); } } } } } var subplots = (xRef && yRef) ? [xRef + yRef] : fullLayout._subplots.cartesian; epmtySplomSelectionBatch(gd); var seenSplom = {}; for(var i = 0; i < subplots.length; i++) { var subplot = subplots[i]; var yAt = subplot.indexOf('y'); var _xRef = subplot.slice(0, yAt); var _yRef = subplot.slice(yAt); var _selectionTesters = (xRef && yRef) ? selectionTesters : undefined; _selectionTesters = addTester(layoutPolygons, _xRef, _yRef, _selectionTesters); if(_selectionTesters) { var _searchTraces = searchTraces; if(!hadSearchTraces) { var _xA = getFromId(gd, _xRef, 'x'); var _yA = getFromId(gd, _yRef, 'y'); _searchTraces = determineSearchTraces( gd, [_xA], [_yA], subplot ); for(var w = 0; w < _searchTraces.length; w++) { var s = _searchTraces[w]; var cd0 = s.cd[0]; var trace = cd0.trace; if(s._module.name === 'scattergl' && !cd0.t.xpx) { var x = trace.x; var y = trace.y; var len = trace._length; // generate stash for scattergl cd0.t.xpx = []; cd0.t.ypx = []; for(var j = 0; j < len; j++) { cd0.t.xpx[j] = _xA.c2p(x[j]); cd0.t.ypx[j] = _yA.c2p(y[j]); } } if(s._module.name === 'splom') { if(!seenSplom[trace.uid]) { seenSplom[trace.uid] = true; } } } } var selection = _doSelect(_selectionTesters, _searchTraces); allSelections = allSelections.concat(selection); allSearchTraces = allSearchTraces.concat(_searchTraces); } } var eventData = {points: allSelections}; updateSelectedState(gd, allSearchTraces, eventData); var clickmode = fullLayout.clickmode; var sendEvents = clickmode.indexOf('event') > -1 && mayEmitSelected; if( !plotinfo && // get called from plot_api & plots mayEmitSelected ) { var activePolygons = getLayoutPolygons(gd, true); if(activePolygons.length) { var xref = activePolygons[0].xref; var yref = activePolygons[0].yref; if(xref && yref) { var poly = castMultiPolygon(activePolygons); var fillRangeItems = makeFillRangeItems([ getFromId(gd, xref, 'x'), getFromId(gd, yref, 'y') ]); fillRangeItems(eventData, poly); } } if(gd._fullLayout._noEmitSelectedAtStart) { gd._fullLayout._noEmitSelectedAtStart = false; } else { if(sendEvents) emitSelected(gd, eventData); } fullLayout._reselect = false; } if( !plotinfo && // get called from plot_api & plots fullLayout._deselect ) { var deselect = fullLayout._deselect; xRef = deselect.xref; yRef = deselect.yref; if(!subplotSelected(xRef, yRef, allSearchTraces)) { deselectSubplot(gd, xRef, yRef, searchTraces); } if(sendEvents) { if(eventData.points.length) { emitSelected(gd, eventData); } else { emitDeselect(gd); } } fullLayout._deselect = false; } return { eventData: eventData, selectionTesters: selectionTesters }; } function epmtySplomSelectionBatch(gd) { var cd = gd.calcdata; if(!cd) return; for(var i = 0; i < cd.length; i++) { var cd0 = cd[i][0]; var trace = cd0.trace; var splomScenes = gd._fullLayout._splomScenes; if(splomScenes) { var scene = splomScenes[trace.uid]; if(scene) { scene.selectBatch = []; } } } } function subplotSelected(xRef, yRef, searchTraces) { for(var i = 0; i < searchTraces.length; i++) { var s = searchTraces[i]; if( (s.xaxis && s.xaxis._id === xRef) && (s.yaxis && s.yaxis._id === yRef) ) { return true; } } return false; } function deselectSubplot(gd, xRef, yRef, searchTraces) { searchTraces = determineSearchTraces( gd, [getFromId(gd, xRef, 'x')], [getFromId(gd, yRef, 'y')], xRef + yRef ); for(var k = 0; k < searchTraces.length; k++) { var searchInfo = searchTraces[k]; searchInfo._module.selectPoints(searchInfo, false); } updateSelectedState(gd, searchTraces); } function addTester(layoutPolygons, xRef, yRef, selectionTesters) { var mergedPolygons; for(var i = 0; i < layoutPolygons.length; i++) { var currentPolygon = layoutPolygons[i]; if(xRef !== currentPolygon.xref || yRef !== currentPolygon.yref) continue; if(mergedPolygons) { var subtract = !!currentPolygon.subtract; mergedPolygons = mergePolygons(mergedPolygons, currentPolygon, subtract); selectionTesters = multiTester(mergedPolygons); } else { mergedPolygons = [currentPolygon]; selectionTesters = polygonTester(currentPolygon); } } return selectionTesters; } function getLayoutPolygons(gd, onlyActiveOnes) { var allPolygons = []; var fullLayout = gd._fullLayout; var allSelections = fullLayout.selections; var len = allSelections.length; for(var i = 0; i < len; i++) { if(onlyActiveOnes && i !== fullLayout._activeSelectionIndex) continue; var selection = allSelections[i]; if(!selection) continue; var xref = selection.xref; var yref = selection.yref; var xaxis = getFromId(gd, xref, 'x'); var yaxis = getFromId(gd, yref, 'y'); var xmin, xmax, ymin, ymax; var polygon; if(selection.type === 'rect') { polygon = []; var x0 = convert(xaxis, selection.x0); var x1 = convert(xaxis, selection.x1); var y0 = convert(yaxis, selection.y0); var y1 = convert(yaxis, selection.y1); polygon = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]]; xmin = Math.min(x0, x1); xmax = Math.max(x0, x1); ymin = Math.min(y0, y1); ymax = Math.max(y0, y1); polygon.xmin = xmin; polygon.xmax = xmax; polygon.ymin = ymin; polygon.ymax = ymax; polygon.xref = xref; polygon.yref = yref; polygon.subtract = false; polygon.isRect = true; allPolygons.push(polygon); } else if(selection.type === 'path') { var segments = selection.path.split('Z'); var multiPolygons = []; for(var j = 0; j < segments.length; j++) { var path = segments[j]; if(!path) continue; path += 'Z'; var allX = shapeHelpers.extractPathCoords(path, shapeConstants.paramIsX, 'raw'); var allY = shapeHelpers.extractPathCoords(path, shapeConstants.paramIsY, 'raw'); xmin = Infinity; xmax = -Infinity; ymin = Infinity; ymax = -Infinity; polygon = []; for(var k = 0; k < allX.length; k++) { var x = convert(xaxis, allX[k]); var y = convert(yaxis, allY[k]); polygon.push([x, y]); xmin = Math.min(x, xmin); xmax = Math.max(x, xmax); ymin = Math.min(y, ymin); ymax = Math.max(y, ymax); } polygon.xmin = xmin; polygon.xmax = xmax; polygon.ymin = ymin; polygon.ymax = ymax; polygon.xref = xref; polygon.yref = yref; polygon.subtract = getSubtract(polygon, multiPolygons); multiPolygons.push(polygon); allPolygons.push(polygon); } } } return allPolygons; } function getSubtract(polygon, previousPolygons) { var subtract = false; for(var i = 0; i < previousPolygons.length; i++) { var previousPolygon = previousPolygons[i]; // find out if a point of polygon is inside previous polygons for(var k = 0; k < polygon.length; k++) { if(pointInPolygon(polygon[k], previousPolygon)) { subtract = !subtract; break; } } } return subtract; } function convert(ax, d) { if(ax.type === 'date') d = d.replace('_', ' '); return ax.type === 'log' ? ax.c2p(d) : ax.r2p(d, null, ax.calendar); } function castMultiPolygon(allPolygons) { var len = allPolygons.length; // descibe multi polygons in one polygon var p = []; for(var i = 0; i < len; i++) { var polygon = allPolygons[i]; p = p.concat(polygon); // add starting vertex to close // which indicates next polygon p = p.concat([polygon[0]]); } return computeRectAndRanges(p); } function computeRectAndRanges(poly) { poly.isRect = poly.length === 5 && poly[0][0] === poly[4][0] && poly[0][1] === poly[4][1] && ( poly[0][0] === poly[1][0] && poly[2][0] === poly[3][0] && poly[0][1] === poly[3][1] && poly[1][1] === poly[2][1] ) || ( poly[0][1] === poly[1][1] && poly[2][1] === poly[3][1] && poly[0][0] === poly[3][0] && poly[1][0] === poly[2][0] ); if(poly.isRect) { poly.xmin = Math.min(poly[0][0], poly[2][0]); poly.xmax = Math.max(poly[0][0], poly[2][0]); poly.ymin = Math.min(poly[0][1], poly[2][1]); poly.ymax = Math.max(poly[0][1], poly[2][1]); } return poly; } function makeFillRangeItems(allAxes) { return function(eventData, poly) { var range; var lassoPoints; for(var i = 0; i < allAxes.length; i++) { var ax = allAxes[i]; var id = ax._id; var axLetter = id.charAt(0); if(poly.isRect) { if(!range) range = {}; var min = poly[axLetter + 'min']; var max = poly[axLetter + 'max']; if(min !== undefined && max !== undefined) { range[id] = [ p2r(ax, min), p2r(ax, max) ].sort(ascending); } } else { if(!lassoPoints) lassoPoints = {}; lassoPoints[id] = poly.map(axValue(ax)); } } if(range) { eventData.range = range; } if(lassoPoints) { eventData.lassoPoints = lassoPoints; } }; } function getFillRangeItems(dragOptions) { var plotinfo = dragOptions.plotinfo; return ( plotinfo.fillRangeItems || // allow subplots (i.e. geo, mapbox, map, sankey) to override fillRangeItems routine makeFillRangeItems(dragOptions.xaxes.concat(dragOptions.yaxes)) ); } function emitSelecting(gd, eventData) { gd.emit('plotly_selecting', eventData); } function emitSelected(gd, eventData) { if(eventData) { eventData.selections = (gd.layout || {}).selections || []; } gd.emit('plotly_selected', eventData); } function emitDeselect(gd) { gd.emit('plotly_deselect', null); } module.exports = { reselect: reselect, prepSelect: prepSelect, clearOutline: clearOutline, clearSelectionsCache: clearSelectionsCache, selectOnClick: selectOnClick };