@@ -17,6 +17,8 @@ var Axes = require('../../plots/cartesian/axes');
1717var Color = require ( '../color' ) ;
1818var Drawing = require ( '../drawing' ) ;
1919
20+ var dragElement = require ( '../dragelement' ) ;
21+ var setCursor = require ( '../../lib/setcursor' ) ;
2022
2123var shapes = module . exports = { } ;
2224
@@ -299,15 +301,7 @@ function updateShape(gd, index, opt, value) {
299301 var options = handleShapeDefaults ( optionsIn , gd . _fullLayout ) ;
300302 gd . _fullLayout . shapes [ index ] = options ;
301303
302- var attrs = {
303- 'data-index' : index ,
304- 'fill-rule' : 'evenodd' ,
305- d : shapePath ( gd , options )
306- } ,
307- clipAxes ;
308-
309- var lineColor = options . line . width ? options . line . color : 'rgba(0,0,0,0)' ;
310-
304+ var clipAxes ;
311305 if ( options . layer !== 'below' ) {
312306 clipAxes = ( options . xref + options . yref ) . replace ( / p a p e r / g, '' ) ;
313307 drawShape ( gd . _fullLayout . _shapeUpperLayer ) ;
@@ -332,6 +326,14 @@ function updateShape(gd, index, opt, value) {
332326 }
333327
334328 function drawShape ( shapeLayer ) {
329+ var attrs = {
330+ 'data-index' : index ,
331+ 'fill-rule' : 'evenodd' ,
332+ d : getPathString ( gd , options )
333+ } ,
334+ lineColor = options . line . width ?
335+ options . line . color : 'rgba(0,0,0,0)' ;
336+
335337 var path = shapeLayer . append ( 'path' )
336338 . attr ( attrs )
337339 . style ( 'opacity' , options . opacity )
@@ -343,6 +345,160 @@ function updateShape(gd, index, opt, value) {
343345 path . call ( Drawing . setClipUrl ,
344346 'clip' + gd . _fullLayout . _uid + clipAxes ) ;
345347 }
348+
349+ if ( gd . _context . editable ) setupDragElement ( gd , path , options , index ) ;
350+ }
351+ }
352+
353+ function setupDragElement ( gd , shapePath , shapeOptions , index ) {
354+ var MINWIDTH = 10 ,
355+ MINHEIGHT = 10 ;
356+
357+ var update ;
358+ var x0 , y0 , x1 , y1 , astrX0 , astrY0 , astrX1 , astrY1 ;
359+ var n0 , s0 , w0 , e0 , astrN , astrS , astrW , astrE , optN , optS , optW , optE ;
360+ var pathIn , astrPath ;
361+
362+ var xa , ya , x2p , y2p , p2x , p2y ;
363+
364+ var dragOptions = {
365+ setCursor : updateDragMode ,
366+ element : shapePath . node ( ) ,
367+ prepFn : startDrag ,
368+ doneFn : endDrag
369+ } ,
370+ dragBBox = dragOptions . element . getBoundingClientRect ( ) ,
371+ dragMode ;
372+
373+ dragElement . init ( dragOptions ) ;
374+
375+ function updateDragMode ( evt ) {
376+ // choose 'move' or 'resize'
377+ // based on initial position of cursor within the drag element
378+ var w = dragBBox . right - dragBBox . left ,
379+ h = dragBBox . bottom - dragBBox . top ,
380+ x = evt . clientX - dragBBox . left ,
381+ y = evt . clientY - dragBBox . top ,
382+ cursor = ( w > MINWIDTH && h > MINHEIGHT && ! evt . shiftKey ) ?
383+ dragElement . getCursor ( x / w , 1 - y / h ) :
384+ 'move' ;
385+
386+ setCursor ( shapePath , cursor ) ;
387+
388+ // possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
389+ dragMode = cursor . split ( '-' ) [ 0 ] ;
390+ }
391+
392+ function startDrag ( evt ) {
393+ // setup conversion functions
394+ xa = Axes . getFromId ( gd , shapeOptions . xref ) ;
395+ ya = Axes . getFromId ( gd , shapeOptions . yref ) ;
396+
397+ x2p = getDataToPixel ( gd , xa ) ;
398+ y2p = getDataToPixel ( gd , ya , true ) ;
399+ p2x = getPixelToData ( gd , xa ) ;
400+ p2y = getPixelToData ( gd , ya , true ) ;
401+
402+ // setup update strings and initial values
403+ var astr = 'shapes[' + index + ']' ;
404+ if ( shapeOptions . type === 'path' ) {
405+ pathIn = shapeOptions . path ;
406+ astrPath = astr + '.path' ;
407+ }
408+ else {
409+ x0 = x2p ( shapeOptions . x0 ) ;
410+ y0 = y2p ( shapeOptions . y0 ) ;
411+ x1 = x2p ( shapeOptions . x1 ) ;
412+ y1 = y2p ( shapeOptions . y1 ) ;
413+
414+ astrX0 = astr + '.x0' ;
415+ astrY0 = astr + '.y0' ;
416+ astrX1 = astr + '.x1' ;
417+ astrY1 = astr + '.y1' ;
418+ }
419+
420+ if ( x0 < x1 ) {
421+ w0 = x0 ; astrW = astr + '.x0' ; optW = 'x0' ;
422+ e0 = x1 ; astrE = astr + '.x1' ; optE = 'x1' ;
423+ }
424+ else {
425+ w0 = x1 ; astrW = astr + '.x1' ; optW = 'x1' ;
426+ e0 = x0 ; astrE = astr + '.x0' ; optE = 'x0' ;
427+ }
428+ if ( y0 < y1 ) {
429+ n0 = y0 ; astrN = astr + '.y0' ; optN = 'y0' ;
430+ s0 = y1 ; astrS = astr + '.y1' ; optS = 'y1' ;
431+ }
432+ else {
433+ n0 = y1 ; astrN = astr + '.y1' ; optN = 'y1' ;
434+ s0 = y0 ; astrS = astr + '.y0' ; optS = 'y0' ;
435+ }
436+
437+ update = { } ;
438+
439+ // setup dragMode and the corresponding handler
440+ updateDragMode ( evt ) ;
441+ dragOptions . moveFn = ( dragMode === 'move' ) ? moveShape : resizeShape ;
442+ }
443+
444+ function endDrag ( dragged ) {
445+ setCursor ( shapePath ) ;
446+ if ( dragged ) {
447+ Plotly . relayout ( gd , update ) ;
448+ }
449+ }
450+
451+ function moveShape ( dx , dy ) {
452+ if ( shapeOptions . type === 'path' ) {
453+ var moveX = function moveX ( x ) { return p2x ( x2p ( x ) + dx ) ; } ;
454+ if ( xa && xa . type === 'date' ) moveX = encodeDate ( moveX ) ;
455+
456+ var moveY = function moveY ( y ) { return p2y ( y2p ( y ) + dy ) ; } ;
457+ if ( ya && ya . type === 'date' ) moveY = encodeDate ( moveY ) ;
458+
459+ shapeOptions . path = movePath ( pathIn , moveX , moveY ) ;
460+ update [ astrPath ] = shapeOptions . path ;
461+ }
462+ else {
463+ update [ astrX0 ] = shapeOptions . x0 = p2x ( x0 + dx ) ;
464+ update [ astrY0 ] = shapeOptions . y0 = p2y ( y0 + dy ) ;
465+ update [ astrX1 ] = shapeOptions . x1 = p2x ( x1 + dx ) ;
466+ update [ astrY1 ] = shapeOptions . y1 = p2y ( y1 + dy ) ;
467+ }
468+
469+ shapePath . attr ( 'd' , getPathString ( gd , shapeOptions ) ) ;
470+ }
471+
472+ function resizeShape ( dx , dy ) {
473+ if ( shapeOptions . type === 'path' ) {
474+ // TODO: implement path resize
475+ var moveX = function moveX ( x ) { return p2x ( x2p ( x ) + dx ) ; } ;
476+ if ( xa && xa . type === 'date' ) moveX = encodeDate ( moveX ) ;
477+
478+ var moveY = function moveY ( y ) { return p2y ( y2p ( y ) + dy ) ; } ;
479+ if ( ya && ya . type === 'date' ) moveY = encodeDate ( moveY ) ;
480+
481+ shapeOptions . path = movePath ( pathIn , moveX , moveY ) ;
482+ update [ astrPath ] = shapeOptions . path ;
483+ }
484+ else {
485+ var newN = ( ~ dragMode . indexOf ( 'n' ) ) ? n0 + dy : n0 ,
486+ newS = ( ~ dragMode . indexOf ( 's' ) ) ? s0 + dy : s0 ,
487+ newW = ( ~ dragMode . indexOf ( 'w' ) ) ? w0 + dx : w0 ,
488+ newE = ( ~ dragMode . indexOf ( 'e' ) ) ? e0 + dx : e0 ;
489+
490+ if ( newS - newN > MINHEIGHT ) {
491+ update [ astrN ] = shapeOptions [ optN ] = p2y ( newN ) ;
492+ update [ astrS ] = shapeOptions [ optS ] = p2y ( newS ) ;
493+ }
494+
495+ if ( newE - newW > MINWIDTH ) {
496+ update [ astrW ] = shapeOptions [ optW ] = p2x ( newW ) ;
497+ update [ astrE ] = shapeOptions [ optE ] = p2x ( newE ) ;
498+ }
499+ }
500+
501+ shapePath . attr ( 'd' , getPathString ( gd , shapeOptions ) ) ;
346502 }
347503}
348504
@@ -372,10 +528,58 @@ function isShapeInSubplot(gd, shape, plotinfo) {
372528}
373529
374530function decodeDate ( convertToPx ) {
375- return function ( v ) { return convertToPx ( v . replace ( '_' , ' ' ) ) ; } ;
531+ return function ( v ) {
532+ if ( v . replace ) v = v . replace ( '_' , ' ' ) ;
533+ return convertToPx ( v ) ;
534+ } ;
535+ }
536+
537+ function encodeDate ( convertToDate ) {
538+ return function ( v ) { return convertToDate ( v ) . replace ( ' ' , '_' ) ; } ;
539+ }
540+
541+ function getDataToPixel ( gd , axis , isVertical ) {
542+ var gs = gd . _fullLayout . _size ,
543+ dataToPixel ;
544+
545+ if ( axis ) {
546+ var d2l = dataToLinear ( axis ) ;
547+
548+ dataToPixel = function ( v ) {
549+ return axis . _offset + axis . l2p ( d2l ( v , true ) ) ;
550+ } ;
551+
552+ if ( axis . type === 'date' ) dataToPixel = decodeDate ( dataToPixel ) ;
553+ }
554+ else if ( isVertical ) {
555+ dataToPixel = function ( v ) { return gs . t + gs . h * ( 1 - v ) ; } ;
556+ }
557+ else {
558+ dataToPixel = function ( v ) { return gs . l + gs . w * v ; } ;
559+ }
560+
561+ return dataToPixel ;
562+ }
563+
564+ function getPixelToData ( gd , axis , isVertical ) {
565+ var gs = gd . _fullLayout . _size ,
566+ pixelToData ;
567+
568+ if ( axis ) {
569+ var l2d = linearToData ( axis ) ;
570+ pixelToData = function ( p ) { return l2d ( axis . p2l ( p - axis . _offset ) ) ; } ;
571+ }
572+ else if ( isVertical ) {
573+ pixelToData = function ( p ) { return 1 - ( p - gs . t ) / gs . h ; } ;
574+ }
575+ else {
576+ pixelToData = function ( p ) { return ( p - gs . l ) / gs . w ; } ;
577+ }
578+
579+ return pixelToData ;
376580}
377581
378- function shapePath ( gd , options ) {
582+ function getPathString ( gd , options ) {
379583 var type = options . type ,
380584 xa = Axes . getFromId ( gd , options . xref ) ,
381585 ya = Axes . getFromId ( gd , options . yref ) ,
@@ -501,6 +705,29 @@ shapes.convertPath = function(pathIn, x2p, y2p) {
501705 } ) ;
502706} ;
503707
708+ function movePath ( pathIn , moveX , moveY ) {
709+ return pathIn . replace ( segmentRE , function ( segment ) {
710+ var paramNumber = 0 ,
711+ segmentType = segment . charAt ( 0 ) ,
712+ xParams = paramIsX [ segmentType ] ,
713+ yParams = paramIsY [ segmentType ] ,
714+ nParams = numParams [ segmentType ] ;
715+
716+ var paramString = segment . substr ( 1 ) . replace ( paramRE , function ( param ) {
717+ if ( paramNumber >= nParams ) return param ;
718+
719+ if ( xParams [ paramNumber ] ) param = moveX ( param ) ;
720+ else if ( yParams [ paramNumber ] ) param = moveY ( param ) ;
721+
722+ paramNumber ++ ;
723+
724+ return param ;
725+ } ) ;
726+
727+ return segmentType + paramString ;
728+ } ) ;
729+ }
730+
504731shapes . calcAutorange = function ( gd ) {
505732 var fullLayout = gd . _fullLayout ,
506733 shapeList = fullLayout . shapes ,
0 commit comments