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