function getButtonGroups(gd) {
    var fullLayout = gd._fullLayout;
    var fullData = gd._fullData;
    var context = gd._context;

    function match(name, B) {
        if(typeof B === 'string') {
            if(B.toLowerCase() === name.toLowerCase()) return true;
        } else {
            var v0 = B.name;
            var v1 = (B._cat || B.name);

            if(v0 === name || v1 === name.toLowerCase()) return true;
        }
        return false;
    }

    var layoutAdd = fullLayout.modebar.add;
    if(typeof layoutAdd === 'string') layoutAdd = [layoutAdd];

    var layoutRemove = fullLayout.modebar.remove;
    if(typeof layoutRemove === 'string') layoutRemove = [layoutRemove];

    var buttonsToAdd = context.modeBarButtonsToAdd.concat(
        layoutAdd.filter(function(e) {
            for(var i = 0; i < context.modeBarButtonsToRemove.length; i++) {
                if(match(e, context.modeBarButtonsToRemove[i])) return false;
            }
            return true;
        })
    );

    var buttonsToRemove = context.modeBarButtonsToRemove.concat(
        layoutRemove.filter(function(e) {
            for(var i = 0; i < context.modeBarButtonsToAdd.length; i++) {
                if(match(e, context.modeBarButtonsToAdd[i])) return false;
            }
            return true;
        })
    );

    var hasCartesian = fullLayout._has('cartesian');
    var hasGL3D = fullLayout._has('gl3d');
    var hasGeo = fullLayout._has('geo');
    var hasPie = fullLayout._has('pie');
    var hasFunnelarea = fullLayout._has('funnelarea');
    var hasTernary = fullLayout._has('ternary');
    var hasMapbox = fullLayout._has('mapbox');
    var hasMap = fullLayout._has('map');
    var hasPolar = fullLayout._has('polar');
    var hasSmith = fullLayout._has('smith');
    var hasSankey = fullLayout._has('sankey');
    var allAxesFixed = areAllAxesFixed(fullLayout);
    var hasUnifiedHoverLabel = isUnifiedHover(fullLayout.hovermode);

    var groups = [];

    function addGroup(newGroup) {
        if(!newGroup.length) return;

        var out = [];

        for(var i = 0; i < newGroup.length; i++) {
            var name = newGroup[i];
            var B = modeBarButtons[name];
            var v0 = B.name.toLowerCase();
            var v1 = (B._cat || B.name).toLowerCase();
            var found = false;
            for(var q = 0; q < buttonsToRemove.length; q++) {
                var t = buttonsToRemove[q].toLowerCase();
                if(t === v0 || t === v1) {
                    found = true;
                    break;
                }
            }
            if(found) continue;
            out.push(modeBarButtons[name]);
        }

        groups.push(out);
    }

    // buttons common to all plot types
    var commonGroup = ['toImage'];
    if(context.showEditInChartStudio) commonGroup.push('editInChartStudio');
    else if(context.showSendToCloud) commonGroup.push('sendDataToCloud');
    addGroup(commonGroup);

    var zoomGroup = [];
    var hoverGroup = [];
    var resetGroup = [];
    var dragModeGroup = [];

    if((hasCartesian || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasMap + hasPolar + hasSmith > 1) {
        // graphs with more than one plot types get 'union buttons'
        // which reset the view or toggle hover labels across all subplots.
        hoverGroup = ['toggleHover'];
        resetGroup = ['resetViews'];
    } else if(hasGeo) {
        zoomGroup = ['zoomInGeo', 'zoomOutGeo'];
        hoverGroup = ['hoverClosestGeo'];
        resetGroup = ['resetGeo'];
    } else if(hasGL3D) {
        hoverGroup = ['hoverClosest3d'];
        resetGroup = ['resetCameraDefault3d', 'resetCameraLastSave3d'];
    } else if(hasMapbox) {
        zoomGroup = ['zoomInMapbox', 'zoomOutMapbox'];
        hoverGroup = ['toggleHover'];
        resetGroup = ['resetViewMapbox'];
    } else if(hasMap) {
        zoomGroup = ['zoomInMap', 'zoomOutMap'];
        hoverGroup = ['toggleHover'];
        resetGroup = ['resetViewMap'];
    } else if(hasPie) {
        hoverGroup = ['hoverClosestPie'];
    } else if(hasSankey) {
        hoverGroup = ['hoverClosestCartesian', 'hoverCompareCartesian'];
        resetGroup = ['resetViewSankey'];
    } else { // hasPolar, hasSmith, hasTernary
        // always show at least one hover icon.
        hoverGroup = ['toggleHover'];
    }
    // if we have cartesian, allow switching between closest and compare
    // regardless of what other types are on the plot, since they'll all
    // just treat any truthy hovermode as 'closest'
    if(hasCartesian) {
        hoverGroup.push('toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian');
    }
    if(hasNoHover(fullData) || hasUnifiedHoverLabel) {
        hoverGroup = [];
    }

    if(hasCartesian && !allAxesFixed) {
        zoomGroup = ['zoomIn2d', 'zoomOut2d', 'autoScale2d'];
        if(resetGroup[0] !== 'resetViews') resetGroup = ['resetScale2d'];
    }

    if(hasGL3D) {
        dragModeGroup = ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'];
    } else if((hasCartesian && !allAxesFixed) || hasTernary) {
        dragModeGroup = ['zoom2d', 'pan2d'];
    } else if(hasMapbox || hasMap || hasGeo) {
        dragModeGroup = ['pan2d'];
    } else if(hasPolar) {
        dragModeGroup = ['zoom2d'];
    }
    if(isSelectable(fullData)) {
        dragModeGroup.push('select2d', 'lasso2d');
    }

    var enabledHoverGroup = [];
    var enableHover = function(a) {
        // return if already added
        if(enabledHoverGroup.indexOf(a) !== -1) return;
        // should be in hoverGroup
        if(hoverGroup.indexOf(a) !== -1) {
            enabledHoverGroup.push(a);
        }
    };
    if(Array.isArray(buttonsToAdd)) {
        var newList = [];
        for(var i = 0; i < buttonsToAdd.length; i++) {
            var b = buttonsToAdd[i];
            if(typeof b === 'string') {
                b = b.toLowerCase();

                if(DRAW_MODES.indexOf(b) !== -1) {
                    // accept pre-defined drag modes i.e. shape drawing features as string
                    if(
                        fullLayout._has('mapbox') || fullLayout._has('map') || // draw shapes in paper coordinate (could be improved in future to support data coordinate, when there is no pitch)
                        fullLayout._has('cartesian') // draw shapes in data coordinate
                    ) {
                        dragModeGroup.push(b);
                    }
                } else if(b === 'togglespikelines') {
                    enableHover('toggleSpikelines');
                } else if(b === 'togglehover') {
                    enableHover('toggleHover');
                } else if(b === 'hovercompare') {
                    enableHover('hoverCompareCartesian');
                } else if(b === 'hoverclosest') {
                    enableHover('hoverClosestCartesian');
                    enableHover('hoverClosestGeo');
                    enableHover('hoverClosest3d');
                    enableHover('hoverClosestPie');
                } else if(b === 'v1hovermode') {
                    enableHover('hoverClosestCartesian');
                    enableHover('hoverCompareCartesian');
                    enableHover('hoverClosestGeo');
                    enableHover('hoverClosest3d');
                    enableHover('hoverClosestPie');
                }
            } else newList.push(b);
        }
        buttonsToAdd = newList;
    }

    addGroup(dragModeGroup);
    addGroup(zoomGroup.concat(resetGroup));
    addGroup(enabledHoverGroup);

    return appendButtonsToGroups(groups, buttonsToAdd);
}
ModeBar wrapper around 'create' and 'update',
chooses buttons to pass to ModeBar constructor based on
plot type and plot config.

@param {object} gd main plot object