define("dojox/treemap/TreeMap", ["dojo/_base/array", "dojo/_base/lang", "dojo/_base/declare", "dojo/_base/event", "dojo/_base/Color", "dojo/touch", "dojo/when", "dojo/on", "dojo/query", "dojo/dom-construct", "dojo/dom-geometry", "dojo/dom-class", "dojo/dom-style", "./_utils", "dijit/_WidgetBase", "dojox/widget/_Invalidating", "dojox/widget/Selection", "dojo/_base/sniff", "dojo/uacss"], function(arr, lang, declare, event, Color, touch, when, on, query, domConstruct, domGeom, domClass, domStyle, utils, _WidgetBase, _Invalidating, Selection, has){ return declare("dojox.treemap.TreeMap", [_WidgetBase, _Invalidating, Selection], { // summary: // A treemap widget. baseClass: "dojoxTreeMap", // store: dojo/store/api/Store // The store that contains the items to display. store: null, // query: Object // A query that can be passed to when querying the store. query: {}, // itemToRenderer: [protected] Object // The associated array item to renderer list. itemToRenderer: null, // Data _dataChanged: false, // rootItem: Object // The root item of the treemap, that is the first visible item. // If null the entire treemap hierarchy is shown. // Default is null. rootItem: null, _rootItemChanged: false, // tooltipAttr: String // The attribute of the store item that contains the tooltip text of a treemap cell. // Default is "". tooltipAttr: "", // areaAttr: String // The attribute of the store item that contains the data used to compute the area of a treemap cell. // Default is "". areaAttr: "", _areaChanged: false, // labelAttr: String // The attribute of the store item that contains the label of a treemap cell. // Default is "label". labelAttr: "label", // labelThreshold: Number // The starting depth level at which the labels are not displayed anymore on cells. // If NaN no threshold is applied. The depth is the visual depth of the items on the screen not // in the data (i.e. after drill down the depth of an item might change). // Default is NaN. labelThreshold: NaN, // colorAttr: String // The attribute of the store item that contains the data used to compute the color of a treemap cell. // Default is "". colorAttr: "", // colorModel: dojox/color/api/ColorModel // The optional color model that converts data to color. // Default is null. colorModel: null, _coloringChanged: false, // groupAttrs: Array // An array of data attributes used to group data in the treemap. // Default is []. groupAttrs: [], // groupFuncs: Array // An array of grouping functions used to group data in the treemap. // When null, groupAttrs is to compute grouping functions. // Default is null. groupFuncs: null, _groupFuncs: null, _groupingChanged: false, constructor: function(){ this.itemToRenderer = {}; this.invalidatingProperties = [ "colorModel", "groupAttrs", "groupFuncs", "areaAttr", "areaFunc", "labelAttr", "labelFunc", "labelThreshold", "tooltipAttr", "tooltipFunc", "colorAttr", "colorFunc", "rootItem" ]; }, getIdentity: function(item){ return item.__treeID?item.__treeID:this.store.getIdentity(item); }, resize: function(box){ if(box){ domGeom.setMarginBox(this.domNode, box); this.invalidateRendering(); } }, postCreate: function(){ this.inherited(arguments); this.connect(this.domNode, "mouseover", this._onMouseOver); this.connect(this.domNode, "mouseout", this._onMouseOut); this.connect(this.domNode, touch.release, this._onMouseUp); this.domNode.setAttribute("role", "presentation"); this.domNode.setAttribute("aria-label", "treemap"); }, buildRendering: function(){ this.inherited(arguments); this.refreshRendering(); }, refreshRendering: function(){ var forceCreate = false; if(this._dataChanged){ this._dataChanged = false; this._groupingChanged = true; this._coloringChanged = true; } if(this._groupingChanged){ this._groupingChanged = false; this._set("rootItem", null); this._updateTreeMapHierarchy(); forceCreate = true; } if(this._rootItemChanged){ this._rootItemChanged = false; forceCreate = true; } if(this._coloringChanged){ this._coloringChanged = false; if(this.colorModel != null && this._data != null && this.colorModel.initialize){ this.colorModel.initialize(this._data, lang.hitch(this, function(item){ return this.colorFunc(item, this.store); })); } } if(this._areaChanged){ this._areaChanged = false; this._removeAreaForGroup(); } if(this.domNode == undefined || this._items == null){ return; } if(forceCreate){ domConstruct.empty(this.domNode); } var rootItem = this.rootItem; if(rootItem != null){ if(this._isLeaf(rootItem)){ rootItem = this._getRenderer(rootItem).parentItem; } } var box = domGeom.getMarginBox(this.domNode); if(rootItem != null){ this._buildRenderer(this.domNode, null, rootItem, { x: box.l, y: box.t, w: box.w, h: box.h }, 0, forceCreate); }else{ this._buildChildrenRenderers(this.domNode, rootItem?rootItem:{ __treeRoot: true, children : this._items }, 0, forceCreate, box); } }, _setRootItemAttr: function(value){ this._rootItemChanged = true; this._set("rootItem", value); }, _setStoreAttr: function(value){ var r; if(value != null){ var results = value.query(this.query); if(results.observe){ // user asked us to observe the store results.observe(lang.hitch(this, this._updateItem), true); } r = when(results, lang.hitch(this, this._initItems)); }else{ r = this._initItems([]); } this._set("store", value); return r; }, _initItems: function(items){ this._dataChanged = true; this._data = items; this.invalidateRendering(); return items; }, _updateItem: function(item, previousIndex, newIndex){ if(previousIndex!=-1){ if(newIndex!=previousIndex){ // this is a remove or a move this._data.splice(previousIndex, 1); }else{ // this is a put, previous and new index identical // we don't know what has change exactly with store API this._data[newIndex] = item; } }else if(newIndex!=-1){ // this is a add this._data.splice(newIndex, 0, item); } // as we have no details let's refresh everything... this._dataChanged = true; this.invalidateRendering(); }, _setGroupAttrsAttr: function(value){ this._groupingChanged = true; if(this.groupFuncs == null){ if(value !=null){ this._groupFuncs = arr.map(value, function(attr){ return function(item){ return item[attr]; }; }); }else{ this._groupFuncs = null; } } this._set("groupAttrs", value); }, _setGroupFuncsAttr: function(value){ this._groupingChanged = true; this._set("groupFuncs", this._groupFuncs = value); if(value == null && this.groupAttrs != null){ this._groupFuncs = arr.map(this.groupAttrs, function(attr){ return function(item){ return item[attr]; }; }); } }, _setAreaAttrAttr: function(value){ this._areaChanged = true; this._set("areaAttr", value); }, // areaFunc: Function // A function that returns the value use to compute the area of cell from a store item. // Default implementation is using areaAttr. areaFunc: function(/*Object*/ item, /*dojo/store/api/Store*/ store){ return (this.areaAttr && this.areaAttr.length > 0)?parseFloat(item[this.areaAttr]):1; }, _setAreaFuncAttr: function(value){ this._areaChanged = true; this._set("areaFunc", value); }, // labelFunc: Function // A function that returns the label of cell from a store item. // Default implementation is using labelAttr. labelFunc: function(/*Object*/ item, /*dojo/store/api/Store*/ store){ var label = (this.labelAttr && this.labelAttr.length > 0)?item[this.labelAttr]:null; return label?label.toString():null; }, // tooltipFunc: Function // A function that returns the tooltip of cell from a store item. // Default implementation is using tooltipAttr. tooltipFunc: function(/*Object*/ item, /*dojo/store/api/Store*/ store){ var tooltip = (this.tooltipAttr && this.tooltipAttr.length > 0)?item[this.tooltipAttr]:null; return tooltip?tooltip.toString():null; }, _setColorModelAttr: function(value){ this._coloringChanged = true; this._set("colorModel", value); }, _setColorAttrAttr: function(value){ this._coloringChanged = true; this._set("colorAttr", value); }, // colorFunc: Function // A function that returns from a store item the color value of cell or the value used by the // ColorModel to compute the cell color. If a color must be returned it must be in form accepted by the // dojo/_base/Color constructor. If a value must be returned it must be a Number. // Default implementation is using colorAttr. colorFunc: function(/*Object*/ item, /*dojo/store/api/Store*/ store){ var color = (this.colorAttr && this.colorAttr.length > 0)?item[this.colorAttr]:0; if(color == null){ color = 0; } return parseFloat(color); }, _setColorFuncAttr: function(value){ this._coloringChanged = true; this._set("colorFunc", value); }, createRenderer: function(item, level, kind){ // summary: // Creates an item renderer of the specified kind. This is called only when the treemap // is created. Default implementation always create div nodes. It also sets overflow // to hidden and position to absolute on non-header renderers. // item: Object // The data item. // level: Number // The item depth level. // kind: String // The specified kind. This can either be "leaf", "group", "header" or "content". // returns: DomNode // The renderer use for the specified kind. // tags: // protected var div = domConstruct.create("div"); if(kind != "header"){ domStyle.set(div, "overflow", "hidden"); domStyle.set(div, "position", "absolute"); } return div; }, styleRenderer: function(renderer, item, level, kind){ // summary: // Style the item renderer. This is called each time the treemap is refreshed. // For leaf items it colors them with the color computed from the color model. // For other items it does nothing. // renderer: DomNode // The item renderer. // item: Object // The data item. // level: Number // The item depth level. // kind: String // The specified kind. This can either be "leaf", "group", "header" or "content". // tags: // protected switch(kind){ case "leaf": domStyle.set(renderer, "background", this.getColorForItem(item).toHex()); case "header": var label = this.getLabelForItem(item); if(label && (isNaN(this.labelThreshold) || level < this.labelThreshold)){ renderer.innerHTML = label; }else{ domConstruct.empty(renderer); } break; default: } }, _updateTreeMapHierarchy: function(){ if(this._data == null){ return; } if(this._groupFuncs != null && this._groupFuncs.length > 0){ this._items = utils.group(this._data, this._groupFuncs, lang.hitch(this, this._getAreaForItem)).children; }else{ this._items = this._data; } }, _removeAreaForGroup: function(item){ var children; if(item != null){ if(item.__treeValue){ delete item.__treeValue; children = item.children; }else{ // not a grouping item return; } }else{ children = this._items; } if(children){ for(var i = 0; i < children.length; ++i){ this._removeAreaForGroup(children[i]); } } }, _getAreaForItem: function(item){ var area = this.areaFunc(item, this.store); return isNaN(area) ? 0 : area; }, _computeAreaForItem: function(item){ var value; if(item.__treeID){ // group value = item.__treeValue; if(!value){ value = 0; var children = item.children; for(var i = 0; i < children.length; ++i){ value += this._computeAreaForItem(children[i]); } item.__treeValue = value; } }else{ value = this._getAreaForItem(item); } return value; }, getColorForItem: function(item){ // summary: // Returns the color for a given item. This either use the colorModel if not null // or just the result of the colorFunc. // item: Object // The data item. // tags: // protected var value = this.colorFunc(item, this.store); if(this.colorModel != null){ return this.colorModel.getColor(value); }else{ return new Color(value); } }, getLabelForItem: function(item){ // summary: // Returns the label for a given item. // item: Object // The data item. // tags: // protected return item.__treeName?item.__treeName:this.labelFunc(item, this.store); }, _buildChildrenRenderers: function(domNode, item, level, forceCreate, delta, anim){ var children = item.children; var box = domGeom.getMarginBox(domNode); var solution = utils.solve(children, box.w, box.h, lang.hitch(this, this._computeAreaForItem), !this.isLeftToRight()); var rectangles = solution.rectangles; if(delta){ rectangles = arr.map(rectangles, function(item){ item.x += delta.l; item.y += delta.t; return item; }); } var rectangle; for(var j = 0; j < children.length; ++j){ rectangle = rectangles[j]; this._buildRenderer(domNode, item, children[j], rectangle, level, forceCreate, anim); } }, _isLeaf: function(item){ return !item.children; }, _isRoot: function(item){ return item.__treeRoot; }, _getRenderer: function(item, anim, parent){ if(anim){ // while animating we do that on a copy of the subtree // so we can use our hash object to get to the renderer for(var i = 0; i < parent.children.length; ++i){ if(parent.children[i].item == item){ return parent.children[i]; } } } return this.itemToRenderer[this.getIdentity(item)]; }, _buildRenderer: function(container, parent, child, rect, level, forceCreate, anim){ var isLeaf = this._isLeaf(child); var renderer = !forceCreate ? this._getRenderer(child, anim, container) : null; renderer = isLeaf ? this._updateLeafRenderer(renderer, child, level) : this._updateGroupRenderer(renderer, child, level); if(forceCreate){ renderer.level = level; renderer.item = child; renderer.parentItem = parent; this.itemToRenderer[this.getIdentity(child)] = renderer; // update its selection status this.updateRenderers(child); } // in some cases the computation might be slightly incorrect (0.0000...1) // and due to the floor this will cause 1px gaps var x = Math.floor(rect.x); var y = Math.floor(rect.y); var w = Math.floor(rect.x + rect.w + 0.00000000001) - x; var h = Math.floor(rect.y + rect.h + 0.00000000001) - y; // before sizing put the item inside its parent so that styling // is applied and taken into account if(forceCreate){ domConstruct.place(renderer, container); } domGeom.setMarginBox(renderer, { l: x, t: y, w: w, h: h }); if(!isLeaf){ var box = domGeom.getContentBox(renderer); this._layoutGroupContent(renderer, box.w, box.h, level + 1, forceCreate, anim); } this.onRendererUpdated({ renderer: renderer, item: child, kind: isLeaf?"leaf":"group", level: level }); }, _layoutGroupContent: function(renderer, width, height, level, forceCreate, anim){ var header = query(".dojoxTreeMapHeader", renderer)[0]; var content = query(".dojoxTreeMapGroupContent", renderer)[0]; if(header == null || content == null){ return; } var box = domGeom.getMarginBox(header); // If the header is too high, reduce its area // and don't show the children.. if(box.h > height){ // TODO: this might cause pb when coming back to visibility later // as the getMarginBox of the header will keep that value? box.h = height; domStyle.set(content, "display", "none"); }else{ domStyle.set(content, "display", "block"); domGeom.setMarginBox(content, { l: 0, t: box.h, w: width, h: (height - box.h) }); this._buildChildrenRenderers(content, renderer.item, level, forceCreate, null, anim); } domGeom.setMarginBox(header, { l: 0, t: 0, w: width, h: box.h }); }, _updateGroupRenderer: function(renderer, item, level){ // summary: // Update a group renderer. This creates the renderer if not already created, // call styleRender for it and recurse into children. // renderer: DomNode // The item renderer. // item: Object // The data item. // level: Number // The item depth level. // tags: // private var forceCreate = renderer == null; if(renderer == null){ renderer = this.createRenderer("div", level, "group"); domClass.add(renderer, "dojoxTreeMapGroup"); } this.styleRenderer(renderer, item, level, "group"); var header = query(".dojoxTreeMapHeader", renderer)[0]; header = this._updateHeaderRenderer(header, item, level); if(forceCreate){ domConstruct.place(header, renderer); } var content = query(".dojoxTreeMapGroupContent", renderer)[0]; content = this._updateGroupContentRenderer(content, item, level); if(forceCreate){ domConstruct.place(content, renderer); } return renderer; }, _updateHeaderRenderer: function(renderer, item, level){ // summary: // Update a leaf renderer. This creates the renderer if not already created, // call styleRender for it and set the label as its innerHTML. // renderer: DomNode // The item renderer. // item: Object // The data item. // level: Number // The item depth level. // tags: // private if(renderer == null){ renderer = this.createRenderer(item, level, "header"); domClass.add(renderer, "dojoxTreeMapHeader"); domClass.add(renderer, "dojoxTreeMapHeader_" + level); } this.styleRenderer(renderer, item, level, "header"); return renderer; }, _updateLeafRenderer: function(renderer, item, level){ // summary: // Update a leaf renderer. This creates the renderer if not already created, // call styleRender for it and set the label as its innerHTML. // renderer: DomNode // The item renderer. // item: Object // The data item. // level: Number // The item depth level. // tags: // private if(renderer == null){ renderer = this.createRenderer(item, level, "leaf"); domClass.add(renderer, "dojoxTreeMapLeaf"); domClass.add(renderer, "dojoxTreeMapLeaf_" + level); } this.styleRenderer(renderer, item, level, "leaf"); var tooltip = this.tooltipFunc(item, this.store); if(tooltip){ renderer.title = tooltip; } return renderer; }, _updateGroupContentRenderer: function(renderer, item, level){ // summary: // Update a group content renderer. This creates the renderer if not already created, // and call styleRender for it. // renderer: // The item renderer. // item: Object // The data item. // level: Number // The item depth level. // tags: // private if(renderer == null){ renderer = this.createRenderer(item, level, "content"); domClass.add(renderer, "dojoxTreeMapGroupContent"); domClass.add(renderer, "dojoxTreeMapGroupContent_" + level); } this.styleRenderer(renderer, item, level, "content"); return renderer; }, _getRendererFromTarget: function(target){ var renderer = target; while(renderer != this.domNode && !renderer.item){ renderer = renderer.parentNode; } return renderer; }, _onMouseOver: function(e){ var renderer = this._getRendererFromTarget(e.target); if(renderer.item){ var item = renderer.item; this._hoveredItem = item; this.updateRenderers(item); this.onItemRollOver({renderer: renderer, item : item, triggerEvent: e}); } }, _onMouseOut: function(e){ var renderer = this._getRendererFromTarget(e.target); if(renderer.item){ var item = renderer.item; this._hoveredItem = null; this.updateRenderers(item); this.onItemRollOut({renderer: renderer, item : item, triggerEvent: e}); } }, _onMouseUp: function(e){ var renderer = this._getRendererFromTarget(e.target); if(renderer.item){ this.selectFromEvent(e, renderer.item, e.currentTarget, true); //event.stop(e); } }, onRendererUpdated: function(){ // summary: // Called when a renderer has been updated. This is called after creation, styling and sizing for // each group and leaf renderers. For group renders this is also called after creation of children // renderers. // tags: // callback }, onItemRollOver: function(){ // summary: // Called when an item renderer has been hovered. // tags: // callback }, onItemRollOut: function(){ // summary: // Called when an item renderer has been rolled out. // tags: // callback }, updateRenderers: function(items){ // summary: // Updates the renderer(s) that represent the specified item(s). // item: Object|Array // The item(s). if(!items){ return; } if(!lang.isArray(items)){ items = [items]; } for(var i=0; i