/** * The 'd3-pack' component uses D3's * [Pack Layout](https://github.com/d3/d3-hierarchy/#pack) * to visualize hierarchical data as a enclosure diagram. * The size of each leaf node’s circle reveals a quantitative dimension * of each data point. The enclosing circles show the approximate cumulative size * of each subtree. * * The pack additionally layout populates the following attributes on each node: * - `r` - the computed node radius. * * @example * Ext.create('Ext.panel.Panel', { * renderTo: Ext.getBody(), * title: 'Pack Chart', * height: 750, * width: 750, * layout: 'fit', * items: [ * { * xtype: 'd3-pack', * tooltip: { * renderer: function(component, tooltip, node) { * var record = node.data; * tooltip.setHtml(record.get('text')); * } * }, * store: { * type: 'tree', * data: [ * { * "text": "DC", * "children": [ * { * "text": "Flash", * "children": [ * { "text": "Flashpoint" } * ] * }, * { * "text": "Green Lantern", * "children": [ * { "text": "Rebirth" }, * { "text": "Sinestro Corps War" } * ] * }, * { * "text": "Batman", * "children": [ * { "text": "Hush" }, * { "text": "The Long Halloween" }, * { "text": "Batman and Robin" }, * { "text": "The Killing Joke" } * ] * } * ] * }, * { * "text": "Marvel", * "children": [ * { * "text": "All", * "children": [ * { "text": "Infinity War" }, * { "text": "Infinity Gauntlet" }, * { "text": "Avengers Disassembled" } * ] * }, * { * "text": "Spiderman", * "children": [ * { "text": "Ultimate Spiderman" } * ] * }, * { * "text": "Vision", * "children": [ * { "text": "The Vision" } * ] * }, * { * "text": "X-Men", * "children": [ * { "text": "Gifted" }, * { "text": "Dark Phoenix Saga" }, * { "text": "Unstoppable" } * ] * } * ] * } * ] * } * } * ] * }); * */Ext.define('Ext.d3.hierarchy.Pack', { extend: 'Ext.d3.hierarchy.Hierarchy', xtype: 'd3-pack', requires: [ 'Ext.d3.Helpers' ], config: { componentCls: 'pack', /** * The padding of a node's text inside its container. * If the length of the text is such that it can't have the specified padding * and still fit into a container, the text will hidden, unless the * {@link #clipText} config is set to `false`. * It's possible to use negative values for the padding to allow the text to * go outside its container by the specified amount. * @cfg {Array} textPadding Array of two values: horizontal and vertical padding. */ textPadding: [3, 3], /** * @cfg {Function/Number} nodeValue * By default, the area occupied by the node depends on the number * of children the node has, but cannot be zero, so that leaf * nodes are still visible. */ nodeValue: function(record) { return record.childNodes.length + 1; }, /** * If `false`, the text will always be visible, whether it fits inside its * container or not. * @cfg {Boolean} [clipText=true] */ clipText: true, /** * @cfg {Boolean} noSizeLayout * @private */ noSizeLayout: false }, applyLayout: function() { return d3.pack(); }, onNodeSelect: function(node, el) { this.callParent(arguments); // Remove the fill given by the `colorAxis`, so that // the CSS style can be used to specify the color // of the selection. el.select('circle').style('fill', null); }, onNodeDeselect: function(node, el) { var me = this, colorAxis = me.getColorAxis(); me.callParent(arguments); // Restore the original color. // (see 'onNodeSelect' comments). el .select('circle') .style('fill', function(node) { return colorAxis.getColor(node); }); }, updateColorAxis: function(colorAxis) { var me = this; if (!me.isConfiguring) { me.getRenderedNodes() .select('circle') .style('fill', function(node) { return colorAxis.getColor(node); }); } }, /** * @private */ textVisibilityFn: function(selection) { // Text padding value is treated as pixels, even if it isn't. var me = this, textPadding = this.getTextPadding(), dx = parseFloat(textPadding[0]) * 2, dy = parseFloat(textPadding[1]) * 2; selection .classed(me.defaultCls.invisible, function(node) { // The 'text' attribute must be hidden via the 'visibility' attribute, // in addition to setting its 'fill-opacity' to 0, as otherwise // it will protrude outside from its 'circle', and may interfere with // click and other events on adjacent node elements. var bbox = this.getBBox(), // 'this' is SVG 'text' element width = node.r - dx, height = node.r - dy; return (bbox.width > width || bbox.height > height); }); }, addNodes: function(selection) { var me = this, nodeTransform = me.getNodeTransform(), clipText = me.getClipText(), labels; selection .call(me.onNodesAdd.bind(me)) .call(nodeTransform) .append('circle'); labels = selection .append('text') .attr('class', me.defaultCls.label); if (clipText) { labels.call(me.textVisibilityFn.bind(me)); } if (Ext.d3.Helpers.noDominantBaseline()) { labels.each(function() { Ext.d3.Helpers.fakeDominantBaseline(this, 'central', true); }); } labels.attr('opacity', 0); }, updateNodes: function(update, enter) { var me = this, colorAxis = me.getColorAxis(), nodeTransform = me.getNodeTransform(), selectionCfg = me.getSelection(), nodeText = me.getNodeText(), clipText = me.getClipText(), selection = update.merge(enter), transition = me.layoutTransition, text; selection .transition(transition) .call(nodeTransform); selection .select('circle') .transition(transition) .attr('r', function(node) { return node.r; }) .style('fill', function(node) { // Don't set the color of selected element to allow for CSS styling. return node.data === selectionCfg ? null : colorAxis.getColor(node); }); text = selection.select('text') .text(function(node) { return nodeText(me, node); }); if (clipText) { text.call(me.textVisibilityFn.bind(me)); } text.transition(transition).attr('opacity', 1); }, removeNodes: function(selection) { selection .attr('opacity', 1) .transition(this.layoutTransition) .attr('opacity', 0) .remove(); }});