Skip to content

Commit 0cf2ee1

Browse files
authored
Merge pull request #1441 from plotly/note-drag-fix
Annotations drag fix
2 parents 8a310b3 + 48603a4 commit 0cf2ee1

File tree

5 files changed

+319
-117
lines changed

5 files changed

+319
-117
lines changed

src/components/annotations/draw.js

+5
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,13 @@ function drawOne(gd, index) {
285285
posPx.text = basePx + textShift;
286286
}
287287

288+
// padplus/minus are used by autorange
288289
options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift;
289290
options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift;
291+
292+
// size/shift are used during dragging
293+
options['_' + axLetter + 'size'] = annSize;
294+
options['_' + axLetter + 'shift'] = textShift;
290295
});
291296

292297
if(annotationIsOffscreen) {

test/jasmine/assets/drag.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
var mouseEvent = require('../assets/mouse_event');
2+
3+
/*
4+
* drag: grab a node and drag it (dx, dy) pixels
5+
* optionally specify an edge ('n', 'se', 'w' etc)
6+
* to grab it by an edge or corner (otherwise the middle is used)
7+
*/
8+
module.exports = function(node, dx, dy, edge) {
9+
10+
edge = edge || '';
11+
var bbox = node.getBoundingClientRect(),
12+
fromX, fromY;
13+
14+
if(edge.indexOf('n') !== -1) fromY = bbox.top;
15+
else if(edge.indexOf('s') !== -1) fromY = bbox.bottom;
16+
else fromY = (bbox.bottom + bbox.top) / 2;
17+
18+
if(edge.indexOf('w') !== -1) fromX = bbox.left;
19+
else if(edge.indexOf('e') !== -1) fromX = bbox.right;
20+
else fromX = (bbox.left + bbox.right) / 2;
21+
22+
23+
var toX = fromX + dx,
24+
toY = fromY + dy;
25+
26+
mouseEvent('mousemove', fromX, fromY, {element: node});
27+
mouseEvent('mousedown', fromX, fromY, {element: node});
28+
29+
var promise = waitForDragCover().then(function(dragCoverNode) {
30+
mouseEvent('mousemove', toX, toY, {element: dragCoverNode});
31+
mouseEvent('mouseup', toX, toY, {element: dragCoverNode});
32+
return waitForDragCoverRemoval();
33+
});
34+
35+
return promise;
36+
};
37+
38+
function waitForDragCover() {
39+
return new Promise(function(resolve) {
40+
var interval = 5,
41+
timeout = 5000;
42+
43+
var id = setInterval(function() {
44+
var dragCoverNode = document.querySelector('.dragcover');
45+
if(dragCoverNode) {
46+
clearInterval(id);
47+
resolve(dragCoverNode);
48+
}
49+
50+
timeout -= interval;
51+
if(timeout < 0) {
52+
clearInterval(id);
53+
throw new Error('waitForDragCover: timeout');
54+
}
55+
}, interval);
56+
});
57+
}
58+
59+
function waitForDragCoverRemoval() {
60+
return new Promise(function(resolve) {
61+
var interval = 5,
62+
timeout = 5000;
63+
64+
var id = setInterval(function() {
65+
var dragCoverNode = document.querySelector('.dragcover');
66+
if(!dragCoverNode) {
67+
clearInterval(id);
68+
resolve(dragCoverNode);
69+
}
70+
71+
timeout -= interval;
72+
if(timeout < 0) {
73+
clearInterval(id);
74+
throw new Error('waitForDragCoverRemoval: timeout');
75+
}
76+
}, interval);
77+
});
78+
}

test/jasmine/assets/mouse_event.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module.exports = function(type, x, y, opts) {
1010
fullOpts.buttons = opts.buttons;
1111
}
1212

13-
var el = document.elementFromPoint(x, y),
13+
var el = (opts && opts.element) || document.elementFromPoint(x, y),
1414
ev;
1515

1616
if(type === 'scroll') {
@@ -20,4 +20,6 @@ module.exports = function(type, x, y, opts) {
2020
}
2121

2222
el.dispatchEvent(ev);
23+
24+
return el;
2325
};

test/jasmine/tests/annotations_test.js

+231
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var customMatchers = require('../assets/custom_matchers');
1111
var createGraphDiv = require('../assets/create_graph_div');
1212
var destroyGraphDiv = require('../assets/destroy_graph_div');
1313
var failTest = require('../assets/fail_test');
14+
var drag = require('../assets/drag');
1415

1516

1617
describe('Test annotations', function() {
@@ -740,3 +741,233 @@ describe('annotation clicktoshow', function() {
740741
.then(done);
741742
});
742743
});
744+
745+
describe('annotation dragging', function() {
746+
var gd;
747+
748+
function textDrag() { return gd.querySelector('.annotation-text-g>g'); }
749+
function arrowDrag() { return gd.querySelector('.annotation-arrow-g>.anndrag'); }
750+
function textBox() { return gd.querySelector('.annotation-text-g'); }
751+
752+
beforeAll(function() {
753+
jasmine.addMatchers(customMatchers);
754+
});
755+
756+
beforeEach(function(done) {
757+
gd = createGraphDiv();
758+
759+
// we've already tested autorange with relayout, so fix the geometry
760+
// completely so we know exactly what we're dealing with
761+
// plot area is 300x300, and covers data range 100x100
762+
Plotly.plot(gd,
763+
[{x: [0, 100], y: [0, 100], mode: 'markers'}],
764+
{
765+
xaxis: {range: [0, 100]},
766+
yaxis: {range: [0, 100]},
767+
width: 500,
768+
height: 500,
769+
margin: {l: 100, r: 100, t: 100, b: 100, pad: 0}
770+
},
771+
{editable: true}
772+
)
773+
.then(done);
774+
});
775+
776+
afterEach(destroyGraphDiv);
777+
778+
function initAnnotation(annotation) {
779+
return Plotly.relayout(gd, {annotations: [annotation]})
780+
.then(function() {
781+
return Plots.previousPromises(gd);
782+
});
783+
}
784+
785+
function dragAndReplot(node, dx, dy, edge) {
786+
return drag(node, dx, dy, edge).then(function() {
787+
return Plots.previousPromises(gd);
788+
});
789+
}
790+
791+
/*
792+
* run through a series of drags of the same annotation
793+
* findDragger: fn that returns the element to drag on
794+
* (either textDrag or ArrowDrag)
795+
* autoshiftX, autoshiftY: how much does the annotation anchor shift
796+
* moving between one region and the next. Zero except if autoanchor
797+
* is active, ie paper-referenced with no arrow
798+
* coordScale: how big is the full plot? paper-referenced has scale 1
799+
* and for the plot defined above, data-referenced has scale 100
800+
*/
801+
function checkDragging(findDragger, autoshiftX, autoshiftY, coordScale) {
802+
var bboxInitial = textBox().getBoundingClientRect();
803+
// first move it within the same auto-anchor zone
804+
return dragAndReplot(findDragger(), 30, -30)
805+
.then(function() {
806+
var bbox = textBox().getBoundingClientRect();
807+
808+
// I'm not sure why these calculations aren't exact - they end up
809+
// being off by a fraction of a pixel, or a full pixel sometimes
810+
// even though as far as I can see in practice the positioning is
811+
// exact. In any event, this precision is enough to ensure that
812+
// anchor: auto is being used.
813+
expect(bbox.left).toBeWithin(bboxInitial.left + 30, 1);
814+
expect(bbox.top).toBeWithin(bboxInitial.top - 30, 1);
815+
816+
var ann = gd.layout.annotations[0];
817+
expect(ann.x).toBeWithin(0.1 * coordScale, 0.01 * coordScale);
818+
expect(ann.y).toBeWithin(0.1 * coordScale, 0.01 * coordScale);
819+
820+
// now move it to the center
821+
// note that we explicitly offset by half the box size because the
822+
// auto-anchor will move to the center
823+
return dragAndReplot(findDragger(), 120 - autoshiftX, -120 + autoshiftY);
824+
})
825+
.then(function() {
826+
var bbox = textBox().getBoundingClientRect();
827+
expect(bbox.left).toBeWithin(bboxInitial.left + 150 - autoshiftX, 2);
828+
expect(bbox.top).toBeWithin(bboxInitial.top - 150 + autoshiftY, 2);
829+
830+
var ann = gd.layout.annotations[0];
831+
expect(ann.x).toBeWithin(0.5 * coordScale, 0.01 * coordScale);
832+
expect(ann.y).toBeWithin(0.5 * coordScale, 0.01 * coordScale);
833+
834+
// next move it near the upper right corner, where the auto-anchor
835+
// moves to the top right corner
836+
// we don't move it all the way to the corner, so the annotation will
837+
// still be entirely on the plot even with an arrow.
838+
return dragAndReplot(findDragger(), 90 - autoshiftX, -90 + autoshiftY);
839+
})
840+
.then(function() {
841+
var bbox = textBox().getBoundingClientRect();
842+
expect(bbox.left).toBeWithin(bboxInitial.left + 240 - 2 * autoshiftX, 2);
843+
expect(bbox.top).toBeWithin(bboxInitial.top - 240 + 2 * autoshiftY, 2);
844+
845+
var ann = gd.layout.annotations[0];
846+
expect(ann.x).toBeWithin(0.8 * coordScale, 0.01 * coordScale);
847+
expect(ann.y).toBeWithin(0.8 * coordScale, 0.01 * coordScale);
848+
849+
// finally move it back to 0, 0
850+
return dragAndReplot(findDragger(), -240 + 2 * autoshiftX, 240 - 2 * autoshiftY);
851+
})
852+
.then(function() {
853+
var bbox = textBox().getBoundingClientRect();
854+
expect(bbox.left).toBeWithin(bboxInitial.left, 2);
855+
expect(bbox.top).toBeWithin(bboxInitial.top, 2);
856+
857+
var ann = gd.layout.annotations[0];
858+
expect(ann.x).toBeWithin(0 * coordScale, 0.01 * coordScale);
859+
expect(ann.y).toBeWithin(0 * coordScale, 0.01 * coordScale);
860+
});
861+
}
862+
863+
// for annotations with arrows: check that dragging the text moves only
864+
// ax and ay (and the textbox itself)
865+
function checkTextDrag() {
866+
var ann = gd.layout.annotations[0],
867+
x0 = ann.x,
868+
y0 = ann.y,
869+
ax0 = ann.ax,
870+
ay0 = ann.ay;
871+
872+
var bboxInitial = textBox().getBoundingClientRect();
873+
874+
return dragAndReplot(textDrag(), 50, -50)
875+
.then(function() {
876+
var bbox = textBox().getBoundingClientRect();
877+
expect(bbox.left).toBeWithin(bboxInitial.left + 50, 1);
878+
expect(bbox.top).toBeWithin(bboxInitial.top - 50, 1);
879+
880+
ann = gd.layout.annotations[0];
881+
882+
expect(ann.x).toBe(x0);
883+
expect(ann.y).toBe(y0);
884+
expect(ann.ax).toBeWithin(ax0 + 50, 1);
885+
expect(ann.ay).toBeWithin(ay0 - 50, 1);
886+
});
887+
}
888+
889+
it('respects anchor: auto when paper-referenced without arrow', function(done) {
890+
initAnnotation({
891+
x: 0,
892+
y: 0,
893+
showarrow: false,
894+
text: 'blah<br>blah blah',
895+
xref: 'paper',
896+
yref: 'paper'
897+
})
898+
.then(function() {
899+
var bbox = textBox().getBoundingClientRect();
900+
901+
return checkDragging(textDrag, bbox.width / 2, bbox.height / 2, 1);
902+
})
903+
.catch(failTest)
904+
.then(done);
905+
});
906+
907+
it('also works paper-referenced with explicit anchors and no arrow', function(done) {
908+
initAnnotation({
909+
x: 0,
910+
y: 0,
911+
showarrow: false,
912+
text: 'blah<br>blah blah',
913+
xref: 'paper',
914+
yref: 'paper',
915+
xanchor: 'left',
916+
yanchor: 'top'
917+
})
918+
.then(function() {
919+
// with offsets 0, 0 because the anchor doesn't change now
920+
return checkDragging(textDrag, 0, 0, 1);
921+
})
922+
.catch(failTest)
923+
.then(done);
924+
});
925+
926+
it('works paper-referenced with arrows', function(done) {
927+
initAnnotation({
928+
x: 0,
929+
y: 0,
930+
text: 'blah<br>blah blah',
931+
xref: 'paper',
932+
yref: 'paper',
933+
ax: 30,
934+
ay: 30
935+
})
936+
.then(function() {
937+
return checkDragging(arrowDrag, 0, 0, 1);
938+
})
939+
.then(checkTextDrag)
940+
.catch(failTest)
941+
.then(done);
942+
});
943+
944+
it('works data-referenced with no arrow', function(done) {
945+
initAnnotation({
946+
x: 0,
947+
y: 0,
948+
showarrow: false,
949+
text: 'blah<br>blah blah'
950+
})
951+
.then(function() {
952+
return checkDragging(textDrag, 0, 0, 100);
953+
})
954+
.catch(failTest)
955+
.then(done);
956+
});
957+
958+
it('works data-referenced with arrow', function(done) {
959+
initAnnotation({
960+
x: 0,
961+
y: 0,
962+
text: 'blah<br>blah blah',
963+
ax: 30,
964+
ay: -30
965+
})
966+
.then(function() {
967+
return checkDragging(arrowDrag, 0, 0, 100);
968+
})
969+
.then(checkTextDrag)
970+
.catch(failTest)
971+
.then(done);
972+
});
973+
});

0 commit comments

Comments
 (0)