Browse Source

HUE-8649 [frontend] Add tooltip and stacked bars for queries

Johan Ahlen 7 năm trước cách đây
mục cha
commit
48b8d74300

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue-embedded.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue3-extra.css


+ 11 - 8
desktop/core/src/desktop/static/desktop/js/apiHelper.js

@@ -943,15 +943,19 @@ var ApiHelper = (function () {
 
     var data = [];
     var currentMax = 100;
-    var currentMin = 50;
+    var currentMin = 10;
 
+    var lastCpuVal = 0;
+    var lastMemVal = 0;
+    var lastIO = 0;
+    var lastQueryCount = 0;
     var addFakeMeasurement = function (data, time) {
-      var lastCpuVal = (currentMax - currentMin) / 2;
-      var lastMemVal = (currentMax - currentMin) / 2;
-      var lastIO = (currentMax - currentMin) / 2;
-      var lastQueryCount = (currentMax - currentMin) / 20;
 
       var newQueryCount = Math.round(Math.max(Math.min(lastQueryCount + (Math.random() - 0.5) * 10, 25), 0));
+
+      var queuedQueryCount = Math.round(newQueryCount * Math.random());
+      var nonQueuedQueryCount = newQueryCount - queuedQueryCount;
+
       var diff = newQueryCount - lastQueryCount;
       if (diff > 0) {
         lastCpuVal = Math.round(Math.max(Math.min(lastCpuVal + Math.random() * 15, Math.min(currentMax + (Math.random() - 0.5) * 10, 100)), Math.max(currentMin + (Math.random() - 0.5) * 10, 0)));
@@ -962,7 +966,7 @@ var ApiHelper = (function () {
         lastMemVal = Math.round(Math.max(Math.min(lastMemVal - Math.random() * 15, Math.min(currentMax + (Math.random() - 0.5) * 10, 100)), Math.max(currentMin + (Math.random() - 0.5) * 10, 0)));
         lastIO = Math.round(Math.max(Math.min(lastIO - Math.random() * 15, Math.min(currentMax + (Math.random() - 0.5) * 10, 100)), Math.max(currentMin + (Math.random() - 0.5) * 10, 0)));
       }
-      data.push([time, lastCpuVal, lastMemVal, lastIO, newQueryCount]);
+      data.push([time, lastCpuVal, lastMemVal, lastIO, nonQueuedQueryCount, queuedQueryCount]);
     };
 
     var generateFakeData = function (data, startTime, endTime, points) {
@@ -973,8 +977,7 @@ var ApiHelper = (function () {
       }
     };
 
-    generateFakeData(data, options.startTime, options.endTime, options.points)
-
+    generateFakeData(data, options.startTime, options.endTime, options.points);
     return $.Deferred().resolve(data).promise();
   };
 

+ 33 - 1
desktop/core/src/desktop/static/desktop/less/components/hue-performance-graph.less

@@ -16,14 +16,39 @@
  limitations under the License.
 */
 
-// NV D3 Adjustments
 .performance-graph {
+  position: relative;
 
   h3 {
     font-size: 34px;
     font-weight: 300;
   }
 
+  .performance-tooltip {
+    border-radius: 3px;
+    position: absolute;
+    padding: 3px;
+    font: 10px @font-family-sans-serif;
+    background: rgba(240, 240, 240, 0.9);
+    pointer-events: none;
+  }
+
+  .performance-tooltip > div {
+    margin: 1px 0;
+  }
+
+  .performance-tooltip .performance-tooltip-label {
+    font-weight: 800;
+  }
+
+  .performance-tooltip .performance-tooltip-indicator {
+    padding-top: 3px;
+    width: 6px;
+    height: 8px;
+    margin-right: 3px;
+    display: inline-block;
+  }
+
   svg {
     .nvtooltip {
       font-family:  @font-family-sans-serif;
@@ -48,6 +73,13 @@
     .nvd3.nv-bullet {
       font: 10px @font-family-sans-serif;
     }
+
+    .brush .selection {
+      stroke: none;
+      fill: steelblue;
+      fill-opacity: .25;
+      shape-rendering: crispEdges;
+    }
   }
 }
 

+ 441 - 462
desktop/core/src/desktop/templates/ko_components/ko_performance_graph.mako

@@ -33,504 +33,483 @@
   </script>
 
   <script type="text/html" id="performance-graph-template">
-    <div class="performance-graph" style="position: relative">
+    <div style="position: relative" data-bind="template: { afterRender: componentRendered }">
       <h3>${ _('Resources') }</h3>
-      <div style="font-size: 12px; position: absolute; right: 0; top: 0;"
-           data-bind="template: { name: 'performance-graph-stats', data: cpuStats }"></div>
-      <div style="font-size: 12px; position: absolute; right: 0; top: 14px;"
-           data-bind="template: { name: 'performance-graph-stats', data: memoryStats }"></div>
-      <!-- ko template: { name: 'performance-graph-d3-template', afterRender: graphContainerRendered } --><!-- /ko -->
+      <div class="performance-graph" data-bind="style: { height: graphHeight + 'px', width: graphWidth + 'px' }"></div>
     </div>
   </script>
 
   <script type="text/javascript">
     (function () {
 
-      function bytesToSize(bytes) {
-        var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
-        if (bytes === 0) {
-          return '0 B';
-        }
-        var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
-        return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
-      }
-
-      /**
-       * @param {object} params
-       *
-       * @constructor
-       */
       function PerformanceGraph(params) {
         var self = this;
-        self.id = UUID();
-        self.type = params.type;
+
         self.graphHeight = 500;
         self.graphWidth = 1000;
 
-        self.data = [];
-
-        self.cpuStats = ko.observable({average: '-', min: '-', max: '-', color: 'blue'});
-        self.memoryStats = ko.observable({average: '-', min: '-', max: '-', color: 'gray'});
-        self.memoryLimit = 64 * 1024 * 1024 * 1024; // 64GB should be fetched from API
+        self.data;
+        self.graphElement;
 
-        self.graphContainerRendered = function (domTree) {
-          ApiHelper.getInstance().fetchResourceStats({
-            startTime: Date.now() - 100000,
-            endTime: Date.now(),
-            points: 100
-          }).done(function (data) {
-            self.data = data;
+        // Load the initial data
+        self.startTime = Date.now() - 1000 * 60 * 60 * 8; // 7 days
+        self.endTime = Date.now();
+        self.initialWindow = 1000 * 60 * 60 * 0.5;
+        var points = 1000; //Math.round((endTime - startTime) / 36000); // 1 hour precision
+        var initialLoadPromise = self.loadData(self.startTime, self.endTime, points);
 
-            var graphElement = domTree[1];
 
-            var subTop = self.graphHeight - 150;
-            var legendTop = self.graphHeight - 50;
+        self.componentRendered = function (elements) {
+          elements.some(function (element) {
+            if (element.className === 'performance-graph') {
+              self.graphElement = element;
+              return true;
+            }
+          });
+          initialLoadPromise.done(function () {
+            self.redrawGraph();
+          })
+        };
+      }
 
-            var mainMargin = {top: 10, right: 70, left: 70, bottom: self.graphHeight - subTop + 40};
-            var subMargin = {top: subTop, right: 70, bottom: 40, left: 70};
-            var legendMargin = {top: legendTop, right: 70, bottom: 10, left: 70};
+      PerformanceGraph.prototype.redrawGraph = function () {
+        var self = this;
 
-            var width = self.graphWidth - mainMargin.left - mainMargin.right;
+        // Margins and sizes
+        var subTop = self.graphHeight - 150;
+        var legendTop = self.graphHeight - 50;
+
+        var mainMargin = { top: 10, right: 70, left: 70, bottom: self.graphHeight - subTop + 40 };
+        var subMargin = { top: subTop, right: 70, bottom: 40, left: 70 };
+        var legendMargin = { top: legendTop, right: 70, bottom: 10, left: 70 };
+
+        var width = self.graphWidth - mainMargin.left - mainMargin.right;
+        var mainHeight = self.graphHeight - mainMargin.top - mainMargin.bottom;
+        var subHeight = self.graphHeight - subMargin.top - subMargin.bottom - 50;
+
+
+        // Scales
+        var percentageYScale = d3.scaleLinear().range([mainHeight, 0]);
+        var queryYScale = d3.scaleLinear().range([mainHeight, 0]);
+        var mainXScale = d3.scaleTime().range([0, width]);
+        var subPercentageYScale = d3.scaleLinear().range([subHeight, 0]);
+        var subQueryYScale = d3.scaleLinear().range([subHeight, 0]);
+        var subXScale = d3.scaleTime().range([0, width]);
+
+
+        // Axes
+        var mainXAxis = d3.axisBottom(mainXScale);
+        var subXAxis = d3.axisBottom(subXScale);
+
+        var queryYAxis = d3.axisLeft(queryYScale);
+        var percentageYAxis = d3.axisRight(percentageYScale).tickFormat(function (y) { return y + '%' });
+
+
+        // Update scale domains from data
+        mainXScale.domain([self.data[0][0], self.data[self.data.length - 1][0]]);
+        percentageYScale.domain([0, 100]);
+        var queryMax = 15;
+        self.data.forEach(function (row) {
+          if (row[4] + row[5] > queryMax) {
+            queryMax = row[4] + row[5];
+          }
+        });
+        queryYScale.domain([0, queryMax + 2]);
+
+        subXScale.domain(mainXScale.domain());
+        subPercentageYScale.domain(percentageYScale.domain());
+        subQueryYScale.domain(queryYScale.domain());
+
+
+        // Draw the graphs
+        var svg = d3.select(self.graphElement)
+                .append('svg')
+                .attr('width', self.graphWidth)
+                .attr('height', self.graphHeight);
+
+
+        // Clipping path
+        svg.append('defs')
+                .append('clipPath')
+                .attr('id', 'clip')
+                .append('rect')
+                .attr('width', width)
+                .attr('height', mainHeight);
+
+
+        // Containers for the graphs and legend
+        var mainGroup = svg.append('g')
+                .attr('class', 'main-group')
+                .attr('transform', 'translate(' + mainMargin.left + ',' + mainMargin.top + ')');
+
+        var subGroup = svg.append('g')
+                .attr('class', 'sub-group')
+                .attr('transform', 'translate(' + subMargin.left + ',' + subMargin.top + ')');
+
+        var legendGroup = svg.append('g')
+                .attr('class', 'legend-group')
+                .attr('transform', 'translate(' + legendMargin.left + ',' + legendMargin.top + ')');
+
+
+        // Draw the query and resource graphs
+        var createLineGraph = function (options) {
+          var line;
+          var enabled = options.enabled;
+          if (options.area) {
+            line = d3.area()
+                    .curve(d3.curveStep)
+                    .x(function (d) { return mainXScale(d[0]) })
+                    .y0(mainHeight)
+                    .y1(options.y)
+          } else {
+            line = d3.line()
+                    .curve(d3.curveMonotoneX)
+                    .x(function (d) { return mainXScale(d[0]) })
+                    .y(options.y)
+          }
+
+          var path = mainGroup.append('path')
+                  .datum(self.data)
+                  .attr('stroke', function (d, j) {
+                    return options.color;
+                  })
+                  .style('display', enabled ? null : 'none')
+                  .attr('fill', options.area ? options.color : 'none')
+                  .attr("clip-path", "url(#clip)")
+                  .classed('line line-' + options.id, true)
+                  .attr('d', line);
+
+          var subRefresh;
+          if (options.subLine) {
+            var subLine;
+            if (options.area) {
+              subLine = d3.area()
+                      .curve(d3.curveStep)
+                      .x(function (d) { return subXScale(d[0]) })
+                      .y0(subHeight)
+                      .y1(options.subY)
+            } else {
+              subLine = d3.line()
+                      .curve(d3.curveMonotoneX)
+                      .x(function (d) { return subXScale(d[0]) })
+                      .y(options.subY)
+            }
+            var subPath = subGroup.append('path')
+                    .datum(self.data)
+                    .attr('stroke', function (d, j) {
+                      return options.subLineColor || options.color;
+                    })
+                    .style('display', enabled ? null : 'none')
+                    .attr('fill', options.area ? options.subLineColor || options.color : 'none')
+                    .attr("clip-path", "url(#clip)")
+                    .classed('line line-' + options.id, true)
+                    .attr('d', subLine);
+            subRefresh = function () {
+              subPath.attr('d', subLine);
+            }
+          }
+
+          var stackedGraph;
+          if (options.stackedGraph) {
+            stackedGraph = options.stackedGraph();
+          }
+
+          return {
+            refresh: function () {
+              path.attr('d', line);
+              if (subRefresh) {
+                subRefresh();
+              }
+              if (stackedGraph) {
+                stackedGraph.refresh();
+              }
+            },
+            id: options.id,
+            enabled: function () {
+              return enabled;
+            },
+            color: options.color,
+            label: options.label,
+            tooltip: options.tooltip,
+            toggle: function () {
+              enabled = !enabled;
+              mainGroup.select('.highlight-point-' + options.id).style('display', enabled ? null : 'none');
+              mainGroup.select('.line-' + options.id).style('display', enabled ? null : 'none');
+              subGroup.select('.line-' + options.id).style('display', enabled ? null : 'none');
+              svg.select('.legend-radio-' + options.id).attr('fill-opacity', enabled ? 1 : 0.2);
+              if (stackedGraph) {
+                stackedGraph.enabled = enabled;
+                mainGroup.select('.highlight-point-' + stackedGraph.id).style('display', enabled ? null : 'none');
+                mainGroup.select('.line-' + stackedGraph.id).style('display', enabled ? null : 'none');
+                subGroup.select('.line-' + stackedGraph.id).style('display', enabled ? null : 'none');
+                svg.select('.legend-radio-' + stackedGraph.id).attr('fill-opacity', enabled ? 1 : 0.2);
+              }
+            },
+            highlightPoint: function (dataPoint) {
+              if (!options.disableHighlight) {
+                mainGroup
+                        .select('.highlight-point-' + options.id)
+                        .remove();
+
+                mainGroup.append('circle')
+                        .attr('class', 'highlight-point highlight-point-' + options.id)
+                        .style('display', enabled ? null : 'none')
+                        .attr('fill', function (d, j) {
+                          return options.color
+                        })
+                        .attr("r", 2.5)
+                        .attr('transform', "translate(" + mainXScale(dataPoint[0]) + "," + options.y(dataPoint) + ")")
+              }
+            }
+          }
+        };
+
+        var graphs = [
+          createLineGraph({
+            id: 'query-count-sum',
+            label: '${ _("Queries") }',
+            enabled: true,
+            color: '#87BAD5',
+            subLineColor: '#AAAAAA',
+            area: true,
+            subLine: true,
+            tooltip: function (d) { return d[4] + d[5] + (d[4] ? ' (' + d[4] + ' ${ _("queued") })' : '') },
+            y: function (d) { return queryYScale(d[4] + d[5]) },
+            subY: function (d) { return subQueryYScale(d[4] + d[5]) },
+            stackedGraph: function () {
+              return createLineGraph({
+                id: 'query-count-queued',
+                enabled: true,
+                color: '#0B7FAD',
+                area: true,
+                disableHighlight: true,
+                y: function (d) { return queryYScale(d[4]) }
+              })
+            }
+          }),
+          createLineGraph({
+            id: 'cpu',
+            enabled: false,
+            label: '${ _("CPU") }',
+            color: '#96C55A',
+            subLine: true,
+            tooltip: function (d) { return d[1] + '%' },
+            y: function (d) { return percentageYScale(d[1]) },
+            subY: function (d) { return subPercentageYScale(d[1]) }
+          }),
+          createLineGraph({
+            id: 'memory',
+            enabled: false,
+            label: '${ _("Memory") }',
+            color: '#E7808D',
+            subLine: true,
+            tooltip: function (d) { return d[2] + '%' },
+            y: function (d) { return percentageYScale(d[2]) },
+            subY: function (d) { return subPercentageYScale(d[2]) }
+          }),
+          createLineGraph({
+            id: 'io',
+            enabled: false,
+            label: '${ _("IO") }',
+            color: '#7A9F9F',
+            subLine: true,
+            tooltip: function (d) { return d[3] + '%' },
+            y: function (d) { return percentageYScale(d[3]) },
+            subY: function (d) { return subPercentageYScale(d[3]) }
+          })
+        ];
+
+
+        // Draw the axes
+        mainGroup.append('g')
+                .attr('class', 'main-axis main-axis-x')
+                .attr('transform', 'translate(0,' + mainHeight + ')')
+                .call(mainXAxis);
+
+        mainGroup.append('g')
+                .attr('class', 'main-axis main-axis-y main-axis-query-count')
+                .call(queryYAxis);
+
+        mainGroup.append('g')
+                .attr('class', 'main-axis main-axis-y main-axis-percentage')
+                .attr('transform', 'translate(' + width + ' ,0)')
+                .call(percentageYAxis);
+
+        subGroup.append('g')
+                .attr('class', 'sub-axis sub-axis-x')
+                .attr('transform', 'translate(0,' + subHeight + ')')
+                .call(subXAxis);
+
+
+        // Add brush
+        var brush = d3.brushX()
+                .extent([[0, 0], [width, subHeight]])
+                .on('start brush end', function () {
+                  var s = d3.event.selection || subXScale.range();
+                  mainXScale.domain(s.map(subXScale.invert, subXScale));
+
+                  graphs.forEach(function (graph) {
+                    graph.refresh();
+                  });
+
+                  mainGroup.select('.main-axis-x').call(mainXAxis);
+                });
 
-            var mainHeight = self.graphHeight - mainMargin.top - mainMargin.bottom;
-            var queryYScale = d3.scaleLinear().range([mainHeight, 0]);
-            var percentageYScale = d3.scaleLinear().range([mainHeight, 0]);
+        subGroup.append('g')
+                .attr('class', 'brush')
+                .call(brush)
+                .call(brush.move, [mainXScale(self.endTime - self.initialWindow), mainXScale(self.endTime)]);
 
-            var subHeight = self.graphHeight - subMargin.top - subMargin.bottom - 50;
+        // TODO: Custom handles
 
-            var legendHeight = self.graphHeight - legendMargin.top - legendMargin.bottom;
-            var subXScale = d3.scaleTime().range([0, width]);
-            var subPercentageYScale = d3.scaleLinear().range([subHeight, 0]);
-            var subQueryYScale = d3.scaleLinear().range([subHeight, 0]);
 
-            var mainXScale = d3.scaleTime().range([0, width]);
+        // Mouse hover overlay
+        var dateBisector = d3.bisector(function (d) {
+          return d[0];
+        }).left;
 
-            var mainXAxis = d3.axisBottom(mainXScale);
+        var pointFocus = function (mouseCoord, focusX) {
+          var x = focusX;
+          var j = dateBisector(self.data, x, 1);
+          var d0 = self.data[j - 1];
+          var d1 = self.data[j];
+          var closest = x - d0 > d1 - x ? d1 : d0;
 
-            var queryYAxis = d3.axisLeft(queryYScale);
-            var percentageYAxis = d3.axisRight(percentageYScale).tickFormat(function (y) {
-              return y + '%';
-            });
+          graphs.forEach(function (graph) {
+            graph.highlightPoint(closest)
+          });
 
-            var subXAxis = d3.axisBottom(subXScale);
-
-            ##  var tip = d3.select(graphElement).append('div').attr('class', 'graph-tip').style('opacity', 0).style('position', 'absolute');
-
-            var series = [{
-              label: '${ _("Queries") }',
-              color: '#BEE4F5',
-              enabled: true,
-              line: d3.area().curve(d3.curveStep).x(function (d) {
-                return mainXScale(d[0])
-              }).y0(mainHeight).y1(function (d) {
-                return queryYScale(d[4])
-              }),
-              subLine: d3.area().curve(d3.curveStep).x(function (d) {
-                return subXScale(d[0])
-              }).y0(subHeight).y1(function (d) {
-                return subQueryYScale(d[4])
-              }),
-              fill: '#BEE4F5',
-              showDot: false,
-              dataIndex: 4,
-              lineYScale: queryYScale,
-              subLineYScale: subQueryYScale
-            }, {
-              label: '${ _("CPU") }',
-              color: '#7B46AD',
-              enabled: window.cpuEnabled || false,
-              line: d3.line().curve(d3.curveMonotoneX).x(function (d) {
-                return mainXScale(d[0])
-              }).y(function (d) {
-                return percentageYScale(d[1])
-              }),
-              subLine: d3.line().curve(d3.curveMonotoneX).x(function (d) {
-                return subXScale(d[0])
-              }).y(function (d) {
-                return subPercentageYScale(d[1])
-              }),
-              fill: 'none',
-              showDot: true,
-              dataIndex: 1,
-              lineYScale: percentageYScale,
-              subLineYScale: subPercentageYScale
-            }, {
-              label: '${ _("Memory") }',
-              color: '#00B9AA',
-              enabled: window.memEnabled || false,
-              line: d3.line().curve(d3.curveMonotoneX).x(function (d) {
-                return mainXScale(d[0])
-              }).y(function (d) {
-                return percentageYScale(d[2])
-              }),
-              subLine: d3.line().curve(d3.curveMonotoneX).x(function (d) {
-                return subXScale(d[0])
-              }).y(function (d) {
-                return subPercentageYScale(d[2])
-              }),
-              fill: 'none',
-              showDot: true,
-              dataIndex: 2,
-              lineYScale: percentageYScale,
-              subLineYScale: subPercentageYScale
-            }, {
-              label: '${ _("IO") }',
-              color: '#1C749B',
-              enabled: window.ioEnabled || false,
-              line: d3.line().curve(d3.curveMonotoneX).x(function (d) {
-                return mainXScale(d[0])
-              }).y(function (d) {
-                return percentageYScale(d[3])
-              }),
-              subLine: d3.line().curve(d3.curveMonotoneX).x(function (d) {
-                return subXScale(d[0])
-              }).y(function (d) {
-                return subPercentageYScale(d[3])
-              }),
-              fill: 'none',
-              showDot: true,
-              dataIndex: 3,
-              lineYScale: percentageYScale,
-              subLineYScale: subPercentageYScale
-            }];
-
-            var dateBisector = d3.bisector(function (d) {
-              return d[0];
-            }).left;
-
-            var toggleSeries = function (serie) {
-              serie.enabled = !serie.enabled;
-              if (serie.dataIndex === 3) {
-                window.ioEnabled = serie.enabled;
-              } else if (serie.dataIndex === 2) {
-                window.memEnabled = serie.enabled;
-              } else if (serie.dataIndex === 1) {
-                window.cpuEnabled = serie.enabled;
+          var generateTooltipHtml = function (dataPoint) {
+            var html = '';
+            graphs.forEach(function (graph) {
+              if (graph.tooltip && graph.enabled()) {
+                html += '<div><div class="performance-tooltip-indicator"><div style="background-color: ' + graph.color + '">&nbsp;</div></div><span class="performance-tooltip-label">' + graph.label + '</span> ' + graph.tooltip(dataPoint) + '</div>';
               }
-              svg.selectAll('.line-' + serie.dataIndex).style('display', serie.enabled ? null : 'none');
-              svg.selectAll('.line-circle-' + serie.dataIndex).style('display', serie.enabled ? null : 'none');
-              svg.select('.legend-circle-' + serie.dataIndex).attr('fill-opacity', serie.enabled ? 1 : 0.2);
-            };
-
-            var svg = d3.select(graphElement).append('svg').attr('width', self.graphWidth).attr('height', self.graphHeight);
-
-            svg.append('defs')
-                    .append('clipPath')
-                    .attr('id', 'clip')
-                    .append('rect')
-                    .attr('width', width)
-                    .attr('height', mainHeight);
-
-            var main = svg.append('g')
-                    .attr('class', 'focus')
-                    .attr('transform', 'translate(' + mainMargin.left + ',' + mainMargin.top + ')');
-
-            var sub = svg.append('g')
-                    .attr('class', 'context')
-                    .attr('transform', 'translate(' + subMargin.left + ',' + subMargin.top + ')');
-
-            var legend = svg.append('g')
-                    .attr('class', 'context')
-                    .attr('transform', 'translate(' + legendMargin.left + ',' + legendMargin.top + ')');
-
-            var nextSeriesX = 5;
-
-            series.forEach(function (serie) {
-              var legendSerie = legend.append('g')
-                      .on('click', function (d, j) {
-                        toggleSeries(serie);
-                      })
-                      .on('mouseover', function (d, j) {
-
-                      })
-                      .on('mouseout', function (d, j) {
-
-                      });
-
-              legendSerie.append('circle')
-                      .attr('class', 'legend-circle-' + serie.dataIndex)
-                      .attr('fill-opacity', serie.enabled ? 1 : 0.2)
-                      .style('fill', function (d, j) {
-                        return serie.color
-                      })
-                      .style('stroke', function (d, j) {
-                        return serie.color
-                      })
-                      .attr('r', 5);
-
-              legendSerie.append('text')
-                      .text(serie.label)
-                      .attr('text-anchor', 'start')
-                      .attr('dy', '.32em')
-                      .attr('dx', '8');
-
-              legendSerie.attr('transform', function (d, j) {
-                var length = d3.select(this).select('text').node().getComputedTextLength() + 28;
-                var xpos = nextSeriesX;
-                nextSeriesX += length;
-                return 'translate(' + xpos + ',' + 5 + ')'
-              });
             });
+            return html;
+          };
+
+          d3.select(self.graphElement)
+                  .select('.performance-tooltip')
+                  .html(generateTooltipHtml(closest))
+                  .style("left", lastMouseCoord[0] + mainMargin.left + 15 + "px")
+                  .style("top", lastMouseCoord[1] + mainMargin.top +  "px");
+
+        };
+
+        var lastMouseCoord = [0, 0];
+        var lastFocusX = 0;
+        mainGroup.append("rect")
+                .attr('class', "overlay")
+                .attr("fill-opacity", "0")
+                .attr("fill", "white")
+                .attr("width", width)
+                .attr("height", mainHeight)
+                .on("mouseout", function () {
+                  mainGroup.selectAll('.highlight-point').remove();
+                  d3.select(self.graphElement)
+                          .selectAll('.performance-tooltip')
+                          .remove()
+                })
+                .on('mouseover', function () {
+                  d3.select(self.graphElement)
+                          .append("div")
+                          .attr("class", "performance-tooltip")
+                })
+                .on("mousemove", function () {
+                  lastMouseCoord = d3.mouse(this);
+                  lastFocusX = mainXScale.invert(lastMouseCoord[0]);
+                  pointFocus(lastMouseCoord, lastFocusX);
+                });
 
-            var brush = d3.brushX()
-                    .extent([[0, 0], [width, subHeight]])
-                    .on('brush end', function () {
-                      if (d3.event.sourceEvent && d3.event.sourceEvent.type === 'zoom') {
-                        return;
-                      }
-                      var s = d3.event.selection || subXScale.range();
-                      mainXScale.domain(s.map(subXScale.invert, subXScale));
-                      series.forEach(function (serie) {
-                        main.select('.line-' + serie.dataIndex).attr('d', serie.line);
-                        main.selectAll('.line-circle-' + serie.dataIndex)
-                                .attr("cx", function (d) {
-                                  return mainXScale(d[0]);
-                                })
-                                .attr("cy", function (d) {
-                                  return serie.lineYScale(d[serie.dataIndex]);
-                                })
-                      });
-                      main.select('.axis--x').call(mainXAxis);
-                      svg.select('.zoom').call(zoom.transform, d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0));
-                    });
-
-            var zoom = d3.zoom()
-                    .scaleExtent([1, Infinity])
-                    .translateExtent([[0, 0], [width, mainHeight]])
-                    .extent([[0, 0], [width, mainHeight]])
-                    .on('zoom', function () {
-                      if (d3.event.sourceEvent && d3.event.sourceEvent.type === 'brush') {
-                        return;
-                      }
-                      var t = d3.event.transform;
-                      mainXScale.domain(t.rescaleX(subXScale).domain());
-                      series.forEach(function (serie) {
-                        main.select('.line-' + serie.dataIndex).attr('d', serie.line);
-                        main.selectAll('.line-circle-' + serie.dataIndex)
-                                .attr("cx", function (d) {
-                                  return mainXScale(d[0]);
-                                })
-                                .attr("cy", function (d) {
-                                  return serie.lineYScale(d[serie.dataIndex]);
-                                })
-                      });
-                      main.select('.axis--x').call(mainXAxis);
-                      sub.select('.brush').call(brush.move, mainXScale.range().map(t.invertX, t));
-                    });
-
-            mainXScale.domain([self.data[0][0], self.data[self.data.length - 1][0]]);
-            percentageYScale.domain([0, 100]);
-            var queryMax = 15;
-            self.data.forEach(function (row) {
-              if (row[4] > queryMax) {
-                queryMax = row[4];
-              }
-            });
-            queryYScale.domain([0, queryMax + 2]);
-
-            subXScale.domain(mainXScale.domain());
-            subPercentageYScale.domain(percentageYScale.domain());
-            subQueryYScale.domain(queryYScale.domain());
-
-            series.forEach(function (serie) {
-
-              main.append('path')
-                      .datum(self.data)
-                      .attr('stroke', function (d, j) {
-                        return serie.color
-                      })
-                      .attr('fill', serie.fill)
-                      .style('display', serie.enabled ? null : 'none')
-                      .attr("clip-path", "url(#clip)")
-                      .classed('line line-' + serie.dataIndex, true)
-                      .attr('d', serie.line);
-
-              if (serie.showDot) {
-                main.selectAll(".line-circle-" + serie.dataIndex)
-                        .data(self.data)
-                        .enter()
-                        .append("circle")
-                        .style('display', serie.enabled ? null : 'none')
-                        .attr('fill', function (d, j) {
-                          return serie.color
-                        })
-                        .attr("clip-path", "url(#clip)")
-                        .attr("class", "line-circle-" + serie.dataIndex)
-                        .attr("r", 1.5)
-                        .attr("cx", function (d) {
-                          return mainXScale(d[0]);
-                        })
-                        .attr("cy", function (d) {
-                          return serie.lineYScale(d[serie.dataIndex]);
-                        });
-              }
 
-              sub.append('path')
-                      .datum(self.data)
-                      .style('display', serie.enabled ? null : 'none')
-                      .attr('stroke', function (d, j) {
-                        return serie.color
-                      })
-                      .attr('fill', serie.fill)
-                      .classed('line line-' + serie.dataIndex, true)
-                      .attr('d', serie.subLine);
-
-              var focus = svg.append("g")
-                      .attr("class", "point-focus point-focus-" + serie.dataIndex)
-                      .attr('fill', 'none')
-                      .attr('stroke', function (d, j) {
-                        return serie.color
-                      })
-                      .style("display", "none");
-
-              focus.append("circle")
-                      .attr("r", 5);
-            });
+        // Add Legend
+        var legend = svg.append('g')
+                .attr('class', 'context')
+                .attr('transform', 'translate(' + legendMargin.left + ',' + legendMargin.top + ')');
+
+        var nextSeriesX = 5;
+
+        graphs.forEach(function (graph) {
+          var legendSerie = legend.append('g')
+                  .on('click', function (d, j) {
+                    graph.toggle();
+                  });
+
+          legendSerie.append('circle')
+                  .attr('class', 'legend-radio-' + graph.id)
+                  .attr('fill-opacity', graph.enabled() ? 1 : 0.2)
+                  .style('fill', function (d, j) {
+                    return graph.color
+                  })
+                  .style('stroke', function (d, j) {
+                    return graph.color
+                  })
+                  .attr('r', 5);
+
+          legendSerie.append('text')
+                  .text(graph.label)
+                  .attr('text-anchor', 'start')
+                  .attr('dy', '.32em')
+                  .attr('dx', '8');
+
+          legendSerie.attr('transform', function (d, j) {
+            var length = d3.select(this).select('text').node().getComputedTextLength() + 28;
+            var xPos = nextSeriesX;
+            nextSeriesX += length;
+            return 'translate(' + xPos + ',' + 5 + ')'
+          });
+        });
 
-            main.append('g')
-                    .attr('class', 'axis axis--x')
-                    .attr('transform', 'translate(0,' + mainHeight + ')')
-                    .call(mainXAxis);
-
-            main.append('g')
-                    .attr('class', 'axis axis--y')
-                    .call(queryYAxis);
-
-            main.append('g')
-                    .attr('class', 'axis axis--y')
-                    .attr("transform", "translate(" + width + " ,0)")
-                    .call(percentageYAxis);
-
-            sub.append('g')
-                    .attr('class', 'axis axis--x')
-                    .attr('transform', 'translate(0,' + subHeight + ')')
-                    .call(subXAxis);
-
-            sub.append('g')
-                    .attr('class', 'brush')
-                    .call(brush)
-                    .call(brush.move, mainXScale.range());
-
-            svg.append('rect')
-                    .attr('class', 'zoom')
-                    .attr('fill', 'none')
-                    .attr('width', width)
-                    .attr('height', mainHeight)
-                    .attr('transform', 'translate(' + mainMargin.left + ',' + mainMargin.top + ')')
-                    .call(zoom);
-
-            var lastMouseCoord = [0, 0];
-            var lastFocusX = 0;
-
-            var pointFocus = function (mouseCoord, focusX) {
-              var x = focusX;
-              var j = dateBisector(self.data, x, 1);
-              var d0 = self.data[j - 1];
-              var d1 = self.data[j];
-              var closest = x - d0 > d1 - x ? d1 : d0;
-
-              var showTip = false;
-              series.forEach(function (serie) {
-                if (serie.enabled) {
-                  var point = svg.select('.point-focus-' + serie.dataIndex);
-                  point.attr("transform", "translate(" + (mainXScale(closest[0]) + mainMargin.left) + "," + (serie.lineYScale(closest[serie.dataIndex]) + mainMargin.top) + ")");
-                  if (point.style('display') !== 'none') {
-                    showTip = true;
-                  }
-                }
-              });
-              if (showTip) {
-                // TODO: show the tip
-              }
-            };
-
-            // Hover overlay
-            svg.append("rect")
-                    .attr("class", "overlay")
-                    .attr("fill-opacity", "0")
-                    .attr("fill", "white")
-                    .attr('transform', 'translate(' + mainMargin.left + ',' + mainMargin.top + ')')
-                    .attr("width", width)
-                    .attr("height", mainHeight)
-                    .on("mouseover", function () {
-                      series.forEach(function (serie) {
-                        if (serie.enabled) {
-                          svg.select('.point-focus-' + serie.dataIndex).style("display", null);
-                          svg.select('.focus-tip').style("display", null);
-                        }
-                      });
-                    })
-                    .on("mouseout", function () {
-                      svg.selectAll('.point-focus').style("display", "none");
-                      svg.select('.focus-tip').style("display", 'none');
-                    })
-                    .on("mousemove", function () {
-                      lastMouseCoord = d3.mouse(this);
-                      lastFocusX = mainXScale.invert(lastMouseCoord[0]);
-                      pointFocus(lastMouseCoord, lastFocusX);
-                    });
-
-            var appendInterval = 1000;
-            var appendPoints = 1;
-
-            var appendData = function () {
-              var currentDomain = mainXScale.domain();
-              var currentSubDomain = subXScale.domain();
-
-              ApiHelper.getInstance().fetchResourceStats({
-                startTime: self.data[self.data.length - 1][0],
-                endTime: Date.now(),
-                points: appendPoints
-              }).done(function (data) {
-                data.forEach(function (newEntry) {
-                  self.data.push(newEntry);
-                });
+        // Fetch and append data at set interval
 
-                var mainStart = currentDomain[0].getTime() !== currentSubDomain[0].getTime() ? currentDomain[0] : self.data[0][0];
-                var mainEnd = currentDomain[1].getTime() !== currentSubDomain[1].getTime() ? currentDomain[1] : self.data[self.data.length - 1][0];
-                mainXScale.domain([mainStart, mainEnd]);
-                subXScale.domain([self.data[0][0], self.data[self.data.length - 1][0]]);
-
-                series.forEach(function (serie) {
-                  main.select('.line-' + serie.dataIndex).attr('d', serie.line);
-                  if (serie.showDot) {
-                    main.selectAll(".line-circle-" + serie.dataIndex).remove();
-
-                    main.selectAll(".line-circle-" + serie.dataIndex)
-                            .data(self.data)
-                            .enter()
-                            .append("circle")
-                            .style('display', serie.enabled ? null : 'none')
-                            .attr('fill', function (d, j) {
-                              return serie.color
-                            })
-                            .attr("clip-path", "url(#clip)")
-                            .attr("class", "line-circle-" + serie.dataIndex)
-                            .attr("r", 1.5)
-                            .attr("cx", function (d) {
-                              return mainXScale(d[0]);
-                            })
-                            .attr("cy", function (d) {
-                              return serie.lineYScale(d[serie.dataIndex]);
-                            });
-                  }
-                  sub.select('.line-' + serie.dataIndex).attr('d', serie.subLine);
-                });
+        var appendInterval = 1000;
+        var appendPoints = 1;
+        var appendData = function () {
+          var currentDomain = mainXScale.domain();
+          var currentSubDomain = subXScale.domain();
 
-                pointFocus(lastMouseCoord, lastFocusX);
+          self.loadData(self.data[self.data.length - 1][0], Date.now(), appendPoints).done(function () {
+            var mainStart = currentDomain[0].getTime() !== currentSubDomain[0].getTime() ? currentDomain[0] : self.data[0][0];
+            var mainEnd = currentDomain[1].getTime() !== currentSubDomain[1].getTime() ? currentDomain[1] : self.data[self.data.length - 1][0];
+            mainXScale.domain([mainStart, mainEnd]);
+            subXScale.domain([self.data[0][0], self.data[self.data.length - 1][0]]);
 
-                sub.select('.axis--x').call(subXAxis);
-                main.select('.axis--x').call(mainXAxis);
+            graphs.forEach(function (graph) {
+              graph.refresh();
+            });
 
-                sub.select('.brush').call(brush);
+            // Update highlight points and tip if data is appended when highlight is active
+            if (mainGroup.selectAll('.highlight-point').size()) {
+              pointFocus(lastMouseCoord, lastFocusX);
+            }
+
+            subGroup.select('.sub-axis-x').call(subXAxis);
+            mainGroup.select('.main-axis-x').call(mainXAxis);
+            subGroup.select('.brush').call(brush);
 
-                self.appendTimeout = window.setTimeout(appendData, appendInterval);
-              })
-            };
             self.appendTimeout = window.setTimeout(appendData, appendInterval);
           });
-        }
-      }
+        };
+        self.appendTimeout = window.setTimeout(appendData, appendInterval);
+      };
+
+      PerformanceGraph.prototype.loadData = function (startTime, endTime, points) {
+        var self = this;
+        return ApiHelper.getInstance().fetchResourceStats({
+          startTime: startTime,
+          endTime: endTime,
+          points: points
+        }).done(function (data) {
+          if (!self.data) {
+            self.data = data;
+          } else {
+            data.forEach(function (newRow) {
+              self.data.push(newRow);
+            })
+          }
+        });
+      };
 
       PerformanceGraph.prototype.dispose = function () {
         var self = this;

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác