jui.define("util.treemap", [], function() { return { sumArray: function (arr) { var sum = 0; for (var i = 0; i < arr.length; i++) { sum += arr[i]; } return sum; } } }); jui.define("chart.brush.treemap.node", [], function() { /** * @class chart.brush.treemap.node * */ var Node = function(data) { var self = this; this.text = data.text; this.value = data.value; this.x = data.x; this.y = data.y; this.width = data.width; this.height = data.height; /** @property {Integer} [index=null] Index of a specified node */ this.index = null; /** @property {Integer} [nodenum=null] Unique number of a specifiede node at the current depth */ this.nodenum = null; /** @property {ui.tree.node} [parent=null] Variable that refers to the parent of the current node */ this.parent = null; /** @property {Array} [children=null] List of child nodes of a specified node */ this.children = []; /** @property {Integer} [depth=0] Depth of a specified node */ this.depth = 0; function setIndexChild(node) { var clist = node.children; for(var i = 0; i < clist.length; i++) { if(clist[i].children.length > 0) { setIndexChild(clist[i]); } } } this.reload = function(nodenum) { this.nodenum = (!isNaN(nodenum)) ? nodenum : this.nodenum; if(self.parent) { if(this.parent.index == null) this.index = "" + this.nodenum; else this.index = self.parent.index + "." + this.nodenum; } // 뎁스 체크 if(this.parent && typeof(self.index) == "string") { this.depth = this.index.split(".").length; } // 자식 인덱스 체크 if(this.children.length > 0) { setIndexChild(this); } } this.isLeaf = function() { return (this.children.length == 0) ? true : false; } this.appendChild = function(node) { this.children.push(node); } this.insertChild = function(nodenum, node) { var preNodes = this.children.splice(0, nodenum); preNodes.push(node); this.children = preNodes.concat(this.children); } this.removeChild = function(index) { for(var i = 0; i < this.children.length; i++) { var node = this.children[i]; if(node.index == index) { this.children.splice(i, 1); // 배열에서 제거 } } } this.lastChild = function() { if(this.children.length > 0) return this.children[this.children.length - 1]; return null; } this.lastChildLeaf = function(lastRow) { var row = (!lastRow) ? this.lastChild() : lastRow; if(row.isLeaf()) return row; else { return this.lastChildLeaf(row.lastChild()); } } } return Node; }); jui.define("chart.brush.treemap.nodemanager", [ "util.base", "chart.brush.treemap.node" ], function(_, Node) { var NodeManager = function() { var self = this, root = new Node({ text: null, value: -1, x: -1, y: -1, width: -1, height: -1 }), iParser = _.index(); function createNode(data, no, pNode) { var node = new Node(data); node.parent = (pNode) ? pNode : null; node.reload(no); return node; } function setNodeChildAll(dataList, node) { var c_nodes = node.children; if(c_nodes.length > 0) { for(var i = 0; i < c_nodes.length; i++) { dataList.push(c_nodes[i]); if(c_nodes[i].children.length > 0) { setNodeChildAll(dataList, c_nodes[i]); } } } } function getNodeChildLeaf(keys, node) { if(!node) return null; var tmpKey = keys.shift(); if(tmpKey == undefined) { return node; } else { return getNodeChildLeaf(keys, node.children[tmpKey]); } } function insertNodeDataChild(index, data) { var keys = iParser.getIndexList(index); var pNode = self.getNodeParent(index), nodenum = keys[keys.length - 1], node = createNode(data, nodenum, pNode); // 데이터 갱신 pNode.insertChild(nodenum, node); return node; } function appendNodeData(data) { var node = createNode(data, root.children.length, root); root.appendChild(node); return node; } function appendNodeDataChild(index, data) { var pNode = self.getNode(index), cNode = createNode(data, pNode.children.length, pNode); pNode.appendChild(cNode); return cNode; } this.appendNode = function() { var index = arguments[0], data = arguments[1]; if(!data) { return appendNodeData(index); } else { return appendNodeDataChild(index, data); } } this.insertNode = function(index, data) { if(root.children.length == 0 && parseInt(index) == 0) { return this.appendNode(data); } else { return insertNodeDataChild(index, data); } } this.updateNode = function(index, data) { var node = this.getNode(index); for(var key in data) { node.data[key] = data[key]; } node.reload(node.nodenum, true); return node; } this.getNode = function(index) { if(index == null) return root.children; else { var nodes = root.children; if(iParser.isIndexDepth(index)) { var keys = iParser.getIndexList(index); return getNodeChildLeaf(keys, nodes[keys.shift()]); } else { return (nodes[index]) ? nodes[index] : null; } } } this.getNodeAll = function(index) { var dataList = [], tmpNodes = (index == null) ? root.children : [ this.getNode(index) ]; for(var i = 0; i < tmpNodes.length; i++) { if(tmpNodes[i]) { dataList.push(tmpNodes[i]); if(tmpNodes[i].children.length > 0) { setNodeChildAll(dataList, tmpNodes[i]); } } } return dataList; } this.getNodeParent = function(index) { // 해당 인덱스의 부모 노드를 가져옴 (단, 해당 인덱스의 노드가 없을 경우) var keys = iParser.getIndexList(index); if(keys.length == 1) { return root; } else if(keys.length == 2) { return this.getNode(keys[0]); } else if(keys.length > 2) { keys.pop(); return this.getNode(keys.join(".")); } } this.getRoot = function() { return root; } } return NodeManager; }); jui.define("chart.brush.treemap.container", [ "util.treemap" ], function(util) { var Container = function(xoffset, yoffset, width, height) { this.xoffset = xoffset; // offset from the the top left hand corner this.yoffset = yoffset; // ditto this.height = height; this.width = width; this.shortestEdge = function () { return Math.min(this.height, this.width); }; // getCoordinates - for a row of boxes which we've placed // return an array of their cartesian coordinates this.getCoordinates = function (row) { var coordinates = [], subxoffset = this.xoffset, subyoffset = this.yoffset, //our offset within the container areawidth = util.sumArray(row) / this.height, areaheight = util.sumArray(row) / this.width; if (this.width >= this.height) { for (var i = 0; i < row.length; i++) { coordinates.push([ subxoffset, subyoffset, subxoffset + areawidth, subyoffset + row[i] / areawidth ]); subyoffset = subyoffset + row[i] / areawidth; } } else { for (var i = 0; i < row.length; i++) { coordinates.push([ subxoffset, subyoffset, subxoffset + row[i] / areaheight, subyoffset + areaheight ]); subxoffset = subxoffset + row[i] / areaheight; } } return coordinates; } // cutArea - once we've placed some boxes into an row we then need to identify the remaining area, // this function takes the area of the boxes we've placed and calculates the location and // dimensions of the remaining space and returns a container box defined by the remaining area this.cutArea = function (area) { if (this.width >= this.height) { var areawidth = area / this.height, newwidth = this.width - areawidth; return new Container(this.xoffset + areawidth, this.yoffset, newwidth, this.height); } else { var areaheight = area / this.width, newheight = this.height - areaheight; return new Container(this.xoffset, this.yoffset + areaheight, this.width, newheight); } } } return Container; }); jui.define("chart.brush.treemap.calculator", [ "util.base", "util.treemap", "chart.brush.treemap.container" ], function(_, util, Container) { // normalize - the Bruls algorithm assumes we're passing in areas that nicely fit into our // container box, this method takes our raw data and normalizes the data values into // area values so that this assumption is valid. function normalize(data, area) { var normalizeddata = [], sum = util.sumArray(data), multiplier = area / sum; for (var i = 0; i < data.length; i++) { normalizeddata[i] = data[i] * multiplier; } return normalizeddata; } // treemapMultidimensional - takes multidimensional data (aka [[23,11],[11,32]] - nested array) // and recursively calls itself using treemapSingledimensional // to create a patchwork of treemaps and merge them function treemapMultidimensional(data, width, height, xoffset, yoffset) { xoffset = (typeof xoffset === "undefined") ? 0 : xoffset; yoffset = (typeof yoffset === "undefined") ? 0 : yoffset; var mergeddata = [], mergedtreemap, results = []; if(_.typeCheck("array", data[0])) { // if we've got more dimensions of depth for(var i = 0; i < data.length; i++) { mergeddata[i] = sumMultidimensionalArray(data[i]); } mergedtreemap = treemapSingledimensional(mergeddata, width, height, xoffset, yoffset); for(var i = 0; i < data.length; i++) { results.push(treemapMultidimensional(data[i], mergedtreemap[i][2] - mergedtreemap[i][0], mergedtreemap[i][3] - mergedtreemap[i][1], mergedtreemap[i][0], mergedtreemap[i][1])); } } else { results = treemapSingledimensional(data,width,height, xoffset, yoffset); } return results; } // treemapSingledimensional - simple wrapper around squarify function treemapSingledimensional(data, width, height, xoffset, yoffset) { xoffset = (typeof xoffset === "undefined") ? 0 : xoffset; yoffset = (typeof yoffset === "undefined") ? 0 : yoffset; //console.log(normalize(data, width * height)) var rawtreemap = squarify(normalize(data, width * height), [], new Container(xoffset, yoffset, width, height), []); return flattenTreemap(rawtreemap); } // flattenTreemap - squarify implementation returns an array of arrays of coordinates // because we have a new array everytime we switch to building a new row // this converts it into an array of coordinates. function flattenTreemap(rawtreemap) { var flattreemap = []; if(rawtreemap) { for (var i = 0; i < rawtreemap.length; i++) { for (var j = 0; j < rawtreemap[i].length; j++) { flattreemap.push(rawtreemap[i][j]); } } } return flattreemap; } // squarify - as per the Bruls paper // plus coordinates stack and containers so we get // usable data out of it function squarify(data, currentrow, container, stack) { var length; var nextdatapoint; var newcontainer; if (data.length === 0) { stack.push(container.getCoordinates(currentrow)); return; } length = container.shortestEdge(); nextdatapoint = data[0]; if (improvesRatio(currentrow, nextdatapoint, length)) { currentrow.push(nextdatapoint); squarify(data.slice(1), currentrow, container, stack); } else { newcontainer = container.cutArea(util.sumArray(currentrow), stack); stack.push(container.getCoordinates(currentrow)); squarify(data, [], newcontainer, stack); } return stack; } // improveRatio - implements the worse calculation and comparision as given in Bruls // (note the error in the original paper; fixed here) function improvesRatio(currentrow, nextnode, length) { var newrow; if (currentrow.length === 0) { return true; } newrow = currentrow.slice(); newrow.push(nextnode); var currentratio = calculateRatio(currentrow, length), newratio = calculateRatio(newrow, length); // the pseudocode in the Bruls paper has the direction of the comparison // wrong, this is the correct one. return currentratio >= newratio; } // calculateRatio - calculates the maximum width to height ratio of the // boxes in this row function calculateRatio(row, length) { var min = Math.min.apply(Math, row), max = Math.max.apply(Math, row), sum = util.sumArray(row); return Math.max(Math.pow(length, 2) * max / Math.pow(sum, 2), Math.pow(sum, 2) / (Math.pow(length, 2) * min)); } // sumMultidimensionalArray - sums the values in a nested array (aka [[0,1],[[2,3]]]) function sumMultidimensionalArray(arr) { var total = 0; if(_.typeCheck("array", arr[0])) { for(var i = 0; i < arr.length; i++) { total += sumMultidimensionalArray(arr[i]); } } else { total = util.sumArray(arr); } return total; } return treemapMultidimensional; }); jui.define("chart.brush.treemap", [ "util.base", "chart.brush.treemap.calculator", "chart.brush.treemap.nodemanager" ], function(_, Calculator, NodeManager) { var TEXT_MARGIN_LEFT = 3; /** * @class chart.brush.treemap * * @extends chart.brush.core */ var TreemapBrush = function() { var nodes = new NodeManager(), titleKeys = {}; function convertNodeToArray(key, nodes, result, now) { if(!now) now = []; for(var i = 0; i < nodes.length; i++) { if(nodes[i].children.length == 0) { now.push(nodes[i][key]); } else { convertNodeToArray(key, nodes[i].children, result, []); } } result.push(now); return result; } function mergeArrayToNode(keys, values) { for(var i = 0; i < keys.length; i++) { if(_.typeCheck("array", keys[i])) { mergeArrayToNode(keys[i], values[i]); } else { var node = nodes.getNode(keys[i]); node.x = values[i][0]; node.y = values[i][1]; node.width = values[i][2] - values[i][0]; node.height = values[i][3] - values[i][1]; } } } function isDrawNode(node) { if(node.width == 0 && node.height == 0 && node.x == 0 && node.y == 0) { return false; } return true; } function getMinimumXY(node, dx, dy) { if(node.children.length == 0) { return { x: Math.min(dx, node.x), y: Math.min(dy, node.y) }; } else { for(var i = 0; i < node.children.length; i++) { return getMinimumXY(node.children[i], dx, dy); } } } function createTitleDepth(self, g, node, sx, sy) { var fontSize = self.chart.theme("treemapTitleFontSize"), w = self.axis.area("width"), h = self.axis.area("height"), xy = getMinimumXY(node, w, h); var text = self.chart.text({ "font-size": fontSize, "font-weight": "bold", fill: self.chart.theme("treemapTitleFontColor"), x: sx + xy.x + TEXT_MARGIN_LEFT, y: sy + xy.y + fontSize, "text-anchor": "start" }, (_.typeCheck("function", self.brush.format) ? self.format(node) : node.text)); g.append(text); titleKeys[node.index] = true; } function getRootNodeSeq(node) { if(node.parent.depth > 0) { return getRootNodeSeq(node.parent); } return node.nodenum; } this.drawBefore = function() { for(var i = 0; i < this.axis.data.length; i++) { var d = this.axis.data[i], k = this.getValue(d, "index"); nodes.insertNode(k, { text: this.getValue(d, "text", ""), value: this.getValue(d, "value", 0), x: this.getValue(d, "x", 0), y: this.getValue(d, "y", 0), width: this.getValue(d, "width", 0), height: this.getValue(d, "height", 0) }); } var nodeList = nodes.getNode(), preData = convertNodeToArray("value", nodeList, []), preKeys = convertNodeToArray("index", nodeList, []), afterData = Calculator(preData, this.axis.area("width"), this.axis.area("height")); mergeArrayToNode(preKeys, afterData); } this.draw = function() { var g = this.svg.group(), sx = this.axis.area("x"), sy = this.axis.area("y"), nodeList = nodes.getNodeAll(); for(var i = 0; i < nodeList.length; i++) { if(this.brush.titleDepth == nodeList[i].depth) { createTitleDepth(this, g, nodeList[i], sx, sy); } if(!isDrawNode(nodeList[i])) continue; var x = sx + nodeList[i].x, y = sy + nodeList[i].y, w = nodeList[i].width, h = nodeList[i].height; if(this.brush.showText && !titleKeys[nodeList[i].index]) { var cx = x + (w / 2), cy = y + (h / 2), fontSize = this.chart.theme("treemapTextFontSize"); if(this.brush.textOrient == "top") { cy = y + fontSize; } else if(this.brush.textOrient == "bottom") { cy = y + h - fontSize/2; } if(this.brush.textAlign == "start") { cx = x + TEXT_MARGIN_LEFT; } else if(this.brush.textAlign == "end") { cx = x + w - TEXT_MARGIN_LEFT; } var text = this.chart.text({ "font-size": fontSize, fill: this.chart.theme("treemapTextFontColor"), x: cx, y: cy, "text-anchor": this.brush.textAlign }, (_.typeCheck("function", this.brush.format) ? this.format(nodeList[i]) : nodeList[i].text)); g.append(text); } var elem = this.svg.rect({ stroke: this.chart.theme("treemapNodeBorderColor"), "stroke-width": this.chart.theme("treemapNodeBorderWidth"), x: x, y: y, width: w, height: h, fill: this.color(getRootNodeSeq(nodeList[i])) }); if(_.typeCheck("function", this.brush.nodeColor)) { var color = this.brush.nodeColor.call(this.chart, nodeList[i]); elem.attr({ fill: this.color(color) }); } this.addEvent(elem, nodeList[i]); g.prepend(elem); } return g; } } TreemapBrush.setup = function() { return { /** @cfg {"top"/"center"/"bottom" } [orient="top"] Determines the side on which the tool tip is displayed (top, center, bottom). */ textOrient: "top", // or bottom /** @cfg {"start"/"middle"/"end" } [align="center"] Aligns the title message (start, middle, end).*/ textAlign: "middle", showText: true, titleDepth: 1, nodeColor: null, clip: false, format: null }; } return TreemapBrush; }, "chart.brush.core");