1
+ import { create , select , dispatch as dispatcher , line , pointer , polygonContains , curveNatural } from "d3" ;
2
+ import { maybeTuple } from "../options.js" ;
3
+ import { Mark } from "../plot.js" ;
4
+ import { selection , selectionEquals } from "../selection.js" ;
5
+ import { applyIndirectStyles } from "../style.js" ;
6
+
7
+ const defaults = {
8
+ ariaLabel : "lasso" ,
9
+ fill : "#777" ,
10
+ fillOpacity : 0.3 ,
11
+ stroke : "#666" ,
12
+ strokeWidth : 2
13
+ } ;
14
+
15
+ export class Lasso extends Mark {
16
+ constructor ( data , { x, y, ...options } = { } ) {
17
+ super (
18
+ data ,
19
+ [
20
+ { name : "x" , value : x , scale : "x" } ,
21
+ { name : "y" , value : y , scale : "y" }
22
+ ] ,
23
+ options ,
24
+ defaults
25
+ ) ;
26
+ this . activeElement = null ;
27
+ }
28
+
29
+ // The lasso polygons follow the even-odd rule in css, matching the way
30
+ // they are computed by polygonContains.
31
+ render ( index , scales , { x : X , y : Y } , dimensions ) {
32
+ const margin = 5 ;
33
+ const { ariaLabel, ariaDescription, ariaHidden, fill, fillOpacity, stroke, strokeWidth} = this ;
34
+ const { marginLeft, width, marginRight, marginTop, height, marginBottom} = dimensions ;
35
+
36
+ const path = line ( ) . curve ( curveNatural ) ;
37
+ const g = create ( "svg:g" )
38
+ . call ( applyIndirectStyles , { ariaLabel, ariaDescription, ariaHidden, fill, fillOpacity, stroke, strokeWidth} ) ;
39
+ g . append ( "rect" )
40
+ . attr ( "x" , marginLeft )
41
+ . attr ( "y" , marginTop )
42
+ . attr ( "width" , width - marginLeft - marginRight )
43
+ . attr ( "height" , height - marginTop - marginBottom )
44
+ . attr ( "fill" , "none" )
45
+ . attr ( "cursor" , "cross" ) // TODO
46
+ . attr ( "pointer-events" , "all" )
47
+ . attr ( "fill-rule" , "evenodd" ) ;
48
+
49
+ g . call ( lassoer ( )
50
+ . extent ( [ [ marginLeft - margin , marginTop - margin ] , [ width - marginRight + margin , height - marginBottom + margin ] ] )
51
+ . on ( "start lasso end cancel" , ( polygons ) => {
52
+ g . selectAll ( "path" )
53
+ . data ( polygons )
54
+ . join ( "path" )
55
+ . attr ( "d" , path ) ;
56
+ const activePolygons = polygons . find ( polygon => polygon . length > 2 ) ;
57
+ const S = ! activePolygons ? null
58
+ : index . filter ( i => polygons . some ( polygon => polygon . length > 2 && polygonContains ( polygon , [ X [ i ] , Y [ i ] ] ) ) ) ;
59
+ if ( ! selectionEquals ( node [ selection ] , S ) ) {
60
+ node [ selection ] = S ;
61
+ node . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
62
+ }
63
+ } ) ) ;
64
+ const node = g . node ( ) ;
65
+ node [ selection ] = null ;
66
+ return node ;
67
+ }
68
+ }
69
+
70
+ export function lasso ( data , { x, y, ...options } = { } ) {
71
+ ( [ x , y ] = maybeTuple ( x , y ) ) ;
72
+ return new Lasso ( data , { ...options , x, y} ) ;
73
+ }
74
+
75
+ // set up listeners that will follow this gesture all along
76
+ // (even outside the target canvas)
77
+ // TODO: in a supporting file
78
+ function trackPointer ( e , { start, move, out, end } ) {
79
+ const tracker = { } ,
80
+ id = ( tracker . id = e . pointerId ) ,
81
+ target = e . target ;
82
+ tracker . point = pointer ( e , target ) ;
83
+ target . setPointerCapture ( id ) ;
84
+
85
+ select ( target )
86
+ . on ( `pointerup.${ id } pointercancel.${ id } ` , e => {
87
+ if ( e . pointerId !== id ) return ;
88
+ tracker . sourceEvent = e ;
89
+ select ( target ) . on ( `.${ id } ` , null ) ;
90
+ target . releasePointerCapture ( id ) ;
91
+ end && end ( tracker ) ;
92
+ } )
93
+ . on ( `pointermove.${ id } ` , e => {
94
+ if ( e . pointerId !== id ) return ;
95
+ tracker . sourceEvent = e ;
96
+ tracker . prev = tracker . point ;
97
+ tracker . point = pointer ( e , target ) ;
98
+ move && move ( tracker ) ;
99
+ } )
100
+ . on ( `pointerout.${ id } ` , e => {
101
+ if ( e . pointerId !== id ) return ;
102
+ tracker . sourceEvent = e ;
103
+ tracker . point = null ;
104
+ out && out ( tracker ) ;
105
+ } ) ;
106
+
107
+ start && start ( tracker ) ;
108
+ }
109
+
110
+ function lassoer ( ) {
111
+ const polygons = [ ] ;
112
+ const dispatch = dispatcher ( "start" , "lasso" , "end" , "cancel" ) ;
113
+ let extent ;
114
+ const lasso = selection => {
115
+ const node = selection . node ( ) ;
116
+ let currentPolygon ;
117
+
118
+ selection
119
+ . on ( "touchmove" , e => e . preventDefault ( ) ) // prevent scrolling
120
+ . on ( "pointerdown" , e => {
121
+ const p = pointer ( e , node ) ;
122
+ for ( let i = polygons . length - 1 ; i >= 0 ; -- i ) {
123
+ if ( polygonContains ( polygons [ i ] , p ) ) {
124
+ polygons . splice ( i , 1 ) ;
125
+ dispatch . call ( "cancel" , node , polygons ) ;
126
+ return ;
127
+ }
128
+ }
129
+ trackPointer ( e , {
130
+ start : p => {
131
+ currentPolygon = [ constrainExtent ( p . point ) ] ;
132
+ polygons . push ( currentPolygon ) ;
133
+ dispatch . call ( "start" , node , polygons ) ;
134
+ } ,
135
+ move : p => {
136
+ currentPolygon . push ( constrainExtent ( p . point ) ) ;
137
+ dispatch . call ( "lasso" , node , polygons ) ;
138
+ } ,
139
+ end : ( ) => {
140
+ dispatch . call ( "end" , node , polygons ) ;
141
+ }
142
+ } ) ;
143
+ } ) ;
144
+ } ;
145
+ lasso . on = function ( type , _ ) {
146
+ return _ ? ( dispatch . on ( ...arguments ) , lasso ) : dispatch . on ( ...arguments ) ;
147
+ } ;
148
+ lasso . extent = function ( _ ) {
149
+ return _ ? ( extent = _ , lasso ) : extent ;
150
+ } ;
151
+
152
+ function constrainExtent ( p ) {
153
+ if ( ! extent ) return p ;
154
+ return [ clamp ( p [ 0 ] , extent [ 0 ] [ 0 ] , extent [ 1 ] [ 0 ] ) , clamp ( p [ 1 ] , extent [ 0 ] [ 1 ] , extent [ 1 ] [ 1 ] ) ] ;
155
+ }
156
+
157
+ function clamp ( x , a , b ) {
158
+ return x < a ? a : x > b ? b : x ;
159
+ }
160
+
161
+ return lasso ;
162
+ }
0 commit comments