@@ -264,13 +264,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
264264 }
265265
266266 // draw selection
267- var paths = [ ] ;
268- for ( i = 0 ; i < mergedPolygons . length ; i ++ ) {
269- var ppts = mergedPolygons [ i ] ;
270- paths . push ( ppts . join ( 'L' ) + 'L' + ppts [ 0 ] ) ;
271- }
272- outlines
273- . attr ( 'd' , 'M' + paths . join ( 'M' ) + 'Z' ) ;
267+ drawSelection ( mergedPolygons , outlines ) ;
268+
274269
275270 throttle . throttle (
276271 throttleID ,
@@ -320,6 +315,65 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
320315 gd . emit ( 'plotly_deselect' , null ) ;
321316 }
322317 else {
318+
319+
320+
321+ var hoverData = gd . _hoverdata ;
322+ var selection = [ ] ;
323+ var traceSelection ;
324+ var thisTracesSelection ;
325+ var pointSelected ;
326+ var subtract ;
327+
328+ if ( isHoverDataSet ( hoverData ) ) {
329+ var clickedPtInfo = extractClickedPtInfo ( hoverData , searchTraces ) ;
330+
331+ // TODO perf: call potentially costly operation (see impl comment) only when needed
332+ pointSelected = isPointSelected ( clickedPtInfo . searchInfo . cd [ 0 ] . trace ,
333+ clickedPtInfo . pointNumber ) ;
334+
335+ if ( pointSelected && isOnlyOnePointSelected ( searchTraces ) ) {
336+ // TODO DRY see doubleClick handling above
337+ outlines . remove ( ) ;
338+ for ( i = 0 ; i < searchTraces . length ; i ++ ) {
339+ searchInfo = searchTraces [ i ] ;
340+ searchInfo . _module . selectPoints ( searchInfo , false ) ;
341+ }
342+
343+ updateSelectedState ( gd , searchTraces ) ;
344+ gd . emit ( 'plotly_deselect' , null ) ;
345+ } else {
346+ subtract = evt . shiftKey && pointSelected ;
347+ currentPolygon = createPtNumTester ( clickedPtInfo . pointNumber ,
348+ clickedPtInfo . searchInfo . cd [ 0 ] . trace . _expandedIndex , subtract ) ;
349+
350+ var concatenatedPolygons = dragOptions . polygons . concat ( [ currentPolygon ] ) ;
351+ testPoly = multipolygonTester ( concatenatedPolygons ) ;
352+
353+ for ( i = 0 ; i < searchTraces . length ; i ++ ) {
354+ traceSelection = searchTraces [ i ] . _module . selectPoints ( searchTraces [ i ] , testPoly ) ;
355+ thisTracesSelection = fillSelectionItem ( traceSelection , searchTraces [ i ] ) ;
356+
357+ if ( selection . length ) {
358+ for ( var j = 0 ; j < thisTracesSelection . length ; j ++ ) {
359+ selection . push ( thisTracesSelection [ j ] ) ;
360+ }
361+ }
362+ else selection = thisTracesSelection ;
363+ }
364+
365+ eventData = { points : selection } ;
366+ updateSelectedState ( gd , searchTraces , eventData ) ;
367+
368+ if ( currentPolygon && dragOptions . polygons ) {
369+ dragOptions . polygons . push ( currentPolygon ) ;
370+ }
371+ }
372+
373+ }
374+
375+ drawSelection ( dragOptions . mergedPolygons , outlines ) ;
376+
323377 // TODO: remove in v2 - this was probably never intended to work as it does,
324378 // but in case anyone depends on it we don't want to break it now.
325379 gd . emit ( 'plotly_selected' , undefined ) ;
@@ -349,6 +403,126 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
349403 } ;
350404}
351405
406+ function drawSelection ( polygons , outlines ) {
407+ var paths = [ ] ;
408+ var i ;
409+ var d ;
410+
411+ for ( i = 0 ; i < polygons . length ; i ++ ) {
412+ var ppts = polygons [ i ] ;
413+ paths . push ( ppts . join ( 'L' ) + 'L' + ppts [ 0 ] ) ;
414+ }
415+
416+ d = polygons . length > 0 ?
417+ 'M' + paths . join ( 'M' ) + 'Z' :
418+ '' ; // TODO empty d attribute works in Chrome, but is it valid / can we rely on it?
419+ outlines . attr ( 'd' , d ) ;
420+ }
421+
422+ function isHoverDataSet ( hoverData ) {
423+ return hoverData &&
424+ Array . isArray ( hoverData ) &&
425+ hoverData [ 0 ] . hoverOnBox !== true ;
426+ }
427+
428+ function extractClickedPtInfo ( hoverData , searchTraces ) {
429+ var hoverDatum = hoverData [ 0 ] ;
430+ var pointNumber = - 1 ;
431+ var pointNumbers = [ ] ;
432+ var searchInfo ;
433+ var i ;
434+
435+ for ( i = 0 ; i < searchTraces . length ; i ++ ) {
436+ searchInfo = searchTraces [ i ] ;
437+ if ( hoverDatum . fullData . _expandedIndex === searchInfo . cd [ 0 ] . trace . _expandedIndex ) {
438+
439+ // Special case for box (and violin)
440+ if ( hoverDatum . hoverOnBox === true ) {
441+ break ;
442+ }
443+
444+ // TODO hoverDatum not having a pointNumber but a binNumber seems to be an oddity of histogram only
445+ // Not deleting .pointNumber in histogram/event_data.js would simplify code here and in addition
446+ // would not break the hover event structure
447+ // documented at https://plot.ly/javascript/hover-events/
448+ if ( hoverDatum . pointNumber !== undefined ) {
449+ pointNumber = hoverDatum . pointNumber ;
450+ } else if ( hoverDatum . binNumber !== undefined ) {
451+ pointNumber = hoverDatum . binNumber ;
452+ pointNumbers = hoverDatum . pointNumbers ;
453+ }
454+
455+ break ;
456+ }
457+ }
458+
459+ return {
460+ pointNumber : pointNumber ,
461+ pointNumbers : pointNumbers ,
462+ searchInfo : searchInfo
463+ } ;
464+ }
465+
466+ // TODO What about passing a searchInfo instead of wantedExpandedTraceIndex?
467+ function createPtNumTester ( wantedPointNumber , wantedExpandedTraceIndex , subtract ) {
468+ return {
469+ xmin : 0 ,
470+ xmax : 0 ,
471+ ymin : 0 ,
472+ ymax : 0 ,
473+ pts : [ ] ,
474+ // TODO Consider making signature of contains more lean
475+ contains : function ( pt , omitFirstEdge , pointNumber , expandedTraceIndex ) {
476+ return expandedTraceIndex === wantedExpandedTraceIndex && pointNumber === wantedPointNumber ;
477+ } ,
478+ isRect : false ,
479+ degenerate : false ,
480+ subtract : subtract
481+ } ;
482+ }
483+
484+ function isPointSelected ( trace , pointNumber ) {
485+ // TODO improve perf
486+ // Primarily we need this function to determine if a click adds or subtracts from a selection.
487+ //
488+ // IME best user experience would be
489+ // - that Shift+Click an unselected points adds to selection
490+ // - and Shift+Click a selected point subtracts from selection.
491+ //
492+ // Several options:
493+ // 1. Avoid problem at all by binding subtract-selection-by-click operation to Shift+Alt-Click.
494+ // Slightly less intuitive. A lot of programs deselect an already selected element when you
495+ // Shift+Click it.
496+ // 2. Delegate decision to the traces module through an additional
497+ // isSelected(searchInfo, pointNumber) function. Traces like scatter or bar have
498+ // a selected flag attached to each calcData element, thus access to that information
499+ // would be fast. However, scattergl only maintains selectBatch and unselectBatch arrays.
500+ // So simply searching through those arrays in scattegl would be slow. Just imagine
501+ // a user selecting all data points with one lasso polygon. So scattergl would require some
502+ // work.
503+ return trace . selectedpoints ? trace . selectedpoints . indexOf ( pointNumber ) > - 1 : false ;
504+ }
505+
506+ function isOnlyOnePointSelected ( searchTraces ) {
507+ var len = 0 ;
508+ var searchInfo ;
509+ var trace ;
510+ var i ;
511+
512+ for ( i = 0 ; i < searchTraces . length ; i ++ ) {
513+ searchInfo = searchTraces [ i ] ;
514+ trace = searchInfo . cd [ 0 ] . trace ;
515+ if ( trace . selectedpoints ) {
516+ if ( trace . selectedpoints . length > 1 ) return false ;
517+
518+ len += trace . selectedpoints . length ;
519+ if ( len > 1 ) return false ;
520+ }
521+ }
522+
523+ return len === 1 ;
524+ }
525+
352526function updateSelectedState ( gd , searchTraces , eventData ) {
353527 var i , j , searchInfo , trace ;
354528
0 commit comments