'use strict'; var d3 = require('@plotly/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('../../components/selections').prepSelect; var selectOnClick = require('../../components/selections').selectOnClick; var clearOutline = require('../../components/selections').clearOutline; var clearSelectionsCache = require('../../components/selections').clearSelectionsCache; var constants = require('../cartesian/constants'); function Ternary(options, fullLayout) { this.id = options.id; this.graphDiv = options.graphDiv; this.init(fullLayout); this.makeFramework(fullLayout); this.updateFx(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.updateFx = function(fullLayout) { fullLayout._ternarylayer .selectAll('g.toplevel') .style('cursor', fullLayout.dragmode === 'pan' ? 'move' : 'crosshair'); }; 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.clearOutline = function() { clearSelectionsCache(this.dragOptions); clearOutline(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.clearOutline(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.clearOutline(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(); }