Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2535b1a

Browse files
authored
Merge pull request #2594 from plotly/move-and-resize-line-shapes
Move and resize line shapes
2 parents 57eb8ef + e34212b commit 2535b1a

14 files changed

+683
-85
lines changed

build/plotcss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var rules = {
3030
"X .cursor-nw-resize": "cursor:nw-resize;",
3131
"X .cursor-n-resize": "cursor:n-resize;",
3232
"X .cursor-ne-resize": "cursor:ne-resize;",
33+
"X .cursor-grab": "cursor:-webkit-grab;cursor:grab;",
3334
"X .modebar": "position:absolute;top:2px;right:2px;z-index:1001;background:rgba(255,255,255,0.7);",
3435
"X .modebar--hover": "opacity:0;-webkit-transition:opacity 0.3s ease 0s;-moz-transition:opacity 0.3s ease 0s;-ms-transition:opacity 0.3s ease 0s;-o-transition:opacity 0.3s ease 0s;transition:opacity 0.3s ease 0s;",
3536
"X:hover .modebar--hover": "opacity:1;",

src/components/annotations/draw.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
132132

133133
var annTextGroupInner = annTextGroup.append('g')
134134
.style('pointer-events', textEvents ? 'all' : null)
135-
.call(setCursor, 'default')
135+
.call(setCursor, 'pointer')
136136
.on('click', function() {
137137
gd._dragging = false;
138138

@@ -533,6 +533,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
533533
var arrowDrag = arrowGroup.append('path')
534534
.classed('annotation-arrow', true)
535535
.classed('anndrag', true)
536+
.classed('cursor-move', true)
536537
.attr({
537538
d: 'M3,3H-3V-3H3ZM0,0L' + (tailX - arrowDragHeadX) + ',' + (tailY - arrowDragHeadY),
538539
transform: 'translate(' + arrowDragHeadX + ',' + arrowDragHeadY + ')'

src/components/shapes/calc_autorange.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function calcPaddingOptions(lineWidth, sizeMode, v0, v1, path, isYAxis) {
6262

6363
if(sizeMode === 'pixel') {
6464
var coords = path ?
65-
extractPathCoords(path, isYAxis ? constants.paramIsY : constants.paramIsX) :
65+
helpers.extractPathCoords(path, isYAxis ? constants.paramIsY : constants.paramIsX) :
6666
[v0, v1];
6767
var maxValue = Lib.aggNums(Math.max, null, coords),
6868
minValue = Lib.aggNums(Math.min, null, coords),
@@ -79,23 +79,6 @@ function calcPaddingOptions(lineWidth, sizeMode, v0, v1, path, isYAxis) {
7979
}
8080
}
8181

82-
function extractPathCoords(path, paramsToUse) {
83-
var extractedCoordinates = [];
84-
85-
var segments = path.match(constants.segmentRE);
86-
segments.forEach(function(segment) {
87-
var relevantParamIdx = paramsToUse[segment.charAt(0)].drawn;
88-
if(relevantParamIdx === undefined) return;
89-
90-
var params = segment.substr(1).match(constants.paramRE);
91-
if(!params || params.length < relevantParamIdx) return;
92-
93-
extractedCoordinates.push(params[relevantParamIdx]);
94-
});
95-
96-
return extractedCoordinates;
97-
}
98-
9982
function shapeBounds(ax, v0, v1, path, paramsToUse) {
10083
var convertVal = (ax.type === 'category') ? ax.r2c : ax.d2c;
10184

src/components/shapes/draw.js

Lines changed: 216 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/paper/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(/paper/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

Comments
 (0)