plotly.js
Version:
The open source javascript graphing library that powers plotly
773 lines (647 loc) • 26.5 kB
JavaScript
/**
* Copyright 2012-2020, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
var Registry = require('../../registry');
var Lib = require('../../lib');
var Axes = require('../../plots/cartesian/axes');
var readPaths = require('./draw_newshape/helpers').readPaths;
var displayOutlines = require('./draw_newshape/display_outlines');
var clearOutlineControllers = require('../../plots/cartesian/handle_outline').clearOutlineControllers;
var Color = require('../color');
var Drawing = require('../drawing');
var arrayEditor = require('../../plot_api/plot_template').arrayEditor;
var dragElement = require('../dragelement');
var setCursor = require('../../lib/setcursor');
var constants = require('./constants');
var helpers = require('./helpers');
// Shapes are stored in gd.layout.shapes, an array of objects
// index can point to one item in this array,
// or non-numeric to simply add a new one
// or -1 to modify all existing
// opt can be the full options object, or one key (to be set to value)
// or undefined to simply redraw
// if opt is blank, val can be 'add' or a full options object to add a new
// annotation at that point in the array, or 'remove' to delete this one
module.exports = {
draw: draw,
drawOne: drawOne,
eraseActiveShape: eraseActiveShape
};
function draw(gd) {
var fullLayout = gd._fullLayout;
// Remove previous shapes before drawing new in shapes in fullLayout.shapes
fullLayout._shapeUpperLayer.selectAll('path').remove();
fullLayout._shapeLowerLayer.selectAll('path').remove();
for(var k in fullLayout._plots) {
var shapelayer = fullLayout._plots[k].shapelayer;
if(shapelayer) shapelayer.selectAll('path').remove();
}
for(var i = 0; i < fullLayout.shapes.length; i++) {
if(fullLayout.shapes[i].visible) {
drawOne(gd, i);
}
}
// may need to resurrect this if we put text (LaTeX) in shapes
// return Plots.previousPromises(gd);
}
function shouldSkipEdits(gd) {
return !!gd._fullLayout._drawing;
}
function couldHaveActiveShape(gd) {
// for now keep config.editable: true as it was before shape-drawing PR
return !gd._context.edits.shapePosition;
}
function drawOne(gd, index) {
// remove the existing shape if there is one.
// because indices can change, we need to look in all shape layers
gd._fullLayout._paperdiv
.selectAll('.shapelayer [data-index="' + index + '"]')
.remove();
var o = helpers.makeOptionsAndPlotinfo(gd, index);
var options = o.options;
var plotinfo = o.plotinfo;
// this shape is gone - quit now after deleting it
// TODO: use d3 idioms instead of deleting and redrawing every time
if(!options._input || options.visible === false) return;
if(options.layer !== 'below') {
drawShape(gd._fullLayout._shapeUpperLayer);
} else if(options.xref === 'paper' || options.yref === 'paper') {
drawShape(gd._fullLayout._shapeLowerLayer);
} else {
if(plotinfo._hadPlotinfo) {
var mainPlot = plotinfo.mainplotinfo || plotinfo;
drawShape(mainPlot.shapelayer);
} else {
// Fall back to _shapeLowerLayer in case the requested subplot doesn't exist.
// This can happen if you reference the shape to an x / y axis combination
// that doesn't have any data on it (and layer is below)
drawShape(gd._fullLayout._shapeLowerLayer);
}
}
function drawShape(shapeLayer) {
var d = getPathString(gd, options);
var attrs = {
'data-index': index,
'fill-rule': options.fillrule,
d: d
};
var opacity = options.opacity;
var fillColor = options.fillcolor;
var lineColor = options.line.width ? options.line.color : 'rgba(0,0,0,0)';
var lineWidth = options.line.width;
var lineDash = options.line.dash;
if(!lineWidth && options.editable === true) {
// ensure invisible border to activate the shape
lineWidth = 5;
lineDash = 'solid';
}
var isOpen = d[d.length - 1] !== 'Z';
var isActiveShape = couldHaveActiveShape(gd) &&
options.editable && gd._fullLayout._activeShapeIndex === index;
if(isActiveShape) {
fillColor = isOpen ? 'rgba(0,0,0,0)' :
gd._fullLayout.activeshape.fillcolor;
opacity = gd._fullLayout.activeshape.opacity;
}
var path = shapeLayer.append('path')
.attr(attrs)
.style('opacity', opacity)
.call(Color.stroke, lineColor)
.call(Color.fill, fillColor)
.call(Drawing.dashLine, lineDash, lineWidth);
setClipPath(path, gd, options);
var editHelpers;
if(isActiveShape || gd._context.edits.shapePosition) editHelpers = arrayEditor(gd.layout, 'shapes', options);
if(isActiveShape) {
path.style({
'cursor': 'move',
});
var dragOptions = {
element: path.node(),
plotinfo: plotinfo,
gd: gd,
editHelpers: editHelpers,
isActiveShape: true // i.e. to enable controllers
};
var polygons = readPaths(d, gd);
// display polygons on the screen
displayOutlines(polygons, path, dragOptions);
} else {
if(gd._context.edits.shapePosition) {
setupDragElement(gd, path, options, index, shapeLayer, editHelpers);
} else if(options.editable === true) {
path.style('pointer-events',
(isOpen || Color.opacity(fillColor) * opacity <= 0.5) ? 'stroke' : 'all'
);
}
}
path.node().addEventListener('click', function() { return activateShape(gd, path); });
}
}
function setClipPath(shapePath, gd, shapeOptions) {
// note that for layer="below" the clipAxes can be different from the
// subplot we're drawing this in. This could cause problems if the shape
// spans two subplots. See https://github.com/plotly/plotly.js/issues/1452
//
// if axis is 'paper' or an axis with " domain" appended, then there is no
// clip axis
var clipAxes = (shapeOptions.xref + shapeOptions.yref).replace(/paper/g, '').replace(/[xyz][1-9]* *domain/g, '');
Drawing.setClipUrl(
shapePath,
clipAxes ? 'clip' + gd._fullLayout._uid + clipAxes : null,
gd
);
}
function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHelpers) {
var MINWIDTH = 10;
var MINHEIGHT = 10;
var xPixelSized = shapeOptions.xsizemode === 'pixel';
var yPixelSized = shapeOptions.ysizemode === 'pixel';
var isLine = shapeOptions.type === 'line';
var isPath = shapeOptions.type === 'path';
var modifyItem = editHelpers.modifyItem;
var x0, y0, x1, y1, xAnchor, yAnchor;
var n0, s0, w0, e0, optN, optS, optW, optE;
var pathIn;
// setup conversion functions
var xa = Axes.getFromId(gd, shapeOptions.xref);
var xRefType = Axes.getRefType(shapeOptions.xref);
var ya = Axes.getFromId(gd, shapeOptions.yref);
var yRefType = Axes.getRefType(shapeOptions.yref);
var x2p = helpers.getDataToPixel(gd, xa, false, xRefType);
var y2p = helpers.getDataToPixel(gd, ya, true, yRefType);
var p2x = helpers.getPixelToData(gd, xa, false, xRefType);
var p2y = helpers.getPixelToData(gd, ya, true, yRefType);
var sensoryElement = obtainSensoryElement();
var dragOptions = {
element: sensoryElement.node(),
gd: gd,
prepFn: startDrag,
doneFn: endDrag,
clickFn: abortDrag
};
var dragMode;
dragElement.init(dragOptions);
sensoryElement.node().onmousemove = updateDragMode;
function obtainSensoryElement() {
return isLine ? createLineDragHandles() : shapePath;
}
function createLineDragHandles() {
var minSensoryWidth = 10;
var sensoryWidth = Math.max(shapeOptions.line.width, minSensoryWidth);
// Helper shapes group
// Note that by setting the `data-index` attr, it is ensured that
// the helper group is purged in this modules `draw` function
var g = shapeLayer.append('g')
.attr('data-index', index);
// Helper path for moving
g.append('path')
.attr('d', shapePath.attr('d'))
.style({
'cursor': 'move',
'stroke-width': sensoryWidth,
'stroke-opacity': '0' // ensure not visible
});
// Helper circles for resizing
var circleStyle = {
'fill-opacity': '0' // ensure not visible
};
var circleRadius = Math.max(sensoryWidth / 2, minSensoryWidth);
g.append('circle')
.attr({
'data-line-point': 'start-point',
'cx': xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x0 : x2p(shapeOptions.x0),
'cy': yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y0 : y2p(shapeOptions.y0),
'r': circleRadius
})
.style(circleStyle)
.classed('cursor-grab', true);
g.append('circle')
.attr({
'data-line-point': 'end-point',
'cx': xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x1 : x2p(shapeOptions.x1),
'cy': yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y1 : y2p(shapeOptions.y1),
'r': circleRadius
})
.style(circleStyle)
.classed('cursor-grab', true);
return g;
}
function updateDragMode(evt) {
if(shouldSkipEdits(gd)) {
dragMode = null;
return;
}
if(isLine) {
if(evt.target.tagName === 'path') {
dragMode = 'move';
} else {
dragMode = evt.target.attributes['data-line-point'].value === 'start-point' ?
'resize-over-start-point' : 'resize-over-end-point';
}
} else {
// element might not be on screen at time of setup,
// so obtain bounding box here
var dragBBox = dragOptions.element.getBoundingClientRect();
// choose 'move' or 'resize'
// based on initial position of cursor within the drag element
var w = dragBBox.right - dragBBox.left;
var h = dragBBox.bottom - dragBBox.top;
var x = evt.clientX - dragBBox.left;
var y = evt.clientY - dragBBox.top;
var cursor = (!isPath && w > MINWIDTH && h > MINHEIGHT && !evt.shiftKey) ?
dragElement.getCursor(x / w, 1 - y / h) :
'move';
setCursor(shapePath, cursor);
// possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
dragMode = cursor.split('-')[0];
}
}
function startDrag(evt) {
if(shouldSkipEdits(gd)) return;
// setup update strings and initial values
if(xPixelSized) {
xAnchor = x2p(shapeOptions.xanchor);
}
if(yPixelSized) {
yAnchor = y2p(shapeOptions.yanchor);
}
if(shapeOptions.type === 'path') {
pathIn = shapeOptions.path;
} else {
x0 = xPixelSized ? shapeOptions.x0 : x2p(shapeOptions.x0);
y0 = yPixelSized ? shapeOptions.y0 : y2p(shapeOptions.y0);
x1 = xPixelSized ? shapeOptions.x1 : x2p(shapeOptions.x1);
y1 = yPixelSized ? shapeOptions.y1 : y2p(shapeOptions.y1);
}
if(x0 < x1) {
w0 = x0;
optW = 'x0';
e0 = x1;
optE = 'x1';
} else {
w0 = x1;
optW = 'x1';
e0 = x0;
optE = 'x0';
}
// For fixed size shapes take opposing direction of y-axis into account.
// Hint: For data sized shapes this is done by the y2p function.
if((!yPixelSized && y0 < y1) || (yPixelSized && y0 > y1)) {
n0 = y0;
optN = 'y0';
s0 = y1;
optS = 'y1';
} else {
n0 = y1;
optN = 'y1';
s0 = y0;
optS = 'y0';
}
// setup dragMode and the corresponding handler
updateDragMode(evt);
renderVisualCues(shapeLayer, shapeOptions);
deactivateClipPathTemporarily(shapePath, shapeOptions, gd);
dragOptions.moveFn = (dragMode === 'move') ? moveShape : resizeShape;
dragOptions.altKey = evt.altKey;
}
function endDrag() {
if(shouldSkipEdits(gd)) return;
setCursor(shapePath);
removeVisualCues(shapeLayer);
// Don't rely on clipPath being activated during re-layout
setClipPath(shapePath, gd, shapeOptions);
Registry.call('_guiRelayout', gd, editHelpers.getUpdateObj());
}
function abortDrag() {
if(shouldSkipEdits(gd)) return;
removeVisualCues(shapeLayer);
}
function moveShape(dx, dy) {
if(shapeOptions.type === 'path') {
var noOp = function(coord) { return coord; };
var moveX = noOp;
var moveY = noOp;
if(xPixelSized) {
modifyItem('xanchor', shapeOptions.xanchor = p2x(xAnchor + dx));
} else {
moveX = function moveX(x) { return p2x(x2p(x) + dx); };
if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
}
if(yPixelSized) {
modifyItem('yanchor', shapeOptions.yanchor = p2y(yAnchor + dy));
} else {
moveY = function moveY(y) { return p2y(y2p(y) + dy); };
if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
}
modifyItem('path', shapeOptions.path = movePath(pathIn, moveX, moveY));
} else {
if(xPixelSized) {
modifyItem('xanchor', shapeOptions.xanchor = p2x(xAnchor + dx));
} else {
modifyItem('x0', shapeOptions.x0 = p2x(x0 + dx));
modifyItem('x1', shapeOptions.x1 = p2x(x1 + dx));
}
if(yPixelSized) {
modifyItem('yanchor', shapeOptions.yanchor = p2y(yAnchor + dy));
} else {
modifyItem('y0', shapeOptions.y0 = p2y(y0 + dy));
modifyItem('y1', shapeOptions.y1 = p2y(y1 + dy));
}
}
shapePath.attr('d', getPathString(gd, shapeOptions));
renderVisualCues(shapeLayer, shapeOptions);
}
function resizeShape(dx, dy) {
if(isPath) {
// TODO: implement path resize, don't forget to update dragMode code
var noOp = function(coord) { return coord; };
var moveX = noOp;
var moveY = noOp;
if(xPixelSized) {
modifyItem('xanchor', shapeOptions.xanchor = p2x(xAnchor + dx));
} else {
moveX = function moveX(x) { return p2x(x2p(x) + dx); };
if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
}
if(yPixelSized) {
modifyItem('yanchor', shapeOptions.yanchor = p2y(yAnchor + dy));
} else {
moveY = function moveY(y) { return p2y(y2p(y) + dy); };
if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
}
modifyItem('path', shapeOptions.path = movePath(pathIn, moveX, moveY));
} else if(isLine) {
if(dragMode === 'resize-over-start-point') {
var newX0 = x0 + dx;
var newY0 = yPixelSized ? y0 - dy : y0 + dy;
modifyItem('x0', shapeOptions.x0 = xPixelSized ? newX0 : p2x(newX0));
modifyItem('y0', shapeOptions.y0 = yPixelSized ? newY0 : p2y(newY0));
} else if(dragMode === 'resize-over-end-point') {
var newX1 = x1 + dx;
var newY1 = yPixelSized ? y1 - dy : y1 + dy;
modifyItem('x1', shapeOptions.x1 = xPixelSized ? newX1 : p2x(newX1));
modifyItem('y1', shapeOptions.y1 = yPixelSized ? newY1 : p2y(newY1));
}
} else {
var has = function(str) { return dragMode.indexOf(str) !== -1; };
var hasN = has('n');
var hasS = has('s');
var hasW = has('w');
var hasE = has('e');
var newN = hasN ? n0 + dy : n0;
var newS = hasS ? s0 + dy : s0;
var newW = hasW ? w0 + dx : w0;
var newE = hasE ? e0 + dx : e0;
if(yPixelSized) {
// Do things in opposing direction for y-axis.
// Hint: for data-sized shapes the reversal of axis direction is done in p2y.
if(hasN) newN = n0 - dy;
if(hasS) newS = s0 - dy;
}
// Update shape eventually. Again, be aware of the
// opposing direction of the y-axis of fixed size shapes.
if(
(!yPixelSized && newS - newN > MINHEIGHT) ||
(yPixelSized && newN - newS > MINHEIGHT)
) {
modifyItem(optN, shapeOptions[optN] = yPixelSized ? newN : p2y(newN));
modifyItem(optS, shapeOptions[optS] = yPixelSized ? newS : p2y(newS));
}
if(newE - newW > MINWIDTH) {
modifyItem(optW, shapeOptions[optW] = xPixelSized ? newW : p2x(newW));
modifyItem(optE, shapeOptions[optE] = xPixelSized ? newE : p2x(newE));
}
}
shapePath.attr('d', getPathString(gd, shapeOptions));
renderVisualCues(shapeLayer, shapeOptions);
}
function renderVisualCues(shapeLayer, shapeOptions) {
if(xPixelSized || yPixelSized) {
renderAnchor();
}
function renderAnchor() {
var isNotPath = shapeOptions.type !== 'path';
// d3 join with dummy data to satisfy d3 data-binding
var visualCues = shapeLayer.selectAll('.visual-cue').data([0]);
// Enter
var strokeWidth = 1;
visualCues.enter()
.append('path')
.attr({
'fill': '#fff',
'fill-rule': 'evenodd',
'stroke': '#000',
'stroke-width': strokeWidth
})
.classed('visual-cue', true);
// Update
var posX = x2p(
xPixelSized ?
shapeOptions.xanchor :
Lib.midRange(
isNotPath ?
[shapeOptions.x0, shapeOptions.x1] :
helpers.extractPathCoords(shapeOptions.path, constants.paramIsX))
);
var posY = y2p(
yPixelSized ?
shapeOptions.yanchor :
Lib.midRange(
isNotPath ?
[shapeOptions.y0, shapeOptions.y1] :
helpers.extractPathCoords(shapeOptions.path, constants.paramIsY))
);
posX = helpers.roundPositionForSharpStrokeRendering(posX, strokeWidth);
posY = helpers.roundPositionForSharpStrokeRendering(posY, strokeWidth);
if(xPixelSized && yPixelSized) {
var crossPath = 'M' + (posX - 1 - strokeWidth) + ',' + (posY - 1 - strokeWidth) +
'h-8v2h8 v8h2v-8 h8v-2h-8 v-8h-2 Z';
visualCues.attr('d', crossPath);
} else if(xPixelSized) {
var vBarPath = 'M' + (posX - 1 - strokeWidth) + ',' + (posY - 9 - strokeWidth) +
'v18 h2 v-18 Z';
visualCues.attr('d', vBarPath);
} else {
var hBarPath = 'M' + (posX - 9 - strokeWidth) + ',' + (posY - 1 - strokeWidth) +
'h18 v2 h-18 Z';
visualCues.attr('d', hBarPath);
}
}
}
function removeVisualCues(shapeLayer) {
shapeLayer.selectAll('.visual-cue').remove();
}
function deactivateClipPathTemporarily(shapePath, shapeOptions, gd) {
var xref = shapeOptions.xref;
var yref = shapeOptions.yref;
var xa = Axes.getFromId(gd, xref);
var ya = Axes.getFromId(gd, yref);
var clipAxes = '';
if(xref !== 'paper' && !xa.autorange) clipAxes += xref;
if(yref !== 'paper' && !ya.autorange) clipAxes += yref;
Drawing.setClipUrl(
shapePath,
clipAxes ? 'clip' + gd._fullLayout._uid + clipAxes : null,
gd
);
}
}
function getPathString(gd, options) {
var type = options.type;
var xRefType = Axes.getRefType(options.xref);
var yRefType = Axes.getRefType(options.yref);
var xa = Axes.getFromId(gd, options.xref);
var ya = Axes.getFromId(gd, options.yref);
var gs = gd._fullLayout._size;
var x2r, x2p, y2r, y2p;
var x0, x1, y0, y1;
if(xa) {
if(xRefType === 'domain') {
x2p = function(v) { return xa._offset + xa._length * v; };
} else {
x2r = helpers.shapePositionToRange(xa);
x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); };
}
} else {
x2p = function(v) { return gs.l + gs.w * v; };
}
if(ya) {
if(yRefType === 'domain') {
y2p = function(v) { return ya._offset + ya._length * (1 - v); };
} else {
y2r = helpers.shapePositionToRange(ya);
y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); };
}
} else {
y2p = function(v) { return gs.t + gs.h * (1 - v); };
}
if(type === 'path') {
if(xa && xa.type === 'date') x2p = helpers.decodeDate(x2p);
if(ya && ya.type === 'date') y2p = helpers.decodeDate(y2p);
return convertPath(options, x2p, y2p);
}
if(options.xsizemode === 'pixel') {
var xAnchorPos = x2p(options.xanchor);
x0 = xAnchorPos + options.x0;
x1 = xAnchorPos + options.x1;
} else {
x0 = x2p(options.x0);
x1 = x2p(options.x1);
}
if(options.ysizemode === 'pixel') {
var yAnchorPos = y2p(options.yanchor);
y0 = yAnchorPos - options.y0;
y1 = yAnchorPos - options.y1;
} else {
y0 = y2p(options.y0);
y1 = y2p(options.y1);
}
if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1;
if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z';
// circle
var cx = (x0 + x1) / 2;
var cy = (y0 + y1) / 2;
var rx = Math.abs(cx - x0);
var ry = Math.abs(cy - y0);
var rArc = 'A' + rx + ',' + ry;
var rightPt = (cx + rx) + ',' + cy;
var topPt = cx + ',' + (cy - ry);
return 'M' + rightPt + rArc + ' 0 1,1 ' + topPt +
rArc + ' 0 0,1 ' + rightPt + 'Z';
}
function convertPath(options, x2p, y2p) {
var pathIn = options.path;
var xSizemode = options.xsizemode;
var ySizemode = options.ysizemode;
var xAnchor = options.xanchor;
var yAnchor = options.yanchor;
return pathIn.replace(constants.segmentRE, function(segment) {
var paramNumber = 0;
var segmentType = segment.charAt(0);
var xParams = constants.paramIsX[segmentType];
var yParams = constants.paramIsY[segmentType];
var nParams = constants.numParams[segmentType];
var paramString = segment.substr(1).replace(constants.paramRE, function(param) {
if(xParams[paramNumber]) {
if(xSizemode === 'pixel') param = x2p(xAnchor) + Number(param);
else param = x2p(param);
} else if(yParams[paramNumber]) {
if(ySizemode === 'pixel') param = y2p(yAnchor) - Number(param);
else param = y2p(param);
}
paramNumber++;
if(paramNumber > nParams) param = 'X';
return param;
});
if(paramNumber > nParams) {
paramString = paramString.replace(/[\s,]*X.*/, '');
Lib.log('Ignoring extra params in segment ' + segment);
}
return segmentType + paramString;
});
}
function movePath(pathIn, moveX, moveY) {
return pathIn.replace(constants.segmentRE, function(segment) {
var paramNumber = 0;
var segmentType = segment.charAt(0);
var xParams = constants.paramIsX[segmentType];
var yParams = constants.paramIsY[segmentType];
var nParams = constants.numParams[segmentType];
var paramString = segment.substr(1).replace(constants.paramRE, function(param) {
if(paramNumber >= nParams) return param;
if(xParams[paramNumber]) param = moveX(param);
else if(yParams[paramNumber]) param = moveY(param);
paramNumber++;
return param;
});
return segmentType + paramString;
});
}
function activateShape(gd, path) {
if(!couldHaveActiveShape(gd)) return;
var element = path.node();
var id = +element.getAttribute('data-index');
if(id >= 0) {
// deactivate if already active
if(id === gd._fullLayout._activeShapeIndex) {
deactivateShape(gd);
return;
}
gd._fullLayout._activeShapeIndex = id;
gd._fullLayout._deactivateShape = deactivateShape;
draw(gd);
}
}
function deactivateShape(gd) {
if(!couldHaveActiveShape(gd)) return;
var id = gd._fullLayout._activeShapeIndex;
if(id >= 0) {
clearOutlineControllers(gd);
delete gd._fullLayout._activeShapeIndex;
draw(gd);
}
}
function eraseActiveShape(gd) {
if(!couldHaveActiveShape(gd)) return;
clearOutlineControllers(gd);
var id = gd._fullLayout._activeShapeIndex;
var shapes = (gd.layout || {}).shapes || [];
if(id < shapes.length) {
var newShapes = [];
for(var q = 0; q < shapes.length; q++) {
if(q !== id) {
newShapes.push(shapes[q]);
}
}
delete gd._fullLayout._activeShapeIndex;
Registry.call('_guiRelayout', gd, {
shapes: newShapes
});
}
}