/** * The 'd3-treemap' component uses D3's * [TreeMap Layout](https://github.com/d3/d3-hierarchy/#treemap) * to recursively subdivide area into rectangles, where the area of any node in the tree * corresponds to its value. * * @example * Ext.create('Ext.panel.Panel', { * renderTo: Ext.getBody(), * title: 'TreeMap Chart', * height: 750, * width: 750, * layout: 'fit', * items: [ * { * xtype: 'd3-treemap', * tooltip: { * renderer: function (component, tooltip, node) { * tooltip.setHtml(node.data.get('text')); * } * }, * nodeValue: function (record) { * // The value in your data to derive the size of the tile from. * return record.get('value'); * }, * store: { * type: 'tree', * data: [ * { text: 'Hulk', * value : 5, * children: [ * { text: 'The Leader', value: 3 }, * { text: 'Abomination', value: 2 }, * { text: 'Sandman', value: 1 } * ] * }, * { text: 'Vision', * value : 4, * children: [ * { text: 'Kang', value: 4 }, * { text: 'Magneto', value: 3 }, * { text: 'Norman Osborn', value: 2 }, * { text: 'Anti-Vision', value: 1 } * ] * }, * { text: 'Ghost Rider', * value : 3, * children: [ * { text: 'Mephisto', value: 1 } * ] * }, * { text: 'Loki', * value : 2, * children: [ * { text: 'Captain America', value: 3 }, * { text: 'Deadpool', value: 4 }, * { text: 'Odin', value: 5 }, * { text: 'Scarlet Witch', value: 2 }, * { text: 'Silver Surfer', value: 1 } * ] * }, * { text: 'Daredevil', * value : 1, * children: [ * { text: 'Purple Man', value: 4 }, * { text: 'Kingpin', value: 3 }, * { text: 'Namor', value: 2 }, * { text: 'Sabretooth', value: 1 } * ] * } * ] * } * } * ] * }); * */Ext.define('Ext.d3.hierarchy.TreeMap', { extend: 'Ext.d3.hierarchy.Hierarchy', xtype: 'd3-treemap', requires: [ 'Ext.d3.Helpers' ], config: { componentCls: 'treemap', /** * @cfg {Function} tiling * The [tiling method](https://github.com/d3/d3-hierarchy#treemap_tile) to use * with the `treemap` layout. For example: * * tiling: 'd3.treemapBinary' * */ tiling: null, /** * @cfg {Object} parentTile * Parent tile options. * * @cfg {Number} [parentTile.padding=4] * Determines the amount of extra space to reserve between * the parent and its children. Uniform on all sides, except the top * padding is calculated by the component itself depending on the height * of the tile's title. * * @cfg {Object} parentTile.label Parent tile label options. * * @cfg {Number} [parentTile.label.offset=[5, 2]] * The offset of the label from the top-left corner of the tile's rect. * * @cfg {Number[]} parentTile.label.clipSize * If the size of a parent node is smaller than this size, its label will be hidden. */ parentTile: { padding: 4, label: { offset: [5, 2], clipSize: [110, 40] } }, /** * @cfg {Object} leafTile * Leaf tile options. * * @cfg {Number} [leafTile.padding=0] * The [padding](https://github.com/d3/d3-hierarchy#treemap_paddingInner) * used to separate a node’s adjacent children. * * @cfg {Object} leafTile.label Child tile label options. * * @cfg {Number[]} [parentTile.label.offset] The offset of the label from the * top-left corner of the tile's rect. Defaults to `[5, 1]`. */ leafTile: { padding: 0 }, nodeTransform: function (selection) { selection.attr('transform', function (node) { return 'translate(' + node.x0 + ',' + node.y0 + ')'; }); }, /** * @cfg {String} busyLayoutText The text to show when the layout is in progress. */ busyLayoutText: 'Layout in progress...', noParentValue: true, noSizeLayout: false, /** * @cfg {Boolean} scaleLabels * @since 6.5.0 * If `true` the bigger tiles will have (more or less) proportionally bigger labels. */ scaleLabels: false }, /** * @private * @property */ labelQuantizer: null, constructor: function (config) { this.labelQuantizer = this.createLabelQuantizer(); this.callParent([config]); }, /** * @private */ createLabelQuantizer: function () { return d3.scaleQuantize().domain([8, 27]).range(['8px', '12px', '18px', '27px']); }, /** * @private */ labelSizer: function (node, element) { return Math.min((node.x1 - node.x0) / 4, (node.y1 - node.y0) / 2); }, applyTiling: function (tiling, oldTiling) { return Ext.d3.Helpers.eval(tiling || oldTiling); }, updateTiling: function (tiling) { if (tiling) { this.getLayout().tile(tiling); if (!this.isConfiguring) { this.performLayout(); } } }, updateLeafTile: function (leafTile) { if (leafTile) { this.getLayout().paddingInner(leafTile.padding || 0); } if (!this.isConfiguring) { this.performLayout(); } }, applyParentTile: function (parentTile) { if (parentTile) { var me = this, padding = parentTile.padding; if (Ext.isNumber(padding)) { parentTile.padding = function (node) { return !me.getRootVisible() && node === me.root ? 0 : padding; }; } } return parentTile; }, updateParentTile: function (parentTile) { if (parentTile) { var layout = this.getLayout(), padding = parentTile.padding; layout.paddingRight(padding); layout.paddingBottom(padding); layout.paddingLeft(padding); } if (!this.isConfiguring) { this.performLayout(); } }, applyLayout: function () { return d3.treemap().round(true); }, setLayoutSize: function (size) { this.callParent([size]); }, deferredLayoutId: null, isLayoutBlocked: function (layout) { var me = this, maskText = me.getBusyLayoutText(), blocked = false; if (!me.deferredLayoutId) { me.showMask(maskText); // Let the mask render... // Note: small timeouts are not always enough to render the mask's DOM, // 100 seems to work every time everywhere. me.deferredLayoutId = setTimeout(me.performLayout.bind(me), 100); blocked = true; } else { clearTimeout(me.deferredLayoutId); me.deferredLayoutId = null; } return blocked; }, setupScene: function (scene) { this.callParent([scene]); this.getScaleLabels(); // interested in side effects }, onAfterRender: function () { this.hideMask(); }, /** * @private * A map of the {nodeId: Boolean} format, * where the Boolean value controls visibility of the label. */ hiddenParentLabels: null, /** * @private * A map of the {nodeId: SVGRect} format, * where the SVGRect value is the bounding box of the label. */ labelSizes: null, /** * Override superclass method here, because getting bbox of the scene won't always * produce the intended result: hidden text that sticks out of its container will * still be measured. */ getContentRect: function () { var sceneRect = this.getSceneRect(), contentRect = this.contentRect || (this.contentRect = {x: 0, y: 0}); // The (x, y) in `contentRect` should be untranslated content position relative // to the scene's origin, which is expected to always be (0, 0) for TreeMap. // But the (x, y) in `sceneRect` are relative to component's origin: // (padding.left, padding.top). if (sceneRect) { contentRect.width = sceneRect.width; contentRect.height = sceneRect.height; } return sceneRect && contentRect; }, 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('rect').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('rect') .style('fill', function (node) { return colorAxis.getColor(node); }); }, renderNodes: function (nodeElements) { var me = this, root = me.root, layout = me.getLayout(), parentTile = me.getParentTile(), parentLabel = parentTile.label, nodeTransform = me.getNodeTransform(), hiddenParentLabels = me.hiddenParentLabels = {}, labelSizes = me.labelSizes = {}, nodes; // To show parent node's label, we need to make space for it during layout by adding // extra top padding to the node. But during the layout, the size of the label // is not known. We can determine label visibility based on the size of the node, // but that alone is not enough, as long nodes can have even longer labels. // Clipping labels instead of hiding them is not possible, because overflow is not // supported in SVG 1.1, and lack of proper nested SVGs support in IE prevents us // from clipping via wrapping labels with 'svg' elements. // So knowing the size of the label is crucial. // Measuring can only be done after a node is rendered, so we do it this way: // 1) first layout pass (in performLayout) has no padding // 2) render nodes (callParent down below) // 3) measure labels // 4) second layout pass (down below) calculates padding for each parent node // 5) adjust postion and size of nodes, and label visibility me.callParent([nodeElements]); layout.paddingTop(function (node) { // This will be called for parent nodes only. // Leaf node label visibility is determined in `textVisibilityFn`. var record = node.data, id = record.data.id, size = labelSizes[id], clipSize = parentLabel.clipSize, padding, dx, dy; if (!me.getRootVisible() && node === me.root) { // Hide the root node by removing its padding, so its children obscure it. padding = 0; hiddenParentLabels[id] = true; } else { padding = parentLabel.offset[1] * 2; dx = node.x1 - node.x0; dy = node.y1 - node.y0; if (size.width < dx && size.height < dy && dx > clipSize[0] && dy > clipSize[1]) { padding += size.height; hiddenParentLabels[id] = false; } else { hiddenParentLabels[id] = true; } } return padding; }); me.root = root = layout(root); nodes = me.nodes = root.descendants(); layout.paddingTop(0); // Reapplying the data to the nodes that were just created in the 'callParent' above. nodeElements = me.getRenderedNodes().data(nodes, me.getNodeKey()); // 'enter' and 'exit' selections are empty at this point. nodeElements .transition(me.layoutTransition) .call(nodeTransform); nodeElements .select('rect') .call(me.nodeSizeFn.bind(me)); nodeElements .select('text') .call(me.textVisibilityFn.bind(me)) .each(function (node) { if (node.data.isLeaf()) { this.setAttribute('x', (node.x1 - node.x0) / 2); this.setAttribute('y', (node.y1 - node.y0) / 2); } else { this.setAttribute('x', parentLabel.offset[0]); this.setAttribute('y', parentLabel.offset[1]); } }); }, addNodes: function (selection) { var me = this, labelSizes = me.labelSizes, colorAxis = me.getColorAxis(), nodeText = me.getNodeText(), scaleLabels = me.getScaleLabels(), cls = me.defaultCls; selection.append('rect') .style('fill', function (node) { return colorAxis.getColor(node); }); selection.append('text') .style('font-size', function (node) { return scaleLabels ? me.getLabelSize(node, this) : null; }) .each(function (node) { var text = nodeText(me, node); this.textContent = text == null ? '' : text; this.setAttribute('class', cls.label); Ext.d3.Helpers.fakeDominantBaseline(this, node.data.isLeaf() ? 'central' : 'text-before-edge'); labelSizes[node.data.id] = this.getBBox(); }); }, updateNodes: function (selection) { var me = this, nodeText = me.getNodeText(), scaleLabels = me.getScaleLabels(), labelSizes = me.labelSizes; selection.select('rect') .call(me.nodeSizeFn.bind(me)); selection.select('text') .style('font-size', function (node) { return scaleLabels ? me.getLabelSize(node, this) : null; }) .each(function (node) { var text = nodeText(me, node); this.textContent = text == null ? '' : text; labelSizes[node.data.id] = this.getBBox(); }); }, /** * @private */ getLabelSize: function (node, element) { if (node.data.isLeaf()) { // If too many different font sizes are used, the SVG rendering may slow down significantly // when scene's content is scaled (e.g. when zooming in/out with the PanZoom interaction), // so we quantize calculated font sizes. return this.labelQuantizer(this.labelSizer(node, element)); } return null; // Parent node font size is set via CSS. }, updateScaleLabels: function () { if (!this.isConfiguring) { this.performLayout(); } }, updateNodeValue: function (nodeValue) { this.callParent(arguments); // The parent method doesn't perform layout automatically, nor should it, // as for some hiearchy components merely re-rendering the scene will be // sufficient. For treemaps however, a layout is necessary to determine // label size and visibility. if (!this.isConfiguring) { this.performLayout(); } }, /** * @private */ nodeSizeFn: function (selection) { selection .attr('width', function (node) { return Math.max(0, node.x1 - node.x0); }) .attr('height', function (node) { return Math.max(0, node.y1 - node.y0); }); }, isLabelVisible: function (element, node) { var me = this, bbox = element.getBBox(), width = node.x1 - node.x0, height = node.y1 - node.y0, isLeaf = node.data.isLeaf(), parentTile = me.getParentTile(), hiddenParentLabels = me.hiddenParentLabels, parentLabelOffset = parentTile.label.offset, result; if (isLeaf) { // At least one pixel gap between the 'text' and 'rect' edges. width -= 2; height -= 2; } else { width -= parentLabelOffset[0] * 2; height -= parentLabelOffset[1] * 2; } if (isLeaf || !node.data.isExpanded()) { result = bbox.width < width && bbox.height < height; } else { result = !hiddenParentLabels[node.data.id]; } return result; }, /** * @private */ textVisibilityFn: function (selection) { var me = this; selection.classed(me.defaultCls.invisible, function (node) { return !me.isLabelVisible(this, node); }); }, destroy: function () { var me = this, colorAxis = me.getColorAxis(); if (me.deferredLayoutId) { clearTimeout(me.deferredLayoutId); } colorAxis.destroy(); me.callParent(); } });