plotly.js
Version:
The open source javascript graphing library that powers plotly
796 lines (674 loc) • 28.6 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 d3 = require('d3');
var Lib = require('../../lib');
var Plots = require('../../plots/plots');
var Registry = require('../../registry');
var Events = require('../../lib/events');
var dragElement = require('../dragelement');
var Drawing = require('../drawing');
var Color = require('../color');
var svgTextUtils = require('../../lib/svg_text_utils');
var handleClick = require('./handle_click');
var constants = require('./constants');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;
var FROM_TL = alignmentConstants.FROM_TL;
var FROM_BR = alignmentConstants.FROM_BR;
var getLegendData = require('./get_legend_data');
var style = require('./style');
var helpers = require('./helpers');
module.exports = function draw(gd, opts) {
var fullLayout = gd._fullLayout;
var clipId = 'legend' + fullLayout._uid;
var layer;
// Check whether this is the main legend (ie. called without any opts)
if(!opts) {
opts = fullLayout.legend || {};
opts._main = true;
layer = fullLayout._infolayer;
} else {
layer = opts.layer;
clipId += '-hover';
}
if(!layer) return;
if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0;
var legendData;
if(opts._main) {
if(!gd.calcdata) return;
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts);
} else {
if(!opts.entries) return;
legendData = getLegendData(opts.entries, opts);
}
var hiddenSlices = fullLayout.hiddenlabels || [];
if(opts._main && (!fullLayout.showlegend || !legendData.length)) {
layer.selectAll('.legend').remove();
fullLayout._topdefs.select('#' + clipId).remove();
return Plots.autoMargin(gd, 'legend');
}
var legend = Lib.ensureSingle(layer, 'g', 'legend', function(s) {
if(opts._main) s.attr('pointer-events', 'all');
});
var clipPath = Lib.ensureSingleById(fullLayout._topdefs, 'clipPath', clipId, function(s) {
s.append('rect');
});
var bg = Lib.ensureSingle(legend, 'rect', 'bg', function(s) {
s.attr('shape-rendering', 'crispEdges');
});
bg.call(Color.stroke, opts.bordercolor)
.call(Color.fill, opts.bgcolor)
.style('stroke-width', opts.borderwidth + 'px');
var scrollBox = Lib.ensureSingle(legend, 'g', 'scrollbox');
var title = opts.title;
opts._titleWidth = 0;
opts._titleHeight = 0;
if(title.text) {
var titleEl = Lib.ensureSingle(scrollBox, 'text', 'legendtitletext');
titleEl.attr('text-anchor', 'start')
.call(Drawing.font, title.font)
.text(title.text);
textLayout(titleEl, scrollBox, gd, opts); // handle mathjax or multi-line text and compute title height
} else {
scrollBox.selectAll('.legendtitletext').remove();
}
var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
s.attr(constants.scrollBarEnterAttrs)
.call(Color.fill, constants.scrollBarColor);
});
var groups = scrollBox.selectAll('g.groups').data(legendData);
groups.enter().append('g').attr('class', 'groups');
groups.exit().remove();
var traces = groups.selectAll('g.traces').data(Lib.identity);
traces.enter().append('g').attr('class', 'traces');
traces.exit().remove();
traces.style('opacity', function(d) {
var trace = d[0].trace;
if(Registry.traceIs(trace, 'pie-like')) {
return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
} else {
return trace.visible === 'legendonly' ? 0.5 : 1;
}
})
.each(function() { d3.select(this).call(drawTexts, gd, opts); })
.call(style, gd, opts)
.each(function() { if(opts._main) d3.select(this).call(setupTraceToggle, gd); });
Lib.syncOrAsync([
Plots.previousPromises,
function() { return computeLegendDimensions(gd, groups, traces, opts); },
function() {
// IF expandMargin return a Promise (which is truthy),
// we're under a doAutoMargin redraw, so we don't have to
// draw the remaining pieces below
if(opts._main && expandMargin(gd)) return;
var gs = fullLayout._size;
var bw = opts.borderwidth;
var lx = gs.l + gs.w * opts.x - FROM_TL[getXanchor(opts)] * opts._width;
var ly = gs.t + gs.h * (1 - opts.y) - FROM_TL[getYanchor(opts)] * opts._effHeight;
if(opts._main && fullLayout.margin.autoexpand) {
var lx0 = lx;
var ly0 = ly;
lx = Lib.constrain(lx, 0, fullLayout.width - opts._width);
ly = Lib.constrain(ly, 0, fullLayout.height - opts._effHeight);
if(lx !== lx0) {
Lib.log('Constrain legend.x to make legend fit inside graph');
}
if(ly !== ly0) {
Lib.log('Constrain legend.y to make legend fit inside graph');
}
}
// Set size and position of all the elements that make up a legend:
// legend, background and border, scroll box and scroll bar as well as title
if(opts._main) Drawing.setTranslate(legend, lx, ly);
// to be safe, remove previous listeners
scrollBar.on('.drag', null);
legend.on('wheel', null);
if(!opts._main || opts._height <= opts._maxHeight || gd._context.staticPlot) {
// if scrollbar should not be shown.
var height = opts._effHeight;
// if not the main legend, let it be its full size
if(!opts._main) height = opts._height;
bg.attr({
width: opts._width - bw,
height: height - bw,
x: bw / 2,
y: bw / 2
});
Drawing.setTranslate(scrollBox, 0, 0);
clipPath.select('rect').attr({
width: opts._width - 2 * bw,
height: height - 2 * bw,
x: bw,
y: bw
});
Drawing.setClipUrl(scrollBox, clipId, gd);
Drawing.setRect(scrollBar, 0, 0, 0, 0);
delete opts._scrollY;
} else {
var scrollBarHeight = Math.max(constants.scrollBarMinHeight,
opts._effHeight * opts._effHeight / opts._height);
var scrollBarYMax = opts._effHeight -
scrollBarHeight -
2 * constants.scrollBarMargin;
var scrollBoxYMax = opts._height - opts._effHeight;
var scrollRatio = scrollBarYMax / scrollBoxYMax;
var scrollBoxY = Math.min(opts._scrollY || 0, scrollBoxYMax);
// increase the background and clip-path width
// by the scrollbar width and margin
bg.attr({
width: opts._width -
2 * bw +
constants.scrollBarWidth +
constants.scrollBarMargin,
height: opts._effHeight - bw,
x: bw / 2,
y: bw / 2
});
clipPath.select('rect').attr({
width: opts._width -
2 * bw +
constants.scrollBarWidth +
constants.scrollBarMargin,
height: opts._effHeight - 2 * bw,
x: bw,
y: bw + scrollBoxY
});
Drawing.setClipUrl(scrollBox, clipId, gd);
scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
// scroll legend by mousewheel or touchpad swipe up/down
legend.on('wheel', function() {
scrollBoxY = Lib.constrain(
opts._scrollY +
((d3.event.deltaY / scrollBarYMax) * scrollBoxYMax),
0, scrollBoxYMax);
scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
if(scrollBoxY !== 0 && scrollBoxY !== scrollBoxYMax) {
d3.event.preventDefault();
}
});
var eventY0, eventY1, scrollBoxY0;
var getScrollBarDragY = function(scrollBoxY0, eventY0, eventY1) {
var y = ((eventY1 - eventY0) / scrollRatio) + scrollBoxY0;
return Lib.constrain(y, 0, scrollBoxYMax);
};
var getNaturalDragY = function(scrollBoxY0, eventY0, eventY1) {
var y = ((eventY0 - eventY1) / scrollRatio) + scrollBoxY0;
return Lib.constrain(y, 0, scrollBoxYMax);
};
// scroll legend by dragging scrollBAR
var scrollBarDrag = d3.behavior.drag()
.on('dragstart', function() {
var e = d3.event.sourceEvent;
if(e.type === 'touchstart') {
eventY0 = e.changedTouches[0].clientY;
} else {
eventY0 = e.clientY;
}
scrollBoxY0 = scrollBoxY;
})
.on('drag', function() {
var e = d3.event.sourceEvent;
if(e.buttons === 2 || e.ctrlKey) return;
if(e.type === 'touchmove') {
eventY1 = e.changedTouches[0].clientY;
} else {
eventY1 = e.clientY;
}
scrollBoxY = getScrollBarDragY(scrollBoxY0, eventY0, eventY1);
scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
});
scrollBar.call(scrollBarDrag);
// scroll legend by touch-dragging scrollBOX
var scrollBoxTouchDrag = d3.behavior.drag()
.on('dragstart', function() {
var e = d3.event.sourceEvent;
if(e.type === 'touchstart') {
eventY0 = e.changedTouches[0].clientY;
scrollBoxY0 = scrollBoxY;
}
})
.on('drag', function() {
var e = d3.event.sourceEvent;
if(e.type === 'touchmove') {
eventY1 = e.changedTouches[0].clientY;
scrollBoxY = getNaturalDragY(scrollBoxY0, eventY0, eventY1);
scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio);
}
});
scrollBox.call(scrollBoxTouchDrag);
}
function scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio) {
opts._scrollY = gd._fullLayout.legend._scrollY = scrollBoxY;
Drawing.setTranslate(scrollBox, 0, -scrollBoxY);
Drawing.setRect(
scrollBar,
opts._width,
constants.scrollBarMargin + scrollBoxY * scrollRatio,
constants.scrollBarWidth,
scrollBarHeight
);
clipPath.select('rect').attr('y', bw + scrollBoxY);
}
if(gd._context.edits.legendPosition) {
var xf, yf, x0, y0;
legend.classed('cursor-move', true);
dragElement.init({
element: legend.node(),
gd: gd,
prepFn: function() {
var transform = Drawing.getTranslate(legend);
x0 = transform.x;
y0 = transform.y;
},
moveFn: function(dx, dy) {
var newX = x0 + dx;
var newY = y0 + dy;
Drawing.setTranslate(legend, newX, newY);
xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor);
yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor);
},
doneFn: function() {
if(xf !== undefined && yf !== undefined) {
Registry.call('_guiRelayout', gd, {'legend.x': xf, 'legend.y': yf});
}
},
clickFn: function(numClicks, e) {
var clickedTrace = layer.selectAll('g.traces').filter(function() {
var bbox = this.getBoundingClientRect();
return (
e.clientX >= bbox.left && e.clientX <= bbox.right &&
e.clientY >= bbox.top && e.clientY <= bbox.bottom
);
});
if(clickedTrace.size() > 0) {
clickOrDoubleClick(gd, legend, clickedTrace, numClicks, e);
}
}
});
}
}], gd);
};
function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
var trace = legendItem.data()[0][0].trace;
var evtData = {
event: evt,
node: legendItem.node(),
curveNumber: trace.index,
expandedIndex: trace._expandedIndex,
data: gd.data,
layout: gd.layout,
frames: gd._transitionData._frames,
config: gd._context,
fullData: gd._fullData,
fullLayout: gd._fullLayout
};
if(trace._group) {
evtData.group = trace._group;
}
if(Registry.traceIs(trace, 'pie-like')) {
evtData.label = legendItem.datum()[0].label;
}
var clickVal = Events.triggerHandler(gd, 'plotly_legendclick', evtData);
if(clickVal === false) return;
if(numClicks === 1) {
legend._clickTimeout = setTimeout(function() {
handleClick(legendItem, gd, numClicks);
}, gd._context.doubleClickDelay);
} else if(numClicks === 2) {
if(legend._clickTimeout) clearTimeout(legend._clickTimeout);
gd._legendMouseDownTime = 0;
var dblClickVal = Events.triggerHandler(gd, 'plotly_legenddoubleclick', evtData);
if(dblClickVal !== false) handleClick(legendItem, gd, numClicks);
}
}
function drawTexts(g, gd, opts) {
var legendItem = g.data()[0][0];
var trace = legendItem.trace;
var isPieLike = Registry.traceIs(trace, 'pie-like');
var traceIndex = trace.index;
var isEditable = opts._main && gd._context.edits.legendText && !isPieLike;
var maxNameLength = opts._maxNameLength;
var name;
if(!opts.entries) {
name = isPieLike ? legendItem.label : trace.name;
if(trace._meta) {
name = Lib.templateString(name, trace._meta);
}
} else {
name = legendItem.text;
}
var textEl = Lib.ensureSingle(g, 'text', 'legendtext');
textEl.attr('text-anchor', 'start')
.call(Drawing.font, opts.font)
.text(isEditable ? ensureLength(name, maxNameLength) : name);
var textGap = opts.itemwidth + constants.itemGap * 2;
svgTextUtils.positionText(textEl, textGap, 0);
if(isEditable) {
textEl.call(svgTextUtils.makeEditable, {gd: gd, text: name})
.call(textLayout, g, gd, opts)
.on('edit', function(newName) {
this.text(ensureLength(newName, maxNameLength))
.call(textLayout, g, gd, opts);
var fullInput = legendItem.trace._fullInput || {};
var update = {};
if(Registry.hasTransform(fullInput, 'groupby')) {
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
var index = groupbyIndices[groupbyIndices.length - 1];
var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');
kcont.set(legendItem.trace._group, newName);
update = kcont.constructUpdate();
} else {
update.name = newName;
}
return Registry.call('_guiRestyle', gd, update, traceIndex);
});
} else {
textLayout(textEl, g, gd, opts);
}
}
/*
* Make sure we have a reasonably clickable region.
* If this string is missing or very short, pad it with spaces out to at least
* 4 characters, up to the max length of other labels, on the assumption that
* most characters are wider than spaces so a string of spaces will usually be
* no wider than the real labels.
*/
function ensureLength(str, maxLength) {
var targetLength = Math.max(4, maxLength);
if(str && str.trim().length >= targetLength / 2) return str;
str = str || '';
for(var i = targetLength - str.length; i > 0; i--) str += ' ';
return str;
}
function setupTraceToggle(g, gd) {
var doubleClickDelay = gd._context.doubleClickDelay;
var newMouseDownTime;
var numClicks = 1;
var traceToggle = Lib.ensureSingle(g, 'rect', 'legendtoggle', function(s) {
if(!gd._context.staticPlot) {
s.style('cursor', 'pointer').attr('pointer-events', 'all');
}
s.call(Color.fill, 'rgba(0,0,0,0)');
});
if(gd._context.staticPlot) return;
traceToggle.on('mousedown', function() {
newMouseDownTime = (new Date()).getTime();
if(newMouseDownTime - gd._legendMouseDownTime < doubleClickDelay) {
// in a click train
numClicks += 1;
} else {
// new click train
numClicks = 1;
gd._legendMouseDownTime = newMouseDownTime;
}
});
traceToggle.on('mouseup', function() {
if(gd._dragged || gd._editing) return;
var legend = gd._fullLayout.legend;
if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) {
numClicks = Math.max(numClicks - 1, 1);
}
clickOrDoubleClick(gd, legend, g, numClicks, d3.event);
});
}
function textLayout(s, g, gd, opts) {
if(!opts._main) s.attr('data-notex', true); // do not process MathJax if not main
svgTextUtils.convertToTspans(s, gd, function() {
computeTextDimensions(g, gd, opts);
});
}
function computeTextDimensions(g, gd, opts) {
var legendItem = g.data()[0][0];
if(opts._main && legendItem && !legendItem.trace.showlegend) {
g.remove();
return;
}
var mathjaxGroup = g.select('g[class*=math-group]');
var mathjaxNode = mathjaxGroup.node();
if(!opts) opts = gd._fullLayout.legend;
var bw = opts.borderwidth;
var lineHeight = (legendItem ? opts : opts.title).font.size * LINE_SPACING;
var height, width;
if(mathjaxNode) {
var mathjaxBB = Drawing.bBox(mathjaxNode);
height = mathjaxBB.height;
width = mathjaxBB.width;
if(legendItem) {
Drawing.setTranslate(mathjaxGroup, 0, height * 0.25);
} else { // case of title
Drawing.setTranslate(mathjaxGroup, bw, height * 0.75 + bw);
}
} else {
var textEl = g.select(legendItem ?
'.legendtext' : '.legendtitletext'
);
var textLines = svgTextUtils.lineCount(textEl);
var textNode = textEl.node();
height = lineHeight * textLines;
width = textNode ? Drawing.bBox(textNode).width : 0;
// approximation to height offset to center the font
// to avoid getBoundingClientRect
var textY = lineHeight * ((textLines - 1) / 2 - 0.3);
if(legendItem) {
var textGap = opts.itemwidth + constants.itemGap * 2;
svgTextUtils.positionText(textEl, textGap, -textY);
} else { // case of title
svgTextUtils.positionText(textEl, constants.titlePad + bw, lineHeight + bw);
}
}
if(legendItem) {
legendItem.lineHeight = lineHeight;
legendItem.height = Math.max(height, 16) + 3;
legendItem.width = width;
} else { // case of title
opts._titleWidth = width;
opts._titleHeight = height;
}
}
function getTitleSize(opts) {
var w = 0;
var h = 0;
var side = opts.title.side;
if(side) {
if(side.indexOf('left') !== -1) {
w = opts._titleWidth;
}
if(side.indexOf('top') !== -1) {
h = opts._titleHeight;
}
}
return [w, h];
}
/*
* Computes in fullLayout.legend:
*
* - _height: legend height including items past scrollbox height
* - _maxHeight: maximum legend height before scrollbox is required
* - _effHeight: legend height w/ or w/o scrollbox
*
* - _width: legend width
* - _maxWidth (for orientation:h only): maximum width before starting new row
*/
function computeLegendDimensions(gd, groups, traces, opts) {
var fullLayout = gd._fullLayout;
if(!opts) opts = fullLayout.legend;
var gs = fullLayout._size;
var isVertical = helpers.isVertical(opts);
var isGrouped = helpers.isGrouped(opts);
var bw = opts.borderwidth;
var bw2 = 2 * bw;
var itemGap = constants.itemGap;
var textGap = opts.itemwidth + itemGap * 2;
var endPad = 2 * (bw + itemGap);
var yanchor = getYanchor(opts);
var isBelowPlotArea = opts.y < 0 || (opts.y === 0 && yanchor === 'top');
var isAbovePlotArea = opts.y > 1 || (opts.y === 1 && yanchor === 'bottom');
// - if below/above plot area, give it the maximum potential margin-push value
// - otherwise, extend the height of the plot area
opts._maxHeight = Math.max(
(isBelowPlotArea || isAbovePlotArea) ? fullLayout.height / 2 : gs.h,
30
);
var toggleRectWidth = 0;
opts._width = 0;
opts._height = 0;
var titleSize = getTitleSize(opts);
if(isVertical) {
traces.each(function(d) {
var h = d[0].height;
Drawing.setTranslate(this,
bw + titleSize[0],
bw + titleSize[1] + opts._height + h / 2 + itemGap
);
opts._height += h;
opts._width = Math.max(opts._width, d[0].width);
});
toggleRectWidth = textGap + opts._width;
opts._width += itemGap + textGap + bw2;
opts._height += endPad;
if(isGrouped) {
groups.each(function(d, i) {
Drawing.setTranslate(this, 0, i * opts.tracegroupgap);
});
opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
}
} else {
var xanchor = getXanchor(opts);
var isLeftOfPlotArea = opts.x < 0 || (opts.x === 0 && xanchor === 'right');
var isRightOfPlotArea = opts.x > 1 || (opts.x === 1 && xanchor === 'left');
var isBeyondPlotAreaY = isAbovePlotArea || isBelowPlotArea;
var hw = fullLayout.width / 2;
// - if placed within x-margins, extend the width of the plot area
// - else if below/above plot area and anchored in the margin, extend to opposite margin,
// - otherwise give it the maximum potential margin-push value
opts._maxWidth = Math.max(
isLeftOfPlotArea ? ((isBeyondPlotAreaY && xanchor === 'left') ? gs.l + gs.w : hw) :
isRightOfPlotArea ? ((isBeyondPlotAreaY && xanchor === 'right') ? gs.r + gs.w : hw) :
gs.w,
2 * textGap);
var maxItemWidth = 0;
var combinedItemWidth = 0;
traces.each(function(d) {
var w = d[0].width + textGap;
maxItemWidth = Math.max(maxItemWidth, w);
combinedItemWidth += w;
});
toggleRectWidth = null;
var maxRowWidth = 0;
if(isGrouped) {
var maxGroupHeightInRow = 0;
var groupOffsetX = 0;
var groupOffsetY = 0;
groups.each(function() {
var maxWidthInGroup = 0;
var offsetY = 0;
d3.select(this).selectAll('g.traces').each(function(d) {
var h = d[0].height;
Drawing.setTranslate(this,
titleSize[0],
titleSize[1] + bw + itemGap + h / 2 + offsetY
);
offsetY += h;
maxWidthInGroup = Math.max(maxWidthInGroup, textGap + d[0].width);
});
maxGroupHeightInRow = Math.max(maxGroupHeightInRow, offsetY);
var next = maxWidthInGroup + itemGap;
if((next + bw + groupOffsetX) > opts._maxWidth) {
maxRowWidth = Math.max(maxRowWidth, groupOffsetX);
groupOffsetX = 0;
groupOffsetY += maxGroupHeightInRow + opts.tracegroupgap;
maxGroupHeightInRow = offsetY;
}
Drawing.setTranslate(this, groupOffsetX, groupOffsetY);
groupOffsetX += next;
});
opts._width = Math.max(maxRowWidth, groupOffsetX) + bw;
opts._height = groupOffsetY + maxGroupHeightInRow + endPad;
} else {
var nTraces = traces.size();
var oneRowLegend = (combinedItemWidth + bw2 + (nTraces - 1) * itemGap) < opts._maxWidth;
var maxItemHeightInRow = 0;
var offsetX = 0;
var offsetY = 0;
var rowWidth = 0;
traces.each(function(d) {
var h = d[0].height;
var w = textGap + d[0].width;
var next = (oneRowLegend ? w : maxItemWidth) + itemGap;
if((next + bw + offsetX - itemGap) >= opts._maxWidth) {
maxRowWidth = Math.max(maxRowWidth, rowWidth);
offsetX = 0;
offsetY += maxItemHeightInRow;
opts._height += maxItemHeightInRow;
maxItemHeightInRow = 0;
}
Drawing.setTranslate(this,
titleSize[0] + bw + offsetX,
titleSize[1] + bw + offsetY + h / 2 + itemGap
);
rowWidth = offsetX + w + itemGap;
offsetX += next;
maxItemHeightInRow = Math.max(maxItemHeightInRow, h);
});
if(oneRowLegend) {
opts._width = offsetX + bw2;
opts._height = maxItemHeightInRow + endPad;
} else {
opts._width = Math.max(maxRowWidth, rowWidth) + bw2;
opts._height += maxItemHeightInRow + endPad;
}
}
}
opts._width = Math.ceil(
Math.max(
opts._width + titleSize[0],
opts._titleWidth + 2 * (bw + constants.titlePad)
)
);
opts._height = Math.ceil(
Math.max(
opts._height + titleSize[1],
opts._titleHeight + 2 * (bw + constants.itemGap)
)
);
opts._effHeight = Math.min(opts._height, opts._maxHeight);
var edits = gd._context.edits;
var isEditable = edits.legendText || edits.legendPosition;
traces.each(function(d) {
var traceToggle = d3.select(this).select('.legendtoggle');
var h = d[0].height;
var w = isEditable ? textGap : (toggleRectWidth || (textGap + d[0].width));
if(!isVertical) w += itemGap / 2;
Drawing.setRect(traceToggle, 0, -h / 2, w, h);
});
}
function expandMargin(gd) {
var fullLayout = gd._fullLayout;
var opts = fullLayout.legend;
var xanchor = getXanchor(opts);
var yanchor = getYanchor(opts);
return Plots.autoMargin(gd, 'legend', {
x: opts.x,
y: opts.y,
l: opts._width * (FROM_TL[xanchor]),
r: opts._width * (FROM_BR[xanchor]),
b: opts._effHeight * (FROM_BR[yanchor]),
t: opts._effHeight * (FROM_TL[yanchor])
});
}
function getXanchor(opts) {
return Lib.isRightAnchor(opts) ? 'right' :
Lib.isCenterAnchor(opts) ? 'center' :
'left';
}
function getYanchor(opts) {
return Lib.isBottomAnchor(opts) ? 'bottom' :
Lib.isMiddleAnchor(opts) ? 'middle' :
'top';
}