/**
 * Abstract class for D3 components using
 * [Hierarchy Layout](https://github.com/d3/d3-hierarchy).
 * The Hierarchy component uses the root {@link Ext.data.TreeModel node} of a bound
 * {@link Ext.data.NodeStore node store} to compute positions of all nodes,
 * as well as objects representing the links from parent to child for each node.
 *
 * Each node is a `d3.hierarchy` instance. Several attributes are populated on each node:
 * - `data` - the Ext.data.TreeModel instance associated with the node.
 * - `parent` - the parent node, or null for the root.
 * - `children` - the array of child nodes, or null for leaf nodes.
 * - `value` - the node value, as returned by the value accessor.
 * - `depth` - the depth of the node, starting at 0 for the root.
 * - `x` - the minimum x-coordinate of the node position.
 * - `y` - the minimum y-coordinate of the node position.
 *
 * Each link is an object with two attributes:
 * - `source` - the parent node.
 * - `target` - the child node.
 *
 * The class also provides an ability to color code each node with the
 * {@link Ext.d3.axis.Color}.
 */
Ext.define('Ext.d3.hierarchy.Hierarchy', {
    extend: 'Ext.d3.svg.Svg',

    requires: [
        'Ext.d3.axis.Color',
        'Ext.plugin.MouseEnter'
    ],

    mixins: [
        'Ext.d3.mixin.ToolTip'
    ],

    config: {
        /**
         * @cfg {String} hierarchyCls
         * The class name added to all hieararchy components (subclasses).
         * See also {@link #componentCls}.
         */
        hierarchyCls: 'hierarchy',

        /**
         * @cfg {Ext.data.TreeModel} selection
         * The selected record. Typically used with {@link #bind binding}.
         */
        selection: null,

        /**
         * @cfg {Ext.d3.axis.Color} colorAxis
         * A {@link Ext.d3.axis.Color} config or an instance.
         * By default (if no 'colorAxis' config is given) all nodes
         * are assigned a unique color from the `d3.scale.category20c`
         * scale (until the colors run out, then we start to reuse them)
         * based on the value of the `name` field.
         */
        colorAxis: {
            scale: {
                type: 'ordinal',
                // The last 4 colors are shades of gray.
                range: 'd3.schemeCategory20c.slice(0, -4)'
            },
            field: 'name'
        },

        /**
         * @cfg {Function} nodeChildren
         * [Children](https://github.com/d3/d3-hierarchy/#hierarchy) accessor function.
         * Defaults to returning node's {@link Ext.data.NodeInterface#childNodes child nodes},
         * if the node is {@link Ext.data.NodeInterface#expanded expanded} or null otherwise
         * (meaning children of collapsed nodes are not rendered).
         * @param {Ext.data.TreeModel} record An instance of the TreeModel class.
         * @return {Ext.data.TreeModel[]}
         */
        nodeChildren: function(record) {
            return record.isExpanded() ? record.childNodes : null;
        },

        /**
         * @cfg {Function} nodeClass
         * A function that updates class attributes of a given selection.
         * By default adds the following classes to node elements:
         * - `x-d3-parent` - if a node is a parent node;
         * - `x-d3-leaf` - if a node is a leaf node;
         * - `x-d3-expanded` - if a node is expanded;
         * - `x-d3-root` - if a node is the root node (represents the root of the store);
         * - `x-d3-layout-root` - if a node is the root node of the current layout.
         * @param {d3.selection} selection
         */
        nodeClass: undefined,

        /**
         * @cfg {Function/String/String[]} nodeText
         * A function that returns a text string, given a component and node (d3.hierarchy)
         * instance. Alternatively, can be a field name or an array of field names used to fetch
         * the text. If array of field names is given, the first non-empty string will be used.
         * A node holds a reference to the {@link Ext.data.TreeModel} instance
         * in its `data` field.
         * For example, to return the value of the record's field `name` as node's text
         * the following function can be used:
         *
         *     nodeText: function (component, node) {
         *         var record = node.data,
         *             text = record.get('name');
         *
         *         return text;
         *     }
         *
         * Or simply:
         *
         *     nodeText: 'name'
         *
         * To return the value of the `title` field, if the `name` field is empty:
         *
         *     nodeText: ['name', 'title']
         *
         * @param {Ext.d3.hierarchy.Hierarchy} component
         * @param {d3.hierarchy} node
         * @return {String}
         */
        nodeText: ['name', 'text'],

        /**
         * @cfg {Function} sorter
         * The [comparator](https://github.com/d3/d3-hierarchy#node_sort)
         * function that determines the sort order of sibling nodes.
         * Invoked for pairs of nodes.
         * Normally, one should use the store's `sorters` config instead of this one.
         * @param {d3.hierarchy} nodeA
         * @param {d3.hierarchy} nodeB
         * @return {Number}
         * @private
         */
        sorter: null,

        /**
         * @cfg {Function} nodeTransform
         * The function that transforms (typically, positions) every node
         * in the given selection.
         * @param {d3.selection} selection
         * @private
         */
        nodeTransform: function(selection) {
            selection.attr('transform', function(node) {
                return 'translate(' + node.x + ',' + node.y + ')';
            });
        },

        /**
         * @cfg {Function/String/Number} nodeValue
         * [The function](https://github.com/d3/d3-hierarchy#node_sum) that receives
         * the node's data (tree store record) and returns the numeric value of the node.
         * This config can also be a field name or a number, in which case it will be
         * converted to a function that returns the value of the specified field,
         * or a function that returns the given number for all nodes.
         * Note: {@link #nodeChildren} does not have effect on this config, even
         * though only expanded nodes will render by default, the `nodeValue`
         * function will be called for all nodes.
         * @param {Ext.data.TreeModel} record The data of the node (store record).
         * @return {Number} Numeric value of the node used to calculate its area.
         */
        nodeValue: 1,

        /**
         * @cfg {Boolean} noParentValue
         * If `true` the {@link #nodeValue} function will not be called for parent nodes,
         * instead they'll get a value of zero.
         * The {@link #nodeChildren} function is used to determine if a node is a parent.
         */
        noParentValue: false,

        /**
         * @cfg {Function} nodeKey
         * The [key](https://github.com/d3/d3-selection/#selection_data) function for nodes.
         * Returns the 'id' of the node's {@link Ext.data.TreeModel data} by default.
         * @param {d3.hierarchy} node
         * @param {Number} index
         * @return {String}
         */
        nodeKey: function(node, index) {
            return node.data.id;
        },

        /**
         * The [key](https://github.com/d3/d3-selection/#selection_data) function for links.
         * Returns the 'id' of the link's target {@link Ext.data.TreeModel data} by default.
         * @cfg {Function} linkKey
         * @param {Object} link
         * @param {d3.hierarchy} link.source
         * @param {d3.hierarchy} link.target
         * @param {Number} index
         * @return {*}
         */
        linkKey: function(link, index) {
            return link.target.data.id;
        },

        /**
         * @cfg {String/String[]} selectEventName
         * The select event(s) to listen for on each node.
         * The node in question will be selected,
         * selection will be removed from the previously selected node.
         * The select event won't be handled when Ctrl/Cmd is pressed.
         * For example, this allows to expand a node by double-clicking
         * without selecting it.
         * `null` can be used to prevent listening for the default event.
         */
        selectEventName: 'click',

        /**
         * @cfg {String/String[]} expandEventName
         * The expand event(s) to listen for on each node.
         * The node in question will be expanded, if collapsed,
         * or collapsed, if expanded.
         * `null` can be used to prevent listening for the default event.
         */
        expandEventName: 'dblclick',

        /**
         * @cfg {Boolean} rootVisible
         * False to hide the root node.
         */
        rootVisible: true,

        /**
         * @cfg {Function} layout
         * @protected
         * Subclasses are expected to create and return the layout inside `applyLayout`.
         */
        layout: undefined,

        /**
         * @cfg {Boolean} noSizeLayout
         * If `true`, layout will be performed on data change
         * even if component has no size yet.
         * @private
         */
        noSizeLayout: true,

        /**
         * @cfg {Boolean} renderLinks
         * @private
         */
        renderLinks: false, // TODO: have a method with the same name!,

        /**
         * @cfg transitions
         * @inheritdoc
         */
        transitions: {
            layout: {
                duration: 500,
                ease: 'cubicInOut'
            },
            select: {
                duration: 150, // 300ms total (scale up + scale down)
                ease: 'cubicInOut',
                sourceScale: 1,
                targetScale: 1.07
            }
        }
    },

    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: 'selection',

    /**
     * @event layout
     * Fires after the layout and render have been completed.
     * If a layout transition is used (default), fires on transition end.
     * @param {Ext.d3.hierarchy.Hierarchy} component
     * @private
     */

    /**
     * @property {d3.hierarchy} root
     * The root node.
     * @private
     */
    root: null,
    /**
     * @private
     * Cached results of the most recent hierarchy layout.
     */
    nodes: null, // layout.nodes result

    links: null, // layout.links result

    /**
     * @property defaultCls
     * @inheritdoc
     */
    defaultCls: {
        links: Ext.baseCSSPrefix + 'd3-links',
        nodes: Ext.baseCSSPrefix + 'd3-nodes',
        link: Ext.baseCSSPrefix + 'd3-link',
        node: Ext.baseCSSPrefix + 'd3-node',
        root: Ext.baseCSSPrefix + 'd3-root',
        label: Ext.baseCSSPrefix + 'd3-label',
        parent: Ext.baseCSSPrefix + 'd3-parent',
        leaf: Ext.baseCSSPrefix + 'd3-leaf',
        selected: Ext.baseCSSPrefix + 'd3-selected',
        expanded: Ext.baseCSSPrefix + 'd3-expanded'
    },

    /**
     * @private
     * Cached config used by {@link #defaultNodeClass}. For example:
     *
     *     {
     *         expanded: function (node) { return node.data.isExpanded(); },
     *         root: function (node) { return node.data.isRoot(); },
     *     }
     */
    nodeClassCfg: null,

    mouseEnterPlugin: null,

    constructor: function(config) {
        var me = this;

        me.callParent([config]);
        me.mixins.d3tooltip.constructor.call(me, config);
    },

    applyTooltip: function(tooltip, oldTooltip) {
        if (tooltip) {
            tooltip.delegate = 'g.' + this.defaultCls.node;
        }

        return this.mixins.d3tooltip.applyTooltip.call(this, tooltip, oldTooltip);
    },

    updateTooltip: null, // Override the updater in Modern component.

    defaultNodeClass: function(selection) {
        var me = this,
            cls = me.defaultCls,
            config = me.nodeClassCfg,
            name;

        if (!config) {
            me.nodeClassCfg = config = {};

            config[cls.parent] = function(node) {
                return !node.data.isLeaf();
            };

            config[cls.leaf] = function(node) {
                return node.data.isLeaf();
            };

            config[cls.expanded] = function(node) {
                return node.data.isExpanded();
            };

            config[cls.root] = function(node) {
                return node === me.root;
            };
        }

        for (name in config) {
            selection.classed(name, config[name]);
        }
    },

    applyColorAxis: function(colorAxis, oldColorAxis) {
        if (colorAxis && !colorAxis.isColorAxis) {
            colorAxis = new Ext.d3.axis.Color(colorAxis);
        }

        return colorAxis || oldColorAxis;
    },

    applyNodeText: function(nodeText) {
        var fn;

        if (typeof nodeText === 'function') {
            fn = nodeText;
        }
        else if (typeof nodeText === 'string') {
            fn = function(component, node) {
                var data = node && node.data && node.data.data;

                return data && data[nodeText] || '';
            };
        }
        else if (Array.isArray(nodeText)) {
            fn = function(component, node) {
                var data = node && node.data && node.data.data,
                    text, i;

                if (data) {
                    for (i = 0; i < nodeText.length && !text; i++) {
                        text = data[nodeText[i]];
                    }
                }

                return text || '';
            };
        }
        //<debug>
        else {
            Ext.raise('nodeText must be a string, array of strings, or a function ' +
                      'that returns a string.');
        }
        //</debug>

        return fn;
    },

    applyNodeClass: function(nodeClass, oldNodeClass) {
        var result;

        if (Ext.isFunction(nodeClass)) {
            result = nodeClass;
        }
        else if (oldNodeClass) {
            result = oldNodeClass;
        }
        else {
            result = this.defaultNodeClass;
        }

        if (result) {
            result = result.bind(this);
        }

        return result;
    },

    applyNodeTransform: function(nodeTransform) {
        if (nodeTransform) {
            nodeTransform = nodeTransform.bind(this);
        }

        return nodeTransform;
    },

    updateHierarchyCls: function(hierarchyCls, oldHierarchyCls) {
        var baseCls = this.baseCls,
            el = this.element;

        if (hierarchyCls && Ext.isString(hierarchyCls)) {
            el.addCls(hierarchyCls, baseCls);

            if (oldHierarchyCls) {
                el.removeCls(oldHierarchyCls, baseCls);
            }
        }
    },

    applyStore: function(store, oldStore) {
        var result = store && Ext.StoreManager.lookup(store, 'tree');

        if (result && !result.isTreeStore) {
            Ext.raise('The store must be a Ext.data.TreeStore.');
        }

        return result;
    },

    applyNodeValue: function(nodeValue) {
        var noParentValue = this.getNoParentValue(),
            nodeChildren = this.getNodeChildren(),
            fn, result;

        if (typeof nodeValue === 'string') {
            fn = function(record) {
                return record.data[nodeValue];
            };
        }
        else if (Ext.isNumber(nodeValue)) {
            fn = function() {
                return nodeValue;
            };
        }
        else if (typeof nodeValue === 'function') {
            fn = nodeValue;
        }

        if (fn) {
            if (noParentValue) {
                result = function(record) {
                    return nodeChildren(record) ? 0 : fn.call(this, record);
                };
            }
            else {
                result = fn.bind(this);
            }
        }

        return result;
    },

    updateNodeValue: function() {
        if (!this.isConfiguring) {
            this.performLayout();
        }
    },

    updateNodeChildren: function() {
        if (!this.isConfiguring) {
            this.performLayout();
        }
    },

    updateSorter: function(sorter) {
        if (!this.isConfiguring) {
            this.performLayout();
        }
    },

    /**
     * @private
     * Looks up `node` in the given `selection` by node's ID and returns node's element,
     * as a D3 selection. Notes:
     * - `selection` should have DOM elements bound (should consist of rendered nodes);
     * - the returned selection can be empty, if the node wasn't found; `selection.empty()`
     *   can be used to check this;
     * - in most cases using the {@link #selectionFromRecord} method is preferable, as it is faster;
     *   however this method will find the node's element even if the enter selection
     *   was not passed to the `onNodesAdd` method.
     * @param {d3.hierarchy} node
     * @param {d3.selection} [selection] Defaults to all rendered nodes, if omitted.
     * @return {d3.selection} Node's element, as a D3 selection.
     */
    findNode: function(node, selection) {
        selection = selection || this.getRenderedNodes();

        return selection.filter(function(d) {
            return node && (d.data.id === node.data.id || d === node);
        });
    },

    /**
     * Returns the associated rendered node.
     * The returned value can be `null`, if the given record doesn't have a DOM representation.
     * @param {Ext.data.TreeModel} record
     * @return {d3.hierarchy} `d3.hierarchy` instance or null, if the node wasn't found.
     */
    nodeFromRecord: function(record) {
        var selection = record && this.selectionFromRecord(record);

        return selection && !selection.empty() ? selection.datum() : null;
    },

    /**
     * Selects the associated DOM element in the scene and returns it as a D3 selection.
     * The returned selection can be empty, if the given record doesn't have a DOM representation.
     * @param {Ext.data.TreeModel} record
     * @return {d3.selection}
     */
    selectionFromRecord: function(record) {
        //<debug>
        if (!record || !record.isModel) {
            Ext.raise('Passed parameter should be a model instance.');
        }
        //</debug>

        return this.scene.select('[data-id="' + record.id + '"]');
    },

    /**
     * @private
     * Checks if the record belongs to the component's store.
     * @param {Ext.data.TreeModel} record
     * @return {Boolean}
     */
    isRecordInStore: function(record) {
        var store = this.getStore();

        return !!(record && record.isModel && store && !store.isEmptyStore &&
                 (record.store === store || store.getNodeById(record.id) === record ||
                 store.getRoot() === record));
    },

    applySelection: function(record) {
        //<debug>
        if (record && !record.isModel) {
            Ext.raise("The 'record' should be a Ext.data.TreeModel instance.");
        }
        //</debug>

        return this.isRecordInStore(record) ? record : null;
    },

    updateSelection: function(record, oldRecord) {
        var me = this,
            recordSelection, oldRecordSelection, hasElement;

        if (!me.hasFirstRender) {
            if (record) {
                me.on({
                    scenerender: me.updateSelection.bind(me, record, oldRecord),
                    single: true
                });
            }

            return;
        }

        if (record) {
            recordSelection = me.selectionFromRecord(record);
            hasElement = !recordSelection.empty();

            if (hasElement) {
                me.onNodeSelect(record, recordSelection);
            }
            else {
                // Set the value of the config to `null` here, as for the applier to return
                // `null` in this case, it should perform the element check as well.
                // If the check is performed in the applier, we still cannot remove it here,
                // because we need to call `selectionFromRecord` anyway to get the element.
                me[me.self.getConfigurator().configs.selection.names.internal] = null;
                Ext.log.warn('Selected record "' + record.id +
                    '" does not have an associated element. E.g.:\n' +
                    '- record was selected before it was rendered;\n' +
                    '- record was selected in some other view, but is not supposed ' +
                    'to be rendered by this D3 component (see "nodeChildren" config).');
            }
        }

        if (oldRecord) {
            oldRecordSelection = me.selectionFromRecord(oldRecord);

            if (!oldRecordSelection.empty()) {
                me.onNodeDeselect(oldRecord, oldRecordSelection);
            }
        }

        if (hasElement) {
            me.fireEvent('selectionchange', me, record, oldRecord);
        }
    },

    /**
     * @protected
     * @param {Ext.data.TreeModel} record
     * @param {d3.selection} selection
     */
    onNodeSelect: function(record, selection) {
        selection.classed(this.defaultCls.selected, true);
        this.fireEvent('select', this, record, selection);
    },

    /**
     * @protected
     * @param {Ext.data.TreeModel} record
     * @param {d3.selection} selection
     */
    onNodeDeselect: function(record, selection) {
        selection.classed(this.defaultCls.selected, false);
        this.fireEvent('deselect', this, record, selection);
    },

    /**
     * @protected
     * All nodes that are added to the scene by the {@link #addNodes} method
     * are expected to be passed to this method (as a D3 selection).
     * @param {d3.selection} selection
     */
    onNodesAdd: function(selection) {
        var me = this;

        selection
            .attr('class', me.defaultCls.node)
            .each(function(node) {
                // A node doesn't store a reference to the associated DOM element
                // (if any), unlike the element, which stores a reference
                // to the associated node in the `__data__` property.
                // To make finding corresponding DOM elements easier, `data-id` of the node's
                // group element will correspond to the ID of the node's record (Ext.data.TreeModel)
                // See the `selectionFromRecord` method for example.
                this.setAttribute('data-id', node.data.id);
            });
    },

    /**
     * @protected
     * Adds delegated listeners to handle pointer events for all child nodes
     */
    addNodeListeners: function() {
        var me = this,
            selectEventName = Ext.Array.from(me.getSelectEventName()),
            expandEventName = Ext.Array.from(me.getExpandEventName()),
            i, len;

        for (i = 0, len = selectEventName.length; i < len; i++) {
            me.addNodeListener(selectEventName[i], me.onSelectEvent);
        }

        for (i = 0, len = expandEventName.length; i < len; i++) {
            me.addNodeListener(expandEventName[i], me.onExpandEvent);
        }
    },

    /**
     * @private
     */
    updateEventName: function(name, oldName, handler) {
        var me = this,
            names = Ext.Array.from(name),
            oldNames = Ext.Array.from(oldName),
            i, ln;

        for (i = 0, ln = oldNames.length; i < ln; i++) {
            me.removeNodeListener(oldNames[i], handler);
        }

        for (i = 0, ln = names.length; i < ln; i++) {
            me.addNodeListener(names[i], handler);
        }
    },

    updateSelectEventName: function(name, oldName) {
        this.updateEventName(name, oldName, this.onSelectEvent);
    },

    updateExpandEventName: function(name, oldName) {
        this.updateEventName(name, oldName, this.onExpandEvent);
    },

    /**
     * @private
     * Adds a delegated node listener.
     * @param {String} eventName
     * @param {Function} handler
     */
    addNodeListener: function(eventName, handler) {
        var me = this,
            targetEl = me.el;

        if (eventName === 'mouseenter') {
            me.mouseEnterPlugin = me.addPlugin({
                type: 'mouseenter',
                element: targetEl,
                delegate: 'g.' + me.defaultCls.node,
                handler: handler
            });
        }
        else if (eventName) {
            targetEl.on(eventName, handler, me, {
                delegate: 'g.' + me.defaultCls.node
            });
        }
    },

    /**
     * @private
     * Removes a delegated node listener.
     * @param {String} eventName
     * @param {Function} handler
     */
    removeNodeListener: function(eventName, handler) {
        var me = this,
            targetEl = me.el;

        if (eventName === 'mouseenter') {
            Ext.destroy(me.mouseEnterPlugin);
        }
        else if (eventName) {
            targetEl.un(eventName, handler, me, {
                delegate: 'g.' + me.defaultCls.node
            });
        }
    },

    onSelectEvent: function(event, target) {
        // Fetching the 'node' and 'element' this way is not exactly pretty,
        // but arguably better than capturing 'addNodeListeners' arguments
        // in a closure for every element listener.
        var selection = d3.select(target),
            element = selection.node(),
            node = selection.datum();

        this.handleSelectEvent(event, node, element);
    },

    handleSelectEvent: function(event, node, element) {
        this.setSelection(node.data);
    },

    onExpandEvent: function(event) {
        var selection = d3.select(event.currentTarget),
            element = selection.node(),
            node = selection.datum();

        this.handleExpandEvent(event, node, element);
    },

    handleExpandEvent: function(event, node, element) {
        var record = node.data;

        if (record.isExpanded()) {
            record.collapse();
        }
        else {
            record.expand();
        }
    },

    /**
     * @protected
     * Sets the size of a hierarchy layout via its 'size' method.
     * @param {Number[]} size The size of the scene.
     */
    setLayoutSize: function(size) {
        var layout = this.getLayout();

        layout.size(size);
    },

    getLayoutSize: function() {
        var layout = this.getLayout();

        return layout.size && layout.size();
    },

    onSceneResize: function(scene, rect) {
        this.callParent([scene, rect]);
        this.setLayoutSize([rect.width, rect.height]);
        this.performLayout();
    },

    hasFirstLayout: false,
    hasFirstRender: false,
    isLayoutTransitionSkipped: false,

    oldLayoutSaving: false, // whether the previous layout should be saved or not
    oldNodes: null, // nodes from the previous layout
    oldLinks: null, // links from the previous layout

    /**
     * @private
     */
    isLayoutBlocked: Ext.emptyFn,

    /**
     * @private
     * Marks the layout transition to be skipped once.
     */
    skipLayoutTransition: function() {
        this.isLayoutTransitionSkipped = true;
    },

    processDataChange: function(store) {
        if (this.getNoSizeLayout() || this.size) {
            this.performLayout();
        }
    },

    setupScene: function(scene) {
        var me = this;

        me.callParent([scene]);

        // Links should render before nodes.
        // A node is rendered at a certain coordinate, which is typically
        // the center of a node, and so a link is a connection between
        // the centers of a pair of nodes. Usually, we want it to appear
        // as if a link goes edge to edge, not center to center.

        // However, this alone is not enough, because if a node itself is not
        // updated, e.g. it's already visible and we simply show its children,
        // the links will still be painted on top. Because SVG has no z-index and
        // the elements are rendered in the order in which they appear in the document,
        // the nodes have to be either sorted or placed in pre-sorted groups. We do
        // the latter here.

        me.linksGroup = scene.append('g').classed(me.defaultCls.links, true);
        me.nodesGroup = scene.append('g').classed(me.defaultCls.nodes, true);
    },

    /**
     * Uses bound store records to calculate the layout of nodes and links
     * and re-renders the scene.
     */
    performLayout: function() {
        var me = this,
            store = me.getStore(),
            storeRoot = store && store.getRoot(),
            rootVisible = me.getRootVisible(),
            layout = me.getLayout(),
            root, nodes, links, layoutRoot, sorter, renderLinks, nodeChildren, nodeValue;

        // Make sure we have the scene created and set up.
        me.getScene();

        if (!storeRoot) {
            me.getRenderedNodes().remove();
            me.getRenderedLinks().remove();
        }

        if (!storeRoot || store.isLoading() || me.isInitializing || me.isLayoutBlocked(layout)) {
            return;
        }

        if (!rootVisible && storeRoot.data.autoRoot && storeRoot.childNodes.length === 1) {
            layoutRoot = storeRoot.firstChild;
        }
        else {
            layoutRoot = storeRoot;
        }

        sorter = me.getSorter();
        renderLinks = me.getRenderLinks();
        nodeChildren = me.getNodeChildren();
        nodeValue = me.getNodeValue();

        if (me.oldLayoutSaving) {
            me.saveLayout(me.nodes, me.links);
        }

        root = d3.hierarchy(layoutRoot, nodeChildren).sum(nodeValue);

        if (sorter) {
            root = root.sort(sorter);
        }

        me.storeRoot = storeRoot;
        me.root = root = layout(root);

        nodes = me.nodes = root.descendants();

        if (renderLinks) {
            links = me.links = root.links();
        }

        me.hasFirstLayout = true;

        me.renderScene(nodes, links);
    },

    saveLayout: function(nodes, links) {
        var me = this,
            map = {},
            i, ln, node;

        me.oldNodes = map;

        if (nodes) {
            for (i = 0, ln = nodes.length; i < ln; i++) {
                node = nodes[i];
                map[node.data.id] = node;
            }
        }

        me.oldLinks = links;
    },

    getOldNode: function(node) {
        return node && this.oldNodes && this.oldNodes[node.data.id];
    },

    /**
     * Renders arrays of nodes and links, returned by the
     * [rootNode.descendants()](https://github.com/d3/d3-hierarchy/#node_descendants)
     * and [rootNode.links()](https://github.com/d3/d3-hierarchy/#node_links)
     * methods, respectively.
     * Both `nodes` and `links` arguments are optional and, if not specified,
     * the method re-renders nodes/links produced by the most recent layout.
     * @param {Array} [nodes]
     * @param {Array} [links]
     */
    renderScene: function(nodes, links) {
        var me = this,
            nodeKey = me.getNodeKey(),
            linkKey = me.getLinkKey(),
            nodeSelection,
            linkSelection;

        if (!me.hasFirstLayout) {
            me.performLayout();
        }

        // If several D3 components are using the same store,
        // updates can be slow. If some of the components are not visible,
        // it may not be obvious why updates are slow.

        nodes = nodes || me.nodes;
        links = links || me.links;

        me.onBeforeRender(nodes, links);

        if (nodes) {
            nodeSelection = me.getRenderedNodes().data(nodes, nodeKey);
            me.renderNodes(nodeSelection);

            if (links) {
                linkSelection = me.getRenderedLinks().data(links, linkKey);
                me.renderLinks(linkSelection);
            }
        }

        me.hideRoot();

        me.hasFirstRender = true;

        me.onAfterRender(nodeSelection, linkSelection);
        me.fireEvent('scenerender', me, nodeSelection, linkSelection);
    },

    getRenderedNodes: function() {
        return this.nodesGroup.selectAll('.' + this.defaultCls.node);
    },

    getRenderedLinks: function() {
        return this.linksGroup.selectAll('.' + this.defaultCls.link);
    },

    /**
     * @private
     */
    hideRoot: function() {
        var me = this,
            rootVisible = me.getRootVisible();

        me.nodesGroup
            .select('.' + me.defaultCls.root)
            .classed(me.defaultCls.invisible, !rootVisible);
    },

    /**
     * @private
     * See `renderNodes` comments.
     * @param {d3.selection} update
     */
    renderLinks: function(update) {
        this.updateLinks(update, this.addLinks(update.enter()));
        this.removeLinks(update.exit());
    },

    /**
     * @private
     * The groups for the entering nodes will be created automatically
     * by this base class and passed to the `updateNodes` as the second
     * parameter, so the `addNodes` doesn't have to return anything.
     * The assumption is that a component will always render nodes, unlike links,
     * so the `addLinks` method should always return the links it created.
     * @param {d3.selection} update
     */
    renderNodes: function(update) {
        var me = this,
            nodeClass = me.getNodeClass(),
            enter = update.enter().append('g');

        enter.call(me.onNodesAdd.bind(me));
        me.addNodes(enter);

        enter.call(nodeClass);
        update.call(nodeClass);

        me.updateNodes(update, enter);
        me.removeNodes(update.exit());

    },

    /**
     * The current layout transition.
     * Only active for its duration from the time of the `renderScene` call.
     * @property
     */
    layoutTransition: null,

    onBeforeRender: function() {
        var me = this,
            useTransition = me.hasFirstRender && !me.isLayoutTransitionSkipped,
            transition = me.createTransition(useTransition ? 'layout' : 'none');

        me.isLayoutTransitionSkipped = false;

        // D3's `on` does not have the scope parameter (the third param is `capture`),
        // so we use a closure here.
        me.layoutTransition = transition.on('end', function() {
            me.layoutTransition = null;

            if (!(me.destroyed || me.destroying)) {
                me.onLayout();
            }
        });
    },

    /**
     * @private
     */
    onLayout: function() {
        var me = this;

        if (me.panZoom) {
            me.panZoom.updateIndicator();
        }

        me.fireEvent('layout', me);
    },

    /**
     * @private
     */
    onAfterRender: Ext.emptyFn,

    /**
     * @protected
     * @param {d3.selection} selection The `update` selection for links.
     */
    updateLinks: Ext.emptyFn,

    /**
     * @protected
     * @param {d3.selection} selection The `enter` selection to populate with links.
     * @return {d3.selection} The added links.
     */
    addLinks: Ext.emptyFn,

    /**
     * @protected
     * @param {d3.selection} selection The `update` selection for nodes.
     */
    updateNodes: Ext.emptyFn,

    /**
     * @protected
     * @param {d3.selection} selection The `enter` selection to populate with nodes.
     * @return {d3.selection} The added nodes.
     */
    addNodes: Ext.emptyFn,

    /**
     * @protected
     * @param {d3.selection} selection The `exit` selection for links.
     */
    removeLinks: function(selection) {
        selection.remove();
    },

    /**
     * @protected
     * @param {d3.selection} selection The `exit` selection for nodes.
     */
    removeNodes: function(selection) {
        selection.remove();
    }

});
