Skip to content

Commit 050d40c

Browse files
authored
Merge pull request #660 from n-riesco/draggable-shapes
Implement draggable shapes
2 parents 567ba90 + 5b965ab commit 050d40c

File tree

3 files changed

+779
-12
lines changed

3 files changed

+779
-12
lines changed

src/components/dragelement/index.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ dragElement.unhoverRaw = unhover.raw;
4343
* dragged is as in moveFn
4444
* numClicks is how many clicks we've registered within
4545
* a doubleclick time
46+
* setCursor (optional) function(event)
47+
* executed on mousemove before mousedown
48+
* the purpose of this callback is to update the mouse cursor before
49+
* the click & drag interaction has been initiated
4650
*/
4751
dragElement.init = function init(options) {
4852
var gd = Lib.getPlotDiv(options.element) || {},
@@ -52,11 +56,15 @@ dragElement.init = function init(options) {
5256
startY,
5357
newMouseDownTime,
5458
dragCover,
55-
initialTarget;
59+
initialTarget,
60+
initialOnMouseMove;
5661

5762
if(!gd._mouseDownTime) gd._mouseDownTime = 0;
5863

5964
function onStart(e) {
65+
// disable call to options.setCursor(evt)
66+
options.element.onmousemove = initialOnMouseMove;
67+
6068
// make dragging and dragged into properties of gd
6169
// so that others can look at and modify them
6270
gd._dragged = false;
@@ -107,6 +115,10 @@ dragElement.init = function init(options) {
107115
}
108116

109117
function onDone(e) {
118+
// re-enable call to options.setCursor(evt)
119+
initialOnMouseMove = options.element.onmousemove;
120+
if(options.setCursor) options.element.onmousemove = options.setCursor;
121+
110122
dragCover.onmousemove = null;
111123
dragCover.onmouseup = null;
112124
dragCover.onmouseout = null;
@@ -139,6 +151,10 @@ dragElement.init = function init(options) {
139151
return Lib.pauseEvent(e);
140152
}
141153

154+
// enable call to options.setCursor(evt)
155+
initialOnMouseMove = options.element.onmousemove;
156+
if(options.setCursor) options.element.onmousemove = options.setCursor;
157+
142158
options.element.onmousedown = onStart;
143159
options.element.style.pointerEvents = 'all';
144160
};

src/components/shapes/index.js

+238-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ var Axes = require('../../plots/cartesian/axes');
1717
var Color = require('../color');
1818
var Drawing = require('../drawing');
1919

20+
var dragElement = require('../dragelement');
21+
var setCursor = require('../../lib/setcursor');
2022

2123
var 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(/paper/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

374530
function 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+
504731
shapes.calcAutorange = function(gd) {
505732
var fullLayout = gd._fullLayout,
506733
shapeList = fullLayout.shapes,

0 commit comments

Comments
 (0)