Home Reference Source

js/BaseTree.js

import d3 from './CustomD3';
import NodeSettings from './NodeSettings';
import LoadOnDemandSettings from './LoadOnDemandSettings';
import EventEmitter from 'events';

/**
 * Recursively find a particular object within a hierarchical dataset.
 * 
 * @param {object} hierarchicalObject The initial hierarchical object to start the recursive find.
 * @param {function} getChildren The callback function that gets the children items of the hierarchical object.
 * @param {function} findCondition The callback function that defines whether the object matches the condition to be returned or not.
 * @returns {object|null} The first object matching the conditions.
 */
function recursiveFind(hierarchicalObject, getChildren, findCondition) {
    if (findCondition(hierarchicalObject))
        return hierarchicalObject;
    var children = getChildren(hierarchicalObject);
    var foundNode = children.find(findCondition);
    if (!foundNode)
    {
        for (var child of children) {
            foundNode = recursiveFind(child, getChildren, findCondition);
            if (foundNode)
                break;
        }
    }
    return foundNode;
}

/**
 * Recursively gets a set of objects within a hierarchical dataset.
 * 
 * @param {object} hierarchicalObject The initial hierarchical object to start the recursive get.
 * @param {function} getChildren The callback function that gets the children items of the hierarchical object.
 */
function recursiveGet(hierarchicalObject, getChildren) {
    var allItems = [];
    var children = getChildren(hierarchicalObject);
    if (children)
    {
        for (var child of children) {
            allItems.push(child);
            var descendants = recursiveGet(child, getChildren);
            if (descendants)
                allItems = [...allItems, ...descendants];
        }
    }
    return allItems;
}

class BaseTree extends EventEmitter {
    /**
     * @param {object} options The options object.
     * @param {string} [options.theme=default] The theme of the tree.
     * @param {string} [options.orientation=leftToRight] The orientation of the tree.
     * @param {boolean} [options.allowPan=true] Enables/disables the mouse drag to pan feature.
     * @param {boolean} [options.allowZoom=true] Enables/disables the mouse wheel to zoom feature.
     * @param {boolean} [options.allowFocus=true] If true, clicking on a node would focus to the node, hiding all irrelevant nodes that's not a parent, ancestor, or sibling.
     * @param {boolean} [options.allowNodeCentering=true] If true, clicking on a node would pan to the node.
     * @param {number} [options.minScale=1] Minimum zoom scaling.
     * @param {number} [options.maxScale=2] Maximum zoom scaling.
     * @param {number} [options.nodeDepthMultiplier=300] The distance between the parent and child nodes.
     * @param {boolean} [options.isFlatData=false] Indicates whether the passed data was a flat array of objects. If true, you must specify the `getParentId` option.
     * @param {getIdCallBack} options.getId
     * @param {getParentIdCallBack} [options.getParentId]
     * @param {getChildrenCallBack} [options.getChildren]
     * @param {number} [options.widthWithoutMargins=960] The width of the tree, not including the margins.
     * @param {number} [options.heightWithoutMargins=800] The height of the tree, not including the margins.
     * @param {object} [options.margins] Object specifying the margins of the tree diagram.
     * @param {number} [options.margins.top] The top margin for the tree diagram.
     * @param {number} [options.margins.right] The right margin for the tree diagram.
     * @param {number} [options.margins.bottom] The bottom margin for the tree diagram.
     * @param {number} [options.margins.left] The left margin for the tree diagram.
     * @param {number} [options.duration] Integer in milliseconds determining the duration of the animations for the tree.
     * @param {LoadOnDemandSettings} [options.loadOnDemandSettings] Object specifying the load-on-demand settings.
     * @param {NodeSettings} [options.nodeSettings] Object specifying the node settings for the tree.
     */
    constructor(options) {
        super();
        options = options || {}; // Defaults options to an empty object

        var mergedOptions = {
            ...BaseTree.defaults,
            ...options
        };

        // We define our prototype properties which would be set later
        this._root = null;
        this._svg = null;
        this._panningContainer = null,
        this._view = null;
        this._treeGenerator = null;
        this._linkPathGenerator = null;
        this._visibleNodes = null;
        this._links = null;
        this._zoomListener = null,

        // Assign/Set prototype properties, using values passed from the options object
        this.setTheme(mergedOptions.theme);
        this.setOrientation(mergedOptions.orientation);
        this.setData(mergedOptions.data);
        this.setElement(mergedOptions.element);
        this.setWidthWithoutMargins(mergedOptions.widthWithoutMargins);
        this.setHeightWithoutMargins(mergedOptions.heightWithoutMargins);
        this.setMargins(mergedOptions.margins);
        this.setDuration(mergedOptions.duration);
        this.setAllowPan(mergedOptions.allowPan);
        this.setAllowZoom(mergedOptions.allowZoom);
        this.setAllowFocus(mergedOptions.allowFocus);
        this.setAllowNodeCentering(mergedOptions.allowNodeCentering);
        this.setMinScale(mergedOptions.minScale);
        this.setMaxScale(mergedOptions.maxScale);
        this.setIsFlatData(mergedOptions.isFlatData);
        this.setNodeDepthMultiplier(mergedOptions.nodeDepthMultiplier)

        // We define our sub-prototype (AKA sub-class) properties

        this.loadOnDemandSettings = new LoadOnDemandSettings(this, mergedOptions.loadOnDemandSettings);
        this.nodeSettings = new NodeSettings(this, mergedOptions.nodeSettings);

        // We define our methods, which derives from our options
        this._getId = mergedOptions.getId;
        this._getChildren = mergedOptions.getChildren;
        this._getParentId = mergedOptions.getParentId;
    }

    /**
     * Defines how to create the nodes for newly
     * added data objects.
     *
     * @param {*} nodeEnter The D3 Enter selection of nodes.
     * @param {*} nodes
     * @returns {object} The tree object.
     */
    _nodeEnter(nodeEnter, nodes) {
        throw 'The function _nodeEnter must be implemented';
    }

    /**
     * Defines how to update the nodes for the
     * data objects.
     *
     * @param {*} nodeUpdate The D3 Update selection of nodes.
     * @param {*} nodeUpdateTransition The D3 transition object for the D3 Update selection of nodes.
     * @param {*} nodes
     * @returns {object} The tree object.
     */
    _nodeUpdate(nodeUpdate, nodeUpdateTransition, nodes) {
        throw 'The function _nodeUpdate must be implemented';
    }

    /**
     * Defines how to remove the nodes for the
     * removed data objects.
     *
     * @param {*} nodeExit The D3 Exit selection of nodes.
     * @param {*} nodeExitTransition The D3 transition object for the D3 Exit selection of nodes.
     * @param {*} nodes
     * @returns {object} The tree object.
     */
    _nodeExit(nodeExit, nodeExitTransition, nodes) {
        throw 'The function _nodeExit must be implemented';
    }

    /**
     * Gets the path generator used to render
     * the links between the nodes.
     *
     * @returns {function} The callback function that generates the SVG path coordinates for the links, given a coordinates object.
     */
    _getLinkPathGenerator() {
        throw 'The function _getLinkPathGenerator must be implemented';
    }

    /**
     * Defines how to create the links for newly
     * added data objects.
     *
     * @param {*} source The original data object that the links are being drawn for.
     * @param {*} linkEnter The D3 Enter selection of links.
     * @param {*} links
     * @param {*} linkPathGenerator
     * @returns {object} The tree object.
     */
    _linkEnter(source, linkEnter, links, linkPathGenerator) {
        throw 'The function _linkEnter must be implemented';
    }

    /**
     * Defines how to update the links for the
     * data objects.
     *
     * @param {*} source The original data object that the links are being drawn for.
     * @param {*} linkUpdate The D3 Update selection of links.
     * @param {*} linkUpdateTransition The D3 transition object for the D3 Update selection of links.
     * @param {*} links
     * @param {*} linkPathGenerator The link path generator function.
     * @returns {object} The tree object.
     */
    _linkUpdate(source, linkUpdate, linkUpdateTransition, links, linkPathGenerator) {
        throw 'The function _linkUpdate must be implemented';
    }

    /**
     * Defines how to remove the links for the
     * removed data objects.
     *
     * @param {object} source The original data object that the links are being drawn for.
     * @param {*} linkExit The D3 Exit selection of links.
     * @param {*} linkExitTransition The D3 transition object for the D3 Update selection of links.
     * @param {*} links
     * @param {*} linkPathGenerator The link path generator function.
     * @returns {object} The tree object.
     */
    _linkExit(source, linkExit, linkExitTransition, links, linkPathGenerator) {
        throw 'The function _linkExit must be implemented';
    }

    /**
     * Called when updating dimensions when
     * node settings is configured to be
     * 'nodesize'.
     * 
     * @returns {number[]} An array with two values, representing the height and width of the node respectively.
     */
    _getNodeSize() {
        throw 'The function _getNodeSize must be implemented';
    }

    /**
     * Focuses and expands all the way through to a node.
     * 
     * @param {*} idOrNodeDataItem The id of the node to focus, or the node data item object.
     * @returns {object} The tree object.
     */
    focusToNode(idOrNodeDataItem) {
        this.removeSelection(this.getRoot());
        
        var nodeDataItem = idOrNodeDataItem;
        if (typeof nodeDataItem !== 'object' && nodeDataItem !== null)
            nodeDataItem = this.getNode(nodeDataItem);
        var parentNode = null;
        
        // Expand every parent/ancestor node
        parentNode = nodeDataItem.parent;
        while(parentNode)
        {
            if (parentNode._children)
                this.expand(parentNode);
            parentNode = parentNode.parent;
        }

        if (this.getAllowFocus())
        {
            // Hide the parent/ancestor node siblings
            parentNode = nodeDataItem.parent;
            while(parentNode)
            {
                this.hideSiblings(parentNode);
                parentNode = parentNode.parent;
            }
            
            this.updateTreeWithFocusOnNode(nodeDataItem);
            nodeDataItem.selected = true;
        }

        this.update(this.getRoot());
        this.centerNode(nodeDataItem);

        return this;
    }

    /**
     * Returns a boolean whether the
     * tree is using flat data or not.
     * 
     * @returns {boolean} Whether the tree is using flat data or not.
     */
    getIsFlatData() {
        return this._isFlatData;
    }
    
    /**
     * Sets the is flat data flag.
     * If set to true, you must specify
     * the `getParentId` option.
     * 
     * @param {boolean} newIsFlatData Whether the tree is using flat data or not.
     */
    setIsFlatData(newIsFlatData) {
        this._isFlatData = newIsFlatData;
        return this;
    }

    /**
     * Regenerates the node data.
     * 
     * @returns {object} The tree object.
     */
    regenerateNodeData() {
        // Assigns parent, children, height, depth
        if (!this.getIsFlatData()) {
            if (!this._getChildren)
                throw "If you are providing hierarchical structured data, then you must set the getChildren accessor property.";

            // Specify your children property here,
            // so that D3's resulting root object
            // has a mapping from its "children" property
            // to your specified children property
            this._root = d3.hierarchy(this.getData(), (data) => this.getChildren.call(this, data));
        }
        else {
            if (!this._getParentId)
                throw "If you are providing flat structured data, then you must set the getParentId accessor property.";

            // stratifier is a function that would convert the flat
            // dataset into hierarchically structured data
            // to be used with D3 trees.
            // It accepts the dataset as its parameter,
            // and returns the converted data.

            // Note that this is used instead of the d3.hierarchy()
            // method as d3.hierarchy() should only be used if the
            // data is already in heirarchical structure, and
            // needs to be converted to D3 hierarchical nodes
            var stratifier = d3.stratify()
                .id((data, index, arr) => this.getId.call(this, data))
                .parentId((data, index, arr) => this.getParentId.call(this, data));
            this._root = stratifier(this.getData());
        }
        return this;
    }

    /**
     * Gets the tree theme.
     * 
     * @returns {string} The theme the tree is using.
     */
    getTheme() {
        return this._theme;
    }

    /**
     * Sets the tree theme.
     * 
     * @param {string} theme The theme to set the tree to.
     * @returns {object} The tree object.
     */
    setTheme(theme) {
        this._theme = theme;
        return this;
    }

    /**
     * Gets the tree orientation.
     * 
     * @returns {string} The orientation the tree is using.
     */
    getOrientation() {
        return this._orientation;
    }

    /**
     * Sets the tree orientation.
     * 
     * @param {string} orientation The orientation to set the tree to.
     * @returns {object} The tree object.
     */
    setOrientation(orientation) {
        this._orientation = orientation;
        return this;
    }

    /**
     * Gets the data items used to render
     * the nodes.
     * 
     * @returns {object[]} The array of data items the tree uses.
     */
    getData() {
        return this._data;
    }

    /**
     * Sets the data items the tree should
     * use to render the nodes.
     * 
     * @param {object[]} newData The new set of data items.
     * @returns {object} The tree object.
     */
    setData(newData) {
        this._data = newData;
        return this;
    }

    /**
     * Gets the node depth multiplier that
     * affects the distance between the
     * parent node and the child node.
     * 
     * @returns {number} The node depth multiplier value
     */
    getNodeDepthMultiplier() {
        return this._nodeDepthMultiplier;
    }

    /**
     * Sets the node depth multiplier value.
     * 
     * @param {number} newNodeDepthMultiplier The value that affects the distance between the parent node and the child node.
     * @returns {object} The tree object.
     */
    setNodeDepthMultiplier(newNodeDepthMultiplier) {
        this._nodeDepthMultiplier = newNodeDepthMultiplier;
        return this;
    }

    /**
     * Gets the duration of animations
     * for the tree.
     * 
     * @returns {number} The animation duration in milliseconds.
     */
    getDuration() {
        return this._duration;
    }

    /**
     * Sets the duration of animations
     * for the tree.
     * 
     * @param {*} newDuration The animation duration in milliseconds.
     * @returns {object} The tree object.
     */
    setDuration(newDuration) {
        this._duration = newDuration;
        return this;
    }

    /**
     * Gets the boolean value indicating
     * whether the drag-to-pan pan feature
     * is enabled or not.
     * 
     * @returns {boolean} Whether panning is enabled or not.
     */
    getAllowPan() {
        return this._allowPan;
    }

    /**
     * Sets the boolean value indicating
     * whether the drag-to-pan pan feature
     * is enabled or not.
     * 
     * @param {*} newAllowPan Whether panning is enabled or not.
     * @returns {object} The tree object.
     */
    setAllowPan(newAllowPan) {
        this._allowPan = newAllowPan;
        return this;
    }

    /**
     * Gets the boolean value indicating
     * whether the mouse wheel to zoom in/out
     * feature is enabled or not.
     * 
     * @returns {boolean} Whether zooming is enabled or not.
     */
    getAllowZoom() {
        return this._allowZoom;
    }

    /**
     * Sets the boolean value indicating
     * whether the mouse wheel to zoom in/out
     * feature is enabled or not.
     * 
     * @param {boolean} newAllowZoom Whether zooming is enabled or not.
     * @returns {object} The tree object.
     */
    setAllowZoom(newAllowZoom) {
        this._allowZoom = newAllowZoom;
        return this;
    }

    /**
     * Gets the boolean value indicating
     * whether to focus to the clicked node
     * or not. Focusing on a node would hide
     * all irrelevant nodes that's not a 
     * parent, sibling or ancestor of the
     * clicked node.
     * 
     * @returns {boolean} Whether to focus to the clicked node.
     */
    getAllowFocus() {
        return this._allowFocus;
    }

    /**
     * Sets the boolean value indicating
     * whether to focus to the clicked node
     * or not. Focusing on a node would hide
     * all irrelevant nodes that's not a 
     * parent, sibling or ancestor of the
     * clicked node.
     * 
     * @param {boolean} newAllowFocus Whether to pan to the clicked node.
     * @returns {object} The tree object.
     */
    setAllowFocus(newAllowFocus) {
        this._allowFocus = newAllowFocus;
        return this;
    }

    /**
     * Gets the boolean value indicating
     * whether to pan to a clicked node.
     * 
     * @returns {boolean} Whether to pan to the clicked node.
     */
    getAllowNodeCentering() {
        return this._allowNodeCentering;
    }

    /**
     * Whether to pan to a clicked node.
     * 
     * @param {boolean} newAllowNodeCentering Whether to pan to the clicked node.
     * @returns {object} The tree object.
     */
    setAllowNodeCentering(newAllowNodeCentering) {
        this._allowNodeCentering = newAllowNodeCentering;
        return this;
    }

    /**
     * Gets the minimum zoom scaling.
     * 
     * @returns {number} The minimum zoom scale value.
     */
    getMinScale() {
        return this._minScale;
    }

    /**
     * Sets the minimum zoom scaling.
     * 
     * @param {*} newMinScale The minimum zoom scale value.
     * @returns {object} The tree object.
     */
    setMinScale(newMinScale) {
        this._minScale = newMinScale;
        return this;
    }

    /**
     * Gets the maximum zoom scaling.
     * 
     * @returns {number} Maximum zoom scale value.
     */
    getMaxScale() {
        return this._maxScale;
    }

    /**
     * Sets the maximum zoom scaling.
     * 
     * @param {*} newMaxScale The maximum zoom scale value.
     * @returns {object} The tree object.
     */
    setMaxScale(newMaxScale) {
        this._maxScale = newMaxScale;
        return this;
    }

    /**
     * Gets the load on demand settings object.
     * 
     * @returns {LoadOnDemandSettings} The load on demand settings.
     */
    getLoadOnDemandSettings() {
        return this.loadOnDemandSettings;
    }

    /**
     * Gets the node settings object.
     * 
     * @returns {NodeSettings} The node settings.
     */
    getNodeSettings() {
        return this.nodeSettings;
    }

    /**
     * Gets the container DOM element.
     * 
     * @returns {object} The container DOM element.
     */
    getElement() {
        return this._element;
    }

    /**
     * Sets the container DOM element
     * 
     * @param {object} newElement The container DOM element.
     * @returns {object} The tree object.
     */
    setElement(newElement) {
        this._element = newElement;
        return this;
    }

    /**
     * Gets the root node object.
     * 
     * @return {object} The root D3 tree node object.
     */
    getRoot() {
        return this._root;
    }

    /**
     * Gets the D3 selection object for the SVG element.
     * 
     * @return {object} Returns the D3 selection object.
     */
    getSvg() {
        return this._svg;
    }

    /**
     * Gets the D3 selection object for the view element.
     * 
     * @returns {object} D3 selection object for the view element.
     */
    getView() {
        return this._view;
    }

    /**
     * Gets the D3 selection object for the
     * panning container element.
     * 
     * @returns {object} D3 selection object for the panning container element.
     */
    getPanningContainer() {
        return this._panningContainer;
    }

    /**
     * Gets the D3 generator object used to
     * generate the tree nodes coordinates.
     * 
     * @returns {function} D3 tree generator object.
     */
    getTreeGenerator() {
        return this._treeGenerator;
    }

    /**
     * Get a single node given an id or a data item.
     * 
     * @param {*|object} idOrDataItem The ID or data item to retrieve the D3 tree node data item with.
     * @returns {object} D3 tree node data item.
     */
    getNode(idOrDataItem) {
        var id = idOrDataItem;
        if (typeof id === 'object' && id !== null)
            id = this.getId(id);        
        var rootNode = this.getRoot();

        var getNodeChildren = (node) => {
            if (node._children)
                return node._children;
            return [];
        }
        var node = recursiveFind(rootNode, getNodeChildren, x => this.getId(x.data) == id);
        return node;
    }

    /**
     * Get a single data item given an id.
     * 
     * @param {*} id The ID to retrieve the data item with.
     * @returns {object} The data item with the given ID.
     */
    getDataItem(id) {
        var node = this.getNode(id);
        return node.data;
    }

    /**
     * Get the array of D3 node data items
     * the D3 tree has generated.
     * 
     * @returns {object[]} Array of D3 node data items.
     */
    getNodes() {
        return this._nodes;
    }

    /**
     * Get the array of visible D3 node
     * data items the D3 tree has generated.
     * 
     * @returns {object[]} Array of D3 node data items.
     */
    getVisibleNodes() {
        return this._visibleNodes;
    }

    /**
     * Get the array of D3 link data items
     * the D3 tree has generated.
     * 
     * @returns {object[]} Array of D3 link data items.
     */
    getLinks() {
        return this._links;
    }

    /**
     * Gets the D3 zoom listener used for
     * the panning, zooming and focus features.
     * 
     * @returns {function} The D3 zoom listener
     */
    getZoomListener() {
        return this._zoomListener;
    }

    /**
     * Gets the ID for a given data item.
     * 
     * @param {object} dataItem The data item to get the ID from.
     * @returns {*} The ID for the given data item.
     */
    getId(dataItem) {
        return this._getId(dataItem);
    }

    /**
     * Gets the children data items for a given data item.
     * 
     * @param {object} dataItem The data item to get the children data items from.
     * @returns {object[]} The array of child data items.
     */
    getChildren(dataItem) {
        return this._getChildren(dataItem);
    }

    /**
     * Gets the parent ID for a given data item.
     * 
     * @param {object} dataItem The data item to get the parent ID from.
     * @returns {*} The parent ID for the given data item.
     */
    getParentId(dataItem) {
        return this._getParentId(dataItem);
    }

    /**
     * Sets the ID accessor callback function,
     * defining how to get a unique ID from a
     * given data item.
     * 
     * @param {getIdCallBack} newIdAccessor Callback function to get the ID for a given data item.
     * @returns {object} The tree object.
     */
    setIdAccessor(newIdAccessor) {
        this._getId = newIdAccessor;
        return this;
    }

    /**
     * Sets the children accessor callback function,
     * defining how to get the children data items
     * from a given data item.
     * 
     * @param {getChildrenCallBack} newChildrenAccessor Callback function to get the children for a given data item.
     * @returns {object} The tree object.
     */
    setChildrenAccessor(newChildrenAccessor) {
        this._getChildren = newChildrenAccessor;
        return this;
    }

    /**
     * Sets the parent ID accessor callback function,
     * defining how to get the parent ID from a
     * given data item.
     * 
     * @param {getParentIdCallBack} newParentIdAccessor Callback function to get the parent id for a given data item.
     * @returns {object} The tree object.
     */
    setParentIdAccessor(newParentIdAccessor) {
        this._getParentId = newParentIdAccessor;
        return this;
    }

    /**
     * Gets the width of SVG, including the margins.
     * 
     * @returns {number} The width of the SVG.
     */
    getWidth() {
        return this.getWidthWithoutMargins() - this.getMargins().left - this.getMargins().right;
    }

    /**
     * Gets the height of SVG, including the margins.
     * 
     * @returns {number} The height of the SVG.
     */
    getHeight() {
        return this.getHeightWithoutMargins() - this.getMargins().top - this.getMargins().bottom;
    }

    /**
     * Sets the margins for the tree diagram.
     * 
     * @param {object} newMargins The margin object.
     * @param {number} newMargins.top The margin top for the tree diagram.
     * @param {number} newMargins.right The margin right for the tree diagram.
     * @param {number} newMargins.bottom The margin bottom for the tree diagram.
     * @param {number} newMargins.left The margin left for the tree diagram.
     * @returns {object} The tree object.
     */
    setMargins(newMargins) {
        this._margins = newMargins;
        return this;
    }

    /**
     * Gets the margins for the tree diagram.
     * 
     * @returns {object} The margins object.
     */
    getMargins() {
        return this._margins;
    }

    /**
     * Sets the width of the SVG for the tree diagram.
     * 
     * @param {*} newWidthWithoutMargin The width of SVG for the tree diagram.
     * @returns {object} The tree object.
     */
    setWidthWithoutMargins(newWidthWithoutMargin) {
        this._widthWithoutMargin = newWidthWithoutMargin;
        return this;
    }

    /**
     * Gets the width of the SVG for the tree diagram.
     * Does not include the margins.
     * 
     * @returns {number} The width (not including the margins) of the SVG for the tree diagram.
     */
    getWidthWithoutMargins() {
        return this._widthWithoutMargin;
    }

    /**
     * Sets the height of the SVG for the tree diagram.
     * 
     * @param {*} newHeightWithoutMargin The height of SVG for the tree diagram.
     * @returns {object} The tree object.
     */
    setHeightWithoutMargins(newHeightWithoutMargin) {
        this._heightWithoutMargin = newHeightWithoutMargin;
        return this;
    }

    /**
     * Gets the height of the SVG for the tree diagram.
     * Does not include the margins.
     * 
     * @returns {number} The height (not including the margins) of the SVG for the tree diagram.
     */
    getHeightWithoutMargins() {
        return this._heightWithoutMargin;
    }

    /**
     * Updates the dimensions of the SVG.
     * 
     * @returns {object} The tree object.
     */
    updateDimensions() {
        // Update SVG with new width and height
        this.getSvg()
            // Use viewBox to set SVG width and height
            // so it is responsive, and can be resized
            // based on the parent element
            .attr("viewBox", "0 0 " + this.getWidthWithoutMargins() + " " + this.getHeightWithoutMargins());

        var margins = this.getMargins();

        var needToCenterView = false;
        // update the tree generator with the new width and height
        var sizingMode = this.nodeSettings.getSizingMode();
        if (typeof sizingMode === 'string')
            sizingMode = sizingMode.trim().toLowerCase();
        if (sizingMode === "nodesize") {
            this.getTreeGenerator()
                .nodeSize(this._getNodeSize());

            // Only perform centering if node centering is turned off,
            // as that would center to the root node anyway. Node
            // centering is turned on when allow focus is turned on.
            if (this.getAllowFocus() === false)
                needToCenterView = true;
        }
        else {
            this.getTreeGenerator()
                .size([this.getHeight(), this.getWidth()]);
        }

        if (needToCenterView === false) {
            // Update the view with the new margins
            this.getView()
                .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
        }
        else {
            // Move the view downwards as to center the root node
            // This is due to when you use node-size, it sets the
            // node origin at 0, 0 instead of automatically
            // centering it as it does with size()
            this.getView()
                .attr("transform", "translate(" + margins.left + ", " + (this.getHeight() / 2 + margins.top) + ")");
        }

        // If we need to center the tree by adjusting the view and the node position
        var x0, y0;
        if (this.getOrientation() === 'topToBottom')
        {
            if (needToCenterView === false) {
                x0 = this.getWidth() / 2;
            }
            else {
                x0 = 0;
            }
            y0 = this.getHeight() / 4;
        }
        else
        {
            if (needToCenterView === false) {
                x0 = this.getHeight() / 2;
            }
            else {
                x0 = 0;
            }
            y0 = 0;
        }

        this.getRoot().x0 = x0;
        this.getRoot().y0 = y0;

        if (this.getZoomListener()) {
            this.getZoomListener()
                .extent([[0, 0], [this.getWidthWithoutMargins(), this.getHeightWithoutMargins()]]);
        }
        
        return this;
    }

    /**
     * Validates the settings to ensure the
     * tree diagram is ready to be generated.
     * 
     * @returns {object} The tree object.
     */
    validateSettings() {
        // Check to make sure compulsory options are provided
        if (!this.getElement())
            throw "Need to pass in an element as part of the options";
        if (!this.getData())
            throw "Need to pass in data as part of the options";

        // Checks if mandatory methods to specify exists
        if (!this._getId)
            throw "Need to define the getId function as part of the options";
        this.loadOnDemandSettings.validateSettings();
        return this;
    }

    /**
     * Creates and set up the the tree diagram.
     * 
     * @returns {object} The tree object.
     */
    initialize() {
        this.validateSettings();
        this.regenerateNodeData();

        while (this.getElement().firstChild) {
            this.getElement().removeChild(this.getElement().firstChild);
        };

        // Create the svg, and set its dimensions
        this._svg = d3.select(this.getElement())
            .append("svg")
                .classed('mitch-d3-tree', true)
                .classed(this.getTheme(), true)
                .attr("preserveAspectRatio", "xMidYMid meet")
                .style("width", "100%")
                .style("height", "100%");

        // Create the view with margins
        this._view = this.getSvg().append("g")
            .classed("view", true);

        // Create tree generator to position the nodes
        this._treeGenerator = d3.tree();

        // Create the panning container which panning should act upon
        this._panningContainer = this.getView().append("g")
            .classed("panningContainer", true);

        this._zoomListener = d3.zoom()
            // Limit zoom level
            .scaleExtent([this.getMinScale(), this.getMaxScale()])
            // Zoom in D3 translates to the native HTML/JS events of:
            // - Double Clicking (i.e. to zoom in)
            // - Dragging (i.e. panning or moving around)
            // - Wheel (i.e. zoom in/out)
            .on("zoom", () => {
                // The "zoom" event populates d3.event with an object that has
                // a "transform" property (an object with three properties
                // of x, y, and k), where x and y is for translation purposes,
                // and k is the scaling factor
                var transform = d3.event.transform;
                this.getPanningContainer().attr("transform", transform);
            });
        this.getSvg().call(this.getZoomListener());

        if (this.getAllowPan() === false) {
            this.getSvg()
                .on("mousedown.zoom", null)
                .on("touchstart.zoom", null)
                .on("touchmove.zoom", null)
                .on("touchend.zoom", null);
        }
        if (this.getAllowZoom() === false) {
            this.getSvg()
                .on("dblclick.zoom", null)
                .on("wheel.zoom", null);
        }

        this.updateDimensions();

        this._populateUnderlyingChildren(this.getRoot());

        if (this.getRoot().children)
            this.getRoot().children.forEach(this.collapseRecursively);
        this.removeSelection(this.getRoot());

        // Call the first update, which renders
        // the initial tree
        this.update(this.getRoot());

        // Centers the root node
        this.centerNode(this.getRoot());

        return this;
    }

    /**
     * Expands the given node data item.
     * 
     * @param {object} nodeDataItem The D3 node data item to expand.
     * @returns {object} The tree object.
     */
    expand(nodeDataItem) {
        nodeDataItem.children = nodeDataItem._children;
        return this;
    }

    /**
     * Expands the given node data item,
     * and its children and descendants.
     * 
     * @param {object} nodeDataItem The D3 node data item to expand.
     * @returns {object} The tree object.
     */
    expandRecursively(nodeDataItem) {
        var rec = function recursive(directNodeDataItem) {
            if (directNodeDataItem.children) {
                directNodeDataItem.children.forEach(recursive);
                directNodeDataItem.children = directNodeDataItem._children;
            }
        };
        rec(nodeDataItem);
        return this;
    }

    /**
     * Collapses the given node data item.
     * 
     * @param {object} nodeDataItem The D3 node data item to collapse.
     * @returns {object} The tree object.
     */
    collapse(nodeDataItem) {
        nodeDataItem.children = null;
        return this;
    }

    /**
     * Collapses the given node data item,
     * and its children and descendants.
     * 
     * @param {object} nodeDataItem The D3 node data item to collapse.
     * @returns {object} The tree object.
     */
    collapseRecursively(nodeDataItem) {
        var rec = function recursive(directNodeDataItem) {
            if (directNodeDataItem.children) {
                directNodeDataItem.children.forEach(recursive);
                directNodeDataItem.children = null;
            }
        };
        rec(nodeDataItem);
        return this;
    }
    
    /**
     * Populates the node's children to a hidden property.
     * 
     * @param {object} nodeDataItem The D3 node data item to collapse.
     * @returns {object} The tree object.
     */
    _populateUnderlyingChildren(nodeDataItem) {
        var rec = function recursive(directNodeDataItem) {
            if (directNodeDataItem.children) {
                directNodeDataItem._children = directNodeDataItem.children;
                directNodeDataItem._children.forEach(recursive);
            }
        };
        rec(nodeDataItem);
        return this;
    }

    /**
     * Remove node selections for a given node and it's children.
     * 
     * @param {object} nodeDataItem The D3 node data item to remove selection from.
     * @returns {object} The tree object.
     */
    removeSelection(nodeDataItem) {
        var rec = function recursive(directNodeDataItem) {
            directNodeDataItem.selected = false;
            if (directNodeDataItem.children) {
                directNodeDataItem.children.forEach(recursive);
            }
        };
        rec(nodeDataItem);
        return this;
    }

    /**
     * Center the view to a D3 tree node.
     * 
     * @param {*} nodeDataItem The D3 node data item to focus on.
     * @returns {object} The tree object.
     */
    centerNode(nodeDataItem) {
        var transform = d3.zoomTransform(this.getSvg().node());
        var scale = transform.k;

        var x, y, translateX, translateY;
        if (this.getOrientation().toLowerCase() === 'toptobottom')
        {
            x = -nodeDataItem.x0;
            y = -nodeDataItem.y0;
            translateX = x * scale + this.getWidth() / 2;
            translateY = y * scale + this.getHeight() / 2;
        }
        else
        {
            x = -nodeDataItem.y0;
            y = -nodeDataItem.x0;
            translateX = x * scale + this.getWidth() / 4;
            translateY = y * scale + this.getHeight() / 2;
        }

        this.getSvg().transition()
            .duration(this.getDuration())
            .call(this.getZoomListener().transform, d3.zoomIdentity.translate(translateX, translateY).scale(scale));
        return this;
    }

    /**
     * Triggers the nodeClick event when a
     * D3 node is clicked on, and proceeds
     * to focus/expand/collapse the node.
     * 
     * @param {object} nodeDataItem The D3 node data item that was clicked.
     * @param {number} index The index of the D3 node being clicked in the array of siblings.
     * @param {object[]} arr Array of siblings D3 node, inclusive of the clicked node data item itself.
     * @emits {nodeClick} Emit node click event.
     * @returns {boolean} True meaning it successfully focused/expanded/collapsed a node. False otherwise.
     */
    _onNodeClick(nodeDataItem, index, arr) {
        var eventType = null;
        if (this.getAllowFocus())
            eventType = 'focus';
        else if (nodeDataItem.children)
            eventType = 'collapse';
        else
            eventType = 'expand';
        
        var event = {
            type: eventType,
            continue: true,
            nodeDataItem: nodeDataItem,
            nodeDataItemIndex: index,
            nodeDataItems: arr,
            preventDefault: function() {
                event.continue = false;
            }
        }
        this.emit('nodeClick', event);
        if (event.continue === false)
            return false;
        if (this.getAllowFocus())
            this.nodeFocus.call(this, nodeDataItem);
        else
            this.nodeToggle.call(this, nodeDataItem);
        return true;
    }

    /**
     * Creates a child D3 tree node.
     * 
     * @param {object} parentNodeDataItem The parent D3 tree node data item.
     * @param {object} dataItem The data item.
     * @returns {object} The newly created D3 node;
     */
    _createNode(parentNodeDataItem, dataItem) {
        // Create a D3 node object from resulting data items using d3.hierarchy()
        var newNode = d3.hierarchy(dataItem);

        // Now add missing properties to Node like child, parent, depth
        newNode.depth = parentNodeDataItem.depth + 1;
        newNode.height = parentNodeDataItem.height - 1;
        newNode.parent = parentNodeDataItem;
        newNode.id = this.getId.call(this, dataItem);
        return newNode;
    }

    /**
     * Creates and adds a child D3 tree
     * node to a given parent D3 tree node.
     * 
     * @param {object} parentNodeDataItem The parent D3 tree node data item.
     * @param {object} dataItem The data item.
     * @returns {object} The newly created and added D3 node;
     */
    _addUnderlyingChildNode(parentNodeDataItem, dataItem) {
        var newNode = this._createNode(parentNodeDataItem, dataItem);
        parentNodeDataItem._children.push(newNode);
        return newNode;
    }
    
    /**
     * Process the loaded data from AJAX
     * resulting from a node expand.
     * 
     * @param {object} nodeDataItem The D3 node data item being expanded.
     * @param {object[]} result The children data items to process.
     * @returns {object} The tree object.
     */
    _processLoadedDataForNodeFocus(nodeDataItem, result) {
        nodeDataItem._children = [];
        result.forEach((currentItem) => this._addUnderlyingChildNode(nodeDataItem, currentItem));

        this._populateUnderlyingChildren(nodeDataItem);

        this.updateTreeWithFocusOnNode(nodeDataItem);

        var wasSelected = nodeDataItem.selected;
        this.removeSelection(this.getRoot());
        nodeDataItem.selected = true;

        this.update(nodeDataItem);

        if (this.getAllowNodeCentering() === true && 
            (wasSelected === false || typeof wasSelected === 'undefined'))
            this.centerNode(nodeDataItem);
        return this;
    }

    /**
     * Focuses to a node, given a node data item.
     * 
     * @param {object} nodeDataItem The node being focused on.
     * @returns {object} The tree object.
     */
    nodeFocus(nodeDataItem) {
        if (!nodeDataItem.children && !nodeDataItem._children
            && this.loadOnDemandSettings.isEnabled()
            && this.loadOnDemandSettings.hasChildren(nodeDataItem.data)) {
            var processData = (result) => this._processLoadedDataForNodeFocus(nodeDataItem, result);
            this.loadOnDemandSettings.loadChildren(nodeDataItem.data, processData);
        }
        else {
            this.updateTreeWithFocusOnNode(nodeDataItem);

            var wasSelected = nodeDataItem.selected;
            this.removeSelection(this.getRoot());
            nodeDataItem.selected = true;

            this.update(nodeDataItem);

            if (this.getAllowNodeCentering() === true && 
                (wasSelected === false || typeof wasSelected === 'undefined'))
                this.centerNode(nodeDataItem);
        }
        return this;
    }

    /**
     * Process the loaded data from AJAX
     * resulting from a node toggle.
     * 
     * @param {object} nodeDataItem The D3 node data item.
     * @param {object[]} result Array of sibling node data items, inclusive the node being toggled.
     * @returns {object} The tree object.
     */
    _processLoadedDataForNodeToggle(nodeDataItem, result) {
        nodeDataItem._children = [];
        result.forEach((currentItem) => this._addUnderlyingChildNode(nodeDataItem, currentItem));

        this.expand(nodeDataItem);

        this.update(nodeDataItem);
        return this;
    }

    /**
     * Toggles the children visibility for a given node data item.
     * 
     * @param {*} nodeDataItem D3 Node data item.
     * @returns {object} The tree object.
     */
    nodeToggle(nodeDataItem) {
        // Clear all selections, and select current node
        this.removeSelection(this.getRoot());
        nodeDataItem.selected = true;
        // If it hasn't been loaded, and it's specified to have children,
        // then perform load-on-demand to load new items from server
        // and add them as child nodes
        if (!nodeDataItem.children && !nodeDataItem._children
            && this.loadOnDemandSettings.isEnabled()
            && this.loadOnDemandSettings.hasChildren(nodeDataItem.data)) {
            var processData = (result) => {
                this._processLoadedDataForNodeToggle(nodeDataItem, result);
                if (this.getAllowNodeCentering() === true)
                    this.centerNode(nodeDataItem);
            }
            this.loadOnDemandSettings.loadChildren(nodeDataItem.data, processData);
        }
        else {
            if (nodeDataItem.children)
                this.collapse(nodeDataItem);
            else
                this.expand(nodeDataItem);
            this.update(nodeDataItem);
            if (this.getAllowNodeCentering() === true)
                this.centerNode(nodeDataItem);
        }
        return this;
    }

    /**
     * Hide the siblings nodes
     * for a given node.
     * 
     * @param {object} nodeDataItem The D3 node to hide siblings for.
     * @returns {object} The tree object.
     */
    hideSiblings(nodeDataItem) {
        var parentNode = nodeDataItem.parent;

        if (parentNode) {
            var nodeId = this.getId(nodeDataItem.data);
            parentNode.children.filter(x => this.getId(x.data) != nodeId).forEach(this.collapseRecursively);
            parentNode.children = [];
            parentNode.children.push(nodeDataItem);
        }
        return this;
    }

    /**
     * Updates the tree diagram so only the relevant
     * focused node and direct parent hierarchies are
     * shown.
     * 
     * @param {object} nodeDataItem D3 Node data item.
     * @returns {object} The tree object.
     */
    updateTreeWithFocusOnNode(nodeDataItem) {
        if (!nodeDataItem.children && nodeDataItem._children) { // If there's no children nodes, but there a some children items to expand
            this.hideSiblings(nodeDataItem);

            this.expand(nodeDataItem);
            // Collapse the current focused node's children, so only direct childrens are shown
            nodeDataItem.children.forEach(this.collapseRecursively);
        }
        else if (nodeDataItem.children) { // If there are rendered children nodes
            // Checks if its children has any rendered children
            var hasNestedChildren = nodeDataItem.children.some((currentItem, index, arr) => currentItem.children);

            var isPreviouslyExpandedNode = !hasNestedChildren;
            // If it is a parent node with children, and
            // is not the previous expanded node,
            // then we'll focus on it, expanding it
            // and showing all of its children
            if (isPreviouslyExpandedNode === false) {
                this.collapseRecursively(nodeDataItem);
                this.expand(nodeDataItem);
            }
        }
        return this;
    }

    /**
     * Updates the tree nodes given
     * a D3 tree node.
     * 
     * @param {object} nodeDataItem The D3 node data item to update.
     * @param {object[]} nodes Array of D3 node data items.
     * @returns {object} The tree object.
     */
    _updateNodes(nodeDataItem, nodes) {
        // Normalize for fixed-depth.
        
        // You can increase the depth multiplication to get more depth,
        // i.e. increasing the distance between the parent node and child node
        nodes.forEach((data) => data.y = data.depth * this.getNodeDepthMultiplier());

        // ****************** Nodes section ***************************

        // Update the nodes...
        var nodes = this.getPanningContainer().selectAll("g.node")
            // The second parameter of data() takes in a 
            // function, determining the key of the data
            // items, which is useful to retrieve items,
            // and databind them
            .data(nodes, (data) => this.getId.call(this, data.data));

        // Enter any new nodes at the parent's previous position.
        var nodeEnter = nodes.enter().append("g")
            .classed("node", true)
            .attr("transform", (data, index, arr) => {
                if (this.getOrientation().toLowerCase() === 'toptobottom')
                    return "translate(" + nodeDataItem.x0 + "," + nodeDataItem.y0 + ")";
                else
                    return "translate(" + nodeDataItem.y0 + "," + nodeDataItem.x0 + ")";
            })
            .on("click", (data, index, arr) => this._onNodeClick.call(this, data, index, arr));

        this._nodeEnter(nodeEnter, nodes);

        // UPDATE
        var nodeUpdate = nodeEnter.merge(nodes);
        var nodeUpdateTransition = nodeUpdate.transition().duration(this.getDuration());

        nodeUpdate
            .classed("collapsed", (data, index, arr) => {
                if (!data.children && data._children)
                    return true;
                else if (this.loadOnDemandSettings.isEnabled()
                            && this.loadOnDemandSettings.hasChildren(data.data)
                            && !data.children && !data._children) // If it does have children to load via AJAX
                    return true;
                return false;
            })
            .classed("expanded", (data, index, arr) => data.children)
            .classed("childless", (data, index, arr) => !data.children && !data._children)
            .classed("selected", (data, index, arr) => data.selected);

        this._nodeUpdate(nodeUpdate, nodeUpdateTransition, nodes);

        // Remove any exiting nodes
        var nodeExit = nodes.exit();
        var nodeExitTransition = nodeExit.transition().duration(this.getDuration());

        this._nodeExit(nodeExit, nodeExitTransition, nodes);
        return this;
    }

    /**
     * Updates the tree node links given
     * a D3 tree node.
     * 
     * @param {object} nodeDataItem The D3 node data item. 
     * @param {object[]} links Array of D3 link data items.
     * @returns {object} The tree object.
     */
    _updateLinks(nodeDataItem, links) {
        var linkPathGenerator = this._getLinkPathGenerator();

        // Update the links...
        var link = this.getPanningContainer().selectAll("path.link")
            .data(links, (data) => this.getId.call(this, data.data));

        // Enter any new links at the parent's previous position.
        var linkEnter = link.enter().insert("path", "g")
            .classed("link", true);
        this._linkEnter(nodeDataItem, linkEnter, link, linkPathGenerator);

        // UPDATE
        var linkUpdate = linkEnter.merge(link);
        var linkUpdateTransition = linkUpdate.transition()
                                             .duration(this.getDuration());

        // Transition back to the parent element position
        this._linkUpdate(nodeDataItem, linkUpdate, linkUpdateTransition, link, linkPathGenerator);

        // Remove any exiting links
        var linkExit = link.exit();
        var linkExitTransition = linkExit.transition()
                                          .duration(this.getDuration())

        this._linkExit(nodeDataItem, linkExit, linkExitTransition, link, linkPathGenerator);

        // Store the old positions for transition.
        this.getVisibleNodes().forEach((data) => {
            data.x0 = data.x;
            data.y0 = data.y;
        });
        return this;
    }

    /**
     * Updates the tree given a D3 tree node.
     * 
     * @param {object} nodeDataItem The D3 node data item.
     * @returns {object} The tree object.
     */
    update(nodeDataItem) {
        var treeGenerator = this.getTreeGenerator();
        // Assigns the x and y position for the nodes
        var treeData = treeGenerator(this.getRoot());
        this._visibleNodes = treeData.descendants();
        this._nodes = [this.getRoot(), ...recursiveGet(this.getRoot(), (node) => node._children)];
        this._links = treeData.descendants().slice(1);

        this._updateNodes(nodeDataItem, this.getVisibleNodes())
            ._updateLinks(nodeDataItem, this.getLinks());

        return this;
    }

    /**
     * Gets the unique ID for a given data item.
     * @callback getIdCallBack
     * @param {object} data Represents the single data item to extract the ID from.
     * @returns {*} The unique ID from the given data item.
     */

    /**
     * Gets the parent ID for a given data item.
     * @callback getParentIdCallBack
     * @param {object} data Represents the single data item to extract the parent ID from.
     * @returns {*} The parent ID from the given data item.
     */

    /**
     * Gets the children items for a given
     * data item.
     * @callback getChildrenCallBack
     * @param {object} data Represents the single data item to extract the children data items from.
     * @returns {object[]} The array of data items representing the children of the given data item.
     */

    /**
     * Node click event, triggered when a
     * user clicks on a tree node.
     * 
     * @typedef {object} nodeClick
     * @property {object} event Object containing various event parameters.
     * @property {string} event.type The type of the operation the click will trigger, whether it's 'focus', 'expand', or 'collapse'.
     * @property {boolean} event.continue Whether to continue the node focusing/expanding/collapsing.
     * @property {function} event.preventDefault Call this function to prevent the default behavior of node focusing/expanding/collapsing.
     * @property {object} event.nodeDataItem Node data item representing the clicked node.
     * @property {object} event.nodeDataItem.data The data item of the clicked node.
     * @property {number} event.nodeDataItemIndex Index of the clicked item in the array of siblings.
     * @property {object[]} event.nodeDataItems The array of sibling rendered SVG elements, inclusive of the node itself.
     */
}

// Define option defaults using a class static property
BaseTree.defaults = {
    theme: 'default',
    orientation: 'leftToRight', // topToBottom, rightToLeft, bottomToTop
    allowPan: true,
    allowZoom: true,
    allowFocus: true,
    allowNodeCentering: true,
    minScale: 1, // Minimum zoom scaling
    maxScale: 2, // Maximum zoom scaling
    // You can increase the depth multiplication to get more depth,
    // i.e. increasing the distance between the parent node and child node
    nodeDepthMultiplier: 300,
    isFlatData: false,
    getId: null,
    getParentId: null,
    getChildren: null,
    widthWithoutMargins: 960,
    heightWithoutMargins: 800,
    margins: {
        top: 40,
        right: 20,
        bottom: 40,
        left: 100
    },
    duration: 750,
    loadOnDemandSettings: {
        // Defaults are defined in the load-on-demand settings prototype
    },
    nodeSettings: {
        // Defaults are defined in the node settings prototype
    },
}

export default BaseTree;