plotly.js
Version:
The open source javascript graphing library that powers plotly
778 lines (651 loc) • 27.7 kB
JavaScript
'use strict';
var d3 = require('@plotly/d3');
var Registry = require('../../registry');
var Lib = require('../../lib');
var Axes = require('../../plots/cartesian/axes');
var readPaths = require('./draw_newshape/helpers').readPaths;
var displayOutlines = require('./display_outlines');
var drawLabel = require('./display_labels');
var clearOutlineControllers = require('./handle_outline').clearOutlineControllers;
var Color = require('../color');
var Drawing = require('../drawing');
var arrayEditor = require('../../plot_api/plot_template').arrayEditor;
var dragElement = require('../dragelement');
var Fx = require('../fx');
var setCursor = require('../../lib/setcursor');
var constants = require('./constants');
var helpers = require('./helpers');
var getPathString = helpers.getPathString;
// 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,
drawLabel: drawLabel
};
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();
fullLayout._shapeUpperLayer.selectAll('text').remove();
fullLayout._shapeLowerLayer.selectAll('text').remove();
for (var k in fullLayout._plots) {
var shapelayer = fullLayout._plots[k].shapelayer;
if (shapelayer) {
shapelayer.selectAll('path').remove();
shapelayer.selectAll('text').remove();
}
}
for (var i = 0; i < fullLayout.shapes.length; i++) {
if (fullLayout.shapes[i].visible === true) {
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._outlining;
}
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.makeShapesOptionsAndPlotinfo(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 !== true) return;
const isMultiAxisShape = Array.isArray(options.xref) || Array.isArray(options.yref);
if (options.layer === 'above') {
drawShape(gd._fullLayout._shapeUpperLayer);
} else if (options.xref.includes('paper') || options.yref.includes('paper')) {
drawShape(gd._fullLayout._shapeLowerLayer);
} else if (options.layer === 'between' && !isMultiAxisShape) {
drawShape(plotinfo.shapelayerBetween);
} 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 shapeGroup = shapeLayer.append('g').classed('shape-group', true).attr({ 'data-index': index });
var path = shapeGroup
.append('path')
.attr(attrs)
.style('opacity', opacity)
.call(Color.stroke, lineColor)
.call(Color.fill, fillColor)
.call(Drawing.dashLine, lineDash, lineWidth);
setClipPath(shapeGroup, gd, options);
// Draw or clear the label
drawLabel(gd, index, options, shapeGroup);
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,
hasText: options.label.text || options.label.texttemplate,
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);
});
// Editable / shape-position-edit shapes get inline pointer-events
// that prevent mouse events from reaching the maindrag, where
// hoveranywhere / clickanywhere are wired up. Forward those events
// from the shape so the layout-level events still fire.
forwardHoverClickAnywhere(gd, path, plotinfo);
}
}
function forwardHoverClickAnywhere(gd, path, plotinfo) {
if (!plotinfo?.id) return;
const node = path.node();
// Fx.hover/Fx.click compute plot-area pixel coordinates from
// evt.clientX/Y relative to evt.target's bounding box.
// The shape path's bbox differs from the plot area, so we
// re-target events to the subplot's nsewdrag element.
function patchedEvt(evt) {
const mainPlot = plotinfo.mainplotinfo || plotinfo;
const nsew = mainPlot?.draglayer?.select('.nsewdrag').node();
if (!nsew) return null;
return { clientX: evt.clientX, clientY: evt.clientY, target: nsew };
}
node.addEventListener('mousemove', (evt) => {
if (gd._dragging) return;
if (gd._fullLayout.hoveranywhere) {
const e = patchedEvt(evt);
if (e) Fx.hover(gd, e, plotinfo.id);
}
});
node.addEventListener('click', (evt) => {
if (gd._dragged) return;
if (gd._fullLayout.clickanywhere) {
const e = patchedEvt(evt);
if (e) Fx.click(gd, e, plotinfo.id);
}
});
}
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
const xref = shapeOptions.xref;
const yref = shapeOptions.yref;
// For multi-axis shapes, create a custom clip path from axis bounds
if (Array.isArray(xref) || Array.isArray(yref)) {
const clipId = 'clip' + gd._fullLayout._uid + 'shape' + shapeOptions._index;
const rect = getMultiAxisClipRect(gd, xref, yref);
Lib.ensureSingleById(gd._fullLayout._clips, 'clipPath', clipId, function (s) {
s.append('rect');
})
.select('rect')
.attr(rect);
Drawing.setClipUrl(shapePath, clipId, gd);
} else {
const clipAxes = (xref + yref).replace(/paper/g, '').replace(/[xyz][0-9]* *domain/g, '');
Drawing.setClipUrl(shapePath, clipAxes ? 'clip' + gd._fullLayout._uid + clipAxes : null, gd);
}
}
function getMultiAxisClipRect(gd, xref, yref) {
const gs = gd._fullLayout._size;
function getBounds(refs, isVertical) {
// Retrieve all existing axes from the references
const axes = (Array.isArray(refs) ? refs : [refs]).map((r) => Axes.getFromId(gd, r)).filter(Boolean);
// If no valid axes, return the bounds of the larger plot area
if (!axes.length) {
return isVertical ? [gs.t, gs.t + gs.h] : [gs.l, gs.l + gs.w];
}
// Otherwise, we find all find and return the smallest start point
// and largest end point to be used as the clip bounds
const startBounds = axes.map(function (ax) {
return ax._offset;
});
const endBounds = axes.map(function (ax) {
return ax._offset + ax._length;
});
return [Math.min(...startBounds), Math.max(...endBounds)];
}
const xb = getBounds(xref, false);
const yb = getBounds(yref, true);
return { x: xb[0], y: yb[0], width: xb[1] - xb[0], height: yb[1] - yb[0] };
}
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;
var shapeGroup = d3.select(shapePath.node().parentNode);
// 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 shiftXStart = shapeOptions.x0shift;
var shiftXEnd = shapeOptions.x1shift;
var shiftYStart = shapeOptions.y0shift;
var shiftYEnd = shapeOptions.y1shift;
var x2p = function (v, shift) {
var dataToPixel = helpers.getDataToPixel(gd, xa, shift, false, xRefType);
return dataToPixel(v);
};
var y2p = function (v, shift) {
var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true, yRefType);
return dataToPixel(v);
};
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).attr('drag-helper', true);
// 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, shiftXStart),
cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y0 : y2p(shapeOptions.y0, shiftYStart),
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, shiftXEnd),
cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y1 : y2p(shapeOptions.y1, shiftYEnd),
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);
drawLabel(gd, index, shapeOptions, shapeGroup);
}
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);
drawLabel(gd, index, shapeOptions, shapeGroup);
}
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 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.slice(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 list = [];
for (var q = 0; q < shapes.length; q++) {
if (q !== id) {
list.push(shapes[q]);
}
}
delete gd._fullLayout._activeShapeIndex;
return Registry.call('_guiRelayout', gd, {
shapes: list
});
}
}