plotly.js
Version:
The open source javascript graphing library that powers plotly
780 lines (663 loc) • 26.8 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 tinycolor = require('tinycolor2');
var Registry = require('../../registry');
var Lib = require('../../lib');
var strTranslate = Lib.strTranslate;
var _ = Lib._;
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var setConvert = require('../cartesian/set_convert');
var extendFlat = require('../../lib/extend').extendFlat;
var Plots = require('../plots');
var Axes = require('../cartesian/axes');
var dragElement = require('../../components/dragelement');
var Fx = require('../../components/fx');
var dragHelpers = require('../../components/dragelement/helpers');
var freeMode = dragHelpers.freeMode;
var rectMode = dragHelpers.rectMode;
var Titles = require('../../components/titles');
var prepSelect = require('../cartesian/select').prepSelect;
var selectOnClick = require('../cartesian/select').selectOnClick;
var clearSelect = require('../cartesian/select').clearSelect;
var clearSelectionsCache = require('../cartesian/select').clearSelectionsCache;
var constants = require('../cartesian/constants');
function Ternary(options, fullLayout) {
this.id = options.id;
this.graphDiv = options.graphDiv;
this.init(fullLayout);
this.makeFramework(fullLayout);
// unfortunately, we have to keep track of some axis tick settings
// as ternary subplots do not implement the 'ticks' editType
this.aTickLayout = null;
this.bTickLayout = null;
this.cTickLayout = null;
}
module.exports = Ternary;
var proto = Ternary.prototype;
proto.init = function(fullLayout) {
this.container = fullLayout._ternarylayer;
this.defs = fullLayout._defs;
this.layoutId = fullLayout._uid;
this.traceHash = {};
this.layers = {};
};
proto.plot = function(ternaryCalcData, fullLayout) {
var _this = this;
var ternaryLayout = fullLayout[_this.id];
var graphSize = fullLayout._size;
_this._hasClipOnAxisFalse = false;
for(var i = 0; i < ternaryCalcData.length; i++) {
var trace = ternaryCalcData[i][0].trace;
if(trace.cliponaxis === false) {
_this._hasClipOnAxisFalse = true;
break;
}
}
_this.updateLayers(ternaryLayout);
_this.adjustLayout(ternaryLayout, graphSize);
Plots.generalUpdatePerTraceModule(_this.graphDiv, _this, ternaryCalcData, ternaryLayout);
_this.layers.plotbg.select('path').call(Color.fill, ternaryLayout.bgcolor);
};
proto.makeFramework = function(fullLayout) {
var _this = this;
var gd = _this.graphDiv;
var ternaryLayout = fullLayout[_this.id];
var clipId = _this.clipId = 'clip' + _this.layoutId + _this.id;
var clipIdRelative = _this.clipIdRelative = 'clip-relative' + _this.layoutId + _this.id;
// clippath for this ternary subplot
_this.clipDef = Lib.ensureSingleById(fullLayout._clips, 'clipPath', clipId, function(s) {
s.append('path').attr('d', 'M0,0Z');
});
// 'relative' clippath (i.e. no translation) for this ternary subplot
_this.clipDefRelative = Lib.ensureSingleById(fullLayout._clips, 'clipPath', clipIdRelative, function(s) {
s.append('path').attr('d', 'M0,0Z');
});
// container for everything in this ternary subplot
_this.plotContainer = Lib.ensureSingle(_this.container, 'g', _this.id);
_this.updateLayers(ternaryLayout);
Drawing.setClipUrl(_this.layers.backplot, clipId, gd);
Drawing.setClipUrl(_this.layers.grids, clipId, gd);
};
proto.updateLayers = function(ternaryLayout) {
var _this = this;
var layers = _this.layers;
// inside that container, we have one container for the data, and
// one each for the three axes around it.
var plotLayers = ['draglayer', 'plotbg', 'backplot', 'grids'];
if(ternaryLayout.aaxis.layer === 'below traces') {
plotLayers.push('aaxis', 'aline');
}
if(ternaryLayout.baxis.layer === 'below traces') {
plotLayers.push('baxis', 'bline');
}
if(ternaryLayout.caxis.layer === 'below traces') {
plotLayers.push('caxis', 'cline');
}
plotLayers.push('frontplot');
if(ternaryLayout.aaxis.layer === 'above traces') {
plotLayers.push('aaxis', 'aline');
}
if(ternaryLayout.baxis.layer === 'above traces') {
plotLayers.push('baxis', 'bline');
}
if(ternaryLayout.caxis.layer === 'above traces') {
plotLayers.push('caxis', 'cline');
}
var toplevel = _this.plotContainer.selectAll('g.toplevel')
.data(plotLayers, String);
var grids = ['agrid', 'bgrid', 'cgrid'];
toplevel.enter().append('g')
.attr('class', function(d) { return 'toplevel ' + d; })
.each(function(d) {
var s = d3.select(this);
layers[d] = s;
// containers for different trace types.
// NOTE - this is different from cartesian, where all traces
// are in front of grids. Here I'm putting maps behind the grids
// so the grids will always be visible if they're requested.
// Perhaps we want that for cartesian too?
if(d === 'frontplot') {
s.append('g').classed('scatterlayer', true);
} else if(d === 'backplot') {
s.append('g').classed('maplayer', true);
} else if(d === 'plotbg') {
s.append('path').attr('d', 'M0,0Z');
} else if(d === 'aline' || d === 'bline' || d === 'cline') {
s.append('path');
} else if(d === 'grids') {
grids.forEach(function(d) {
layers[d] = s.append('g').classed('grid ' + d, true);
});
}
});
toplevel.order();
};
var whRatio = Math.sqrt(4 / 3);
proto.adjustLayout = function(ternaryLayout, graphSize) {
var _this = this;
var domain = ternaryLayout.domain;
var xDomainCenter = (domain.x[0] + domain.x[1]) / 2;
var yDomainCenter = (domain.y[0] + domain.y[1]) / 2;
var xDomain = domain.x[1] - domain.x[0];
var yDomain = domain.y[1] - domain.y[0];
var wmax = xDomain * graphSize.w;
var hmax = yDomain * graphSize.h;
var sum = ternaryLayout.sum;
var amin = ternaryLayout.aaxis.min;
var bmin = ternaryLayout.baxis.min;
var cmin = ternaryLayout.caxis.min;
var x0, y0, w, h, xDomainFinal, yDomainFinal;
if(wmax > whRatio * hmax) {
h = hmax;
w = h * whRatio;
} else {
w = wmax;
h = w / whRatio;
}
xDomainFinal = xDomain * w / wmax;
yDomainFinal = yDomain * h / hmax;
x0 = graphSize.l + graphSize.w * xDomainCenter - w / 2;
y0 = graphSize.t + graphSize.h * (1 - yDomainCenter) - h / 2;
_this.x0 = x0;
_this.y0 = y0;
_this.w = w;
_this.h = h;
_this.sum = sum;
// set up the x and y axis objects we'll use to lay out the points
_this.xaxis = {
type: 'linear',
range: [amin + 2 * cmin - sum, sum - amin - 2 * bmin],
domain: [
xDomainCenter - xDomainFinal / 2,
xDomainCenter + xDomainFinal / 2
],
_id: 'x'
};
setConvert(_this.xaxis, _this.graphDiv._fullLayout);
_this.xaxis.setScale();
_this.xaxis.isPtWithinRange = function(d) {
return (
d.a >= _this.aaxis.range[0] &&
d.a <= _this.aaxis.range[1] &&
d.b >= _this.baxis.range[1] &&
d.b <= _this.baxis.range[0] &&
d.c >= _this.caxis.range[1] &&
d.c <= _this.caxis.range[0]
);
};
_this.yaxis = {
type: 'linear',
range: [amin, sum - bmin - cmin],
domain: [
yDomainCenter - yDomainFinal / 2,
yDomainCenter + yDomainFinal / 2
],
_id: 'y'
};
setConvert(_this.yaxis, _this.graphDiv._fullLayout);
_this.yaxis.setScale();
_this.yaxis.isPtWithinRange = function() { return true; };
// set up the modified axes for tick drawing
var yDomain0 = _this.yaxis.domain[0];
// aaxis goes up the left side. Set it up as a y axis, but with
// fictitious angles and domain, but then rotate and translate
// it into place at the end
var aaxis = _this.aaxis = extendFlat({}, ternaryLayout.aaxis, {
range: [amin, sum - bmin - cmin],
side: 'left',
// tickangle = 'auto' means 0 anyway for a y axis, need to coerce to 0 here
// so we can shift by 30.
tickangle: (+ternaryLayout.aaxis.tickangle || 0) - 30,
domain: [yDomain0, yDomain0 + yDomainFinal * whRatio],
anchor: 'free',
position: 0,
_id: 'y',
_length: w
});
setConvert(aaxis, _this.graphDiv._fullLayout);
aaxis.setScale();
// baxis goes across the bottom (backward). We can set it up as an x axis
// without any enclosing transformation.
var baxis = _this.baxis = extendFlat({}, ternaryLayout.baxis, {
range: [sum - amin - cmin, bmin],
side: 'bottom',
domain: _this.xaxis.domain,
anchor: 'free',
position: 0,
_id: 'x',
_length: w
});
setConvert(baxis, _this.graphDiv._fullLayout);
baxis.setScale();
// caxis goes down the right side. Set it up as a y axis, with
// post-transformation similar to aaxis
var caxis = _this.caxis = extendFlat({}, ternaryLayout.caxis, {
range: [sum - amin - bmin, cmin],
side: 'right',
tickangle: (+ternaryLayout.caxis.tickangle || 0) + 30,
domain: [yDomain0, yDomain0 + yDomainFinal * whRatio],
anchor: 'free',
position: 0,
_id: 'y',
_length: w
});
setConvert(caxis, _this.graphDiv._fullLayout);
caxis.setScale();
var triangleClip = 'M' + x0 + ',' + (y0 + h) + 'h' + w + 'l-' + (w / 2) + ',-' + h + 'Z';
_this.clipDef.select('path').attr('d', triangleClip);
_this.layers.plotbg.select('path').attr('d', triangleClip);
var triangleClipRelative = 'M0,' + h + 'h' + w + 'l-' + (w / 2) + ',-' + h + 'Z';
_this.clipDefRelative.select('path').attr('d', triangleClipRelative);
var plotTransform = strTranslate(x0, y0);
_this.plotContainer.selectAll('.scatterlayer,.maplayer')
.attr('transform', plotTransform);
_this.clipDefRelative.select('path').attr('transform', null);
// TODO: shift axes to accommodate linewidth*sin(30) tick mark angle
// TODO: there's probably an easier way to handle these translations/offsets now...
var bTransform = strTranslate(x0 - baxis._offset, y0 + h);
_this.layers.baxis.attr('transform', bTransform);
_this.layers.bgrid.attr('transform', bTransform);
var aTransform = strTranslate(x0 + w / 2, y0) +
'rotate(30)' + strTranslate(0, -aaxis._offset);
_this.layers.aaxis.attr('transform', aTransform);
_this.layers.agrid.attr('transform', aTransform);
var cTransform = strTranslate(x0 + w / 2, y0) +
'rotate(-30)' + strTranslate(0, -caxis._offset);
_this.layers.caxis.attr('transform', cTransform);
_this.layers.cgrid.attr('transform', cTransform);
_this.drawAxes(true);
_this.layers.aline.select('path')
.attr('d', aaxis.showline ?
'M' + x0 + ',' + (y0 + h) + 'l' + (w / 2) + ',-' + h : 'M0,0')
.call(Color.stroke, aaxis.linecolor || '#000')
.style('stroke-width', (aaxis.linewidth || 0) + 'px');
_this.layers.bline.select('path')
.attr('d', baxis.showline ?
'M' + x0 + ',' + (y0 + h) + 'h' + w : 'M0,0')
.call(Color.stroke, baxis.linecolor || '#000')
.style('stroke-width', (baxis.linewidth || 0) + 'px');
_this.layers.cline.select('path')
.attr('d', caxis.showline ?
'M' + (x0 + w / 2) + ',' + y0 + 'l' + (w / 2) + ',' + h : 'M0,0')
.call(Color.stroke, caxis.linecolor || '#000')
.style('stroke-width', (caxis.linewidth || 0) + 'px');
if(!_this.graphDiv._context.staticPlot) {
_this.initInteractions();
}
Drawing.setClipUrl(
_this.layers.frontplot,
_this._hasClipOnAxisFalse ? null : _this.clipId,
_this.graphDiv
);
};
proto.drawAxes = function(doTitles) {
var _this = this;
var gd = _this.graphDiv;
var titlesuffix = _this.id.substr(7) + 'title';
var layers = _this.layers;
var aaxis = _this.aaxis;
var baxis = _this.baxis;
var caxis = _this.caxis;
_this.drawAx(aaxis);
_this.drawAx(baxis);
_this.drawAx(caxis);
if(doTitles) {
var apad = Math.max(aaxis.showticklabels ? aaxis.tickfont.size / 2 : 0,
(caxis.showticklabels ? caxis.tickfont.size * 0.75 : 0) +
(caxis.ticks === 'outside' ? caxis.ticklen * 0.87 : 0));
var bpad = (baxis.showticklabels ? baxis.tickfont.size : 0) +
(baxis.ticks === 'outside' ? baxis.ticklen : 0) + 3;
layers['a-title'] = Titles.draw(gd, 'a' + titlesuffix, {
propContainer: aaxis,
propName: _this.id + '.aaxis.title',
placeholder: _(gd, 'Click to enter Component A title'),
attributes: {
x: _this.x0 + _this.w / 2,
y: _this.y0 - aaxis.title.font.size / 3 - apad,
'text-anchor': 'middle'
}
});
layers['b-title'] = Titles.draw(gd, 'b' + titlesuffix, {
propContainer: baxis,
propName: _this.id + '.baxis.title',
placeholder: _(gd, 'Click to enter Component B title'),
attributes: {
x: _this.x0 - bpad,
y: _this.y0 + _this.h + baxis.title.font.size * 0.83 + bpad,
'text-anchor': 'middle'
}
});
layers['c-title'] = Titles.draw(gd, 'c' + titlesuffix, {
propContainer: caxis,
propName: _this.id + '.caxis.title',
placeholder: _(gd, 'Click to enter Component C title'),
attributes: {
x: _this.x0 + _this.w + bpad,
y: _this.y0 + _this.h + caxis.title.font.size * 0.83 + bpad,
'text-anchor': 'middle'
}
});
}
};
proto.drawAx = function(ax) {
var _this = this;
var gd = _this.graphDiv;
var axName = ax._name;
var axLetter = axName.charAt(0);
var axId = ax._id;
var axLayer = _this.layers[axName];
var counterAngle = 30;
var stashKey = axLetter + 'tickLayout';
var newTickLayout = strTickLayout(ax);
if(_this[stashKey] !== newTickLayout) {
axLayer.selectAll('.' + axId + 'tick').remove();
_this[stashKey] = newTickLayout;
}
ax.setScale();
var vals = Axes.calcTicks(ax);
var valsClipped = Axes.clipEnds(ax, vals);
var transFn = Axes.makeTransTickFn(ax);
var tickSign = Axes.getTickSigns(ax)[2];
var caRad = Lib.deg2rad(counterAngle);
var pad = tickSign * (ax.linewidth || 1) / 2;
var len = tickSign * ax.ticklen;
var w = _this.w;
var h = _this.h;
var tickPath = axLetter === 'b' ?
'M0,' + pad + 'l' + (Math.sin(caRad) * len) + ',' + (Math.cos(caRad) * len) :
'M' + pad + ',0l' + (Math.cos(caRad) * len) + ',' + (-Math.sin(caRad) * len);
var gridPath = {
a: 'M0,0l' + h + ',-' + (w / 2),
b: 'M0,0l-' + (w / 2) + ',-' + h,
c: 'M0,0l-' + h + ',' + (w / 2)
}[axLetter];
Axes.drawTicks(gd, ax, {
vals: ax.ticks === 'inside' ? valsClipped : vals,
layer: axLayer,
path: tickPath,
transFn: transFn,
crisp: false
});
Axes.drawGrid(gd, ax, {
vals: valsClipped,
layer: _this.layers[axLetter + 'grid'],
path: gridPath,
transFn: transFn,
crisp: false
});
Axes.drawLabels(gd, ax, {
vals: vals,
layer: axLayer,
transFn: transFn,
labelFns: Axes.makeLabelFns(ax, 0, counterAngle)
});
};
function strTickLayout(axLayout) {
return axLayout.ticks + String(axLayout.ticklen) + String(axLayout.showticklabels);
}
// hard coded paths for zoom corners
// uses the same sizing as cartesian, length is MINZOOM/2, width is 3px
var CLEN = constants.MINZOOM / 2 + 0.87;
var BLPATH = 'm-0.87,.5h' + CLEN + 'v3h-' + (CLEN + 5.2) +
'l' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) +
'l2.6,1.5l-' + (CLEN / 2) + ',' + (CLEN * 0.87) + 'Z';
var BRPATH = 'm0.87,.5h-' + CLEN + 'v3h' + (CLEN + 5.2) +
'l-' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) +
'l-2.6,1.5l' + (CLEN / 2) + ',' + (CLEN * 0.87) + 'Z';
var TOPPATH = 'm0,1l' + (CLEN / 2) + ',' + (CLEN * 0.87) +
'l2.6,-1.5l-' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) +
'l-' + (CLEN / 2 + 2.6) + ',' + (CLEN * 0.87 + 4.5) +
'l2.6,1.5l' + (CLEN / 2) + ',-' + (CLEN * 0.87) + 'Z';
var STARTMARKER = 'm0.5,0.5h5v-2h-5v-5h-2v5h-5v2h5v5h2Z';
// I guess this could be shared with cartesian... but for now it's separate.
var SHOWZOOMOUTTIP = true;
proto.clearSelect = function() {
clearSelectionsCache(this.dragOptions);
clearSelect(this.dragOptions.gd);
};
proto.initInteractions = function() {
var _this = this;
var dragger = _this.layers.plotbg.select('path').node();
var gd = _this.graphDiv;
var zoomLayer = gd._fullLayout._zoomlayer;
var scaleX;
var scaleY;
// use plotbg for the main interactions
this.dragOptions = {
element: dragger,
gd: gd,
plotinfo: {
id: _this.id,
domain: gd._fullLayout[_this.id].domain,
xaxis: _this.xaxis,
yaxis: _this.yaxis
},
subplot: _this.id,
prepFn: function(e, startX, startY) {
// these aren't available yet when initInteractions
// is called
_this.dragOptions.xaxes = [_this.xaxis];
_this.dragOptions.yaxes = [_this.yaxis];
scaleX = gd._fullLayout._invScaleX;
scaleY = gd._fullLayout._invScaleY;
var dragModeNow = _this.dragOptions.dragmode = gd._fullLayout.dragmode;
if(freeMode(dragModeNow)) _this.dragOptions.minDrag = 1;
else _this.dragOptions.minDrag = undefined;
if(dragModeNow === 'zoom') {
_this.dragOptions.moveFn = zoomMove;
_this.dragOptions.clickFn = clickZoomPan;
_this.dragOptions.doneFn = zoomDone;
zoomPrep(e, startX, startY);
} else if(dragModeNow === 'pan') {
_this.dragOptions.moveFn = plotDrag;
_this.dragOptions.clickFn = clickZoomPan;
_this.dragOptions.doneFn = dragDone;
panPrep();
_this.clearSelect(gd);
} else if(rectMode(dragModeNow) || freeMode(dragModeNow)) {
prepSelect(e, startX, startY, _this.dragOptions, dragModeNow);
}
}
};
var x0, y0, mins0, span0, mins, lum, path0, dimmed, zb, corners;
function makeUpdate(_mins) {
var attrs = {};
attrs[_this.id + '.aaxis.min'] = _mins.a;
attrs[_this.id + '.baxis.min'] = _mins.b;
attrs[_this.id + '.caxis.min'] = _mins.c;
return attrs;
}
function clickZoomPan(numClicks, evt) {
var clickMode = gd._fullLayout.clickmode;
removeZoombox(gd);
if(numClicks === 2) {
gd.emit('plotly_doubleclick', null);
Registry.call('_guiRelayout', gd, makeUpdate({a: 0, b: 0, c: 0}));
}
if(clickMode.indexOf('select') > -1 && numClicks === 1) {
selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, _this.dragOptions);
}
if(clickMode.indexOf('event') > -1) {
Fx.click(gd, evt, _this.id);
}
}
function zoomPrep(e, startX, startY) {
var dragBBox = dragger.getBoundingClientRect();
x0 = startX - dragBBox.left;
y0 = startY - dragBBox.top;
gd._fullLayout._calcInverseTransform(gd);
var inverse = gd._fullLayout._invTransform;
var transformedCoords = Lib.apply3DTransform(inverse)(x0, y0);
x0 = transformedCoords[0];
y0 = transformedCoords[1];
mins0 = {
a: _this.aaxis.range[0],
b: _this.baxis.range[1],
c: _this.caxis.range[1]
};
mins = mins0;
span0 = _this.aaxis.range[1] - mins0.a;
lum = tinycolor(_this.graphDiv._fullLayout[_this.id].bgcolor).getLuminance();
path0 = 'M0,' + _this.h + 'L' + (_this.w / 2) + ', 0L' + _this.w + ',' + _this.h + 'Z';
dimmed = false;
zb = zoomLayer.append('path')
.attr('class', 'zoombox')
.attr('transform', strTranslate(_this.x0, _this.y0))
.style({
'fill': lum > 0.2 ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)',
'stroke-width': 0
})
.attr('d', path0);
corners = zoomLayer.append('path')
.attr('class', 'zoombox-corners')
.attr('transform', strTranslate(_this.x0, _this.y0))
.style({
fill: Color.background,
stroke: Color.defaultLine,
'stroke-width': 1,
opacity: 0
})
.attr('d', 'M0,0Z');
_this.clearSelect(gd);
}
function getAFrac(x, y) { return 1 - (y / _this.h); }
function getBFrac(x, y) { return 1 - ((x + (_this.h - y) / Math.sqrt(3)) / _this.w); }
function getCFrac(x, y) { return ((x - (_this.h - y) / Math.sqrt(3)) / _this.w); }
function zoomMove(dx0, dy0) {
var x1 = x0 + dx0 * scaleX;
var y1 = y0 + dy0 * scaleY;
var afrac = Math.max(0, Math.min(1, getAFrac(x0, y0), getAFrac(x1, y1)));
var bfrac = Math.max(0, Math.min(1, getBFrac(x0, y0), getBFrac(x1, y1)));
var cfrac = Math.max(0, Math.min(1, getCFrac(x0, y0), getCFrac(x1, y1)));
var xLeft = ((afrac / 2) + cfrac) * _this.w;
var xRight = (1 - (afrac / 2) - bfrac) * _this.w;
var xCenter = (xLeft + xRight) / 2;
var xSpan = xRight - xLeft;
var yBottom = (1 - afrac) * _this.h;
var yTop = yBottom - xSpan / whRatio;
if(xSpan < constants.MINZOOM) {
mins = mins0;
zb.attr('d', path0);
corners.attr('d', 'M0,0Z');
} else {
mins = {
a: mins0.a + afrac * span0,
b: mins0.b + bfrac * span0,
c: mins0.c + cfrac * span0
};
zb.attr('d', path0 + 'M' + xLeft + ',' + yBottom +
'H' + xRight + 'L' + xCenter + ',' + yTop +
'L' + xLeft + ',' + yBottom + 'Z');
corners.attr('d', 'M' + x0 + ',' + y0 + STARTMARKER +
'M' + xLeft + ',' + yBottom + BLPATH +
'M' + xRight + ',' + yBottom + BRPATH +
'M' + xCenter + ',' + yTop + TOPPATH);
}
if(!dimmed) {
zb.transition()
.style('fill', lum > 0.2 ? 'rgba(0,0,0,0.4)' :
'rgba(255,255,255,0.3)')
.duration(200);
corners.transition()
.style('opacity', 1)
.duration(200);
dimmed = true;
}
gd.emit('plotly_relayouting', makeUpdate(mins));
}
function zoomDone() {
removeZoombox(gd);
if(mins === mins0) return;
Registry.call('_guiRelayout', gd, makeUpdate(mins));
if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) {
Lib.notifier(_(gd, 'Double-click to zoom back out'), 'long');
SHOWZOOMOUTTIP = false;
}
}
function panPrep() {
mins0 = {
a: _this.aaxis.range[0],
b: _this.baxis.range[1],
c: _this.caxis.range[1]
};
mins = mins0;
}
function plotDrag(dx, dy) {
var dxScaled = dx / _this.xaxis._m;
var dyScaled = dy / _this.yaxis._m;
mins = {
a: mins0.a - dyScaled,
b: mins0.b + (dxScaled + dyScaled) / 2,
c: mins0.c - (dxScaled - dyScaled) / 2
};
var minsorted = [mins.a, mins.b, mins.c].sort(Lib.sorterAsc);
var minindices = {
a: minsorted.indexOf(mins.a),
b: minsorted.indexOf(mins.b),
c: minsorted.indexOf(mins.c)
};
if(minsorted[0] < 0) {
if(minsorted[1] + minsorted[0] / 2 < 0) {
minsorted[2] += minsorted[0] + minsorted[1];
minsorted[0] = minsorted[1] = 0;
} else {
minsorted[2] += minsorted[0] / 2;
minsorted[1] += minsorted[0] / 2;
minsorted[0] = 0;
}
mins = {
a: minsorted[minindices.a],
b: minsorted[minindices.b],
c: minsorted[minindices.c]
};
dy = (mins0.a - mins.a) * _this.yaxis._m;
dx = (mins0.c - mins.c - mins0.b + mins.b) * _this.xaxis._m;
}
// move the data (translate, don't redraw)
var plotTransform = strTranslate(_this.x0 + dx, _this.y0 + dy);
_this.plotContainer.selectAll('.scatterlayer,.maplayer')
.attr('transform', plotTransform);
var plotTransform2 = strTranslate(-dx, -dy);
_this.clipDefRelative.select('path').attr('transform', plotTransform2);
// move the ticks
_this.aaxis.range = [mins.a, _this.sum - mins.b - mins.c];
_this.baxis.range = [_this.sum - mins.a - mins.c, mins.b];
_this.caxis.range = [_this.sum - mins.a - mins.b, mins.c];
_this.drawAxes(false);
if(_this._hasClipOnAxisFalse) {
_this.plotContainer
.select('.scatterlayer').selectAll('.trace')
.call(Drawing.hideOutsideRangePoints, _this);
}
gd.emit('plotly_relayouting', makeUpdate(mins));
}
function dragDone() {
Registry.call('_guiRelayout', gd, makeUpdate(mins));
}
// finally, set up hover and click
// these event handlers must already be set before dragElement.init
// so it can stash them and override them.
dragger.onmousemove = function(evt) {
Fx.hover(gd, evt, _this.id);
gd._fullLayout._lasthover = dragger;
gd._fullLayout._hoversubplot = _this.id;
};
dragger.onmouseout = function(evt) {
if(gd._dragging) return;
dragElement.unhover(gd, evt);
};
dragElement.init(this.dragOptions);
};
function removeZoombox(gd) {
d3.select(gd)
.selectAll('.zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners')
.remove();
}