/** * The 'd3-horizontal-tree' component is a perfect way to visualize hierarchical * data as an actual tree in case where the relative size of nodes is of little * interest, and the focus is on the relative position of each node in the hierarchy. * A horizontal tree makes for a more consistent look and more efficient use of space * when text labels are shown next to each node. * * @example * Ext.create('Ext.panel.Panel', { * renderTo: Ext.getBody(), * title: 'Tree Chart', * layout: 'fit', * height: 500, * width: 1000, * items: [ * { * xtype: 'd3-tree', * * store: { * type: 'tree', * data: [ * { * text: "IT", * expanded: false, * children: [ * {leaf: true, text: 'Norrin Radd'}, * {leaf: true, text: 'Adam Warlock'} * ] * }, * { * text: "Engineering", * expanded: false, * children: [ * {leaf: true, text: 'Mathew Murdoch'}, * {leaf: true, text: 'Lucas Cage'} * ] * }, * { * text: "Support", * expanded: false, * children: [ * {leaf: true, text: 'Peter Quill'} * ] * } * ] * }, * * interactions: { * type: 'panzoom', * zoom: { * extent: [0.3, 3], * doubleTap: false * } * }, * * nodeSize: [20, 350] * } * ] * }); * */Ext.define('Ext.d3.hierarchy.tree.HorizontalTree', { extend: 'Ext.d3.hierarchy.tree.Tree', xtype: [ 'd3-tree', 'd3-horizontal-tree' ], config: { componentCls: 'horizontal-tree', diagonal: undefined, nodeTransform: function (selection) { selection.attr('transform', function (node) { return 'translate(' + node.y + ',' + node.x + ')'; }); } }, applyDiagonal: function (diagonal, oldDiagonal) { if (!Ext.isFunction(diagonal)) { if (oldDiagonal) { diagonal = oldDiagonal; } else { // A D3 entity cannot be a default config, nor can it be on the prototype // of a class, because then it is accessed at Ext.define time, which is // likely to cause loading errors. diagonal = d3.svg.diagonal().projection(function (node) { return [node.y, node.x]; }); } } return diagonal; }, pendingTreeAlign: false, onSceneResize: function (scene, rect) { var me = this, layout = me.getLayout(); if (layout.nodeSize()) { if (!me.size) { me.performLayout(); // This is the first resize, so the scene is empty prior to `performLayout` call. if (me.hasFirstRender) { me.alignTree(); } else { // The scene didn't render for whatever reason (no store, blocked layout, etc.). me.pendingTreeAlign = true; } } } else { // No need to set layout size and perform layout on resize, if the node size // is fixed, as layout.size and layout.nodeSize are mutually exclusive. me.callParent(arguments); } }, onAfterRender: function () { if (this.pendingTreeAlign) { this.pendingTreeAlign = false; this.alignTree(); } }, /** * @private */ alignTree: function () { this.alignContent('left', 'center'); }, setLayoutSize: function (size) { // For trees the first entry in the `size` array represents the tree's breadth, // and the second one - depth. var _ = size[0]; size[0] = size[1]; size[1] = _; this.callParent([size]); }, addNodes: function (selection) { var me = this, group = selection.append('g'), colorAxis = me.getColorAxis(), nodeText = me.getNodeText(), nodeRadius = me.getNodeRadius(), nodeTransform = me.getNodeTransform(), nodeTransition = me.getNodeTransition(), labels; // If we select a node, the highlight transition kicks off in 'onNodeSelect'. // But this can trigger a layout change, if selected node has children and // starts to expand, which triggers another transition that cancels the // highlight transition. // // So we need two groups: // 1) the outer one will have a translation transition applied to it // on layout change; // 2) and the inner one will have a scale transition applied to it on // selection highlight. group .attr('class', me.defaultCls.node) .call(me.onNodesAdd.bind(me)) .call(nodeTransform.bind(me)); group .append('circle') .attr('class', 'circle') .style('fill', function (node) { return colorAxis.getColor(node); }) .call(function (selection) { if (nodeTransition) { selection .attr('r', 0) .transition(nodeTransition.name) .duration(nodeTransition.duration) .attr('r', nodeRadius); } else { selection .attr('r', nodeRadius) } }); labels = group .append('text') .text(function (node) { return nodeText(me, node); }) .attr('class', me.defaultCls.label) .each(function (node) { // Note that we can't use node.children here to determine // whether the node has children or not, because the // default accessor returns node.childNodes (that are saved // as node.children) only when the node is expanded. var isLeaf = node.isLeaf(); this.setAttribute('x', isLeaf ? nodeRadius + 5 : -5 - nodeRadius); }) .call(function (selection) { if (nodeTransition) { selection .style('fill-opacity', 0) .transition(nodeTransition.name) .duration(nodeTransition.duration) .style('fill-opacity', 1) } else { selection .style('fill-opacity', 1); } }); if (Ext.d3.Helpers.noDominantBaseline()) { labels.each(function () { Ext.d3.Helpers.fakeDominantBaseline(this, 'central', true); }); } }, updateNodes: function (selection) { var me = this, nodeTransform = me.getNodeTransform(), nodeClass = me.getNodeClass(); selection .call(nodeClass.bind(me)) .transition() .call(nodeTransform.bind(me)); }, addLinks: function (selection) { selection .append('path') .classed(this.defaultCls.link, true) .attr('d', this.getDiagonal()); }, updateLinks: function (selection) { selection .transition() .attr('d', this.getDiagonal()); } });