@@ -264,13 +264,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
264
264
}
265
265
266
266
// 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
+
274
269
275
270
throttle . throttle (
276
271
throttleID ,
@@ -320,6 +315,65 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
320
315
gd . emit ( 'plotly_deselect' , null ) ;
321
316
}
322
317
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
+
323
377
// TODO: remove in v2 - this was probably never intended to work as it does,
324
378
// but in case anyone depends on it we don't want to break it now.
325
379
gd . emit ( 'plotly_selected' , undefined ) ;
@@ -349,6 +403,126 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
349
403
} ;
350
404
}
351
405
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
+
352
526
function updateSelectedState ( gd , searchTraces , eventData ) {
353
527
var i , j , searchInfo , trace ;
354
528
0 commit comments