function stripObj(d, keepFunction) {
        if(typeof d === 'function') {
            return keepFunction ? '_function_' : null;
        }
        if(Lib.isPlainObject(d)) {
            var o = {};
            var src;
            Object.keys(d).sort().forEach(function(v) {
                // remove private elements and functions
                // _ is for private, [ is a mistake ie [object Object]
                if(['_', '['].indexOf(v.charAt(0)) !== -1) return;

                // if a function, add if necessary then move on
                if(typeof d[v] === 'function') {
                    if(keepFunction) o[v] = '_function';
                    return;
                }

                // look for src/data matches and remove the appropriate one
                if(mode === 'keepdata') {
                    // keepdata: remove all ...src tags
                    if(v.substr(v.length - 3) === 'src') {
                        return;
                    }
                } else if(mode === 'keepstream') {
                    // keep sourced data if it's being streamed.
                    // similar to keepref, but if the 'stream' object exists
                    // in a trace, we will keep the data array.
                    src = d[v + 'src'];
                    if(typeof src === 'string' && src.indexOf(':') > 0) {
                        if(!Lib.isPlainObject(d.stream)) {
                            return;
                        }
                    }
                } else if(mode !== 'keepall') {
                    // keepref: remove sourced data but only
                    // if the source tag is well-formed
                    src = d[v + 'src'];
                    if(typeof src === 'string' && src.indexOf(':') > 0) {
                        return;
                    }
                }

                // OK, we're including this... recurse into it
                o[v] = stripObj(d[v], keepFunction);
            });
            return o;
        }

        var dIsArray = Array.isArray(d);
        var dIsTypedArray = Lib.isTypedArray(d);

        if((dIsArray || dIsTypedArray) && d.dtype && d.shape) {
            var bdata = d.bdata;
            return stripObj({
                dtype: d.dtype,
                shape: d.shape,

                bdata:
                    // case of ArrayBuffer
                    Lib.isArrayBuffer(bdata) ? b64encode.encode(bdata) :
                    // case of b64 string
                    bdata

            }, keepFunction);
        }

        if(dIsArray) {
            return d.map(function(x) {return stripObj(x, keepFunction);});
        }

        if(dIsTypedArray) {
            return Lib.simpleMap(d, Lib.identity);
        }

        // convert native dates to date strings...
        // mostly for external users exporting to plotly
        if(Lib.isJSDate(d)) return Lib.ms2DateTimeLocal(+d);

        return d;
    }
JSONify the graph data and layout

This function needs to recurse because some src can be inside
sub-objects.

It also strips out functions and private (starts with _) elements.
Therefore, we can add temporary things to data and layout that don't
get saved.

@param gd The graphDiv
@param {Boolean} dataonly If true, don't return layout.
@param {'keepref'|'keepdata'|'keepall'} [mode='keepref'] Filter what's kept
     keepref: remove data for which there's a src present
         eg if there's xsrc present (and xsrc is well-formed,
         ie has : and some chars before it), strip out x
     keepdata: remove all src tags, don't remove the data itself
     keepall: keep data and src
@param {String} output If you specify 'object', the result will not be stringified
@param {Boolean} useDefaults If truthy, use _fullLayout and _fullData
@param {Boolean} includeConfig If truthy, include _context
@returns {Object|String}