function handleCartesian(gd, ev) {
    var button = ev.currentTarget;
    var astr = button.getAttribute('data-attr');
    var val = button.getAttribute('data-val') || true;
    var fullLayout = gd._fullLayout;
    var aobj = {};
    var axList = axisIds.list(gd, null, true);
    var allSpikesEnabled = fullLayout._cartesianSpikesEnabled;

    var ax, i;

    if(astr === 'zoom') {
        var mag = (val === 'in') ? 0.5 : 2;
        var r0 = (1 + mag) / 2;
        var r1 = (1 - mag) / 2;
        var axName;

        for(i = 0; i < axList.length; i++) {
            ax = axList[i];

            if(!ax.fixedrange) {
                axName = ax._name;
                if(val === 'auto') {
                    aobj[axName + '.autorange'] = true;
                } else if(val === 'reset') {
                    if(ax._rangeInitial0 === undefined && ax._rangeInitial1 === undefined) {
                        aobj[axName + '.autorange'] = true;
                    } else if(ax._rangeInitial0 === undefined) {
                        aobj[axName + '.autorange'] = ax._autorangeInitial;
                        aobj[axName + '.range'] = [null, ax._rangeInitial1];
                    } else if(ax._rangeInitial1 === undefined) {
                        aobj[axName + '.range'] = [ax._rangeInitial0, null];
                        aobj[axName + '.autorange'] = ax._autorangeInitial;
                    } else {
                        aobj[axName + '.range'] = [ax._rangeInitial0, ax._rangeInitial1];
                    }

                    // N.B. "reset" also resets showspikes
                    if(ax._showSpikeInitial !== undefined) {
                        aobj[axName + '.showspikes'] = ax._showSpikeInitial;
                        if(allSpikesEnabled === 'on' && !ax._showSpikeInitial) {
                            allSpikesEnabled = 'off';
                        }
                    }
                } else {
                    var rangeNow = [
                        ax.r2l(ax.range[0]),
                        ax.r2l(ax.range[1]),
                    ];

                    var rangeNew = [
                        r0 * rangeNow[0] + r1 * rangeNow[1],
                        r0 * rangeNow[1] + r1 * rangeNow[0]
                    ];

                    aobj[axName + '.range[0]'] = ax.l2r(rangeNew[0]);
                    aobj[axName + '.range[1]'] = ax.l2r(rangeNew[1]);
                }
            }
        }
    } else {
        // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y'
        if(astr === 'hovermode' && (val === 'x' || val === 'y')) {
            val = fullLayout._isHoriz ? 'y' : 'x';
            button.setAttribute('data-val', val);
        }

        aobj[astr] = val;
    }

    fullLayout._cartesianSpikesEnabled = allSpikesEnabled;

    Registry.call('_guiRelayout', gd, aobj);
}
ModeBar buttons configuration

@param {string} name
     name / id of the buttons (for tracking)
@param {string} title
     text that appears while hovering over the button,
     enter null, false or '' for no hover text
@param {string} icon
     svg icon object associated with the button
     can be linked to Plotly.Icons to use the default plotly icons
@param {string} [gravity]
     icon positioning
@param {function} click
     click handler associated with the button, a function of
     'gd' (the main graph object) and
     'ev' (the event object)
@param {string} [attr]
     attribute associated with button,
     use this with 'val' to keep track of the state
@param {*} [val]
     initial 'attr' value, can be a function of gd
@param {boolean} [toggle]
     is the button a toggle button?