Source: json-schema-viewer.js

//fix for IE
if (!window.location.origin) {
  window.location.origin = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port: '');
}


if (typeof JSV === 'undefined') {
    /**
     * JSV namespace for JSON Schema Viewer.
     * @namespace
     */
    var JSV = {
        /**
         * The root schema to load.
         */
        schema: '',

        /**
         * If true, render diagram only on init, without the jQuery Mobile UI.
         * The legend and nav tools will be rendered with any event listeners.
         */
        plain: false,

        /**
         * The version of the schema.
         */
        version: '',

        /**
         * Currently focused node
         */
        focusNode: false,

        /**
         * Currently loaded example
         */
        example: false,

        /**
         * @property {object} treeData The diagram nodes
         */
        treeData: null,

        /**
         * The initialization status of the viewer page
         */
        viewerInit: false,

        /**
         * The current viewer height
         */
        viewerHeight: 0,

        /**
         * The current viewer width
         */
        viewerWidth: 0,

        /**
         * The default duration of the node transitions
         */
        duration: 750,

        /**
         * Counter for generating unique ids
         */
        counter: 0,

        maxLabelLength: 0,

        /**
         * Default maximum depth for recursive schemas
         */
        maxDepth: 20,

        /**
         * @property {object} labels Nodes to render as non-clickable in the tree. They will auto-expand if child nodes are present.
         */
        labels: {
            allOf: true,
            anyOf: true,
            oneOf: true,
            'object{ }': true
        },

        /**
         * @property {array} baseSvg The base SVG element for the d3 diagram
         */
        baseSvg: null,

        /**
         * @property {array} svgGroup SVG group which holds all nodes and which the zoom Listener can act upon.
         */
        svgGroup: null,

        /**
         * Initializes the viewer.
         *
         * @param {object} config The configuration.
         * @param {function} callback Function to run after schemas are loaded and
         * diagram is created.
         */

        init: function(config, callback) {
            var i;
            //apply config
            for (i in config) {
                if (JSV.hasOwnProperty(i)) {
                    JSV[i] = config[i];
                }
            }

            if(JSV.plain) {
              JSV.createDiagram(callback);
                          //setup controls
                        d3.selectAll('#zoom-controls>a').on('click', JSV.zoomClick);
                        d3.select('#tree-controls>a#reset-tree').on('click', JSV.resetViewer);
              JSV.viewerInit = true;
              return;
            }

            JSV.contentHeight();
            JSV.resizeViewer();

            $(document).on('pagecontainertransition', this.contentHeight);
            $(window).on('throttledresize orientationchange', this.contentHeight);
            $(window).on('resize', this.contentHeight);

            JSV.resizeBtn();
            $(document).on('pagecontainershow', JSV.resizeBtn);
            $(window).on('throttledresize', JSV.resizeBtn);

            var cb = function() {
                callback();

                //setup search
                var items = [];

                JSV.visit(JSV.treeData, function(me) {
                    if (me.isReal) {
                        items.push(me.plainName + '|' + JSV.getNodePath(me).join('-'));
                    }
                }, function(me) {
                    return me.children || me._children;
                });

                items.sort();
                JSV.buildSearchList(items, true);

                $('#loading').fadeOut('slow');
            };
            JSV.createDiagram(cb);

            JSV.initValidator();

            //initialize error popup
            $( '#popup-error' ).enhanceWithin().popup();

            ///highlight plugin
            $.fn.highlight = function (str, className, quote) {
                var string = quote ? '\\"\\b'+str+'\\b\\"' : '\\b'+str+'\\b',
                    regex = new RegExp(string, 'g');

                return this.each(function () {
                    this.innerHTML = this.innerHTML.replace(regex, function(matched) {return '<span class="' + className + '">' + matched + '</span>';});
                });
            };

            //restore info-panel state
            $('body').on('pagecontainershow', function(event, ui) {
                var page = ui.toPage;

                if(page.attr('id') === 'viewer-page' && JSV.viewerInit) {
                    if(page.jqmData('infoOpen')) {
                        $('#info-panel'). panel('open');
                    }
                    //TODO: add this to 'pagecontainercreate' handler on refactor???
                    JSV.contentHeight();
                    if($('svg#jsv-tree').height() === 0) {
                        $('svg#jsv-tree').attr('width', $('#main-body').width())
                                         .attr('height', $('#main-body').height());
                        JSV.resizeViewer();
                        JSV.resetViewer();

                    }

                }
            });

            //store info-panel state
            $('body').on('pagecontainerbeforehide', function(event, ui) {
                var page = ui.prevPage;
                if(page.attr('id') === 'viewer-page') {
                    page.jqmData('infoOpen', !!page.find('#info-panel.ui-panel-open').length);
                }
            });

            //resize viewer on panel open/close
            $('#info-panel').on('panelopen', function() {
                var focus = JSV.focusNode;

                JSV.resizeViewer();
                if(focus) {
                    d3.select('#n-' + focus.id).classed('focus',true);
                    JSV.setPermalink(focus);
                }
            });

            $('#info-panel').on('panelclose', function() {
                var focus = JSV.focusNode;

                JSV.resizeViewer();
                if (focus) {
                    d3.select('#n-' + focus.id).classed('focus', false);
                    $('#permalink').html('Select a Node...');
                    $('#sharelink').val('');
                }
            });

            //scroll example/schema when tab is activated
            $('#info-panel').on( 'tabsactivate', function( event, ui ) {
                var id = ui.newPanel.attr('id');

                if(id === 'info-tab-example' || id === 'info-tab-schema') {
                    var pre = ui.newPanel.find('pre'),
                        highEl = pre.find('span.highlight')[0];

                    if(highEl) {
                        pre.scrollTo(highEl, 900);
                    }
                }
            });

            //setup example links
            $('.load-example').each(function(idx, link) {
                var ljq = $(link);
                ljq.on('click', function(evt) {
                    evt.preventDefault();
                    JSV.loadInputExample(link.href, ljq.data('target'));
                });
            });

            //setup controls
            d3.selectAll('#zoom-controls>a').on('click', JSV.zoomClick);
            d3.select('#tree-controls>a#reset-tree').on('click', JSV.resetViewer);

            $('#sharelink').on('click', function () {
               $(this).select();
            });

            JSV.viewerInit = true;

        },

        /**
         * (Re)set the viewer page height, set the diagram dimensions.
         */
        contentHeight: function() {
            var screen = $.mobile.getScreenHeight(),
                header = $('.ui-header').hasClass('ui-header-fixed') ? $('.ui-header').outerHeight() - 1 : $('.ui-header').outerHeight(),
                footer = $('.ui-footer').hasClass('ui-footer-fixed') ? $('.ui-footer').outerHeight() - 1 : $('.ui-footer').outerHeight(),
                contentCurrent = $('#main-body.ui-content').outerHeight() - $('#main-body.ui-content').height(),
                content = screen - header - footer - contentCurrent;

            $('#main-body.ui-content').css('min-height', content + 'px');
        },

        /**
         * Hides navbar button text on smaller window sizes.
         *
         * @param {number} minSize The navbar width breakpoint.
         */
        resizeBtn: function(minSize) {
            var bp = typeof minSize  === 'number' ? minSize : 800;
            var activePage = $.mobile.pageContainer.pagecontainer('getActivePage');
            if ($('.md-navbar', activePage).width() <= bp) {
                $('.md-navbar .md-flex-btn.ui-btn-icon-left').toggleClass('ui-btn-icon-notext ui-btn-icon-left');
            } else {
                $('.md-navbar .md-flex-btn.ui-btn-icon-notext').toggleClass('ui-btn-icon-left ui-btn-icon-notext');
            }
        },

        /**
         * Set version of the schema and the content
         * of any elemant with the class *schema-version*.
         *
         * @param {string} version
         */
        setVersion: function(version) {
            JSV.version = version;

            $('.schema-version').text(version);
        },

        /**
         * Display an error message.
         *
         * @param {string} msg The message to display.
         */
        showError: function(msg) {
            $('#popup-error .error-message').html(msg);
            $('#popup-error').popup('open');
        },

        initValidator: function() {
            var opts = {
                readAsDefault: 'Text',
                on: {
                    load: function(e, file) {
                        var data = e.currentTarget.result;

                        try {
                            $.parseJSON(data);
                            //console.info(data);
                            $('#textarea-json').val(data);
                        } catch(err) {
                            //JSV.showError('Unable to parse JSON: <br/>' + e);
                            JSV.showError('Failed to load ' + file.name + '. The file is not valid JSON. <br/>The error: <i>' + err + '</i>');
                        }

                    },
                    error: function(e, file) {
                        var msg = 'Failed to load ' + file.name + '. ' + e.currentTarget.error.message;

                        JSV.showError(msg);
                    }
                }
            };


            $('#file-upload, #textarea-json').fileReaderJS(opts);
            $('body').fileClipboard(opts);


            $('#button-validate').click(function() {
                var result = JSV.validate();

                if (result) {
                    JSV.showValResult(result);
                }
                //console.info(result);
            });
        },

        /**
         * Validate using tv4 and currently loaded schema(s).
         */
        validate: function() {
            var data;

            try {
                 data = $.parseJSON($('#textarea-json').val());
            } catch(e) {
                JSV.showError('Unable to parse JSON: <br/>' + e);
            }

            if (data) {
                var stop = $('#checkbox-stop').is(':checked'),
                    strict = $('#checkbox-strict').is(':checked'),
                    schema = tv4.getSchemaMap()[JSV.schema],
                    result;

                if (stop) {
                    var r = tv4.validate(data, schema, false, strict);
                    result = {
                        valid: r,
                        errors: !r ? [tv4.error] : []
                    };
                } else {
                    result = tv4.validateMultiple(data, schema, false, strict);
                }

                return result;
            }

        },

        /**
         * Display the validation result
         *
         * @param {object} result A result object, ouput from [validate]{@link JSV.validate}
         */
        showValResult: function(result) {
            var cont = $('#validation-results'), ui;

            if(cont.children().length) {
                cont.css('opacity', 0);
            }

            if(result.valid) {
                cont.html('<p class=ui-content>JSON is valid!</p>');
            } else {
                ui = cont.html('<div class=ui-content>JSON is <b>NOT</b> valid!</div>');
                $.each(result.errors, function(i, err){
                    var me = JSV.buildValError(err, 'Error ' + (i+1) + ': ');

                    if(err.subErrors) {
                        $.each(err.subErrors, function(i, sub){
                            me.append(JSV.buildValError(sub, 'SubError ' + (i+1) + ': '));
                        });
                    }

                    ui.children('.ui-content').first().append(me).enhanceWithin();
                });
            }

            cont.toggleClass('error', !result.valid);
            $('#validator-page').animate({
                scrollTop: $('#validation-results').offset().top + 20
            }, 1000);

            cont.fadeTo(350, 1);
        },

        /**
         * Build a collapsible validation block.
         *
         * @param {object} err The error object
         * @param {string} title The title for the error block
         */
        buildValError: function(err, title) {
            var main = '<div data-role="collapsible" data-collapsed="true" data-mini="true">' +
                            '<h4>' + (title || 'Error: ') + err.message + '</h4>' +
                            '<ul><li>Message: '+ err.message + '</li>' +
                            '<li>Data Path: '+ err.dataPath + '</li>' +
                            '<li>Schema Path: '+ err.schemaPath + '</li></ul></div>';

           return $(main);
        },

        /**
         * Set the content for the info panel.
         *
         * @param {object} node The d3 tree node.
         */
        setInfo: function(node) {
            var schema = $('#info-tab-schema');
            var def = $('#info-tab-def');
            var ex = $('#info-tab-example');

            var height = ($('#info-panel').innerHeight() - $('#info-panel .ui-panel-inner').outerHeight() + $('#info-panel #info-tabs').height()) -
                $('#info-panel #info-tabs-navbar').height() - (schema.outerHeight(true) - schema.height());

            $.each([schema, def, ex], function(i, e){
                e.height(height);
            });

            $('#info-definition').html(node.description || 'No definition provided.');
            $('#info-type').html(node.displayType.toString());

            if(node.translation) {
                var trans = $('<ul></ul>');

                $.each(node.translation, function(p, v) {
                    var li = $('<li>' + p + '</li>');
                    var ul = $('<ul></ul>');

                    $.each(v, function(i, e) {
                       ul.append('<li>' + e + '</li>');
                    });

                    trans.append(li.append(ul));
                });

                $('#info-translation').html(trans);
            } else {
                $('#info-translation').html('No translations available.');
            }


            JSV.createPre(schema, tv4.getSchema(node.schema), false, node.plainName);

            var example = (!node.example && node.parent && node.parent.example && node.parent.type === 'object' ? node.parent.example : node.example);

            if(example) {
                if(example !== JSV.example) {
                    $.getJSON(node.schema.match( /^(.*?)(?=[^\/]*\.json)/g ) + example, function(data) {
                        var pointer = example.split('#')[1];

                        if(pointer) {
                            data = jsonpointer.get(data, pointer);
                        }

                        JSV.createPre(ex, data, false, node.plainName);
                        JSV.example = example;
                    }).fail(function() {
                        ex.html('<h3>No example found.</h3>');
                        JSV.example = false;
                    });
                } else {
                    var pre = ex.find('pre'),
                        highEl;

                    pre.find('span.highlight').removeClass('highlight');

                    if(node.plainName) {
                        pre.highlight(node.plainName, 'highlight', true);
                    }
                    //scroll to highlighted property
                    highEl = pre.find('span.highlight')[0];

                    if (highEl) {
                        pre.scrollTo(highEl, 900);
                    }
                }
            } else {
                ex.html('<h3>No example available.</h3>');
                JSV.example = false;
            }
        },

        /**
         * Create a *pre* block and append it to the passed element.
         *
         * @param {object} el jQuery element
         * @param {object} obj The obj to stringify and display
         * @param {string} title The title for the new window
         * @param {string} exp The string to highlight
         */
        createPre: function(el, obj, title, exp) {
            var pre = $('<pre><code class="language-json">' + JSON.stringify(obj, null, '  ') + '</code></pre>');
            var btn = $('<a href="#" class="ui-btn ui-mini ui-icon-action ui-btn-icon-right">Open in new window</a>').click(function() {
                var w = window.open('', 'pre', null, true);

                $(w.document.body).html($('<div>').append(pre.clone().height('95%')).html());
                hljs.highlightBlock($(w.document.body).children('pre')[0]);
                $(w.document.body).append('<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.1/styles/default.min.css">');
                w.document.title = title || 'JSON Schema Viewer';
                w.document.close();
            });

            el.html(btn);

            if(exp) {
                pre.highlight(exp, 'highlight', true);
            }
            el.append(pre);
            pre.height(el.height() - btn.outerHeight(true) - (pre.outerHeight(true) - pre.height()));

            //scroll to highlighted property
            var highEl = pre.find('span.highlight')[0];

            if(highEl) {
                pre.scrollTo(highEl, 900);
            }
        },

        /**
         * Create a "breadcrumb" for the node.
         */
        compilePath: function(node, path) {
            var p;

            if(node.parent) {
                p = path ? node.name + ' > ' + path : node.name;
                return JSV.compilePath(node.parent, p);
            } else {
                p = path ? node.name + ' > ' + path : node.name;
            }

            return p;
        },

        /**
         * Load an example in the specified input field.
         */
        loadInputExample: function(uri, target) {
            $.getJSON(uri).done(function(fetched) {
                $('#' + target).val(JSON.stringify(fetched, null, '  '));
            }).fail(function(jqXHR, textStatus, errorThrown) {
                JSV.showError('Failed to load example: ' + errorThrown);
            });
        },

        /**
         * Create a "permalink" for the node.
         */
        setPermalink: function(node) {
            var uri = new URI(),
                path = JSV.getNodePath(node).join('-');

            //uri.search({ v: path});
            uri.hash($.mobile.activePage.attr('id') + '?v=' + path);
            $('#permalink').html(JSV.compilePath(node));
            $('#sharelink').val(uri.toString());
        },

        /**
         * Create an index-based path for the node from the root.
         */
        getNodePath: function(node, path) {
            var p = path || [],
                parent = node.parent;

            if(parent) {
                var children = parent.children || parent._children;

                p.unshift(children.indexOf(node));
                return JSV.getNodePath(parent, p);
            } else {
                return p;
            }
        },

        /**
         * Expand an index-based path for the node from the root.
         */
        expandNodePath: function(path) {
            var i,
                node = JSV.treeData; //start with root

            for (i = 0; i < path.length; i++) {
                if(node._children) {
                    JSV.expand(node);
                }
                node = node.children[path[i]];
            }

            JSV.update(JSV.treeData);
            JSV.centerNode(node);

            return node;
        },

        /**
         * Build Search.
         */
        buildSearchList: function(items, init) {
            var ul = $('ul#search-result');

            $.each(items, function(i,v) {
                var data = v.split('|');
                var li = $('<li/>').attr('data-icon', 'false').appendTo(ul);

                $('<a/>').attr('data-path', data[1]).text(data[0]).appendTo(li);
            });

            if(init) {
              ul.filterable();
            }
            ul.filterable('refresh');

            ul.on('click', function(e) {
                var path = $(e.target).attr('data-path');
                var node = JSV.expandNodePath(path.split('-'));

                JSV.flashNode(node);
            });

        },

        /**
         * Flash node text
         */
        flashNode: function(node, times) {
            var t = times || 4,
            text = $('#n-' + node.id + ' text');
            //flash node text
            while (t--) {
                text.fadeTo(350, 0).fadeTo(350, 1);
            }
        },

        /**
         * A recursive helper function for performing some setup by walking
         * through all nodes
         */
        visit: function (parent, visitFn, childrenFn) {
            if (!parent) {
                return;
            }
            visitFn(parent);

            var children = childrenFn(parent);

            if (children) {
                var count = children.length, i;
                for ( i = 0; i < count; i++) {
                    JSV.visit(children[i], visitFn, childrenFn);
                }
            }
        },

        /**
         * Create the tree data object from the schema(s)
         */
        compileData: function (schema, parent, name, real, depth) {
            // Ensure healthy amount of recursion
            depth = depth || 0;
            if (depth > this.maxDepth) {
                return;
            }
            var key, node,
                s = schema.$ref ? tv4.getSchema(schema.$ref) : schema,
                props = s.properties,
                items = s.items,
                owns = Object.prototype.hasOwnProperty,
                all = {},
                parentSchema = function(node) {
                    var schema = node.id || node.$ref || node.schema;

                    if (schema) {
                        return schema;
                    } else if (node.parentSchema) {
                        return parentSchema(node.parentSchema);
                    } else {
                        return null;
                    }
                };

            if (s.allOf) {
                all.allOf = s.allOf;
            }

            if (s.oneOf) {
                all.oneOf = s.oneOf;
            }

            if (s.anyOf) {
                all.anyOf = s.anyOf;
            }

            node = {
                description: schema.description || s.description,
                name: (schema.$ref && real ? name : false) || s.title || name || 'schema',
                isReal: real,
                plainName: name,
                type: s.type,
                displayType: s.type || (s['enum'] ? 'enum: ' + s['enum'].join(', ') : s.items ? 'array' : s.properties ? 'object' : 'ambiguous'),
                translation: schema.translation || s.translation,
                example: schema.example || s.example,
                opacity: real ? 1 : 0.5,
                required: s.required,
                schema: s.id || schema.$ref || parentSchema(parent),
                parentSchema: parent,
                deprecated: schema.deprecated || s.deprecated
            };

            node.require = parent && parent.required ? parent.required.indexOf(node.name) > -1 : false;

            if (parent) {
                if (node.name === 'item') {
                    node.parent = parent;
                    if(node.type) {
                        node.name = node.type;
                        parent.children.push(node);
                    }
                } else if (parent.name === 'item') {
                    parent.parent.children.push(node);
                } else {
                    parent.children.push(node);
                }
            } else {
                JSV.treeData = node;
            }

            if(node.type === 'array') {
                node.name += '[' + (s.minItems || ' ') + ']';
                node.minItems = s.minItems;
            }

            if(node.type === 'object' && node.name !== 'item') {
                node.name += '{ }';
            }

            if(props || items || all) {
                node.children = [];
            }

            for (key in props) {
                if (owns.call(props, key)) {
                    JSV.compileData(props[key],  node, key, true, depth + 1);
                }
            }

            for (key in all) {
                if (owns.call(all, key)) {
                    if(all[key]) {
                        var allNode = {
                            name: key,
                            children: [],
                            opacity: 0.5,
                            parentSchema: parent,
                            schema: schema.$ref || parentSchema(parent)
                        };

                        if (node.name === 'item') {
                            node.parent.children.push(allNode);
                        } else {
                            node.children.push(allNode);
                        }

                        for (var i = 0; i < all[key].length; i++) {
                            JSV.compileData(all[key][i], allNode, s.title || key, false, depth + 1);
                        }


                    }
                }
            }

            if (Object.prototype.toString.call(items) === '[object Object]') {
                JSV.compileData(items, node, 'item', false, depth + 1);
            } else if (Object.prototype.toString.call(items) === '[object Array]') {

                items.forEach(function(itm, idx, arr) {
                    JSV.compileData(itm, node, idx.toString(), false, depth + 1);
                });
            }

        },

        /**
         * Resize the diagram
         */
        resizeViewer: function() {
            JSV.viewerWidth = $('#main-body').width();
            JSV.viewerHeight = $('#main-body').height();
            if(JSV.focusNode) {
                JSV.centerNode(JSV.focusNode);
            }
        },

        /**
         * Reset the tree starting from the passed source.
         */
        resetTree: function (source, level) {
            JSV.visit(source, function(d) {
                if (d.children && d.children.length > 0 && d.depth > level && !JSV.labels[d.name]) {
                    JSV.collapse(d);
                    //d._children = d.children;
                    //d.children = null;
                }else if(JSV.labels[d.name]){
                    JSV.expand(d);
                }
            }, function(d) {
                if (d.children && d.children.length > 0) {
                    return d.children;
                } else if (d._children && d._children.length > 0) {
                    return d._children;
                } else {
                    return null;
                }
            });
        },

        /**
         * Reset and center the tree.
         */
        resetViewer: function () {
            //Firefox will choke if the viewer-page is not visible
            //TODO: fix on refactor to use pagecontainer event
            var page = $('#viewer-page');

            page.css('display','block');

            // Define the root
            var root = JSV.treeData;
            root.x0 = JSV.viewerHeight / 2;
            root.y0 = 0;

            // Layout the tree initially and center on the root node.
            // Call visit function to set initial depth
            JSV.tree.nodes(root);
            JSV.resetTree(root, 1);
            JSV.update(root);

            //reset the style for viewer-page
            page.css('display', '');

            JSV.centerNode(root, 4);
        },

        /**
         * Function to center node when clicked so node doesn't get lost when collapsing with large amount of children.
         */
        centerNode: function (source, ratioX) {
            var rX = ratioX ? ratioX : 2,
                zl = JSV.zoomListener,
                scale = zl.scale(),
                x = -source.y0 * scale + JSV.viewerWidth / rX,
                y = -source.x0 * scale + JSV.viewerHeight / 2;

            d3.select('g#node-group').transition()
                .duration(JSV.duration)
                .attr('transform', 'translate(' + x + ',' + y + ')scale(' + scale + ')');
            zl.scale(scale);
            zl.translate([x, y]);
        },

        /**
         * Helper functions for collapsing nodes.
         */
        collapse: function (d) {
            if (d.children) {
                d._children = d.children;
                //d._children.forEach(collapse);
                d.children = null;
            }
        },

        /**
         * Helper functions for expanding nodes.
         */
        expand: function (d) {
            if (d._children) {
                d.children = d._children;
                //d.children.forEach(expand);
                d._children = null;
            }

            if (d.children) {
                var count = d.children.length, i;
                for (i = 0; i < count; i++) {
                    if(JSV.labels[d.children[i].name]) {
                        JSV.expand(d.children[i]);
                    }
                }
            }
        },

        /**
         * Toggle children function
         */
        toggleChildren: function (d) {
            if (d.children) {
                JSV.collapse(d);
            } else if (d._children) {
                JSV.expand(d);
            }
            return d;
        },

        /**
         * Toggle children on node click.
         */
        click: function (d) {
            if(!JSV.labels[d.name]) {
                if (d3.event && d3.event.defaultPrevented) {return;} // click suppressed
                d = JSV.toggleChildren(d);
                JSV.update(d);
                JSV.centerNode(d);
            }
        },

        /**
         * Show info on node title click.
         */
       clickTitle: function (d) {
            if(!JSV.labels[d.name]) {
                if (d3.event && d3.event.defaultPrevented) {return;} // click suppressed
                var panel = $( '#info-panel' );

                if(JSV.focusNode) {
                    d3.select('#n-' + JSV.focusNode.id).classed('focus',false);
                }
                JSV.focusNode = d;
                JSV.centerNode(d);
                d3.select('#n-' + d.id).classed('focus',true);

                if(!JSV.plain) {
                  JSV.setPermalink(d);

                  $('#info-title')
                    .text('Info: ' + d.name)
                    .toggleClass('deprecated', !!d.deprecated);
                  JSV.setInfo(d);
                  panel.panel( 'open' );
                }
            }
        },

        /**
         * Zoom the tree
         */
        zoom: function () {
            JSV.svgGroup.attr('transform', 'translate(' + JSV.zoomListener.translate() + ')' + 'scale(' + JSV.zoomListener.scale() + ')');
        },

        /**
         * Perform the d3 zoom based on position and scale
         */
        interpolateZoom: function  (translate, scale) {
            return d3.transition().duration(350).tween('zoom', function () {
                var iTranslate = d3.interpolate(JSV.zoomListener.translate(), translate),
                    iScale = d3.interpolate(JSV.zoomListener.scale(), scale);
                return function (t) {
                    JSV.zoomListener
                        .scale(iScale(t))
                        .translate(iTranslate(t));
                    JSV.zoom();
                };
            });
        },

        /**
         * Click handler for the zoom control
         */
        zoomClick: function () {
            var clicked = d3.event.target,
                direction = 1,
                factor = 0.2,
                target_zoom = 1,
                center = [JSV.viewerWidth / 2, JSV.viewerHeight / 2],
                zl = JSV.zoomListener,
                extent = zl.scaleExtent(),
                translate = zl.translate(),
                translate0 = [],
                l = [],
                view = {x: translate[0], y: translate[1], k: zl.scale()};

            d3.event.preventDefault();
            direction = (this.id === 'zoom_in') ? 1 : -1;
            target_zoom = zl.scale() * (1 + factor * direction);

            if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; }

            translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
            view.k = target_zoom;
            l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];

            view.x += center[0] - l[0];
            view.y += center[1] - l[1];

            JSV.interpolateZoom([view.x, view.y], view.k);
        },

        /**
         * The zoomListener which calls the zoom function on the 'zoom' event constrained within the scaleExtents
         */
        zoomListener: null,

        /**
         * Sort the tree according to the node names
         */
        sortTree: function (tree) {
            tree.sort(function(a, b) {
                return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
            });
        },

        /**
         * The d3 diagonal projection for use by the node paths.
         */
        diagonal1: function(d) {
            var src = d.source,
                node = d3.select('#n-' + (src.id))[0][0],
                dia,
                width = 0 ;

            if(node) {
                width = node.getBBox().width;
            }

            dia = 'M' + (src.y + width) + ',' + src.x +
                'H' + (d.target.y - 30) + 'V' + d.target.x +
                //+ (d.target.children ? '' : 'h' + 30);
                ('h' + 30);

           return dia;
        },

        /**
         * Update the tree, removing or adding nodes from/to the passed source node
         */
        update: function (source) {
            var duration = JSV.duration;
            var root = JSV.treeData;
            // Compute the new height, function counts total children of root node and sets tree height accordingly.
            // This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
            // This makes the layout more consistent.
            var levelWidth = [1];
            var childCount = function(level, n) {

                if (n.children && n.children.length > 0) {
                    if (levelWidth.length <= level + 1) {levelWidth.push(0);}

                    levelWidth[level + 1] += n.children.length;
                    n.children.forEach(function(d) {
                        childCount(level + 1, d);
                    });
                }
            };
            childCount(0, root);
            var newHeight = d3.max(levelWidth) * 45; // 25 pixels per line
            JSV.tree.size([newHeight, JSV.viewerWidth]);

            // Compute the new tree layout.
            var nodes = JSV.tree.nodes(root).reverse(),
                links = JSV.tree.links(nodes);

            // Set widths between levels based on maxLabelLength.
            nodes.forEach(function(d) {
                d.y = (d.depth * (JSV.maxLabelLength * 8)); //maxLabelLength * 8px
                // alternatively to keep a fixed scale one can set a fixed depth per level
                // Normalize for fixed-depth by commenting out below line
                // d.y = (d.depth * 500); //500px per level.
            });
            // Update the nodes…
            var node = JSV.svgGroup.selectAll('g.node')
                .data(nodes, function(d) {
                    return d.id || (d.id = ++JSV.counter);
                });

            // Enter any new nodes at the parent's previous position.
            var nodeEnter = node.enter().append('g')
                .attr('class', function(d) {
                    return JSV.labels[d.name] ? 'node label' : 'node';
                })
                .classed('deprecated', function(d) {
                    return d.deprecated;
                })
                .attr('id', function(d, i) {
                    return 'n-' + d.id;
                })
                .attr('transform', function(d) {
                    return 'translate(' + source.y0 + ',' + source.x0 + ')';
                });

            nodeEnter.append('circle')
                //.attr('class', 'nodeCircle')
                .attr('r', 0)
                .classed('collapsed', function(d) {
                    return d._children ? true : false;
                })
                .on('click', JSV.click);

            nodeEnter.append('text')
                .attr('x', function(d) {
                    return 10;
    //                return d.children || d._children ? -10 : 10;
                })
                .attr('dy', '.35em')
                .attr('class', function(d) {
                    return (d.children || d._children) ? 'node-text node-branch' : 'node-text';
                })
                .classed('abstract', function(d) {
                    return d.opacity < 1;
                })
                .attr('text-anchor', function(d) {
                    //return d.children || d._children ? 'end' : 'start';
                    return 'start';
                })
                .text(function(d) {
                    return d.name + (d.require ? '*' : '');
                })
                .style('fill-opacity', 0)
                .on('click', JSV.clickTitle)
                .on('dblclick', function(d) {
                    JSV.click(d);
                    JSV.clickTitle(d);
                    d3.event.stopPropagation();
                });


            // Change the circle fill depending on whether it has children and is collapsed
            node.select('.node circle')
                .attr('r', 6.5)
                .classed('collapsed', function(d) {
                    return (d._children ? true : false);
                });

            // Transition nodes to their new position.
            var nodeUpdate = node.transition()
                .duration(duration)
                .attr('transform', function(d) {
                    return 'translate(' + d.y + ',' + d.x + ')';
                });

            // Fade the text in
            nodeUpdate.select('text')
                .style('fill-opacity', function(d) {
                    return d.opacity || 1;
                });

            // Transition exiting nodes to the parent's new position.
            var nodeExit = node.exit().transition()
                .duration(duration)
                .attr('transform', function(d) {
                    return 'translate(' + source.y + ',' + source.x + ')';
                })
                .remove();

            nodeExit.select('circle')
                .attr('r', 0);

            nodeExit.select('text')
                .style('fill-opacity', 0);

            // Update the links…
            var link = JSV.svgGroup.selectAll('path.link')
                .data(links, function(d) {
                    return d.target.id;
                });

            // Enter any new links at the parent's previous position.
            link.enter().insert('path', 'g')
                .attr('class', 'link')
                .attr('d', function(d) {
                    var o = {
                        x: source.x0,
                        y: source.y0
                    };

                    //console.info(d3.select('#n-'+d.source.id)[0][0].getBBox());

                    return JSV.diagonal1({
                        source: o,
                        target: o
                    });
                });

            // Transition links to their new position.
            link.transition()
                .duration(duration)
                .attr('d', JSV.diagonal1);

            // Transition exiting nodes to the parent's new position.
            link.exit().transition()
                .duration(duration)
                .attr('d', function(d) {
                    var o = {
                        x: source.x,
                        y: source.y
                    };
                    return JSV.diagonal1({
                        source: o,
                        target: o
                    });
                })
                .remove();

            // Stash the old positions for transition.
            nodes.forEach(function(d) {
                d.x0 = d.x;
                d.y0 = d.y;
            });
        },

        /**
         * Create the d3 diagram.
         *
         * @param {function} callback Function to run after the diagram is created
         */
        createDiagram: function(callback) {
            tv4.asyncLoad([JSV.schema], function() {

                JSV.compileData(tv4.getSchema(JSV.schema),false,'schema');

                // Calculate total nodes, max label length
                var totalNodes = 0;
                // panning variables
                //var panSpeed = 200;
                //var panBoundary = 20; // Within 20px from edges will pan when dragging.

                // size of the diagram
                var viewerWidth = JSV.viewerWidth;
                var viewerHeight = JSV.viewerHeight;

                JSV.zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on('zoom', JSV.zoom);

                JSV.baseSvg = d3.select('#main-body').append('svg')
                    .attr('id', 'jsv-tree')
                    .attr('class', 'overlay')
                    .attr('width', viewerWidth)
                    .attr('height', viewerHeight)
                    .call(JSV.zoomListener);

                JSV.tree = d3.layout.tree()
                    .size([viewerHeight, viewerWidth]);

                // Call JSV.visit function to establish maxLabelLength
                JSV.visit(JSV.treeData, function(d) {
                    totalNodes++;
                    JSV.maxLabelLength = Math.max(d.name.length, JSV.maxLabelLength);

                }, function(d) {
                    return d.children && d.children.length > 0 ? d.children : null;
                });

                // Sort the tree initially in case the JSON isn't in a sorted order.
                //JSV.sortTree();

                JSV.svgGroup = JSV.baseSvg.append('g')
                    .attr('id', 'node-group');

                // Layout the tree initially and center on the root node.
                JSV.resetViewer();

                JSV.centerNode(JSV.treeData, 4);

                // define the legend svg, attaching a class for styling
                var legendData = [{
                    text: 'Expanded',
                    y: 20
                }, {
                    text: 'Collapsed',
                    iconCls: 'collapsed',
                    y: 40
                }, {
                    text: 'Selected',
                    itemCls: 'focus',
                    y: 60
                },{
                    text: 'Required*',
                    y: 80
                },{
                    text: 'Object{ }',
                    iconCls: 'collapsed',
                    y: 100
                },{
                    text: 'Array[minimum #]',
                    iconCls: 'collapsed',
                    y: 120
                },{
                    text: 'Abstract Property',
                    itemCls: 'abstract',
                    y: 140,
                    opacity: 0.5
                },{
                    text: 'Deprecated',
                    itemCls: 'deprecated',
                    y: 160
                }];


                var legendSvg = d3.select('#legend-items').append('svg')
                    .attr('width', 170)
                    .attr('height', 180);

                // Update the nodes…
                var legendItem = legendSvg.selectAll('g.item-group')
                    .data(legendData)
                    .enter()
                    .append('g')
                    .attr('class', function(d) {
                        var cls = 'item-group ';

                        cls += d.itemCls || '';
                        return cls;
                    })
                    .attr('transform', function(d) {
                        return 'translate(10, ' + d.y + ')';
                    });

                legendItem.append('circle')
                    .attr('r', 6.5)
                    .attr('class', function(d) {
                        return d.iconCls;
                    });

                legendItem.append('text')
                    .attr('x', 15)
                    .attr('dy', '.35em')
                    .attr('class', 'item-text')
                    .attr('text-anchor', 'start')
                    .style('fill-opacity', function(d) {
                        return d.opacity || 1;
                    })
                    .text(function(d) {
                        return d.text;
                    });

                if(typeof callback === 'function') {callback();}
            });
        }
    };
}