Perf dashboard should automatically select ranges for A/B testing
[WebKit-https.git] / Websites / perf.webkit.org / public / v2 / interactive-chart.js
index 56468567d5fb8010b36b8df5eee223977eb30caf..6eaeac3dca44cd76cf604f64faf8cc0ced62670e 100644 (file)
@@ -18,6 +18,8 @@ App.InteractiveChartComponent = Ember.Component.extend({
         if (!chartData)
             return;
         this._needsConstruction = true;
+        this._totalWidth = undefined;
+        this._totalHeight = undefined;
         this._constructGraphIfPossible(chartData);
     }.observes('chartData').on('init'),
     didInsertElement: function ()
@@ -25,6 +27,14 @@ App.InteractiveChartComponent = Ember.Component.extend({
         var chartData = this.get('chartData');
         if (chartData)
             this._constructGraphIfPossible(chartData);
+
+        if (this.get('interactive')) {
+            var element = this.get('element');
+            this._attachEventListener(element, "mousemove", this._mouseMoved.bind(this));
+            this._attachEventListener(element, "mouseleave", this._mouseLeft.bind(this));
+            this._attachEventListener(element, "mousedown", this._mouseDown.bind(this));
+            this._attachEventListener($(element).parents("[tabindex]"), "keydown", this._keyPressed.bind(this));
+        }
     },
     willClearRender: function ()
     {
@@ -47,7 +57,8 @@ App.InteractiveChartComponent = Ember.Component.extend({
         this._x = d3.time.scale();
         this._y = d3.scale.linear();
 
-        // FIXME: Tear down the old SVG element.
+        if (this._svgElement)
+            this._svgElement.remove();
         this._svgElement = d3.select(element).append("svg")
                 .attr("width", "100%")
                 .attr("height", "100%");
@@ -65,11 +76,15 @@ App.InteractiveChartComponent = Ember.Component.extend({
                 .attr("class", "x axis");
         }
 
+        var isInteractive = this.get('interactive');
         if (this.get('showYAxis')) {
-            this._yAxis = d3.svg.axis().scale(this._y).orient("left").ticks(6).tickFormat(
-                chartData.useSI ? d3.format("s") : d3.format(".3g"));
-            this._yAxisLabels = svg.append("g")
-                .attr("class", "y axis");
+            this._yAxis = d3.svg.axis().scale(this._y).orient("left").ticks(6).tickFormat(chartData.formatter);
+            
+            this._yAxisLabels = svg.append('g').attr('class', 'y axis' + (isInteractive ? ' interactive' : ''));
+            if (isInteractive) {
+                var self = this;
+                this._yAxisLabels.on('click', function () { self.toggleProperty('showFullYAxis'); });
+            }
         }
 
         this._clippedContainer = svg.append("g")
@@ -87,69 +102,64 @@ App.InteractiveChartComponent = Ember.Component.extend({
             .y0(function(point) { return point.interval ? yScale(point.interval[0]) : null; })
             .y1(function(point) { return point.interval ? yScale(point.interval[1]) : null; });
 
-        if (this._paths)
-            this._paths.forEach(function (path) { path.remove(); });
         this._paths = [];
-        if (this._areas)
-            this._areas.forEach(function (area) { area.remove(); });
         this._areas = [];
-        if (this._dots)
-            this._dots.forEach(function (dot) { dots.remove(); });
         this._dots = [];
-        if (this._highlights)
-            this._highlights.remove();
         this._highlights = null;
 
-        this._currentTimeSeries = chartData.current.timeSeriesByCommitTime();
+        this._currentTimeSeries = chartData.current;
         this._currentTimeSeriesData = this._currentTimeSeries.series();
-        this._baselineTimeSeries = chartData.baseline ? chartData.baseline.timeSeriesByCommitTime() : null;
-        this._targetTimeSeries = chartData.target ? chartData.target.timeSeriesByCommitTime() : null;
+        this._baselineTimeSeries = chartData.baseline;
+        this._targetTimeSeries = chartData.target;
+        this._movingAverageTimeSeries = chartData.movingAverage;
 
         this._yAxisUnit = chartData.unit;
 
-        var minMax = this._minMaxForAllTimeSeries();
-        var smallEnoughValue = minMax[0] - (minMax[1] - minMax[0]) * 10;
-        var largeEnoughValue = minMax[1] + (minMax[1] - minMax[0]) * 10;
-
-        // FIXME: Flip the sides based on smallerIsBetter-ness.
         if (this._baselineTimeSeries) {
-            var data = this._baselineTimeSeries.series();
-            this._areas.push(this._clippedContainer
+            this._paths.push(this._clippedContainer
                 .append("path")
-                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [point.value, largeEnoughValue]}; }))
-                .attr("class", "area baseline"));
+                .datum(this._baselineTimeSeries.series())
+                .attr("class", "baseline"));
         }
         if (this._targetTimeSeries) {
-            var data = this._targetTimeSeries.series();
-            this._areas.push(this._clippedContainer
+            this._paths.push(this._clippedContainer
                 .append("path")
-                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [smallEnoughValue, point.value]}; }))
-                .attr("class", "area target"));
+                .datum(this._targetTimeSeries.series())
+                .attr("class", "target"));
         }
 
+        var movingAverageIsVisible = this._movingAverageTimeSeries;
+        var foregroundClass = movingAverageIsVisible ? '' : ' foreground';
         this._areas.push(this._clippedContainer
             .append("path")
             .datum(this._currentTimeSeriesData)
-            .attr("class", "area"));
+            .attr("class", "area" + foregroundClass));
 
         this._paths.push(this._clippedContainer
             .append("path")
             .datum(this._currentTimeSeriesData)
-            .attr("class", "commit-time-line"));
+            .attr("class", "current" + foregroundClass));
 
         this._dots.push(this._clippedContainer
             .selectAll(".dot")
                 .data(this._currentTimeSeriesData)
             .enter().append("circle")
-                .attr("class", "dot")
+                .attr("class", "dot" + foregroundClass)
                 .attr("r", this.get('chartPointRadius') || 1));
 
-        if (this.get('interactive')) {
-            this._attachEventListener(element, "mousemove", this._mouseMoved.bind(this));
-            this._attachEventListener(element, "mouseleave", this._mouseLeft.bind(this));
-            this._attachEventListener(element, "mousedown", this._mouseDown.bind(this));
-            this._attachEventListener($(element).parents("[tabindex]"), "keydown", this._keyPressed.bind(this));
+        if (movingAverageIsVisible) {
+            this._paths.push(this._clippedContainer
+                .append("path")
+                .datum(this._movingAverageTimeSeries.series())
+                .attr("class", "movingAverage"));
+
+            this._areas.push(this._clippedContainer
+                .append("path")
+                .datum(this._movingAverageTimeSeries.series())
+                .attr("class", "envelope"));
+        }
 
+        if (isInteractive) {
             this._currentItemLine = this._clippedContainer
                 .append("line")
                 .attr("class", "current-item");
@@ -187,14 +197,19 @@ App.InteractiveChartComponent = Ember.Component.extend({
     _updateDomain: function ()
     {
         var xDomain = this.get('domain');
+        if (!xDomain || !this._currentTimeSeriesData)
+            return null;
         var intrinsicXDomain = this._computeXAxisDomain(this._currentTimeSeriesData);
         if (!xDomain)
             xDomain = intrinsicXDomain;
-        var currentDomain = this._x.domain();
-        if (currentDomain && App.domainsAreEqual(currentDomain, xDomain))
-            return currentDomain;
-
         var yDomain = this._computeYAxisDomain(xDomain[0], xDomain[1]);
+
+        var currentXDomain = this._x.domain();
+        var currentYDomain = this._y.domain();
+        if (currentXDomain && App.domainsAreEqual(currentXDomain, xDomain)
+            && currentYDomain && App.domainsAreEqual(currentYDomain, yDomain))
+            return currentXDomain;
+
         this._x.domain(xDomain);
         this._y.domain(yDomain);
         return xDomain;
@@ -223,8 +238,8 @@ App.InteractiveChartComponent = Ember.Component.extend({
             margin.left += 3 * rem;
 
         this._margin = margin;
-        this._contentWidth = this._totalWidth - margin.left - margin.right;
-        this._contentHeight = this._totalHeight - margin.top - margin.bottom;
+        this._contentWidth = Math.max(0, this._totalWidth - margin.left - margin.right);
+        this._contentHeight = Math.max(0, this._totalHeight - margin.top - margin.bottom);
 
         this._svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
 
@@ -236,12 +251,15 @@ App.InteractiveChartComponent = Ember.Component.extend({
         this._y.range([this._contentHeight, 0]);
 
         if (this._xAxis) {
+            this._xAxis.ticks(Math.round(this._contentWidth / 4 / rem));
             this._xAxis.tickSize(-this._contentHeight);
             this._xAxisLabels.attr("transform", "translate(0," + this._contentHeight + ")");
         }
 
-        if (this._yAxis)
+        if (this._yAxis) {
+            this._yAxis.ticks(Math.round(this._contentHeight / 2 / rem));
             this._yAxis.tickSize(-this._contentWidth);
+        }
 
         if (this._currentItemLine) {
             this._currentItemLine
@@ -257,7 +275,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
             .call(this._brush)
         .selectAll("rect")
             .attr("y", 1)
-            .attr("height", this._contentHeight - 2);
+            .attr("height", Math.max(0, this._contentHeight - 2));
         this._updateSelectionToolbar();
     },
     _relayoutDataAndAxes: function (selection)
@@ -296,7 +314,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
         this._yAxisLabels.call(this._yAxis);
         if (this._yAxisUnitContainer)
             this._yAxisUnitContainer.remove();
-        var x = - 3 * this._rem;
+        var x = - 3.2 * this._rem;
         var y = this._contentHeight / 2;
         this._yAxisUnitContainer = this._yAxisLabels.append("text")
             .attr("transform", "rotate(90 0 0) translate(" + y + ", " + (-x) + ")")
@@ -321,28 +339,43 @@ App.InteractiveChartComponent = Ember.Component.extend({
     },
     _computeYAxisDomain: function (startTime, endTime)
     {
-        var range = this._minMaxForAllTimeSeries(startTime, endTime);
+        var shouldShowFullYAxis = this.get('showFullYAxis');
+        var range = this._minMaxForAllTimeSeries(startTime, endTime, !shouldShowFullYAxis);
         var min = range[0];
         var max = range[1];
+
+        var highlightedItems = this.get('highlightedItems');
+        if (highlightedItems) {
+            var data = this._currentTimeSeriesData
+                .filter(function (point) { return startTime <= point.time && point.time <= endTime && highlightedItems[point.measurement.id()]; })
+                .map(function (point) { return point.value });
+            min = Math.min(min, Statistics.min(data));
+            max = Math.max(max, Statistics.max(data));
+        }
+
         if (max < min)
             min = max = 0;
+        else if (shouldShowFullYAxis)
+            min = Math.min(min, 0);
         var diff = max - min;
         var margin = diff * 0.05;
 
         yExtent = [min - margin, max + margin];
-//        if (yMin !== undefined)
-//            yExtent[0] = parseInt(yMin);
         return yExtent;
     },
-    _minMaxForAllTimeSeries: function (startTime, endTime)
+    _minMaxForAllTimeSeries: function (startTime, endTime, ignoreOutliners)
     {
-        var currentRange = this._currentTimeSeries.minMaxForTimeRange(startTime, endTime);
-        var baselineRange = this._baselineTimeSeries ? this._baselineTimeSeries.minMaxForTimeRange(startTime, endTime) : [Number.MAX_VALUE, Number.MIN_VALUE];
-        var targetRange = this._targetTimeSeries ? this._targetTimeSeries.minMaxForTimeRange(startTime, endTime) : [Number.MAX_VALUE, Number.MIN_VALUE];
-        return [
-            Math.min(currentRange[0], baselineRange[0], targetRange[0]),
-            Math.max(currentRange[1], baselineRange[1], targetRange[1]),
-        ];
+        var seriesList = [this._currentTimeSeries, this._movingAverageTimeSeries, this._baselineTimeSeries, this._targetTimeSeries];
+        var min = Infinity;
+        var max = -Infinity;
+        for (var i = 0; i < seriesList.length; i++) {
+            if (seriesList[i]) {
+                var minMax = seriesList[i].minMaxForTimeRange(startTime, endTime, ignoreOutliners);
+                min = Math.min(min, minMax[0]);
+                max = Math.max(max, minMax[1]);
+            }
+        }
+        return [min, max];
     },
     _currentSelection: function ()
     {
@@ -352,12 +385,14 @@ App.InteractiveChartComponent = Ember.Component.extend({
     {
         var selection = this._currentSelection() || this.get('sharedSelection');
         var newXDomain = this._updateDomain();
+        if (!newXDomain)
+            return;
 
         if (selection && newXDomain && selection[0] <= newXDomain[0] && newXDomain[1] <= selection[1])
             selection = null; // Otherwise the user has no way of clearing the selection.
 
         this._relayoutDataAndAxes(selection);
-    }.observes('domain'),
+    }.observes('domain', 'showFullYAxis'),
     _selectionChanged: function ()
     {
         this._updateSelection(this.get('selection'));
@@ -386,7 +421,6 @@ App.InteractiveChartComponent = Ember.Component.extend({
             if (!this._brushExtent)
                 return;
 
-            this.set('selectionIsLocked', false);
             this._setCurrentSelection(undefined);
 
             // Avoid locking the indicator in _mouseDown when the brush was cleared in the same mousedown event.
@@ -399,7 +433,6 @@ App.InteractiveChartComponent = Ember.Component.extend({
             return;
         }
 
-        this.set('selectionIsLocked', true);
         this._setCurrentSelection(this._brush.extent());
     },
     _keyPressed: function (event)
@@ -491,9 +524,6 @@ App.InteractiveChartComponent = Ember.Component.extend({
             var yDiff = mY - point.y;
             return xDiff * xDiff + yDiff * yDiff / 16; // Favor horizontal affinity.
         };
-        distanceHeuristics = function (m) {
-            return Math.abs(xScale(m.time) - point.x);
-        }
 
         var newItemIndex;
         if (point && !this._currentSelection()) {
@@ -576,11 +606,11 @@ App.InteractiveChartComponent = Ember.Component.extend({
         }
     }.observes('selectedItem').on('init'),
     _highlightedItemsChanged: function () {
-        if (!this._clippedContainer)
-            return;
-
         var highlightedItems = this.get('highlightedItems');
 
+        if (!this._clippedContainer || !highlightedItems)
+            return;
+
         var data = this._currentTimeSeriesData.filter(function (item) { return highlightedItems[item.measurement.id()]; });
 
         if (this._highlights)
@@ -592,7 +622,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
                 .attr("class", "highlight")
                 .attr("r", (this.get('chartPointRadius') || 1) * 1.8);
 
-        this._updateHighlightPositions();
+        this._domainChanged();
     }.observes('highlightedItems'),
     _rangesChanged: function ()
     {
@@ -623,6 +653,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
                 linkRoute: linkRoute,
                 linkId: range.get('id'),
                 label: range.get('label'),
+                status: range.get('status'),
             });
         }));
 
@@ -734,7 +765,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
     },
     _updateSelectionToolbar: function ()
     {
-        if (!this.get('interactive'))
+        if (!this.get('zoomable'))
             return;
 
         var selection = this._currentSelection();