@@ -108,77 +108,143 @@ function drawOne(gd, index) {
108108 . call ( Color . fill , options . fillcolor )
109109 . call ( Drawing . dashLine , options . line . dash , options . line . width ) ;
110110
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 ) ;
120112
121- if ( gd . _context . edits . shapePosition ) setupDragElement ( gd , path , options , index ) ;
113+ if ( gd . _context . edits . shapePosition ) setupDragElement ( gd , path , options , index , shapeLayer ) ;
122114 }
123115}
124116
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 ) {
126130 var MINWIDTH = 10 ,
127131 MINHEIGHT = 10 ;
128132
129133 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' ;
131137
132138 var update ;
133139 var x0 , y0 , x1 , y1 , xAnchor , yAnchor , astrX0 , astrY0 , astrX1 , astrY1 , astrXAnchor , astrYAnchor ;
134140 var n0 , s0 , w0 , e0 , astrN , astrS , astrW , astrE , optN , optS , optW , optE ;
135141 var pathIn , astrPath ;
136142
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 ) ;
138150
151+ var sensoryElement = obtainSensoryElement ( ) ;
139152 var dragOptions = {
140- element : shapePath . node ( ) ,
153+ element : sensoryElement . node ( ) ,
141154 gd : gd ,
142155 prepFn : startDrag ,
143- doneFn : endDrag
156+ doneFn : endDrag ,
157+ clickFn : abortDrag
144158 } ,
145159 dragMode ;
146160
147161 dragElement . init ( dragOptions ) ;
148162
149- shapePath . node ( ) . onmousemove = updateDragMode ;
163+ sensoryElement . node ( ) . onmousemove = updateDragMode ;
150164
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 ;
170167 }
171168
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+ }
176216
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+ }
181246
247+ function startDrag ( evt ) {
182248 // setup update strings and initial values
183249 var astr = 'shapes[' + index + ']' ;
184250
@@ -231,14 +297,24 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
231297
232298 // setup dragMode and the corresponding handler
233299 updateDragMode ( evt ) ;
300+ renderVisualCues ( shapeLayer , shapeOptions ) ;
301+ deactivateClipPathTemporarily ( shapePath , shapeOptions , gd ) ;
234302 dragOptions . moveFn = ( dragMode === 'move' ) ? moveShape : resizeShape ;
235303 }
236304
237305 function endDrag ( ) {
238306 setCursor ( shapePath ) ;
307+ removeVisualCues ( shapeLayer ) ;
308+
309+ // Don't rely on clipPath being activated during re-layout
310+ setClipPath ( shapePath , gd , shapeOptions ) ;
239311 Registry . call ( 'relayout' , gd , update ) ;
240312 }
241313
314+ function abortDrag ( ) {
315+ removeVisualCues ( shapeLayer ) ;
316+ }
317+
242318 function moveShape ( dx , dy ) {
243319 if ( shapeOptions . type === 'path' ) {
244320 var noOp = function ( coord ) { return coord ; } ,
@@ -279,11 +355,12 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
279355 }
280356
281357 shapePath . attr ( 'd' , getPathString ( gd , shapeOptions ) ) ;
358+ renderVisualCues ( shapeLayer , shapeOptions ) ;
282359 }
283360
284361 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
287364 var noOp = function ( coord ) { return coord ; } ,
288365 moveX = noOp ,
289366 moveY = noOp ;
@@ -305,6 +382,19 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
305382 shapeOptions . path = movePath ( pathIn , moveX , moveY ) ;
306383 update [ astrPath ] = shapeOptions . path ;
307384 }
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+ }
308398 else {
309399 var newN = ( ~ dragMode . indexOf ( 'n' ) ) ? n0 + dy : n0 ,
310400 newS = ( ~ dragMode . indexOf ( 's' ) ) ? s0 + dy : s0 ,
@@ -330,6 +420,87 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {
330420 }
331421
332422 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+ ) ;
333504 }
334505}
335506
0 commit comments