1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 /** 6 * This is a view class showing flot graph. 7 * @param {Object} profiler Must have addListener method. 8 * @construct 9 */ 10 var GraphView = function(profiler) { 11 this.profiler_ = profiler; 12 this.placeholder_ = '#graph-div'; 13 // Update graph view and menu view when profiler model changed. 14 profiler.addListener('changed', this.redraw_.bind(this)); 15 }; 16 17 /** 18 * Generate lines for flot plotting. 19 * @param {Array.<Object>} models 20 * @return {Array.<Object>} 21 * @private 22 */ 23 GraphView.prototype.generateLines_ = function(models) { 24 function mergeCategoryTree(snapNode, treeNode) { 25 if ('children' in snapNode) { 26 // If |snapNode| is not a leaf node, we should go deeper. 27 if (!('children' in treeNode)) 28 treeNode.children = {}; 29 snapNode.children.forEach(function(child) { 30 if (!(child.id in treeNode.children)) 31 treeNode.children[child.id] = {}; 32 mergeCategoryTree(child, treeNode.children[child.id]); 33 }); 34 } else { 35 treeNode.name = snapNode.name; 36 } 37 } 38 39 function getCategoriesMap(node, id, categories) { 40 if ('children' in node) { 41 Object.keys(node.children).forEach(function(id) { 42 getCategoriesMap(node.children[id], id, categories); 43 }); 44 } else { 45 if (!(id in categories)) { 46 categories[id] = { 47 name: node.name, 48 data: [] 49 }; 50 for (var i = 0; i < models.length; ++i) 51 categories[id].data.push([models[i].time - models[0].time, 0]); 52 } 53 } 54 } 55 56 function getLineValues(snapNode, index, categories) { 57 if ('children' in snapNode) { 58 snapNode.children.forEach(function(child) { 59 getLineValues(child, index, categories); 60 }); 61 } else { 62 categories[snapNode.id].data[index][1] = snapNode.size; 63 } 64 } 65 66 // TODO(dmikurube): Remove this function after adding "color" attribute 67 // in each category. 68 function getHashColorCode(id) { 69 var color = 0; 70 for (var i = 0; i < id.length; ++i) 71 color = (color * 0x57 + id.charCodeAt(i)) & 0xffffff; 72 color = color.toString(16); 73 while (color.length < 6) 74 color = '0' + color; 75 return '#' + color; 76 } 77 78 var categoryTree = {}; 79 models.forEach(function(model) { 80 mergeCategoryTree(model, categoryTree); 81 }); 82 // Convert layout of categories from tree style to hash map style. 83 var categoryMap = {}; 84 getCategoriesMap(categoryTree, '', categoryMap); 85 // Get size of each category. 86 models.forEach(function(model, index) { 87 getLineValues(model, index, categoryMap); 88 }); 89 90 return Object.keys(categoryMap).map(function(id) { 91 return { 92 color: getHashColorCode(id), 93 data: categoryMap[id].data, 94 id: id, 95 label: categoryMap[id].name 96 }; 97 }); 98 }; 99 100 /** 101 * Update graph view when model updated. 102 * TODO(junjianx): use redraw function to improve perfomance. 103 * @param {Array.<Object>} models 104 * @private 105 */ 106 GraphView.prototype.redraw_ = function(models) { 107 var self = this; 108 var data = this.generateLines_(models); 109 if (!this.graph_) { 110 var $graph = $(this.placeholder_); 111 this.graph_ = $.plot($graph, data, { 112 series: { 113 stack: true, 114 lines: { show: true, fill: true } 115 }, 116 grid: { 117 hoverable: true, 118 clickable: true 119 } 120 }); 121 122 // Bind click event so that user can select category by clicking stack 123 // area. It firstly checks x range which clicked point is in, and all lines 124 // share same x values, so it is checked only once at first. Secondly, it 125 // checked y range by accumulated y values because this is a stack graph. 126 $graph.bind('plotclick', function(event, pos, item) { 127 // Get newest lines data from graph. 128 var lines = self.graph_.getData(); 129 // If only <=1 line exists or axis area clicked, return. 130 var right = binarySearch.call(lines[0].data.map(function(point) { 131 return point[0]; 132 }), pos.x); 133 if (lines.length <= 1 || right === lines.length || right === 0) 134 return; 135 136 // Calculate interpolate y value of every line. 137 for (var i = 0; i < lines.length; ++i) { 138 var line = lines[i].data; 139 // [left, right] is the range including clicked point. 140 var left = right - 1; 141 var leftPoint = { 142 x: line[left][0], 143 y: (leftPoint ? leftPoint.y : 0) + line[left][1] 144 }; 145 var rightPoint = { 146 x: line[right][0], 147 y: (rightPoint ? rightPoint.y : 0) + line[right][1] 148 }; 149 150 // Calculate slope of the linear equation. 151 var slope = (rightPoint.y - leftPoint.y) / (rightPoint.x - leftPoint.x); 152 var interpolateY = slope * (pos.x - rightPoint.x) + rightPoint.y; 153 if (interpolateY >= pos.y) 154 break; 155 } 156 157 // If pos.y is higher than all lines, return. 158 if (i === lines.length) { 159 self.profiler_.setSelected(null); 160 return; 161 } 162 163 self.profiler_.setSelected(lines[i].id, pos); 164 }); 165 } else { 166 this.graph_.setData(data); 167 this.graph_.setupGrid(); 168 this.graph_.draw(); 169 } 170 }; 171