UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

1,589 lines 50.7 kB
'use strict';

var d3 = require('@plotly/d3');

var Registry = require('../../registry');
var Lib = require('../../lib');
var strRotate = Lib.strRotate;
var strTranslate = Lib.strTranslate;
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var Plots = require('../plots');
var Axes = require('../../plots/cartesian/axes');
var setConvertCartesian = require('../cartesian/set_convert');
var setConvertPolar = require('./set_convert');
var doAutoRange = require('../cartesian/autorange').doAutoRange;
var dragBox = require('../cartesian/dragbox');
var dragElement = require('../../components/dragelement');
var Fx = require('../../components/fx');
var Titles = require('../../components/titles');
var prepSelect = require('../../components/selections').prepSelect;
var selectOnClick = require('../../components/selections').selectOnClick;
var clearOutline = require('../../components/selections').clearOutline;
var setCursor = require('../../lib/setcursor');
var clearGlCanvases = require('../../lib/clear_gl_canvases');
var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces;

var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
var constants = require('./constants');
var helpers = require('./helpers');

var smithHelpers = require('../smith/helpers');
var smith = smithHelpers.smith;
var reactanceArc = smithHelpers.reactanceArc;
var resistanceArc = smithHelpers.resistanceArc;
var smithTransform = smithHelpers.smithTransform;

var _ = Lib._;
var mod = Lib.mod;
var deg2rad = Lib.deg2rad;
var rad2deg = Lib.rad2deg;

function Polar(gd, id, isSmith) {
    this.isSmith = isSmith || false;
    this.id = id;
    this.gd = gd;

    this._hasClipOnAxisFalse = null;
    this.vangles = null;
    this.radialAxisAngle = null;
    this.traceHash = {};
    this.layers = {};
    this.clipPaths = {};
    this.clipIds = {};
    this.viewInitial = {};

    var fullLayout = gd._fullLayout;
    var clipIdBase = 'clip' + fullLayout._uid + id;

    this.clipIds.forTraces = clipIdBase + '-for-traces';
    this.clipPaths.forTraces = fullLayout._clips.append('clipPath')
        .attr('id', this.clipIds.forTraces);
    this.clipPaths.forTraces.append('path');

    this.framework = fullLayout['_' + (isSmith ? 'smith' : 'polar') + 'layer'].append('g')
        .attr('class', id);

    this.getHole = function(s) {
        return this.isSmith ? 0 : s.hole;
    };

    this.getSector = function(s) {
        return this.isSmith ? [0, 360] : s.sector;
    };

    this.getRadial = function(s) {
        return this.isSmith ? s.realaxis : s.radialaxis;
    };

    this.getAngular = function(s) {
        return this.isSmith ? s.imaginaryaxis : s.angularaxis;
    };

    if(!isSmith) {
        // unfortunately, we have to keep track of some axis tick settings
        // as polar subplots do not implement the 'ticks' editType
        this.radialTickLayout = null;
        this.angularTickLayout = null;
    }
}

var proto = Polar.prototype;

module.exports = function createPolar(gd, id, isSmith) {
    return new Polar(gd, id, isSmith);
};

proto.plot = function(polarCalcData, fullLayout) {
    var _this = this;
    var polarLayout = fullLayout[_this.id];

    var found = false;
    for(var i = 0; i < polarCalcData.length; i++) {
        var trace = polarCalcData[i][0].trace;
        if(trace.cliponaxis === false) {
            found = true;
            break;
        }
    }
    _this._hasClipOnAxisFalse = found;

    _this.updateLayers(fullLayout, polarLayout);
    _this.updateLayout(fullLayout, polarLayout);
    Plots.generalUpdatePerTraceModule(_this.gd, _this, polarCalcData, polarLayout);
    _this.updateFx(fullLayout, polarLayout);

    if(_this.isSmith) {
        delete polarLayout.realaxis.range;
        delete polarLayout.imaginaryaxis.range;
    }
};

proto.updateLayers = function(fullLayout, polarLayout) {
    var _this = this;
    var isSmith = _this.isSmith;
    var layers = _this.layers;
    var radialLayout = _this.getRadial(polarLayout);
    var angularLayout = _this.getAngular(polarLayout);
    var layerNames = constants.layerNames;

    var frontPlotIndex = layerNames.indexOf('frontplot');
    var layerData = layerNames.slice(0, frontPlotIndex);
    var isAngularAxisBelowTraces = angularLayout.layer === 'below traces';
    var isRadialAxisBelowTraces = radialLayout.layer === 'below traces';

    if(isAngularAxisBelowTraces) layerData.push('angular-line');
    if(isRadialAxisBelowTraces) layerData.push('radial-line');
    if(isAngularAxisBelowTraces) layerData.push('angular-axis');
    if(isRadialAxisBelowTraces) layerData.push('radial-axis');

    layerData.push('frontplot');

    if(!isAngularAxisBelowTraces) layerData.push('angular-line');
    if(!isRadialAxisBelowTraces) layerData.push('radial-line');
    if(!isAngularAxisBelowTraces) layerData.push('angular-axis');
    if(!isRadialAxisBelowTraces) layerData.push('radial-axis');

    var subLayer = (isSmith ? 'smith' : 'polar') + 'sublayer';

    var join = _this.framework.selectAll('.' + subLayer)
        .data(layerData, String);

    join.enter().append('g')
        .attr('class', function(d) { return subLayer + ' ' + d;})
        .each(function(d) {
            var sel = layers[d] = d3.select(this);

            switch(d) {
                case 'frontplot':
                    // TODO add option to place in 'backplot' layer??
                    if(!isSmith) {
                        sel.append('g').classed('barlayer', true);
                    }
                    sel.append('g').classed('scatterlayer', true);
                    break;
                case 'backplot':
                    sel.append('g').classed('maplayer', true);
                    break;
                case 'plotbg':
                    layers.bg = sel.append('path');
                    break;
                case 'radial-grid':
                    sel.style('fill', 'none');
                    break;
                case 'angular-grid':
                    sel.style('fill', 'none');
                    break;
                case 'radial-line':
                    sel.append('line').style('fill', 'none');
                    break;
                case 'angular-line':
                    sel.append('path').style('fill', 'none');
                    break;
            }
        });

    join.order();
};

/* Polar subplots juggle with 6 'axis objects' (!), these are:
 *
 * - getRadial(polarLayout) (aka radialLayout in this file):
 * - getAngular(polarLayout) (aka angularLayout in this file):
 *   used for data -> calcdata conversions (aka d2c) during the calc step
 *
 * - this.radialAxis
 *   extends getRadial(polarLayout), adds mocked 'domain' and
 *   few other keys in order to reuse Cartesian doAutoRange and the Axes
 *   drawing routines.
 *   used for calcdata -> geometric conversions (aka c2g) during the plot step
 *   + setGeometry setups ax.c2g for given ax.range
 *   + setScale setups ax._m,ax._b for given ax.range
 *
 * - this.angularAxis
 *   extends getAngular(polarLayout), adds mocked 'range' and 'domain' and
 *   a few other keys in order to reuse the Axes drawing routines.
 *   used for calcdata -> geometric conversions (aka c2g) during the plot step
 *   + setGeometry setups ax.c2g given ax.rotation, ax.direction & ax._categories,
 *                 and mocks ax.range
 *   + setScale setups ax._m,ax._b with that mocked ax.range
 *
 * - this.xaxis
 * - this.yaxis
 *   setup so that polar traces can reuse plot methods of Cartesian traces
 *   which mostly rely on 2pixel methods (e.g ax.c2p)
 */
proto.updateLayout = function(fullLayout, polarLayout) {
    var _this = this;
    var layers = _this.layers;
    var gs = fullLayout._size;

    // axis attributes
    var radialLayout = _this.getRadial(polarLayout);
    var angularLayout = _this.getAngular(polarLayout);
    // layout domains
    var xDomain = polarLayout.domain.x;
    var yDomain = polarLayout.domain.y;
    // offsets from paper edge to layout domain box
    _this.xOffset = gs.l + gs.w * xDomain[0];
    _this.yOffset = gs.t + gs.h * (1 - yDomain[1]);
    // lengths of the layout domain box
    var xLength = _this.xLength = gs.w * (xDomain[1] - xDomain[0]);
    var yLength = _this.yLength = gs.h * (yDomain[1] - yDomain[0]);
    // sector to plot
    var sector = _this.getSector(polarLayout);
    _this.sectorInRad = sector.map(deg2rad);
    var sectorBBox = _this.sectorBBox = computeSectorBBox(sector);
    var dxSectorBBox = sectorBBox[2] - sectorBBox[0];
    var dySectorBBox = sectorBBox[3] - sectorBBox[1];
    // aspect ratios
    var arDomain = yLength / xLength;
    var arSector = Math.abs(dySectorBBox / dxSectorBBox);
    // actual lengths and domains of subplot box
    var xLength2, yLength2;
    var xDomain2, yDomain2;
    var gap;
    if(arDomain > arSector) {
        xLength2 = xLength;
        yLength2 = xLength * arSector;
        gap = (yLength - yLength2) / gs.h / 2;
        xDomain2 = [xDomain[0], xDomain[1]];
        yDomain2 = [yDomain[0] + gap, yDomain[1] - gap];
    } else {
        xLength2 = yLength / arSector;
        yLength2 = yLength;
        gap = (xLength - xLength2) / gs.w / 2;
        xDomain2 = [xDomain[0] + gap, xDomain[1] - gap];
        yDomain2 = [yDomain[0], yDomain[1]];
    }
    _this.xLength2 = xLength2;
    _this.yLength2 = yLength2;
    _this.xDomain2 = xDomain2;
    _this.yDomain2 = yDomain2;
    // actual offsets from paper edge to the subplot box top-left corner
    var xOffset2 = _this.xOffset2 = gs.l + gs.w * xDomain2[0];
    var yOffset2 = _this.yOffset2 = gs.t + gs.h * (1 - yDomain2[1]);
    // circle radius in px
    var radius = _this.radius = xLength2 / dxSectorBBox;
    // 'inner' radius in px (when polar.hole is set)
    var innerRadius = _this.innerRadius = _this.getHole(polarLayout) * radius;
    // circle center position in px
    var cx = _this.cx = xOffset2 - radius * sectorBBox[0];
    var cy = _this.cy = yOffset2 + radius * sectorBBox[3];
    // circle center in the coordinate system of plot area
    var cxx = _this.cxx = cx - xOffset2;
    var cyy = _this.cyy = cy - yOffset2;

    var side = radialLayout.side;
    var trueSide;
    if(side === 'counterclockwise') {
        trueSide = side;
        side = 'top';
    } else if(side === 'clockwise') {
        trueSide = side;
        side = 'bottom';
    }

    _this.radialAxis = _this.mockAxis(fullLayout, polarLayout, radialLayout, {
        // make this an 'x' axis to make positioning (especially rotation) easier
        _id: 'x',
        // convert to 'x' axis equivalent
        side: side,
        // keep track of real side
        _trueSide: trueSide,
        // spans length 1 radius
        domain: [innerRadius / gs.w, radius / gs.w]
    });

    _this.angularAxis = _this.mockAxis(fullLayout, polarLayout, angularLayout, {
        side: 'right',
        // to get auto nticks right
        domain: [0, Math.PI],
        // don't pass through autorange logic
        autorange: false
    });

    _this.doAutoRange(fullLayout, polarLayout);
    // N.B. this sets _this.vangles
    _this.updateAngularAxis(fullLayout, polarLayout);
    // N.B. this sets _this.radialAxisAngle
    _this.updateRadialAxis(fullLayout, polarLayout);
    _this.updateRadialAxisTitle(fullLayout, polarLayout);

    _this.xaxis = _this.mockCartesianAxis(fullLayout, polarLayout, {
        _id: 'x',
        domain: xDomain2
    });

    _this.yaxis = _this.mockCartesianAxis(fullLayout, polarLayout, {
        _id: 'y',
        domain: yDomain2
    });

    var dPath = _this.pathSubplot();

    _this.clipPaths.forTraces.select('path')
        .attr('d', dPath)
        .attr('transform', strTranslate(cxx, cyy));

    layers.frontplot
        .attr('transform', strTranslate(xOffset2, yOffset2))
        .call(Drawing.setClipUrl, _this._hasClipOnAxisFalse ? null : _this.clipIds.forTraces, _this.gd);

    layers.bg
        .attr('d', dPath)
        .attr('transform', strTranslate(cx, cy))
        .call(Color.fill, polarLayout.bgcolor);
};

proto.mockAxis = function(fullLayout, polarLayout, axLayout, opts) {
    var ax = Lib.extendFlat({}, axLayout, opts);
    setConvertPolar(ax, polarLayout, fullLayout);
    return ax;
};

proto.mockCartesianAxis = function(fullLayout, polarLayout, opts) {
    var _this = this;
    var isSmith = _this.isSmith;
    var axId = opts._id;

    var ax = Lib.extendFlat({type: 'linear'}, opts);
    setConvertCartesian(ax, fullLayout);

    var bboxIndices = {
        x: [0, 2],
        y: [1, 3]
    };

    ax.setRange = function() {
        var sectorBBox = _this.sectorBBox;
        var ind = bboxIndices[axId];
        var rl = _this.radialAxis._rl;
        var drl = (rl[1] - rl[0]) / (1 - _this.getHole(polarLayout));
        ax.range = [sectorBBox[ind[0]] * drl, sectorBBox[ind[1]] * drl];
    };

    ax.isPtWithinRange = axId === 'x' && !isSmith ?
        function(d) { return _this.isPtInside(d); } :
        function() { return true; };

    ax.setRange();
    ax.setScale();

    return ax;
};

proto.doAutoRange = function(fullLayout, polarLayout) {
    var _this = this;
    var gd = _this.gd;
    var radialAxis = _this.radialAxis;
    var radialLayout = _this.getRadial(polarLayout);

    doAutoRange(gd, radialAxis);

    var rng = radialAxis.range;
    radialLayout.range = rng.slice();
    radialLayout._input.range = rng.slice();

    radialAxis._rl = [
        radialAxis.r2l(rng[0], null, 'gregorian'),
        radialAxis.r2l(rng[1], null, 'gregorian')
    ];

    if(radialAxis.minallowed !== undefined) {
        var minallowed = radialAxis.r2l(radialAxis.minallowed);
        if(radialAxis._rl[0] > radialAxis._rl[1]) {
            radialAxis._rl[1] = Math.max(radialAxis._rl[1], minallowed);
        } else {
            radialAxis._rl[0] = Math.max(radialAxis._rl[0], minallowed);
        }
    }

    if(radialAxis.maxallowed !== undefined) {
        var maxallowed = radialAxis.r2l(radialAxis.maxallowed);
        if(radialAxis._rl[0] < radialAxis._rl[1]) {
            radialAxis._rl[1] = Math.min(radialAxis._rl[1], maxallowed);
        } else {
            radialAxis._rl[0] = Math.min(radialAxis._rl[0], maxallowed);
        }
    }
};

proto.updateRadialAxis = function(fullLayout, polarLayout) {
    var _this = this;
    var gd = _this.gd;
    var layers = _this.layers;
    var radius = _this.radius;
    var innerRadius = _this.innerRadius;
    var cx = _this.cx;
    var cy = _this.cy;
    var radialLayout = _this.getRadial(polarLayout);
    var a0 = mod(_this.getSector(polarLayout)[0], 360);
    var ax = _this.radialAxis;
    var hasRoomForIt = innerRadius < radius;

    var isSmith = _this.isSmith;
    if(!isSmith) {
        _this.fillViewInitialKey('radialaxis.angle', radialLayout.angle);
        _this.fillViewInitialKey('radialaxis.range', ax.range.slice());

        ax.setGeometry();
    }

    // rotate auto tick labels by 180 if in quadrant II and III to make them
    // readable from left-to-right
    //
    // TODO try moving deeper in Axes.drawLabels for better results?
    if(ax.tickangle === 'auto' && (a0 > 90 && a0 <= 270)) {
        ax.tickangle = 180;
    }

    // easier to set rotate angle with custom translate function
    var transFn = isSmith ?
        function(d) {
            var t = smithTransform(_this, smith([d.x, 0]));
            return strTranslate(t[0] - cx, t[1] - cy);
        } :
        function(d) {
            return strTranslate(ax.l2p(d.x) + innerRadius, 0);
        };

    // set special grid path function
    var gridPathFn = isSmith ?
        function(d) {
            return resistanceArc(_this, d.x, -Infinity, Infinity);
        } :
        function(d) {
            return _this.pathArc(ax.r2p(d.x) + innerRadius);
        };

    var newTickLayout = strTickLayout(radialLayout);
    if(_this.radialTickLayout !== newTickLayout) {
        layers['radial-axis'].selectAll('.xtick').remove();
        _this.radialTickLayout = newTickLayout;
    }

    if(hasRoomForIt) {
        ax.setScale();

        var labelShift = 0;

        var vals = isSmith ?
            (ax.tickvals || []).filter(function(x) {
                // filter negative
                return x >= 0;
            }).map(function(x) {
                return Axes.tickText(ax, x, true, false);
            }) : Axes.calcTicks(ax);

        var valsClipped = isSmith ? vals : Axes.clipEnds(ax, vals);
        var tickSign = Axes.getTickSigns(ax)[2];
        if(isSmith) {
            if(
                (ax.ticks === 'top' && ax.side === 'bottom') ||
                (ax.ticks === 'bottom' && ax.side === 'top')
            ) {
                // invert sign
                tickSign = -tickSign;
            }

            if(ax.ticks === 'top' && ax.side === 'top') labelShift = -ax.ticklen;
            if(ax.ticks === 'bottom' && ax.side === 'bottom') labelShift = ax.ticklen;
        }

        Axes.drawTicks(gd, ax, {
            vals: vals,
            layer: layers['radial-axis'],
            path: Axes.makeTickPath(ax, 0, tickSign),
            transFn: transFn,
            crisp: false
        });

        Axes.drawGrid(gd, ax, {
            vals: valsClipped,
            layer: layers['radial-grid'],
            path: gridPathFn,
            transFn: Lib.noop,
            crisp: false
        });

        Axes.drawLabels(gd, ax, {
            vals: vals,
            layer: layers['radial-axis'],
            transFn: transFn,
            labelFns: Axes.makeLabelFns(ax, labelShift)
        });
    }

    // stash 'actual' radial axis angle for drag handlers (in degrees)
    var angle = _this.radialAxisAngle = _this.vangles ?
        rad2deg(snapToVertexAngle(deg2rad(radialLayout.angle), _this.vangles)) :
        radialLayout.angle;

    var tLayer = strTranslate(cx, cy);
    var tLayer2 = tLayer + strRotate(-angle);

    updateElement(
        layers['radial-axis'],
        hasRoomForIt && (radialLayout.showticklabels || radialLayout.ticks),
        {transform: tLayer2}
    );

    updateElement(
        layers['radial-grid'],
        hasRoomForIt && radialLayout.showgrid,
        {transform: isSmith ? '' : tLayer}
    );

    updateElement(
        layers['radial-line'].select('line'),
        hasRoomForIt && radialLayout.showline,
        {
            x1: isSmith ? -radius : innerRadius,
            y1: 0,
            x2: radius,
            y2: 0,
            transform: tLayer2
        }
    )
    .attr('stroke-width', radialLayout.linewidth)
    .call(Color.stroke, radialLayout.linecolor);
};

proto.updateRadialAxisTitle = function(fullLayout, polarLayout, _angle) {
    if(this.isSmith) return;

    var _this = this;
    var gd = _this.gd;
    var radius = _this.radius;
    var cx = _this.cx;
    var cy = _this.cy;
    var radialLayout = _this.getRadial(polarLayout);
    var titleClass = _this.id + 'title';

    var pad = 0;

    // Hint: no need to check if there is in fact a title.text set
    // because if plot is editable, pad needs to be calculated anyways
    // to properly show placeholder text when title is empty.
    if(radialLayout.title) {
        var h = Drawing.bBox(_this.layers['radial-axis'].node()).height;
        var ts = radialLayout.title.font.size;
        var side = radialLayout.side;
        pad =
            side === 'top' ? ts :
            side === 'counterclockwise' ?
                -(h + ts * 0.4) :
                h + ts * 0.8;
    }

    var angle = _angle !== undefined ? _angle : _this.radialAxisAngle;

    var angleRad = deg2rad(angle);
    var cosa = Math.cos(angleRad);
    var sina = Math.sin(angleRad);

    var x = cx + (radius / 2) * cosa + pad * sina;
    var y = cy - (radius / 2) * sina + pad * cosa;

    _this.layers['radial-axis-title'] = Titles.draw(gd, titleClass, {
        propContainer: radialLayout,
        propName: _this.id + '.radialaxis.title.text',
        placeholder: _(gd, 'Click to enter radial axis title'),
        attributes: {
            x: x,
            y: y,
            'text-anchor': 'middle'
        },
        transform: {rotate: -angle}
    });
};

proto.updateAngularAxis = function(fullLayout, polarLayout) {
    var _this = this;
    var gd = _this.gd;
    var layers = _this.layers;
    var radius = _this.radius;
    var innerRadius = _this.innerRadius;
    var cx = _this.cx;
    var cy = _this.cy;
    var angularLayout = _this.getAngular(polarLayout);
    var ax = _this.angularAxis;

    var isSmith = _this.isSmith;
    if(!isSmith) {
        _this.fillViewInitialKey('angularaxis.rotation', angularLayout.rotation);

        ax.setGeometry();
        ax.setScale();
    }

    // 't'ick to 'g'eometric radians is used all over the place here
    var t2g = isSmith ?
        function(d) {
            var t = smithTransform(_this, smith([0, d.x]));
            return Math.atan2(t[0] - cx, t[1] - cy) - Math.PI / 2;
        } :
        function(d) { return ax.t2g(d.x); };

    // run rad2deg on tick0 and ditck for thetaunit: 'radians' axes
    if(ax.type === 'linear' && ax.thetaunit === 'radians') {
        ax.tick0 = rad2deg(ax.tick0);
        ax.dtick = rad2deg(ax.dtick);
    }

    var _transFn = function(rad) {
        return strTranslate(cx + radius * Math.cos(rad), cy - radius * Math.sin(rad));
    };

    var transFn = isSmith ?
        function(d) {
            var t = smithTransform(_this, smith([0, d.x]));
            return strTranslate(t[0], t[1]);
        } :
        function(d) {
            return _transFn(t2g(d));
        };

    var transFn2 = isSmith ?
        function(d) {
            var t = smithTransform(_this, smith([0, d.x]));
            var rad = Math.atan2(t[0] - cx, t[1] - cy) - Math.PI / 2;
            return strTranslate(t[0], t[1]) + strRotate(-rad2deg(rad));
        } :
        function(d) {
            var rad = t2g(d);
            return _transFn(rad) + strRotate(-rad2deg(rad));
        };

    var gridPathFn = isSmith ?
        function(d) {
            return reactanceArc(_this, d.x, 0, Infinity);
        } :
        function(d) {
            var rad = t2g(d);
            var cosRad = Math.cos(rad);
            var sinRad = Math.sin(rad);
            return 'M' + [cx + innerRadius * cosRad, cy - innerRadius * sinRad] +
                'L' + [cx + radius * cosRad, cy - radius * sinRad];
        };

    var out = Axes.makeLabelFns(ax, 0);
    var labelStandoff = out.labelStandoff;
    var labelFns = {};

    labelFns.xFn = function(d) {
        var rad = t2g(d);
        return Math.cos(rad) * labelStandoff;
    };

    labelFns.yFn = function(d) {
        var rad = t2g(d);
        var ff = Math.sin(rad) > 0 ? 0.2 : 1;
        return -Math.sin(rad) * (labelStandoff + d.fontSize * ff) +
            Math.abs(Math.cos(rad)) * (d.fontSize * MID_SHIFT);
    };

    labelFns.anchorFn = function(d) {
        var rad = t2g(d);
        var cos = Math.cos(rad);
        return Math.abs(cos) < 0.1 ?
            'middle' :
            (cos > 0 ? 'start' : 'end');
    };

    labelFns.heightFn = function(d, a, h) {
        var rad = t2g(d);
        return -0.5 * (1 + Math.sin(rad)) * h;
    };

    var newTickLayout = strTickLayout(angularLayout);
    if(_this.angularTickLayout !== newTickLayout) {
        layers['angular-axis'].selectAll('.' + ax._id + 'tick').remove();
        _this.angularTickLayout = newTickLayout;
    }

    var vals = isSmith ?
        [Infinity].concat(ax.tickvals || []).map(function(x) {
            return Axes.tickText(ax, x, true, false);
        }) : Axes.calcTicks(ax);

    if(isSmith) {
        vals[0].text = '∞';
        vals[0].fontSize *= 1.75;
    }

    // angle of polygon vertices in geometric radians (null means circles)
    // TODO what to do when ax.period > ax._categories ??
    var vangles;
    if(polarLayout.gridshape === 'linear') {
        vangles = vals.map(t2g);

        // ax._vals should be always ordered, make them
        // always turn counterclockwise for convenience here
        if(Lib.angleDelta(vangles[0], vangles[1]) < 0) {
            vangles = vangles.slice().reverse();
        }
    } else {
        vangles = null;
    }
    _this.vangles = vangles;

    // Use tickval filter for category axes instead of tweaking
    // the range w.r.t sector, so that sectors that cross 360 can
    // show all their ticks.
    if(ax.type === 'category') {
        vals = vals.filter(function(d) {
            return Lib.isAngleInsideSector(t2g(d), _this.sectorInRad);
        });
    }

    if(ax.visible) {
        var tickSign = ax.ticks === 'inside' ? -1 : 1;

        var pad = (ax.linewidth || 1) / 2;

        Axes.drawTicks(gd, ax, {
            vals: vals,
            layer: layers['angular-axis'],
            path: 'M' + (tickSign * pad) + ',0h' + (tickSign * ax.ticklen),
            transFn: transFn2,
            crisp: false
        });

        Axes.drawGrid(gd, ax, {
            vals: vals,
            layer: layers['angular-grid'],
            path: gridPathFn,
            transFn: Lib.noop,
            crisp: false
        });

        Axes.drawLabels(gd, ax, {
            vals: vals,
            layer: layers['angular-axis'],
            repositionOnUpdate: true,
            transFn: transFn,
            labelFns: labelFns
        });
    }

    // TODO maybe two arcs is better here?
    // maybe split style attributes between inner and outer angular axes?

    updateElement(layers['angular-line'].select('path'), angularLayout.showline, {
        d: _this.pathSubplot(),
        transform: strTranslate(cx, cy)
    })
    .attr('stroke-width', angularLayout.linewidth)
    .call(Color.stroke, angularLayout.linecolor);
};

proto.updateFx = function(fullLayout, polarLayout) {
    if(!this.gd._context.staticPlot) {
        var hasDrag = !this.isSmith;
        if(hasDrag) {
            this.updateAngularDrag(fullLayout);
            this.updateRadialDrag(fullLayout, polarLayout, 0);
            this.updateRadialDrag(fullLayout, polarLayout, 1);
        }
        this.updateHoverAndMainDrag(fullLayout);
    }
};

proto.updateHoverAndMainDrag = function(fullLayout) {
    var _this = this;
    var isSmith = _this.isSmith;
    var gd = _this.gd;
    var layers = _this.layers;
    var zoomlayer = fullLayout._zoomlayer;
    var MINZOOM = constants.MINZOOM;
    var OFFEDGE = constants.OFFEDGE;
    var radius = _this.radius;
    var innerRadius = _this.innerRadius;
    var cx = _this.cx;
    var cy = _this.cy;
    var cxx = _this.cxx;
    var cyy = _this.cyy;
    var sectorInRad = _this.sectorInRad;
    var vangles = _this.vangles;
    var radialAxis = _this.radialAxis;
    var clampTiny = helpers.clampTiny;
    var findXYatLength = helpers.findXYatLength;
    var findEnclosingVertexAngles = helpers.findEnclosingVertexAngles;
    var chw = constants.cornerHalfWidth;
    var chl = constants.cornerLen / 2;

    var scaleX;
    var scaleY;

    var mainDrag = dragBox.makeDragger(layers, 'path', 'maindrag', fullLayout.dragmode === false ? 'none' : 'crosshair');

    d3.select(mainDrag)
        .attr('d', _this.pathSubplot())
        .attr('transform', strTranslate(cx, cy));

    mainDrag.onmousemove = function(evt) {
        Fx.hover(gd, evt, _this.id);
        gd._fullLayout._lasthover = mainDrag;
        gd._fullLayout._hoversubplot = _this.id;
    };

    mainDrag.onmouseout = function(evt) {
        if(gd._dragging) return;
        dragElement.unhover(gd, evt);
    };

    var dragOpts = {
        element: mainDrag,
        gd: gd,
        subplot: _this.id,
        plotinfo: {
            id: _this.id,
            xaxis: _this.xaxis,
            yaxis: _this.yaxis
        },
        xaxes: [_this.xaxis],
        yaxes: [_this.yaxis]
    };

    // mouse px position at drag start (0), move (1)
    var x0, y0;
    // radial distance from circle center at drag start (0), move (1)
    var r0, r1;
    // zoombox persistent quantities
    var path0, dimmed, lum;
    // zoombox, corners elements
    var zb, corners;

    function norm(x, y) {
        return Math.sqrt(x * x + y * y);
    }

    function xy2r(x, y) {
        return norm(x - cxx, y - cyy);
    }

    function xy2a(x, y) {
        return Math.atan2(cyy - y, x - cxx);
    }

    function ra2xy(r, a) {
        return [r * Math.cos(a), r * Math.sin(-a)];
    }

    function pathCorner(r, a) {
        if(r === 0) return _this.pathSector(2 * chw);

        var da = chl / r;
        var am = a - da;
        var ap = a + da;
        var rb = Math.max(0, Math.min(r, radius));
        var rm = rb - chw;
        var rp = rb + chw;

        return 'M' + ra2xy(rm, am) +
            'A' + [rm, rm] + ' 0,0,0 ' + ra2xy(rm, ap) +
            'L' + ra2xy(rp, ap) +
            'A' + [rp, rp] + ' 0,0,1 ' + ra2xy(rp, am) +
            'Z';
    }

    // (x,y) is the pt at middle of the va0 <-> va1 edge
    //
    // ... we could eventually add another mode for cursor
    // angles 'close to' enough to a particular vertex.
    function pathCornerForPolygons(r, va0, va1) {
        if(r === 0) return _this.pathSector(2 * chw);

        var xy0 = ra2xy(r, va0);
        var xy1 = ra2xy(r, va1);
        var x = clampTiny((xy0[0] + xy1[0]) / 2);
        var y = clampTiny((xy0[1] + xy1[1]) / 2);
        var innerPts, outerPts;

        if(x && y) {
            var m = y / x;
            var mperp = -1 / m;
            var midPts = findXYatLength(chw, m, x, y);
            innerPts = findXYatLength(chl, mperp, midPts[0][0], midPts[0][1]);
            outerPts = findXYatLength(chl, mperp, midPts[1][0], midPts[1][1]);
        } else {
            var dx, dy;
            if(y) {
                // horizontal handles
                dx = chl;
                dy = chw;
            } else {
                // vertical handles
                dx = chw;
                dy = chl;
            }
            innerPts = [[x - dx, y - dy], [x + dx, y - dy]];
            outerPts = [[x - dx, y + dy], [x + dx, y + dy]];
        }

        return 'M' + innerPts.join('L') +
            'L' + outerPts.reverse().join('L') + 'Z';
    }

    function zoomPrep() {
        r0 = null;
        r1 = null;
        path0 = _this.pathSubplot();
        dimmed = false;

        var polarLayoutNow = gd._fullLayout[_this.id];
        lum = Color.luminosity(polarLayoutNow.bgcolor);

        zb = dragBox.makeZoombox(zoomlayer, lum, cx, cy, path0);
        zb.attr('fill-rule', 'evenodd');
        corners = dragBox.makeCorners(zoomlayer, cx, cy);
        clearOutline(gd);
    }

    // N.B. this sets scoped 'r0' and 'r1'
    // return true if 'valid' zoom distance, false otherwise
    function clampAndSetR0R1(rr0, rr1) {
        rr1 = Math.max(Math.min(rr1, radius), innerRadius);

        // starting or ending drag near center (outer edge),
        // clamps radial distance at origin (at r=radius)
        if(rr0 < OFFEDGE) rr0 = 0;
        else if((radius - rr0) < OFFEDGE) rr0 = radius;
        else if(rr1 < OFFEDGE) rr1 = 0;
        else if((radius - rr1) < OFFEDGE) rr1 = radius;

        // make sure r0 < r1,
        // to get correct fill pattern in path1 below
        if(Math.abs(rr1 - rr0) > MINZOOM) {
            if(rr0 < rr1) {
                r0 = rr0;
                r1 = rr1;
            } else {
                r0 = rr1;
                r1 = rr0;
            }
            return true;
        } else {
            r0 = null;
            r1 = null;
            return false;
        }
    }

    function applyZoomMove(path1, cpath) {
        path1 = path1 || path0;
        cpath = cpath || 'M0,0Z';

        zb.attr('d', path1);
        corners.attr('d', cpath);
        dragBox.transitionZoombox(zb, corners, dimmed, lum);
        dimmed = true;

        var updateObj = {};
        computeZoomUpdates(updateObj);
        gd.emit('plotly_relayouting', updateObj);
    }

    function zoomMove(dx, dy) {
        dx = dx * scaleX;
        dy = dy * scaleY;

        var x1 = x0 + dx;
        var y1 = y0 + dy;

        var rr0 = xy2r(x0, y0);
        var rr1 = Math.min(xy2r(x1, y1), radius);
        var a0 = xy2a(x0, y0);
        var path1;
        var cpath;

        if(clampAndSetR0R1(rr0, rr1)) {
            path1 = path0 + _this.pathSector(r1);
            if(r0) path1 += _this.pathSector(r0);
            // keep 'starting' angle
            cpath = pathCorner(r0, a0) + pathCorner(r1, a0);
        }
        applyZoomMove(path1, cpath);
    }

    function findPolygonRadius(x, y, va0, va1) {
        var xy = helpers.findIntersectionXY(va0, va1, va0, [x - cxx, cyy - y]);
        return norm(xy[0], xy[1]);
    }

    function zoomMoveForPolygons(dx, dy) {
        var x1 = x0 + dx;
        var y1 = y0 + dy;
        var a0 = xy2a(x0, y0);
        var a1 = xy2a(x1, y1);
        var vangles0 = findEnclosingVertexAngles(a0, vangles);
        var vangles1 = findEnclosingVertexAngles(a1, vangles);
        var rr0 = findPolygonRadius(x0, y0, vangles0[0], vangles0[1]);
        var rr1 = Math.min(findPolygonRadius(x1, y1, vangles1[0], vangles1[1]), radius);
        var path1;
        var cpath;

        if(clampAndSetR0R1(rr0, rr1)) {
            path1 = path0 + _this.pathSector(r1);
            if(r0) path1 += _this.pathSector(r0);
            // keep 'starting' angle here too
            cpath = [
                pathCornerForPolygons(r0, vangles0[0], vangles0[1]),
                pathCornerForPolygons(r1, vangles0[0], vangles0[1])
            ].join(' ');
        }
        applyZoomMove(path1, cpath);
    }

    function zoomDone() {
        dragBox.removeZoombox(gd);

        if(r0 === null || r1 === null) return;
        var updateObj = {};
        computeZoomUpdates(updateObj);

        dragBox.showDoubleClickNotifier(gd);

        Registry.call('_guiRelayout', gd, updateObj);
    }

    function computeZoomUpdates(update) {
        var rl = radialAxis._rl;
        var m = (rl[1] - rl[0]) / (1 - innerRadius / radius) / radius;
        var newRng = [
            rl[0] + (r0 - innerRadius) * m,
            rl[0] + (r1 - innerRadius) * m
        ];
        update[_this.id + '.radialaxis.range'] = newRng;
    }

    function zoomClick(numClicks, evt) {
        var clickMode = gd._fullLayout.clickmode;

        dragBox.removeZoombox(gd);

        // TODO double once vs twice logic (autorange vs fixed range)
        if(numClicks === 2) {
            var updateObj = {};
            for(var k in _this.viewInitial) {
                updateObj[_this.id + '.' + k] = _this.viewInitial[k];
            }

            gd.emit('plotly_doubleclick', null);
            Registry.call('_guiRelayout', gd, updateObj);
        }

        if(clickMode.indexOf('select') > -1 && numClicks === 1) {
            selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, dragOpts);
        }

        if(clickMode.indexOf('event') > -1) {
            Fx.click(gd, evt, _this.id);
        }
    }

    dragOpts.prepFn = function(evt, startX, startY) {
        var dragModeNow = gd._fullLayout.dragmode;

        var bbox = mainDrag.getBoundingClientRect();
        gd._fullLayout._calcInverseTransform(gd);
        var inverse = gd._fullLayout._invTransform;
        scaleX = gd._fullLayout._invScaleX;
        scaleY = gd._fullLayout._invScaleY;
        var transformedCoords = Lib.apply3DTransform(inverse)(startX - bbox.left, startY - bbox.top);
        x0 = transformedCoords[0];
        y0 = transformedCoords[1];

        // need to offset x/y as bbox center does not
        // match origin for asymmetric polygons
        if(vangles) {
            var offset = helpers.findPolygonOffset(radius, sectorInRad[0], sectorInRad[1], vangles);
            x0 += cxx + offset[0];
            y0 += cyy + offset[1];
        }

        switch(dragModeNow) {
            case 'zoom':
                dragOpts.clickFn = zoomClick;

                if(!isSmith) {
                    if(vangles) {
                        dragOpts.moveFn = zoomMoveForPolygons;
                    } else {
                        dragOpts.moveFn = zoomMove;
                    }

                    dragOpts.doneFn = zoomDone;
                    zoomPrep(evt, startX, startY);
                }
                break;
            case 'select':
            case 'lasso':
                prepSelect(evt, startX, startY, dragOpts, dragModeNow);
                break;
        }
    };

    dragElement.init(dragOpts);
};

proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) {
    var _this = this;
    var gd = _this.gd;
    var layers = _this.layers;
    var radius = _this.radius;
    var innerRadius = _this.innerRadius;
    var cx = _this.cx;
    var cy = _this.cy;
    var radialAxis = _this.radialAxis;
    var bl = constants.radialDragBoxSize;
    var bl2 = bl / 2;

    if(!radialAxis.visible) return;

    var angle0 = deg2rad(_this.radialAxisAngle);
    var rl = radialAxis._rl;
    var rl0 = rl[0];
    var rl1 = rl[1];
    var rbase = rl[rngIndex];
    var m = 0.75 * (rl[1] - rl[0]) / (1 - _this.getHole(polarLayout)) / radius;

    var tx, ty, className;
    if(rngIndex) {
        tx = cx + (radius + bl2) * Math.cos(angle0);
        ty = cy - (radius + bl2) * Math.sin(angle0);
        className = 'radialdrag';
    } else {
        // the 'inner' box can get called:
        // - when polar.hole>0
        // - when polar.sector isn't a full circle
        // otherwise it is hidden behind the main drag.
        tx = cx + (innerRadius - bl2) * Math.cos(angle0);
        ty = cy - (innerRadius - bl2) * Math.sin(angle0);
        className = 'radialdrag-inner';
    }

    var radialDrag = dragBox.makeRectDragger(layers, className, 'crosshair', -bl2, -bl2, bl, bl);
    var dragOpts = {element: radialDrag, gd: gd};
    if(fullLayout.dragmode === false) {
        dragOpts.dragmode = false;
    }

    updateElement(d3.select(radialDrag), radialAxis.visible && innerRadius < radius, {
        transform: strTranslate(tx, ty)
    });

    // move function (either rotate or re-range flavor)
    var moveFn2;
    // rotate angle on done
    var angle1;
    // re-range range[1] (or range[0]) on done
    var rprime;

    function moveFn(dx, dy) {
        if(moveFn2) {
            moveFn2(dx, dy);
        } else {
            var dvec = [dx, -dy];
            var rvec = [Math.cos(angle0), Math.sin(angle0)];
            var comp = Math.abs(Lib.dot(dvec, rvec) / Math.sqrt(Lib.dot(dvec, dvec)));

            // mostly perpendicular motions rotate,
            // mostly parallel motions re-range
            if(!isNaN(comp)) {
                moveFn2 = comp < 0.5 ? rotateMove : rerangeMove;
            }
        }

        var update = {};
        computeRadialAxisUpdates(update);
        gd.emit('plotly_relayouting', update);
    }

    function computeRadialAxisUpdates(update) {
        if(angle1 !== null) {
            update[_this.id + '.radialaxis.angle'] = angle1;
        } else if(rprime !== null) {
            update[_this.id + '.radialaxis.range[' + rngIndex + ']'] = rprime;
        }
    }

    function doneFn() {
        if(angle1 !== null) {
            Registry.call('_guiRelayout', gd, _this.id + '.radialaxis.angle', angle1);
        } else if(rprime !== null) {
            Registry.call('_guiRelayout', gd, _this.id + '.radialaxis.range[' + rngIndex + ']', rprime);
        }
    }

    function rotateMove(dx, dy) {
        // disable for inner drag boxes
        if(rngIndex === 0) return;

        var x1 = tx + dx;
        var y1 = ty + dy;

        angle1 = Math.atan2(cy - y1, x1 - cx);
        if(_this.vangles) angle1 = snapToVertexAngle(angle1, _this.vangles);
        angle1 = rad2deg(angle1);

        var transform = strTranslate(cx, cy) + strRotate(-angle1);
        layers['radial-axis'].attr('transform', transform);
        layers['radial-line'].select('line').attr('transform', transform);

        var fullLayoutNow = _this.gd._fullLayout;
        var polarLayoutNow = fullLayoutNow[_this.id];
        _this.updateRadialAxisTitle(fullLayoutNow, polarLayoutNow, angle1);
    }

    function rerangeMove(dx, dy) {
        // project (dx, dy) unto unit radial axis vector
        var dr = Lib.dot([dx, -dy], [Math.cos(angle0), Math.sin(angle0)]);
        rprime = rbase - m * dr;

        // make sure rprime does not change the range[0] -> range[1] sign
        if((m > 0) !== (rngIndex ? rprime > rl0 : rprime < rl1)) {
            rprime = null;
            return;
        }

        var fullLayoutNow = gd._fullLayout;
        var polarLayoutNow = fullLayoutNow[_this.id];

        // update radial range -> update c2g -> update _m,_b
        radialAxis.range[rngIndex] = rprime;
        radialAxis._rl[rngIndex] = rprime;
        _this.updateRadialAxis(fullLayoutNow, polarLayoutNow);

        _this.xaxis.setRange();
        _this.xaxis.setScale();
        _this.yaxis.setRange();
        _this.yaxis.setScale();

        var hasRegl = false;

        for(var traceType in _this.traceHash) {
            var moduleCalcData = _this.traceHash[traceType];
            var moduleCalcDataVisible = Lib.filterVisible(moduleCalcData);
            var _module = moduleCalcData[0][0].trace._module;
            _module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow);
            if(Registry.traceIs(traceType, 'gl') && moduleCalcDataVisible.length) hasRegl = true;
        }

        if(hasRegl) {
            clearGlCanvases(gd);
            redrawReglTraces(gd);
        }
    }

    dragOpts.prepFn = function() {
        moveFn2 = null;
        angle1 = null;
        rprime = null;

        dragOpts.moveFn = moveFn;
        dragOpts.doneFn = doneFn;

        clearOutline(gd);
    };

    dragOpts.clampFn = function(dx, dy) {
        if(Math.sqrt(dx * dx + dy * dy) < constants.MINDRAG) {
            dx = 0;
            dy = 0;
        }
        return [dx, dy];
    };

    dragElement.init(dragOpts);
};

proto.updateAngularDrag = function(fullLayout) {
    var _this = this;
    var gd = _this.gd;
    var layers = _this.layers;
    var radius = _this.radius;
    var angularAxis = _this.angularAxis;
    var cx = _this.cx;
    var cy = _this.cy;
    var cxx = _this.cxx;
    var cyy = _this.cyy;
    var dbs = constants.angularDragBoxSize;

    var angularDrag = dragBox.makeDragger(layers, 'path', 'angulardrag', fullLayout.dragmode === false ? 'none' : 'move');
    var dragOpts = {element: angularDrag, gd: gd};

    if(fullLayout.dragmode === false) {
        dragOpts.dragmode = false;
    } else {
        d3.select(angularDrag)
            .attr('d', _this.pathAnnulus(radius, radius + dbs))
            .attr('transform', strTranslate(cx, cy))
            .call(setCursor, 'move');
    }

    function xy2a(x, y) {
        return Math.atan2(cyy + dbs - y, x - cxx - dbs);
    }

    // scatter trace, points and textpoints selections
    var scatterTraces = layers.frontplot.select('.scatterlayer').selectAll('.trace');
    var scatterPoints = scatterTraces.selectAll('.point');
    var scatterTextPoints = scatterTraces.selectAll('.textpoint');

    // mouse px position at drag start (0), move (1)
    var x0, y0;
    // angular axis angle rotation at drag start (0), move (1)
    var rot0, rot1;
    // induced radial axis rotation (only used on polygon grids)
    var rrot1;
    // angle about circle center at drag start
    var a0;

    function moveFn(dx, dy) {
        var fullLayoutNow = _this.gd._fullLayout;
        var polarLayoutNow = fullLayoutNow[_this.id];

        var x1 = x0 + dx * fullLayout._invScaleX;
        var y1 = y0 + dy * fullLayout._invScaleY;
        var a1 = xy2a(x1, y1);
        var da = rad2deg(a1 - a0);
        rot1 = rot0 + da;

        layers.frontplot.attr('transform',
            strTranslate(_this.xOffset2, _this.yOffset2) + strRotate([-da, cxx, cyy])
        );

        if(_this.vangles) {
            rrot1 = _this.radialAxisAngle + da;

            var trans = strTranslate(cx, cy) + strRotate(-da);
            var trans2 = strTranslate(cx, cy) + strRotate(-rrot1);

            layers.bg.attr('transform', trans);
            layers['radial-grid'].attr('transform', trans);
            layers['radial-axis'].attr('transform', trans2);
            layers['radial-line'].select('line').attr('transform', trans2);
            _this.updateRadialAxisTitle(fullLayoutNow, polarLayoutNow, rrot1);
        } else {
            _this.clipPaths.forTraces.select('path').attr('transform',
                strTranslate(cxx, cyy) + strRotate(da)
            );
        }

        // 'un-rotate' marker and text points
        scatterPoints.each(function() {
            var sel = d3.select(this);
            var xy = Drawing.getTranslate(sel);
            sel.attr('transform', strTranslate(xy.x, xy.y) + strRotate([da]));
        });
        scatterTextPoints.each(function() {
            var sel = d3.select(this);
            var tx = sel.select('text');
            var xy = Drawing.getTranslate(sel);
            // N.B rotate -> translate ordering matters
            sel.attr('transform', strRotate([da, tx.attr('x'), tx.attr('y')]) + strTranslate(xy.x, xy.y));
        });

        // update rotation -> range -> _m,_b
        angularAxis.rotation = Lib.modHalf(rot1, 360);
        _this.updateAngularAxis(fullLayoutNow, polarLayoutNow);

        if(_this._hasClipOnAxisFalse && !Lib.isFullCircle(_this.sectorInRad)) {
            scatterTraces.call(Drawing.hideOutsideRangePoints, _this);
        }

        var hasRegl = false;

        for(var traceType in _this.traceHash) {
            if(Registry.traceIs(traceType, 'gl')) {
                var moduleCalcData = _this.traceHash[traceType];
                var moduleCalcDataVisible = Lib.filterVisible(moduleCalcData);
                var _module = moduleCalcData[0][0].trace._module;
                _module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow);
                if(moduleCalcDataVisible.length) hasRegl = true;
            }
        }

        if(hasRegl) {
            clearGlCanvases(gd);
            redrawReglTraces(gd);
        }

        var update = {};
        computeRotationUpdates(update);
        gd.emit('plotly_relayouting', update);
    }

    function computeRotationUpdates(updateObj) {
        updateObj[_this.id + '.angularaxis.rotation'] = rot1;

        if(_this.vangles) {
            updateObj[_this.id + '.radialaxis.angle'] = rrot1;
        }
    }

    function doneFn() {
        scatterTextPoints.select('text').attr('transform', null);

        var updateObj = {};
        computeRotationUpdates(updateObj);
        Registry.call('_guiRelayout', gd, updateObj);
    }

    dragOpts.prepFn = function(evt, startX, startY) {
        var polarLayoutNow = fullLayout[_this.id];
        rot0 = polarLayoutNow.angularaxis.rotation;

        var bbox = angularDrag.getBoundingClientRect();
        x0 = startX - bbox.left;
        y0 = startY - bbox.top;

        gd._fullLayout._calcInverseTransform(gd);
        var transformedCoords = Lib.apply3DTransform(fullLayout._invTransform)(x0, y0);
        x0 = transformedCoords[0];
        y0 = transformedCoords[1];

        a0 = xy2a(x0, y0);

        dragOpts.moveFn = moveFn;
        dragOpts.doneFn = doneFn;

        clearOutline(gd);
    };

    // I don't what we should do in this case, skip we now
    if(_this.vangles && !Lib.isFullCircle(_this.sectorInRad)) {
        dragOpts.prepFn = Lib.noop;
        setCursor(d3.select(angularDrag), null);
    }

    dragElement.init(dragOpts);
};

proto.isPtInside = function(d) {
    if(this.isSmith) return true;

    var sectorInRad = this.sectorInRad;
    var vangles = this.vangles;
    var thetag = this.angularAxis.c2g(d.theta);
    var radialAxis = this.radialAxis;
    var r = radialAxis.c2l(d.r);
    var rl = radialAxis._rl;

    var fn = vangles ? helpers.isPtInsidePolygon : Lib.isPtInsideSector;
    return fn(r, thetag, rl, sectorInRad, vangles);
};

proto.pathArc = function(r) {
    var sectorInRad = this.sectorInRad;
    var vangles = this.vangles;
    var fn = vangles ? helpers.pathPolygon : Lib.pathArc;
    return fn(r, sectorInRad[0], sectorInRad[1], vangles);
};

proto.pathSector = function(r) {
    var sectorInRad = this.sectorInRad;
    var vangles = this.vangles;
    var fn = vangles ? helpers.pathPolygon : Lib.pathSector;
    return fn(r, sectorInRad[0], sectorInRad[1], vangles);
};

proto.pathAnnulus = function(r0, r1) {
    var sectorInRad = this.sectorInRad;
    var vangles = this.vangles;
    var fn = vangles ? helpers.pathPolygonAnnulus : Lib.pathAnnulus;
    return fn(r0, r1, sectorInRad[0], sectorInRad[1], vangles);
};

proto.pathSubplot = function() {
    var r0 = this.innerRadius;
    var r1 = this.radius;
    return r0 ? this.pathAnnulus(r0, r1) : this.pathSector(r1);
};

proto.fillViewInitialKey = function(key, val) {
    if(!(key in this.viewInitial)) {
        this.viewInitial[key] = val;
    }
};

function strTickLayout(axLayout) {
    var out = axLayout.ticks + String(axLayout.ticklen) + String(axLayout.showticklabels);
    if('side' in axLayout) out += axLayout.side;
    return out;
}

// Finds the bounding box of a given circle sector,
// inspired by https://math.stackexchange.com/q/1852703
//
// assumes:
// - sector[0] < sector[1]
// - counterclockwise rotation
function computeSectorBBox(sector) {
    var s0 = sector[0];
    var s1 = sector[1];
    var arc = s1 - s0;
    var a0 = mod(s0, 360);
    var a1 = a0 + arc;

    var ax0 = Math.cos(deg2rad(a0));
    var ay0 = Math.sin(deg2rad(a0));
    var ax1 = Math.cos(deg2rad(a1));
    var ay1 = Math.sin(deg2rad(a1));

    var x0, y0, x1, y1;

    if((a0 <= 90 && a1 >= 90) || (a0 > 90 && a1 >= 450)) {
        y1 = 1;
    } else if(ay0 <= 0 && ay1 <= 0) {
        y1 = 0;
    } else {
        y1 = Math.max(ay0, ay1);
    }

    if((a0 <= 180 && a1 >= 180) || (a0 > 180 && a1 >= 540)) {
        x0 = -1;
    } else if(ax0 >= 0 && ax1 >= 0) {
        x0 = 0;
    } else {
        x0 = Math.min(ax0, ax1);
    }

    if((a0 <= 270 && a1 >= 270) || (a0 > 270 && a1 >= 630)) {
        y0 = -1;
    } else if(ay0 >= 0 && ay1 >= 0) {
        y0 = 0;
    } else {
        y0 = Math.min(ay0, ay1);
    }

    if(a1 >= 360) {
        x1 = 1;
    } else if(ax0 <= 0 && ax1 <= 0) {
        x1 = 0;
    } else {
        x1 = Math.max(ax0, ax1);
    }

    return [x0, y0, x1, y1];
}

function snapToVertexAngle(a, vangles) {
    var fn = function(v) { return Lib.angleDist(a, v); };
    var ind = Lib.findIndexOfMin(vangles, fn);
    return vangles[ind];
}

function updateElement(sel, showAttr, attrs) {
    if(showAttr) {
        sel.attr('display', null);
        sel.attr(attrs);
    } else if(sel) {
        sel.attr('display', 'none');
    }
    return sel;
}