plotly.js
Version:
The open source javascript graphing library that powers plotly
1,025 lines (876 loc) • 36.6 kB
JavaScript
'use strict';
var d3 = require('@plotly/d3');
var tinycolor = require('tinycolor2');
var Plots = require('../../plots/plots');
var Registry = require('../../registry');
var Axes = require('../../plots/cartesian/axes');
var dragElement = require('../dragelement');
var Lib = require('../../lib');
var strTranslate = Lib.strTranslate;
var extendFlat = require('../../lib/extend').extendFlat;
var setCursor = require('../../lib/setcursor');
var Drawing = require('../drawing');
var Color = require('../color');
var Titles = require('../titles');
var svgTextUtils = require('../../lib/svg_text_utils');
var flipScale = require('../colorscale/helpers').flipScale;
var handleAxisDefaults = require('../../plots/cartesian/axis_defaults');
var handleAxisPositionDefaults = require('../../plots/cartesian/position_defaults');
var axisLayoutAttrs = require('../../plots/cartesian/layout_attributes');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;
var FROM_TL = alignmentConstants.FROM_TL;
var FROM_BR = alignmentConstants.FROM_BR;
var cn = require('./constants').cn;
function draw(gd) {
var fullLayout = gd._fullLayout;
var colorBars = fullLayout._infolayer
.selectAll('g.' + cn.colorbar)
.data(makeColorBarData(gd), function(opts) { return opts._id; });
colorBars.enter().append('g')
.attr('class', function(opts) { return opts._id; })
.classed(cn.colorbar, true);
colorBars.each(function(opts) {
var g = d3.select(this);
Lib.ensureSingle(g, 'rect', cn.cbbg);
Lib.ensureSingle(g, 'g', cn.cbfills);
Lib.ensureSingle(g, 'g', cn.cblines);
Lib.ensureSingle(g, 'g', cn.cbaxis, function(s) { s.classed(cn.crisp, true); });
Lib.ensureSingle(g, 'g', cn.cbtitleunshift, function(s) { s.append('g').classed(cn.cbtitle, true); });
Lib.ensureSingle(g, 'rect', cn.cboutline);
var done = drawColorBar(g, opts, gd);
if(done && done.then) (gd._promises || []).push(done);
if(gd._context.edits.colorbarPosition) {
makeEditable(g, opts, gd);
}
});
colorBars.exit()
.each(function(opts) { Plots.autoMargin(gd, opts._id); })
.remove();
colorBars.order();
}
function makeColorBarData(gd) {
var fullLayout = gd._fullLayout;
var calcdata = gd.calcdata;
var out = [];
// single out item
var opts;
// colorbar attr parent container
var cont;
// trace attr container
var trace;
// colorbar options
var cbOpt;
function initOpts(opts) {
return extendFlat(opts, {
// fillcolor can be a d3 scale, domain is z values, range is colors
// or leave it out for no fill,
// or set to a string constant for single-color fill
_fillcolor: null,
// line.color has the same options as fillcolor
_line: {color: null, width: null, dash: null},
// levels of lines to draw.
// note that this DOES NOT determine the extent of the bar
// that's given by the domain of fillcolor
// (or line.color if no fillcolor domain)
_levels: {start: null, end: null, size: null},
// separate fill levels (for example, heatmap coloring of a
// contour map) if this is omitted, fillcolors will be
// evaluated halfway between levels
_filllevels: null,
// for continuous colorscales: fill with a gradient instead of explicit levels
// value should be the colorscale [[0, c0], [v1, c1], ..., [1, cEnd]]
_fillgradient: null,
// when using a gradient, we need the data range specified separately
_zrange: null
});
}
function calcOpts() {
if(typeof cbOpt.calc === 'function') {
cbOpt.calc(gd, trace, opts);
} else {
opts._fillgradient = cont.reversescale ?
flipScale(cont.colorscale) :
cont.colorscale;
opts._zrange = [cont[cbOpt.min], cont[cbOpt.max]];
}
}
for(var i = 0; i < calcdata.length; i++) {
var cd = calcdata[i];
trace = cd[0].trace;
if(!trace._module) continue;
var moduleOpts = trace._module.colorbar;
if(trace.visible === true && moduleOpts) {
var allowsMultiplotCbs = Array.isArray(moduleOpts);
var cbOpts = allowsMultiplotCbs ? moduleOpts : [moduleOpts];
for(var j = 0; j < cbOpts.length; j++) {
cbOpt = cbOpts[j];
var contName = cbOpt.container;
cont = contName ? trace[contName] : trace;
if(cont && cont.showscale) {
opts = initOpts(cont.colorbar);
opts._id = 'cb' + trace.uid + (allowsMultiplotCbs && contName ? '-' + contName : '');
opts._traceIndex = trace.index;
opts._propPrefix = (contName ? contName + '.' : '') + 'colorbar.';
opts._meta = trace._meta;
calcOpts();
out.push(opts);
}
}
}
}
for(var k in fullLayout._colorAxes) {
cont = fullLayout[k];
if(cont.showscale) {
var colorAxOpts = fullLayout._colorAxes[k];
opts = initOpts(cont.colorbar);
opts._id = 'cb' + k;
opts._propPrefix = k + '.colorbar.';
opts._meta = fullLayout._meta;
cbOpt = {min: 'cmin', max: 'cmax'};
if(colorAxOpts[0] !== 'heatmap') {
trace = colorAxOpts[1];
cbOpt.calc = trace._module.colorbar.calc;
}
calcOpts();
out.push(opts);
}
}
return out;
}
function drawColorBar(g, opts, gd) {
var isVertical = opts.orientation === 'v';
var len = opts.len;
var lenmode = opts.lenmode;
var thickness = opts.thickness;
var thicknessmode = opts.thicknessmode;
var outlinewidth = opts.outlinewidth;
var borderwidth = opts.borderwidth;
var bgcolor = opts.bgcolor;
var xanchor = opts.xanchor;
var yanchor = opts.yanchor;
var xpad = opts.xpad;
var ypad = opts.ypad;
var optsX = opts.x;
var optsY = isVertical ? opts.y : 1 - opts.y;
var isPaperY = opts.yref === 'paper';
var isPaperX = opts.xref === 'paper';
var fullLayout = gd._fullLayout;
var gs = fullLayout._size;
var fillColor = opts._fillcolor;
var line = opts._line;
var title = opts.title;
var titleSide = title.side;
var zrange = opts._zrange ||
d3.extent((typeof fillColor === 'function' ? fillColor : line.color).domain());
var lineColormap = typeof line.color === 'function' ?
line.color :
function() { return line.color; };
var fillColormap = typeof fillColor === 'function' ?
fillColor :
function() { return fillColor; };
var levelsIn = opts._levels;
var levelsOut = calcLevels(gd, opts, zrange);
var fillLevels = levelsOut.fill;
var lineLevels = levelsOut.line;
// we calculate pixel sizes based on the specified graph size,
// not the actual (in case something pushed the margins around)
// which is a little odd but avoids an odd iterative effect
// when the colorbar itself is pushing the margins.
// but then the fractional size is calculated based on the
// actual graph size, so that the axes will size correctly.
var thickPx = Math.round(thickness * (thicknessmode === 'fraction' ? (isVertical ? gs.w : gs.h) : 1));
var thickFrac = thickPx / (isVertical ? gs.w : gs.h);
var lenPx = Math.round(len * (lenmode === 'fraction' ? (isVertical ? gs.h : gs.w) : 1));
var lenFrac = lenPx / (isVertical ? gs.h : gs.w);
var posW = isPaperX ? gs.w : gd._fullLayout.width;
var posH = isPaperY ? gs.h : gd._fullLayout.height;
// x positioning: do it initially just for left anchor,
// then fix at the end (since we don't know the width yet)
var uPx = Math.round(isVertical ?
optsX * posW + xpad :
optsY * posH + ypad
);
var xRatio = {center: 0.5, right: 1}[xanchor] || 0;
var yRatio = {top: 1, middle: 0.5}[yanchor] || 0;
// for dragging... this is getting a little muddled...
var uFrac = isVertical ?
optsX - xRatio * thickFrac :
optsY - yRatio * thickFrac;
// y/x positioning (for v/h) we can do correctly from the start
var vFrac = isVertical ?
optsY - yRatio * lenFrac :
optsX - xRatio * lenFrac;
var vPx = Math.round(isVertical ?
posH * (1 - vFrac) :
posW * vFrac
);
// stash a few things for makeEditable
opts._lenFrac = lenFrac;
opts._thickFrac = thickFrac;
opts._uFrac = uFrac;
opts._vFrac = vFrac;
// stash mocked axis for contour label formatting
var ax = opts._axis = mockColorBarAxis(gd, opts, zrange);
// position can't go in through supplyDefaults
// because that restricts it to [0,1]
ax.position = thickFrac + (isVertical ?
optsX + xpad / gs.w :
optsY + ypad / gs.h
);
var topOrBottom = ['top', 'bottom'].indexOf(titleSide) !== -1;
if(isVertical && topOrBottom) {
ax.title.side = titleSide;
ax.titlex = optsX + xpad / gs.w;
ax.titley = vFrac + (title.side === 'top' ? lenFrac - ypad / gs.h : ypad / gs.h);
}
if(!isVertical && !topOrBottom) {
ax.title.side = titleSide;
ax.titley = optsY + ypad / gs.h;
ax.titlex = vFrac + xpad / gs.w; // right side
}
if(line.color && opts.tickmode === 'auto') {
ax.tickmode = 'linear';
ax.tick0 = levelsIn.start;
var dtick = levelsIn.size;
// expand if too many contours, so we don't get too many ticks
var autoNtick = Lib.constrain(lenPx / 50, 4, 15) + 1;
var dtFactor = (zrange[1] - zrange[0]) / ((opts.nticks || autoNtick) * dtick);
if(dtFactor > 1) {
var dtexp = Math.pow(10, Math.floor(Math.log(dtFactor) / Math.LN10));
dtick *= dtexp * Lib.roundUp(dtFactor / dtexp, [2, 5, 10]);
// if the contours are at round multiples, reset tick0
// so they're still at round multiples. Otherwise,
// keep the first label on the first contour level
if((Math.abs(levelsIn.start) / levelsIn.size + 1e-6) % 1 < 2e-6) {
ax.tick0 = 0;
}
}
ax.dtick = dtick;
}
// set domain after init, because we may want to
// allow it outside [0,1]
ax.domain = isVertical ? [
vFrac + ypad / gs.h,
vFrac + lenFrac - ypad / gs.h
] : [
vFrac + xpad / gs.w,
vFrac + lenFrac - xpad / gs.w
];
ax.setScale();
g.attr('transform', strTranslate(Math.round(gs.l), Math.round(gs.t)));
var titleCont = g.select('.' + cn.cbtitleunshift)
.attr('transform', strTranslate(-Math.round(gs.l), -Math.round(gs.t)));
var ticklabelposition = ax.ticklabelposition;
var titleFontSize = ax.title.font.size;
var axLayer = g.select('.' + cn.cbaxis);
var titleEl;
var titleHeight = 0;
var titleWidth = 0;
function drawTitle(titleClass, titleOpts) {
var dfltTitleOpts = {
propContainer: ax,
propName: opts._propPrefix + 'title',
traceIndex: opts._traceIndex,
_meta: opts._meta,
placeholder: fullLayout._dfltTitle.colorbar,
containerGroup: g.select('.' + cn.cbtitle)
};
// this class-to-rotate thing with convertToTspans is
// getting hackier and hackier... delete groups with the
// wrong class (in case earlier the colorbar was drawn on
// a different side, I think?)
var otherClass = titleClass.charAt(0) === 'h' ?
titleClass.substr(1) :
'h' + titleClass;
g.selectAll('.' + otherClass + ',.' + otherClass + '-math-group').remove();
Titles.draw(gd, titleClass, extendFlat(dfltTitleOpts, titleOpts || {}));
}
function drawDummyTitle() {
// draw the title so we know how much room it needs
// when we squish the axis.
// On vertical colorbars this only applies to top or bottom titles, not right side.
// On horizontal colorbars this only applies to right, etc.
if(
(isVertical && topOrBottom) ||
(!isVertical && !topOrBottom)
) {
var x, y;
if(titleSide === 'top') {
x = xpad + gs.l + posW * optsX;
y = ypad + gs.t + posH * (1 - vFrac - lenFrac) + 3 + titleFontSize * 0.75;
}
if(titleSide === 'bottom') {
x = xpad + gs.l + posW * optsX;
y = ypad + gs.t + posH * (1 - vFrac) - 3 - titleFontSize * 0.25;
}
if(titleSide === 'right') {
y = ypad + gs.t + posH * optsY + 3 + titleFontSize * 0.75;
x = xpad + gs.l + posW * vFrac;
}
drawTitle(ax._id + 'title', {
attributes: {x: x, y: y, 'text-anchor': isVertical ? 'start' : 'middle'}
});
}
}
function drawCbTitle() {
if(
(isVertical && !topOrBottom) ||
(!isVertical && topOrBottom)
) {
var pos = ax.position || 0;
var mid = ax._offset + ax._length / 2;
var x, y;
if(titleSide === 'right') {
y = mid;
x = gs.l + posW * pos + 10 + titleFontSize * (
ax.showticklabels ? 1 : 0.5
);
} else {
x = mid;
if(titleSide === 'bottom') {
y = gs.t + posH * pos + 10 + (
ticklabelposition.indexOf('inside') === -1 ?
ax.tickfont.size :
0
) + (
ax.ticks !== 'intside' ?
opts.ticklen || 0 :
0
);
}
if(titleSide === 'top') {
var nlines = title.text.split('<br>').length;
y = gs.t + posH * pos + 10 - thickPx - LINE_SPACING * titleFontSize * nlines;
}
}
drawTitle((isVertical ?
// the 'h' + is a hack to get around the fact that
// convertToTspans rotates any 'y...' class by 90 degrees.
// TODO: find a better way to control this.
'h' :
'v'
) + ax._id + 'title', {
avoid: {
selection: d3.select(gd).selectAll('g.' + ax._id + 'tick'),
side: titleSide,
offsetTop: isVertical ? 0 : gs.t,
offsetLeft: isVertical ? gs.l : 0,
maxShift: isVertical ? fullLayout.width : fullLayout.height
},
attributes: {x: x, y: y, 'text-anchor': 'middle'},
transform: {rotate: isVertical ? -90 : 0, offset: 0}
});
}
}
function drawAxis() {
if(
(!isVertical && !topOrBottom) ||
(isVertical && topOrBottom)
) {
// squish the axis top to make room for the title
var titleGroup = g.select('.' + cn.cbtitle);
var titleText = titleGroup.select('text');
var titleTrans = [-outlinewidth / 2, outlinewidth / 2];
var mathJaxNode = titleGroup
.select('.h' + ax._id + 'title-math-group')
.node();
var lineSize = 15.6;
if(titleText.node()) {
lineSize = parseInt(titleText.node().style.fontSize, 10) * LINE_SPACING;
}
var bb;
if(mathJaxNode) {
bb = Drawing.bBox(mathJaxNode);
titleWidth = bb.width;
titleHeight = bb.height;
if(titleHeight > lineSize) {
// not entirely sure how mathjax is doing
// vertical alignment, but this seems to work.
titleTrans[1] -= (titleHeight - lineSize) / 2;
}
} else if(titleText.node() && !titleText.classed(cn.jsPlaceholder)) {
bb = Drawing.bBox(titleText.node());
titleWidth = bb.width;
titleHeight = bb.height;
}
if(isVertical) {
if(titleHeight) {
// buffer btwn colorbar and title
// TODO: configurable
titleHeight += 5;
if(titleSide === 'top') {
ax.domain[1] -= titleHeight / gs.h;
titleTrans[1] *= -1;
} else {
ax.domain[0] += titleHeight / gs.h;
var nlines = svgTextUtils.lineCount(titleText);
titleTrans[1] += (1 - nlines) * lineSize;
}
titleGroup.attr('transform', strTranslate(titleTrans[0], titleTrans[1]));
ax.setScale();
}
} else { // horizontal colorbars
if(titleWidth) {
if(titleSide === 'right') {
ax.domain[0] += (titleWidth + titleFontSize / 2) / gs.w;
}
titleGroup.attr('transform', strTranslate(titleTrans[0], titleTrans[1]));
ax.setScale();
}
}
}
g.selectAll('.' + cn.cbfills + ',.' + cn.cblines)
.attr('transform', isVertical ?
strTranslate(0, Math.round(gs.h * (1 - ax.domain[1]))) :
strTranslate(Math.round(gs.w * ax.domain[0]), 0)
);
axLayer.attr('transform', isVertical ?
strTranslate(0, Math.round(-gs.t)) :
strTranslate(Math.round(-gs.l), 0)
);
var fills = g.select('.' + cn.cbfills)
.selectAll('rect.' + cn.cbfill)
.attr('style', '')
.data(fillLevels);
fills.enter().append('rect')
.classed(cn.cbfill, true)
.attr('style', '');
fills.exit().remove();
var zBounds = zrange
.map(ax.c2p)
.map(Math.round)
.sort(function(a, b) { return a - b; });
fills.each(function(d, i) {
var z = [
(i === 0) ? zrange[0] : (fillLevels[i] + fillLevels[i - 1]) / 2,
(i === fillLevels.length - 1) ? zrange[1] : (fillLevels[i] + fillLevels[i + 1]) / 2
]
.map(ax.c2p)
.map(Math.round);
// offset the side adjoining the next rectangle so they
// overlap, to prevent antialiasing gaps
if(isVertical) {
z[1] = Lib.constrain(z[1] + (z[1] > z[0]) ? 1 : -1, zBounds[0], zBounds[1]);
} /* else {
// TODO: horizontal case
} */
// Colorbar cannot currently support opacities so we
// use an opaque fill even when alpha channels present
var fillEl = d3.select(this)
.attr(isVertical ? 'x' : 'y', uPx)
.attr(isVertical ? 'y' : 'x', d3.min(z))
.attr(isVertical ? 'width' : 'height', Math.max(thickPx, 2))
.attr(isVertical ? 'height' : 'width', Math.max(d3.max(z) - d3.min(z), 2));
if(opts._fillgradient) {
Drawing.gradient(fillEl, gd, opts._id, isVertical ? 'vertical' : 'horizontalreversed', opts._fillgradient, 'fill');
} else {
// tinycolor can't handle exponents and
// at this scale, removing it makes no difference.
var colorString = fillColormap(d).replace('e-', '');
fillEl.attr('fill', tinycolor(colorString).toHexString());
}
});
var lines = g.select('.' + cn.cblines)
.selectAll('path.' + cn.cbline)
.data(line.color && line.width ? lineLevels : []);
lines.enter().append('path')
.classed(cn.cbline, true);
lines.exit().remove();
lines.each(function(d) {
var a = uPx;
var b = (Math.round(ax.c2p(d)) + (line.width / 2) % 1);
d3.select(this)
.attr('d', 'M' +
(isVertical ? a + ',' + b : b + ',' + a) +
(isVertical ? 'h' : 'v') +
thickPx
)
.call(Drawing.lineGroupStyle, line.width, lineColormap(d), line.dash);
});
// force full redraw of labels and ticks
axLayer.selectAll('g.' + ax._id + 'tick,path').remove();
var shift = uPx + thickPx +
(outlinewidth || 0) / 2 - (opts.ticks === 'outside' ? 1 : 0);
var vals = Axes.calcTicks(ax);
var tickSign = Axes.getTickSigns(ax)[2];
Axes.drawTicks(gd, ax, {
vals: ax.ticks === 'inside' ? Axes.clipEnds(ax, vals) : vals,
layer: axLayer,
path: Axes.makeTickPath(ax, shift, tickSign),
transFn: Axes.makeTransTickFn(ax)
});
return Axes.drawLabels(gd, ax, {
vals: vals,
layer: axLayer,
transFn: Axes.makeTransTickLabelFn(ax),
labelFns: Axes.makeLabelFns(ax, shift)
});
}
// wait for the axis & title to finish rendering before
// continuing positioning
// TODO: why are we redrawing multiple times now with this?
// I guess autoMargin doesn't like being post-promise?
function positionCB() {
var bb;
var innerThickness = thickPx + outlinewidth / 2;
if(ticklabelposition.indexOf('inside') === -1) {
bb = Drawing.bBox(axLayer.node());
innerThickness += isVertical ? bb.width : bb.height;
}
titleEl = titleCont.select('text');
var titleWidth = 0;
var topSideVertical = isVertical && titleSide === 'top';
var rightSideHorizontal = !isVertical && titleSide === 'right';
var moveY = 0;
if(titleEl.node() && !titleEl.classed(cn.jsPlaceholder)) {
var _titleHeight;
var mathJaxNode = titleCont.select('.h' + ax._id + 'title-math-group').node();
if(mathJaxNode && (
(isVertical && topOrBottom) ||
(!isVertical && !topOrBottom)
)) {
bb = Drawing.bBox(mathJaxNode);
titleWidth = bb.width;
_titleHeight = bb.height;
} else {
// note: the formula below works for all title sides,
// (except for top/bottom mathjax, above)
// but the weird gs.l is because the titleunshift
// transform gets removed by Drawing.bBox
bb = Drawing.bBox(titleCont.node());
titleWidth = bb.right - gs.l - (isVertical ? uPx : vPx);
_titleHeight = bb.bottom - gs.t - (isVertical ? vPx : uPx);
if(
!isVertical && titleSide === 'top'
) {
innerThickness += bb.height;
moveY = bb.height;
}
}
if(rightSideHorizontal) {
titleEl.attr('transform', strTranslate(titleWidth / 2 + titleFontSize / 2, 0));
titleWidth *= 2;
}
innerThickness = Math.max(innerThickness,
isVertical ? titleWidth : _titleHeight
);
}
var outerThickness = (isVertical ?
xpad :
ypad
) * 2 + innerThickness + borderwidth + outlinewidth / 2;
var hColorbarMoveTitle = 0;
if(!isVertical && title.text && yanchor === 'bottom' && optsY <= 0) {
hColorbarMoveTitle = outerThickness / 2;
outerThickness += hColorbarMoveTitle;
moveY += hColorbarMoveTitle;
}
fullLayout._hColorbarMoveTitle = hColorbarMoveTitle;
fullLayout._hColorbarMoveCBTitle = moveY;
var extraW = borderwidth + outlinewidth;
// TODO - are these the correct positions?
var lx = (isVertical ? uPx : vPx) - extraW / 2 - (isVertical ? xpad : 0);
var ly = (isVertical ? vPx : uPx) - (isVertical ? lenPx : ypad + moveY - hColorbarMoveTitle);
g.select('.' + cn.cbbg)
.attr('x', lx)
.attr('y', ly)
.attr(isVertical ? 'width' : 'height', Math.max(outerThickness - hColorbarMoveTitle, 2))
.attr(isVertical ? 'height' : 'width', Math.max(lenPx + extraW, 2))
.call(Color.fill, bgcolor)
.call(Color.stroke, opts.bordercolor)
.style('stroke-width', borderwidth);
var moveX = rightSideHorizontal ? Math.max(titleWidth - 10, 0) : 0;
g.selectAll('.' + cn.cboutline)
.attr('x', (isVertical ? uPx : vPx + xpad) + moveX)
.attr('y', (isVertical ? vPx + ypad - lenPx : uPx) + (topSideVertical ? titleHeight : 0))
.attr(isVertical ? 'width' : 'height', Math.max(thickPx, 2))
.attr(isVertical ? 'height' : 'width', Math.max(lenPx - (isVertical ?
2 * ypad + titleHeight :
2 * xpad + moveX
), 2))
.call(Color.stroke, opts.outlinecolor)
.style({
fill: 'none',
'stroke-width': outlinewidth
});
var xShift = ((isVertical ? xRatio * outerThickness : 0));
var yShift = ((isVertical ? 0 : (1 - yRatio) * outerThickness - moveY));
xShift = isPaperX ? gs.l - xShift : -xShift;
yShift = isPaperY ? gs.t - yShift : -yShift;
g.attr('transform', strTranslate(
xShift,
yShift
));
if(!isVertical && (
borderwidth || (
tinycolor(bgcolor).getAlpha() &&
!tinycolor.equals(fullLayout.paper_bgcolor, bgcolor)
)
)) {
// for horizontal colorbars when there is a border line or having different background color
// hide/adjust x positioning for the first/last tick labels if they go outside the border
var tickLabels = axLayer.selectAll('text');
var numTicks = tickLabels[0].length;
var border = g.select('.' + cn.cbbg).node();
var oBb = Drawing.bBox(border);
var oTr = Drawing.getTranslate(g);
var TEXTPAD = 2;
tickLabels.each(function(d, i) {
var first = 0;
var last = numTicks - 1;
if(i === first || i === last) {
var iBb = Drawing.bBox(this);
var iTr = Drawing.getTranslate(this);
var deltaX;
if(i === last) {
var iRight = iBb.right + iTr.x;
var oRight = oBb.right + oTr.x + vPx - borderwidth - TEXTPAD + optsX;
deltaX = oRight - iRight;
if(deltaX > 0) deltaX = 0;
} else if(i === first) {
var iLeft = iBb.left + iTr.x;
var oLeft = oBb.left + oTr.x + vPx + borderwidth + TEXTPAD;
deltaX = oLeft - iLeft;
if(deltaX < 0) deltaX = 0;
}
if(deltaX) {
if(numTicks < 3) { // adjust position
this.setAttribute('transform',
'translate(' + deltaX + ',0) ' +
this.getAttribute('transform')
);
} else { // hide
this.setAttribute('visibility', 'hidden');
}
}
}
});
}
// auto margin adjustment
var marginOpts = {};
var lFrac = FROM_TL[xanchor];
var rFrac = FROM_BR[xanchor];
var tFrac = FROM_TL[yanchor];
var bFrac = FROM_BR[yanchor];
var extraThickness = outerThickness - thickPx;
if(isVertical) {
if(lenmode === 'pixels') {
marginOpts.y = optsY;
marginOpts.t = lenPx * tFrac;
marginOpts.b = lenPx * bFrac;
} else {
marginOpts.t = marginOpts.b = 0;
marginOpts.yt = optsY + len * tFrac;
marginOpts.yb = optsY - len * bFrac;
}
if(thicknessmode === 'pixels') {
marginOpts.x = optsX;
marginOpts.l = outerThickness * lFrac;
marginOpts.r = outerThickness * rFrac;
} else {
marginOpts.l = extraThickness * lFrac;
marginOpts.r = extraThickness * rFrac;
marginOpts.xl = optsX - thickness * lFrac;
marginOpts.xr = optsX + thickness * rFrac;
}
} else { // horizontal colorbars
if(lenmode === 'pixels') {
marginOpts.x = optsX;
marginOpts.l = lenPx * lFrac;
marginOpts.r = lenPx * rFrac;
} else {
marginOpts.l = marginOpts.r = 0;
marginOpts.xl = optsX + len * lFrac;
marginOpts.xr = optsX - len * rFrac;
}
if(thicknessmode === 'pixels') {
marginOpts.y = 1 - optsY;
marginOpts.t = outerThickness * tFrac;
marginOpts.b = outerThickness * bFrac;
} else {
marginOpts.t = extraThickness * tFrac;
marginOpts.b = extraThickness * bFrac;
marginOpts.yt = optsY - thickness * tFrac;
marginOpts.yb = optsY + thickness * bFrac;
}
}
var sideY = opts.y < 0.5 ? 'b' : 't';
var sideX = opts.x < 0.5 ? 'l' : 'r';
gd._fullLayout._reservedMargin[opts._id] = {};
var possibleReservedMargins = {
r: (fullLayout.width - lx - xShift),
l: lx + marginOpts.r,
b: (fullLayout.height - ly - yShift),
t: ly + marginOpts.b
};
if(isPaperX && isPaperY) {
Plots.autoMargin(gd, opts._id, marginOpts);
} else if(isPaperX) {
gd._fullLayout._reservedMargin[opts._id][sideY] = possibleReservedMargins[sideY];
} else if(isPaperY) {
gd._fullLayout._reservedMargin[opts._id][sideX] = possibleReservedMargins[sideX];
} else {
if(isVertical) {
gd._fullLayout._reservedMargin[opts._id][sideX] = possibleReservedMargins[sideX];
} else {
gd._fullLayout._reservedMargin[opts._id][sideY] = possibleReservedMargins[sideY];
}
}
}
return Lib.syncOrAsync([
Plots.previousPromises,
drawDummyTitle,
drawAxis,
drawCbTitle,
Plots.previousPromises,
positionCB
], gd);
}
function makeEditable(g, opts, gd) {
var isVertical = opts.orientation === 'v';
var fullLayout = gd._fullLayout;
var gs = fullLayout._size;
var t0, xf, yf;
dragElement.init({
element: g.node(),
gd: gd,
prepFn: function() {
t0 = g.attr('transform');
setCursor(g);
},
moveFn: function(dx, dy) {
g.attr('transform', t0 + strTranslate(dx, dy));
xf = dragElement.align(
(isVertical ? opts._uFrac : opts._vFrac) + (dx / gs.w),
isVertical ? opts._thickFrac : opts._lenFrac,
0, 1, opts.xanchor);
yf = dragElement.align(
(isVertical ? opts._vFrac : (1 - opts._uFrac)) - (dy / gs.h),
isVertical ? opts._lenFrac : opts._thickFrac,
0, 1, opts.yanchor);
var csr = dragElement.getCursor(xf, yf, opts.xanchor, opts.yanchor);
setCursor(g, csr);
},
doneFn: function() {
setCursor(g);
if(xf !== undefined && yf !== undefined) {
var update = {};
update[opts._propPrefix + 'x'] = xf;
update[opts._propPrefix + 'y'] = yf;
if(opts._traceIndex !== undefined) {
Registry.call('_guiRestyle', gd, update, opts._traceIndex);
} else {
Registry.call('_guiRelayout', gd, update);
}
}
}
});
}
function calcLevels(gd, opts, zrange) {
var levelsIn = opts._levels;
var lineLevels = [];
var fillLevels = [];
var l;
var i;
var l0 = levelsIn.end + levelsIn.size / 100;
var ls = levelsIn.size;
var zr0 = (1.001 * zrange[0] - 0.001 * zrange[1]);
var zr1 = (1.001 * zrange[1] - 0.001 * zrange[0]);
for(i = 0; i < 1e5; i++) {
l = levelsIn.start + i * ls;
if(ls > 0 ? (l >= l0) : (l <= l0)) break;
if(l > zr0 && l < zr1) lineLevels.push(l);
}
if(opts._fillgradient) {
fillLevels = [0];
} else if(typeof opts._fillcolor === 'function') {
var fillLevelsIn = opts._filllevels;
if(fillLevelsIn) {
l0 = fillLevelsIn.end + fillLevelsIn.size / 100;
ls = fillLevelsIn.size;
for(i = 0; i < 1e5; i++) {
l = fillLevelsIn.start + i * ls;
if(ls > 0 ? (l >= l0) : (l <= l0)) break;
if(l > zrange[0] && l < zrange[1]) fillLevels.push(l);
}
} else {
fillLevels = lineLevels.map(function(v) {
return v - levelsIn.size / 2;
});
fillLevels.push(fillLevels[fillLevels.length - 1] + levelsIn.size);
}
} else if(opts._fillcolor && typeof opts._fillcolor === 'string') {
// doesn't matter what this value is, with a single value
// we'll make a single fill rect covering the whole bar
fillLevels = [0];
}
if(levelsIn.size < 0) {
lineLevels.reverse();
fillLevels.reverse();
}
return {line: lineLevels, fill: fillLevels};
}
function mockColorBarAxis(gd, opts, zrange) {
var fullLayout = gd._fullLayout;
var isVertical = opts.orientation === 'v';
var cbAxisIn = {
type: 'linear',
range: zrange,
tickmode: opts.tickmode,
nticks: opts.nticks,
tick0: opts.tick0,
dtick: opts.dtick,
tickvals: opts.tickvals,
ticktext: opts.ticktext,
ticks: opts.ticks,
ticklen: opts.ticklen,
tickwidth: opts.tickwidth,
tickcolor: opts.tickcolor,
showticklabels: opts.showticklabels,
labelalias: opts.labelalias,
ticklabelposition: opts.ticklabelposition,
ticklabeloverflow: opts.ticklabeloverflow,
ticklabelstep: opts.ticklabelstep,
tickfont: opts.tickfont,
tickangle: opts.tickangle,
tickformat: opts.tickformat,
exponentformat: opts.exponentformat,
minexponent: opts.minexponent,
separatethousands: opts.separatethousands,
showexponent: opts.showexponent,
showtickprefix: opts.showtickprefix,
tickprefix: opts.tickprefix,
showticksuffix: opts.showticksuffix,
ticksuffix: opts.ticksuffix,
title: opts.title,
showline: true,
anchor: 'free',
side: isVertical ? 'right' : 'bottom',
position: 1
};
var letter = isVertical ? 'y' : 'x';
var cbAxisOut = {
type: 'linear',
_id: letter + opts._id
};
var axisOptions = {
letter: letter,
font: fullLayout.font,
noAutotickangles: letter === 'y',
noHover: true,
noTickson: true,
noTicklabelmode: true,
noInsideRange: true,
calendar: fullLayout.calendar // not really necessary (yet?)
};
function coerce(attr, dflt) {
return Lib.coerce(cbAxisIn, cbAxisOut, axisLayoutAttrs, attr, dflt);
}
handleAxisDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions, fullLayout);
handleAxisPositionDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions);
return cbAxisOut;
}
module.exports = {
draw: draw
};