@@ -108,77 +108,143 @@ function drawOne(gd, index) {
108
108
. call ( Color . fill , options . fillcolor )
109
109
. call ( Drawing . dashLine , options . line . dash , options . line . width ) ;
110
110
111
- // note that for layer="below" the clipAxes can be different from the
112
- // subplot we're drawing this in. This could cause problems if the shape
113
- // spans two subplots. See https://github.com/plotly/plotly.js/issues/1452
114
- var clipAxes = ( options . xref + options . yref ) . replace ( / p a p e r / g, '' ) ;
115
-
116
- path . call ( Drawing . setClipUrl , clipAxes ?
117
- ( 'clip' + gd . _fullLayout . _uid + clipAxes ) :
118
- null
119
- ) ;
111
+ setClipPath ( path , gd , options ) ;
120
112
121
- if ( gd . _context . edits . shapePosition ) setupDragElement ( gd , path , options , index ) ;
113
+ if ( gd . _context . edits . shapePosition ) setupDragElement ( gd , path , options , index , shapeLayer ) ;
122
114
}
123
115
}
124
116
125
- function setupDragElement ( gd , shapePath , shapeOptions , index ) {
117
+ function setClipPath ( shapePath , gd , shapeOptions ) {
118
+ // note that for layer="below" the clipAxes can be different from the
119
+ // subplot we're drawing this in. This could cause problems if the shape
120
+ // spans two subplots. See https://github.com/plotly/plotly.js/issues/1452
121
+ var clipAxes = ( shapeOptions . xref + shapeOptions . yref ) . replace ( / p a p e r / g, '' ) ;
122
+
123
+ shapePath . call ( Drawing . setClipUrl , clipAxes ?
124
+ ( 'clip' + gd . _fullLayout . _uid + clipAxes ) :
125
+ null
126
+ ) ;
127
+ }
128
+
129
+ function setupDragElement ( gd , shapePath , shapeOptions , index , shapeLayer ) {
126
130
var MINWIDTH = 10 ,
127
131
MINHEIGHT = 10 ;
128
132
129
133
var xPixelSized = shapeOptions . xsizemode === 'pixel' ,
130
- yPixelSized = shapeOptions . ysizemode === 'pixel' ;
134
+ yPixelSized = shapeOptions . ysizemode === 'pixel' ,
135
+ isLine = shapeOptions . type === 'line' ,
136
+ isPath = shapeOptions . type === 'path' ;
131
137
132
138
var update ;
133
139
var x0 , y0 , x1 , y1 , xAnchor , yAnchor , astrX0 , astrY0 , astrX1 , astrY1 , astrXAnchor , astrYAnchor ;
134
140
var n0 , s0 , w0 , e0 , astrN , astrS , astrW , astrE , optN , optS , optW , optE ;
135
141
var pathIn , astrPath ;
136
142
137
- var xa , ya , x2p , y2p , p2x , p2y ;
143
+ // setup conversion functions
144
+ var xa = Axes . getFromId ( gd , shapeOptions . xref ) ,
145
+ ya = Axes . getFromId ( gd , shapeOptions . yref ) ,
146
+ x2p = helpers . getDataToPixel ( gd , xa ) ,
147
+ y2p = helpers . getDataToPixel ( gd , ya , true ) ,
148
+ p2x = helpers . getPixelToData ( gd , xa ) ,
149
+ p2y = helpers . getPixelToData ( gd , ya , true ) ;
138
150
151
+ var sensoryElement = obtainSensoryElement ( ) ;
139
152
var dragOptions = {
140
- element : shapePath . node ( ) ,
153
+ element : sensoryElement . node ( ) ,
141
154
gd : gd ,
142
155
prepFn : startDrag ,
143
- doneFn : endDrag
156
+ doneFn : endDrag ,
157
+ clickFn : abortDrag
144
158
} ,
145
159
dragMode ;
146
160
147
161
dragElement . init ( dragOptions ) ;
148
162
149
- shapePath . node ( ) . onmousemove = updateDragMode ;
163
+ sensoryElement . node ( ) . onmousemove = updateDragMode ;
150
164
151
- function updateDragMode ( evt ) {
152
- // element might not be on screen at time of setup,
153
- // so obtain bounding box here
154
- var dragBBox = dragOptions . element . getBoundingClientRect ( ) ;
155
-
156
- // choose 'move' or 'resize'
157
- // based on initial position of cursor within the drag element
158
- var w = dragBBox . right - dragBBox . left ,
159
- h = dragBBox . bottom - dragBBox . top ,
160
- x = evt . clientX - dragBBox . left ,
161
- y = evt . clientY - dragBBox . top ,
162
- cursor = ( w > MINWIDTH && h > MINHEIGHT && ! evt . shiftKey ) ?
163
- dragElement . getCursor ( x / w , 1 - y / h ) :
164
- 'move' ;
165
-
166
- setCursor ( shapePath , cursor ) ;
167
-
168
- // possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
169
- dragMode = cursor . split ( '-' ) [ 0 ] ;
165
+ function obtainSensoryElement ( ) {
166
+ return isLine ? createLineDragHandles ( ) : shapePath ;
170
167
}
171
168
172
- function startDrag ( evt ) {
173
- // setup conversion functions
174
- xa = Axes . getFromId ( gd , shapeOptions . xref ) ;
175
- ya = Axes . getFromId ( gd , shapeOptions . yref ) ;
169
+ function createLineDragHandles ( ) {
170
+ var minSensoryWidth = 10 ,
171
+ sensoryWidth = Math . max ( shapeOptions . line . width , minSensoryWidth ) ;
172
+
173
+ // Helper shapes group
174
+ // Note that by setting the `data-index` attr, it is ensured that
175
+ // the helper group is purged in this modules `draw` function
176
+ var g = shapeLayer . append ( 'g' )
177
+ . attr ( 'data-index' , index ) ;
178
+
179
+ // Helper path for moving
180
+ g . append ( 'path' )
181
+ . attr ( 'd' , shapePath . attr ( 'd' ) )
182
+ . style ( {
183
+ 'cursor' : 'move' ,
184
+ 'stroke-width' : sensoryWidth ,
185
+ 'stroke-opacity' : '0' // ensure not visible
186
+ } ) ;
187
+
188
+ // Helper circles for resizing
189
+ var circleStyle = {
190
+ 'fill-opacity' : '0' // ensure not visible
191
+ } ;
192
+ var circleRadius = sensoryWidth / 2 > minSensoryWidth ? sensoryWidth / 2 : minSensoryWidth ;
193
+
194
+ g . append ( 'circle' )
195
+ . attr ( {
196
+ 'data-line-point' : 'start-point' ,
197
+ 'cx' : xPixelSized ? x2p ( shapeOptions . xanchor ) + shapeOptions . x0 : x2p ( shapeOptions . x0 ) ,
198
+ 'cy' : yPixelSized ? y2p ( shapeOptions . yanchor ) - shapeOptions . y0 : y2p ( shapeOptions . y0 ) ,
199
+ 'r' : circleRadius
200
+ } )
201
+ . style ( circleStyle )
202
+ . classed ( 'cursor-grab' , true ) ;
203
+
204
+ g . append ( 'circle' )
205
+ . attr ( {
206
+ 'data-line-point' : 'end-point' ,
207
+ 'cx' : xPixelSized ? x2p ( shapeOptions . xanchor ) + shapeOptions . x1 : x2p ( shapeOptions . x1 ) ,
208
+ 'cy' : yPixelSized ? y2p ( shapeOptions . yanchor ) - shapeOptions . y1 : y2p ( shapeOptions . y1 ) ,
209
+ 'r' : circleRadius
210
+ } )
211
+ . style ( circleStyle )
212
+ . classed ( 'cursor-grab' , true ) ;
213
+
214
+ return g ;
215
+ }
176
216
177
- x2p = helpers . getDataToPixel ( gd , xa ) ;
178
- y2p = helpers . getDataToPixel ( gd , ya , true ) ;
179
- p2x = helpers . getPixelToData ( gd , xa ) ;
180
- p2y = helpers . getPixelToData ( gd , ya , true ) ;
217
+ function updateDragMode ( evt ) {
218
+ if ( isLine ) {
219
+ if ( evt . target . tagName === 'path' ) {
220
+ dragMode = 'move' ;
221
+ } else {
222
+ dragMode = evt . target . attributes [ 'data-line-point' ] . value === 'start-point' ?
223
+ 'resize-over-start-point' : 'resize-over-end-point' ;
224
+ }
225
+ } else {
226
+ // element might not be on screen at time of setup,
227
+ // so obtain bounding box here
228
+ var dragBBox = dragOptions . element . getBoundingClientRect ( ) ;
229
+
230
+ // choose 'move' or 'resize'
231
+ // based on initial position of cursor within the drag element
232
+ var w = dragBBox . right - dragBBox . left ,
233
+ h = dragBBox . bottom - dragBBox . top ,
234
+ x = evt . clientX - dragBBox . left ,
235
+ y = evt . clientY - dragBBox . top ,
236
+ cursor = ( ! isPath && w > MINWIDTH && h > MINHEIGHT && ! evt . shiftKey ) ?
237
+ dragElement . getCursor ( x / w , 1 - y / h ) :
238
+ 'move' ;
239
+
240
+ setCursor ( shapePath , cursor ) ;
241
+
242
+ // possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
243
+ dragMode = cursor . split ( '-' ) [ 0 ] ;
244
+ }
245
+ }
181
246
247
+ function startDrag ( evt ) {
182
248
// setup update strings and initial values
183
249
var astr = 'shapes[' + index + ']' ;
184
250
@@ -231,14 +297,24 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
231
297
232
298
// setup dragMode and the corresponding handler
233
299
updateDragMode ( evt ) ;
300
+ renderVisualCues ( shapeLayer , shapeOptions ) ;
301
+ deactivateClipPathTemporarily ( shapePath , shapeOptions , gd ) ;
234
302
dragOptions . moveFn = ( dragMode === 'move' ) ? moveShape : resizeShape ;
235
303
}
236
304
237
305
function endDrag ( ) {
238
306
setCursor ( shapePath ) ;
307
+ removeVisualCues ( shapeLayer ) ;
308
+
309
+ // Don't rely on clipPath being activated during re-layout
310
+ setClipPath ( shapePath , gd , shapeOptions ) ;
239
311
Registry . call ( 'relayout' , gd , update ) ;
240
312
}
241
313
314
+ function abortDrag ( ) {
315
+ removeVisualCues ( shapeLayer ) ;
316
+ }
317
+
242
318
function moveShape ( dx , dy ) {
243
319
if ( shapeOptions . type === 'path' ) {
244
320
var noOp = function ( coord ) { return coord ; } ,
@@ -279,11 +355,12 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
279
355
}
280
356
281
357
shapePath . attr ( 'd' , getPathString ( gd , shapeOptions ) ) ;
358
+ renderVisualCues ( shapeLayer , shapeOptions ) ;
282
359
}
283
360
284
361
function resizeShape ( dx , dy ) {
285
- if ( shapeOptions . type === 'path' ) {
286
- // TODO: implement path resize
362
+ if ( isPath ) {
363
+ // TODO: implement path resize, don't forget to update dragMode code
287
364
var noOp = function ( coord ) { return coord ; } ,
288
365
moveX = noOp ,
289
366
moveY = noOp ;
@@ -305,6 +382,19 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
305
382
shapeOptions . path = movePath ( pathIn , moveX , moveY ) ;
306
383
update [ astrPath ] = shapeOptions . path ;
307
384
}
385
+ else if ( isLine ) {
386
+ if ( dragMode === 'resize-over-start-point' ) {
387
+ var newX0 = x0 + dx ;
388
+ var newY0 = yPixelSized ? y0 - dy : y0 + dy ;
389
+ update [ astrX0 ] = shapeOptions . x0 = xPixelSized ? newX0 : p2x ( newX0 ) ;
390
+ update [ astrY0 ] = shapeOptions . y0 = yPixelSized ? newY0 : p2y ( newY0 ) ;
391
+ } else if ( dragMode === 'resize-over-end-point' ) {
392
+ var newX1 = x1 + dx ;
393
+ var newY1 = yPixelSized ? y1 - dy : y1 + dy ;
394
+ update [ astrX1 ] = shapeOptions . x1 = xPixelSized ? newX1 : p2x ( newX1 ) ;
395
+ update [ astrY1 ] = shapeOptions . y1 = yPixelSized ? newY1 : p2y ( newY1 ) ;
396
+ }
397
+ }
308
398
else {
309
399
var newN = ( ~ dragMode . indexOf ( 'n' ) ) ? n0 + dy : n0 ,
310
400
newS = ( ~ dragMode . indexOf ( 's' ) ) ? s0 + dy : s0 ,
@@ -330,6 +420,87 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
330
420
}
331
421
332
422
shapePath . attr ( 'd' , getPathString ( gd , shapeOptions ) ) ;
423
+ renderVisualCues ( shapeLayer , shapeOptions ) ;
424
+ }
425
+
426
+ function renderVisualCues ( shapeLayer , shapeOptions ) {
427
+ if ( xPixelSized || yPixelSized ) {
428
+ renderAnchor ( ) ;
429
+ }
430
+
431
+ function renderAnchor ( ) {
432
+ var isNotPath = shapeOptions . type !== 'path' ;
433
+
434
+ // d3 join with dummy data to satisfy d3 data-binding
435
+ var visualCues = shapeLayer . selectAll ( '.visual-cue' ) . data ( [ 0 ] ) ;
436
+
437
+ // Enter
438
+ var strokeWidth = 1 ;
439
+ visualCues . enter ( )
440
+ . append ( 'path' )
441
+ . attr ( {
442
+ 'fill' : '#fff' ,
443
+ 'fill-rule' : 'evenodd' ,
444
+ 'stroke' : '#000' ,
445
+ 'stroke-width' : strokeWidth
446
+ } )
447
+ . classed ( 'visual-cue' , true ) ;
448
+
449
+ // Update
450
+ var posX = x2p (
451
+ xPixelSized ?
452
+ shapeOptions . xanchor :
453
+ Lib . midRange (
454
+ isNotPath ?
455
+ [ shapeOptions . x0 , shapeOptions . x1 ] :
456
+ helpers . extractPathCoords ( shapeOptions . path , constants . paramIsX ) )
457
+ ) ;
458
+ var posY = y2p (
459
+ yPixelSized ?
460
+ shapeOptions . yanchor :
461
+ Lib . midRange (
462
+ isNotPath ?
463
+ [ shapeOptions . y0 , shapeOptions . y1 ] :
464
+ helpers . extractPathCoords ( shapeOptions . path , constants . paramIsY ) )
465
+ ) ;
466
+
467
+ posX = helpers . roundPositionForSharpStrokeRendering ( posX , strokeWidth ) ;
468
+ posY = helpers . roundPositionForSharpStrokeRendering ( posY , strokeWidth ) ;
469
+
470
+ if ( xPixelSized && yPixelSized ) {
471
+ var crossPath = 'M' + ( posX - 1 - strokeWidth ) + ',' + ( posY - 1 - strokeWidth ) +
472
+ 'h-8v2h8 v8h2v-8 h8v-2h-8 v-8h-2 Z' ;
473
+ visualCues . attr ( 'd' , crossPath ) ;
474
+ } else if ( xPixelSized ) {
475
+ var vBarPath = 'M' + ( posX - 1 - strokeWidth ) + ',' + ( posY - 9 - strokeWidth ) +
476
+ 'v18 h2 v-18 Z' ;
477
+ visualCues . attr ( 'd' , vBarPath ) ;
478
+ } else {
479
+ var hBarPath = 'M' + ( posX - 9 - strokeWidth ) + ',' + ( posY - 1 - strokeWidth ) +
480
+ 'h18 v2 h-18 Z' ;
481
+ visualCues . attr ( 'd' , hBarPath ) ;
482
+ }
483
+ }
484
+ }
485
+
486
+ function removeVisualCues ( shapeLayer ) {
487
+ shapeLayer . selectAll ( '.visual-cue' ) . remove ( ) ;
488
+ }
489
+
490
+ function deactivateClipPathTemporarily ( shapePath , shapeOptions , gd ) {
491
+ var xref = shapeOptions . xref ,
492
+ yref = shapeOptions . yref ,
493
+ xa = Axes . getFromId ( gd , xref ) ,
494
+ ya = Axes . getFromId ( gd , yref ) ;
495
+
496
+ var clipAxes = '' ;
497
+ if ( xref !== 'paper' && ! xa . autorange ) clipAxes += xref ;
498
+ if ( yref !== 'paper' && ! ya . autorange ) clipAxes += yref ;
499
+
500
+ shapePath . call ( Drawing . setClipUrl , clipAxes ?
501
+ 'clip' + gd . _fullLayout . _uid + clipAxes :
502
+ null
503
+ ) ;
333
504
}
334
505
}
335
506
0 commit comments