Home | History | Annotate | Download | only in static
      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