• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
10var 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 */
23GraphView.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 */
106GraphView.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