<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[177424] trunk/Websites/perf.webkit.org</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/177424">177424</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2014-12-16 18:29:22 -0800 (Tue, 16 Dec 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Split InteractiveChartComponent and CommitsViewerComponent into separate files
https://bugs.webkit.org/show_bug.cgi?id=139716

Rubber-stamped by Benjamin Poulain.

Refactored InteractiveChartComponent and CommitsViewerComponent out of app.js into commits-viewer.js
and interactive-chart.js respectively since app.js has gotten really large.

* public/v2/app.js:
* public/v2/commits-viewer.js: Added.
* public/v2/interactive-chart.js: Added.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2appjs">trunk/Websites/perf.webkit.org/public/v2/app.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2commitsviewerjs">trunk/Websites/perf.webkit.org/public/v2/commits-viewer.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2interactivechartjs">trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (177423 => 177424)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2014-12-17 02:11:58 UTC (rev 177423)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2014-12-17 02:29:22 UTC (rev 177424)
</span><span class="lines">@@ -1,3 +1,17 @@
</span><ins>+2014-12-16  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Split InteractiveChartComponent and CommitsViewerComponent into separate files
+        https://bugs.webkit.org/show_bug.cgi?id=139716
+
+        Rubber-stamped by Benjamin Poulain.
+
+        Refactored InteractiveChartComponent and CommitsViewerComponent out of app.js into commits-viewer.js
+        and interactive-chart.js respectively since app.js has gotten really large.
+
+        * public/v2/app.js:
+        * public/v2/commits-viewer.js: Added.
+        * public/v2/interactive-chart.js: Added.
+
</ins><span class="cx"> 2014-12-02  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         New perf dashboard's chart UI is buggy
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (177423 => 177424)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.js        2014-12-17 02:11:58 UTC (rev 177423)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js        2014-12-17 02:29:22 UTC (rev 177424)
</span><span class="lines">@@ -763,819 +763,7 @@
</span><span class="cx">     }.observes('newAnalysisTaskName'),
</span><span class="cx"> });
</span><span class="cx"> 
</span><del>-App.InteractiveChartComponent = Ember.Component.extend({
-    chartData: null,
-    showXAxis: true,
-    showYAxis: true,
-    interactive: false,
-    enableSelection: true,
-    classNames: ['chart'],
-    init: function ()
-    {
-        this._super();
-        this._needsConstruction = true;
-        this._eventHandlers = [];
-        $(window).resize(this._updateDimensionsIfNeeded.bind(this));
-    },
-    chartDataDidChange: function ()
-    {
-        var chartData = this.get('chartData');
-        if (!chartData)
-            return;
-        this._needsConstruction = true;
-        this._constructGraphIfPossible(chartData);
-    }.observes('chartData').on('init'),
-    didInsertElement: function ()
-    {
-        var chartData = this.get('chartData');
-        if (chartData)
-            this._constructGraphIfPossible(chartData);
-    },
-    willClearRender: function ()
-    {
-        this._eventHandlers.forEach(function (item) {
-            $(item[0]).off(item[1], item[2]);
-        })
-    },
-    _attachEventListener: function(target, eventName, listener)
-    {
-        this._eventHandlers.push([target, eventName, listener]);
-        $(target).on(eventName, listener);
-    },
-    _constructGraphIfPossible: function (chartData)
-    {
-        if (!this._needsConstruction || !this.get('element'))
-            return;
</del><span class="cx"> 
</span><del>-        var element = this.get('element');
-
-        this._x = d3.time.scale();
-        this._y = d3.scale.linear();
-
-        // FIXME: Tear down the old SVG element.
-        this._svgElement = d3.select(element).append(&quot;svg&quot;)
-                .attr(&quot;width&quot;, &quot;100%&quot;)
-                .attr(&quot;height&quot;, &quot;100%&quot;);
-
-        var svg = this._svg = this._svgElement.append(&quot;g&quot;);
-
-        var clipId = element.id + &quot;-clip&quot;;
-        this._clipPath = svg.append(&quot;defs&quot;).append(&quot;clipPath&quot;)
-            .attr(&quot;id&quot;, clipId)
-            .append(&quot;rect&quot;);
-
-        if (this.get('showXAxis')) {
-            this._xAxis = d3.svg.axis().scale(this._x).orient(&quot;bottom&quot;).ticks(6);
-            this._xAxisLabels = svg.append(&quot;g&quot;)
-                .attr(&quot;class&quot;, &quot;x axis&quot;);
-        }
-
-        if (this.get('showYAxis')) {
-            this._yAxis = d3.svg.axis().scale(this._y).orient(&quot;left&quot;).ticks(6).tickFormat(d3.format(&quot;s&quot;));
-            this._yAxisLabels = svg.append(&quot;g&quot;)
-                .attr(&quot;class&quot;, &quot;y axis&quot;);
-        }
-
-        this._clippedContainer = svg.append(&quot;g&quot;)
-            .attr(&quot;clip-path&quot;, &quot;url(#&quot; + clipId + &quot;)&quot;);
-
-        var xScale = this._x;
-        var yScale = this._y;
-        this._timeLine = d3.svg.line()
-            .x(function(point) { return xScale(point.time); })
-            .y(function(point) { return yScale(point.value); });
-
-        this._confidenceArea = d3.svg.area()
-//            .interpolate(&quot;cardinal&quot;)
-            .x(function(point) { return xScale(point.time); })
-            .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.forEach(function (highlight) { highlight.remove(); });
-        this._highlights = [];
-
-        this._currentTimeSeries = chartData.current.timeSeriesByCommitTime();
-        this._currentTimeSeriesData = this._currentTimeSeries.series();
-        this._baselineTimeSeries = chartData.baseline ? chartData.baseline.timeSeriesByCommitTime() : null;
-        this._targetTimeSeries = chartData.target ? chartData.target.timeSeriesByCommitTime() : null;
-
-        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
-                .append(&quot;path&quot;)
-                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [point.value, largeEnoughValue]}; }))
-                .attr(&quot;class&quot;, &quot;area baseline&quot;));
-        }
-        if (this._targetTimeSeries) {
-            var data = this._targetTimeSeries.series();
-            this._areas.push(this._clippedContainer
-                .append(&quot;path&quot;)
-                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [smallEnoughValue, point.value]}; }))
-                .attr(&quot;class&quot;, &quot;area target&quot;));
-        }
-
-        this._areas.push(this._clippedContainer
-            .append(&quot;path&quot;)
-            .datum(this._currentTimeSeriesData)
-            .attr(&quot;class&quot;, &quot;area&quot;));
-
-        this._paths.push(this._clippedContainer
-            .append(&quot;path&quot;)
-            .datum(this._currentTimeSeriesData)
-            .attr(&quot;class&quot;, &quot;commit-time-line&quot;));
-
-        this._dots.push(this._clippedContainer
-            .selectAll(&quot;.dot&quot;)
-                .data(this._currentTimeSeriesData)
-            .enter().append(&quot;circle&quot;)
-                .attr(&quot;class&quot;, &quot;dot&quot;)
-                .attr(&quot;r&quot;, this.get('chartPointRadius') || 1));
-
-        if (this.get('interactive')) {
-            this._attachEventListener(element, &quot;mousemove&quot;, this._mouseMoved.bind(this));
-            this._attachEventListener(element, &quot;mouseleave&quot;, this._mouseLeft.bind(this));
-            this._attachEventListener(element, &quot;mousedown&quot;, this._mouseDown.bind(this));
-            this._attachEventListener($(element).parents(&quot;[tabindex]&quot;), &quot;keydown&quot;, this._keyPressed.bind(this));
-
-            this._currentItemLine = this._clippedContainer
-                .append(&quot;line&quot;)
-                .attr(&quot;class&quot;, &quot;current-item&quot;);
-
-            this._currentItemCircle = this._clippedContainer
-                .append(&quot;circle&quot;)
-                .attr(&quot;class&quot;, &quot;dot current-item&quot;)
-                .attr(&quot;r&quot;, 3);
-        }
-
-        this._brush = null;
-        if (this.get('enableSelection')) {
-            this._brush = d3.svg.brush()
-                .x(this._x)
-                .on(&quot;brush&quot;, this._brushChanged.bind(this));
-
-            this._brushRect = this._clippedContainer
-                .append(&quot;g&quot;)
-                .attr(&quot;class&quot;, &quot;x brush&quot;);
-        }
-
-        this._updateDomain();
-        this._updateDimensionsIfNeeded();
-
-        // Work around the fact the brush isn't set if we updated it synchronously here.
-        setTimeout(this._selectionChanged.bind(this), 0);
-
-        setTimeout(this._selectedItemChanged.bind(this), 0);
-
-        this._needsConstruction = false;
-
-        this._rangesChanged();
-    },
-    _updateDomain: function ()
-    {
-        var xDomain = this.get('domain');
-        var intrinsicXDomain = this._computeXAxisDomain(this._currentTimeSeriesData);
-        if (!xDomain)
-            xDomain = intrinsicXDomain;
-        var currentDomain = this._x.domain();
-        if (currentDomain &amp;&amp; App.domainsAreEqual(currentDomain, xDomain))
-            return currentDomain;
-
-        var yDomain = this._computeYAxisDomain(xDomain[0], xDomain[1]);
-        this._x.domain(xDomain);
-        this._y.domain(yDomain);
-        return xDomain;
-    },
-    _updateDimensionsIfNeeded: function (newSelection)
-    {
-        var element = $(this.get('element'));
-
-        var newTotalWidth = element.width();
-        var newTotalHeight = element.height();
-        if (this._totalWidth == newTotalWidth &amp;&amp; this._totalHeight == newTotalHeight)
-            return;
-
-        this._totalWidth = newTotalWidth;
-        this._totalHeight = newTotalHeight;
-
-        if (!this._rem)
-            this._rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
-        var rem = this._rem;
-
-        var padding = 0.5 * rem;
-        var margin = {top: padding, right: padding, bottom: padding, left: padding};
-        if (this._xAxis)
-            margin.bottom += rem;
-        if (this._yAxis)
-            margin.left += 3 * rem;
-
-        this._margin = margin;
-        this._contentWidth = this._totalWidth - margin.left - margin.right;
-        this._contentHeight = this._totalHeight - margin.top - margin.bottom;
-
-        this._svg.attr(&quot;transform&quot;, &quot;translate(&quot; + margin.left + &quot;,&quot; + margin.top + &quot;)&quot;);
-
-        this._clipPath
-            .attr(&quot;width&quot;, this._contentWidth)
-            .attr(&quot;height&quot;, this._contentHeight);
-
-        this._x.range([0, this._contentWidth]);
-        this._y.range([this._contentHeight, 0]);
-
-        if (this._xAxis) {
-            this._xAxis.tickSize(-this._contentHeight);
-            this._xAxisLabels.attr(&quot;transform&quot;, &quot;translate(0,&quot; + this._contentHeight + &quot;)&quot;);
-        }
-
-        if (this._yAxis)
-            this._yAxis.tickSize(-this._contentWidth);
-
-        if (this._currentItemLine) {
-            this._currentItemLine
-                .attr(&quot;y1&quot;, 0)
-                .attr(&quot;y2&quot;, margin.top + this._contentHeight);
-        }
-
-        this._relayoutDataAndAxes(this._currentSelection());
-    },
-    _updateBrush: function ()
-    {
-        this._brushRect
-            .call(this._brush)
-        .selectAll(&quot;rect&quot;)
-            .attr(&quot;y&quot;, 1)
-            .attr(&quot;height&quot;, this._contentHeight - 2);
-        this._updateSelectionToolbar();
-    },
-    _relayoutDataAndAxes: function (selection)
-    {
-        var timeline = this._timeLine;
-        this._paths.forEach(function (path) { path.attr(&quot;d&quot;, timeline); });
-
-        var confidenceArea = this._confidenceArea;
-        this._areas.forEach(function (path) { path.attr(&quot;d&quot;, confidenceArea); });
-
-        var xScale = this._x;
-        var yScale = this._y;
-        this._dots.forEach(function (dot) {
-            dot
-                .attr(&quot;cx&quot;, function(measurement) { return xScale(measurement.time); })
-                .attr(&quot;cy&quot;, function(measurement) { return yScale(measurement.value); });
-        });
-        this._updateMarkedDots();
-        this._updateHighlightPositions();
-        this._updateRangeBarRects();
-
-        if (this._brush) {
-            if (selection)
-                this._brush.extent(selection);
-            else
-                this._brush.clear();
-            this._updateBrush();
-        }
-
-        this._updateCurrentItemIndicators();
-
-        if (this._xAxis)
-            this._xAxisLabels.call(this._xAxis);
-        if (!this._yAxis)
-            return;
-
-        this._yAxisLabels.call(this._yAxis);
-        if (this._yAxisUnitContainer)
-            this._yAxisUnitContainer.remove();
-        this._yAxisUnitContainer = this._yAxisLabels.append(&quot;text&quot;)
-            .attr(&quot;x&quot;, 0.5 * this._rem)
-            .attr(&quot;y&quot;, 0.2 * this._rem)
-            .attr(&quot;dy&quot;, 0.8 * this._rem)
-            .style(&quot;text-anchor&quot;, &quot;start&quot;)
-            .style(&quot;z-index&quot;, &quot;100&quot;)
-            .text(this._yAxisUnit);
-    },
-    _updateMarkedDots: function () {
-        var markedPoints = this.get('markedPoints') || {};
-        var defaultDotRadius = this.get('chartPointRadius') || 1;
-        this._dots.forEach(function (dot) {
-            dot.classed('marked', function (point) { return markedPoints[point.measurement.id()]; });
-            dot.attr('r', function (point) {
-                return markedPoints[point.measurement.id()] ? defaultDotRadius * 1.5 : defaultDotRadius; });
-        });
-    }.observes('markedPoints'),
-    _updateHighlightPositions: function () {
-        var xScale = this._x;
-        var yScale = this._y;
-        var y2 = this._margin.top + this._contentHeight;
-        this._highlights.forEach(function (highlight) {
-            highlight
-                .attr(&quot;y1&quot;, 0)
-                .attr(&quot;y2&quot;, y2)
-                .attr(&quot;y&quot;, function(measurement) { return yScale(measurement.value); })
-                .attr(&quot;x1&quot;, function(measurement) { return xScale(measurement.time); })
-                .attr(&quot;x2&quot;, function(measurement) { return xScale(measurement.time); });
-        });
-    },
-    _computeXAxisDomain: function (timeSeries)
-    {
-        var extent = d3.extent(timeSeries, function(point) { return point.time; });
-        var margin = 3600 * 1000; // Use x.inverse to compute the right amount from a margin.
-        return [+extent[0] - margin, +extent[1] + margin];
-    },
-    _computeYAxisDomain: function (startTime, endTime)
-    {
-        var range = this._minMaxForAllTimeSeries(startTime, endTime);
-        var min = range[0];
-        var max = range[1];
-        if (max &lt; min)
-            min = max = 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)
-    {
-        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]),
-        ];
-    },
-    _currentSelection: function ()
-    {
-        return this._brush &amp;&amp; !this._brush.empty() ? this._brush.extent() : null;
-    },
-    _domainChanged: function ()
-    {
-        var selection = this._currentSelection() || this.get('sharedSelection');
-        var newXDomain = this._updateDomain();
-
-        if (selection &amp;&amp; newXDomain &amp;&amp; selection[0] &lt;= newXDomain[0] &amp;&amp; newXDomain[1] &lt;= selection[1])
-            selection = null; // Otherwise the user has no way of clearing the selection.
-
-        this._relayoutDataAndAxes(selection);
-    }.observes('domain'),
-    _selectionChanged: function ()
-    {
-        this._updateSelection(this.get('selection'));
-    }.observes('selection'),
-    _updateSelection: function (newSelection)
-    {
-        if (!this._brush)
-            return;
-
-        var currentSelection = this._currentSelection();
-        if (newSelection &amp;&amp; currentSelection &amp;&amp; App.domainsAreEqual(newSelection, currentSelection))
-            return;
-
-        var domain = this._x.domain();
-        if (!newSelection || App.domainsAreEqual(domain, newSelection))
-            this._brush.clear();
-        else
-            this._brush.extent(newSelection);
-        this._updateBrush();
-
-        this._setCurrentSelection(newSelection);
-    },
-    _brushChanged: function ()
-    {
-        if (this._brush.empty()) {
-            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.
-            this._brushJustChanged = true;
-            var self = this;
-            setTimeout(function () {
-                self._brushJustChanged = false;
-            }, 0);
-
-            return;
-        }
-
-        this.set('selectionIsLocked', true);
-        this._setCurrentSelection(this._brush.extent());
-    },
-    _keyPressed: function (event)
-    {
-        if (!this._currentItemIndex || this._currentSelection())
-            return;
-
-        var newIndex;
-        switch (event.keyCode) {
-        case 37: // Left
-            newIndex = this._currentItemIndex - 1;
-            break;
-        case 39: // Right
-            newIndex = this._currentItemIndex + 1;
-            break;
-        case 38: // Up
-        case 40: // Down
-        default:
-            return;
-        }
-
-        // Unlike mousemove, keydown shouldn't move off the edge.
-        if (this._currentTimeSeriesData[newIndex])
-            this._setCurrentItem(newIndex);
-    },
-    _mouseMoved: function (event)
-    {
-        if (!this._margin || this._currentSelection() || this._currentItemLocked)
-            return;
-
-        var point = this._mousePointInGraph(event);
-
-        this._selectClosestPointToMouseAsCurrentItem(point);
-    },
-    _mouseLeft: function (event)
-    {
-        if (!this._margin || this._currentItemLocked)
-            return;
-
-        this._selectClosestPointToMouseAsCurrentItem(null);
-    },
-    _mouseDown: function (event)
-    {
-        if (!this._margin || this._currentSelection() || this._brushJustChanged)
-            return;
-
-        var point = this._mousePointInGraph(event);
-        if (!point)
-            return;
-
-        if (this._currentItemLocked) {
-            this._currentItemLocked = false;
-            this.set('selectedItem', null);
-            return;
-        }
-
-        this._currentItemLocked = true;
-        this._selectClosestPointToMouseAsCurrentItem(point);
-    },
-    _mousePointInGraph: function (event)
-    {
-        var offset = $(this.get('element')).offset();
-        if (!offset || !$(event.target).closest('svg').length)
-            return null;
-
-        var point = {
-            x: event.pageX - offset.left - this._margin.left,
-            y: event.pageY - offset.top - this._margin.top
-        };
-
-        var xScale = this._x;
-        var yScale = this._y;
-        var xDomain = xScale.domain();
-        var yDomain = yScale.domain();
-        if (point.x &gt;= xScale(xDomain[0]) &amp;&amp; point.x &lt;= xScale(xDomain[1])
-            &amp;&amp; point.y &lt;= yScale(yDomain[0]) &amp;&amp; point.y &gt;= yScale(yDomain[1]))
-            return point;
-
-        return null;
-    },
-    _selectClosestPointToMouseAsCurrentItem: function (point)
-    {
-        var xScale = this._x;
-        var yScale = this._y;
-        var distanceHeuristics = function (m) {
-            var mX = xScale(m.time);
-            var mY = yScale(m.value);
-            var xDiff = mX - point.x;
-            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 &amp;&amp; !this._currentSelection()) {
-            var distances = this._currentTimeSeriesData.map(distanceHeuristics);
-            var minDistance = Number.MAX_VALUE;
-            for (var i = 0; i &lt; distances.length; i++) {
-                if (distances[i] &lt; minDistance) {
-                    newItemIndex = i;
-                    minDistance = distances[i];
-                }
-            }
-        }
-
-        this._setCurrentItem(newItemIndex);
-        this._updateSelectionToolbar();
-    },
-    _currentTimeChanged: function ()
-    {
-        if (!this._margin || this._currentSelection() || this._currentItemLocked)
-            return
-
-        var currentTime = this.get('currentTime');
-        if (currentTime) {
-            for (var i = 0; i &lt; this._currentTimeSeriesData.length; i++) {
-                var point = this._currentTimeSeriesData[i];
-                if (point.time &gt;= currentTime) {
-                    this._setCurrentItem(i, /* doNotNotify */ true);
-                    return;
-                }
-            }
-        }
-        this._setCurrentItem(undefined, /* doNotNotify */ true);
-    }.observes('currentTime'),
-    _setCurrentItem: function (newItemIndex, doNotNotify)
-    {
-        if (newItemIndex === this._currentItemIndex) {
-            if (this._currentItemLocked)
-                this.set('selectedItem', this.get('currentItem') ? this.get('currentItem').measurement.id() : null);
-            return;
-        }
-
-        var newItem = this._currentTimeSeriesData[newItemIndex];
-        this._brushExtent = undefined;
-        this._currentItemIndex = newItemIndex;
-
-        if (!newItem) {
-            this._currentItemLocked = false;
-            this.set('selectedItem', null);
-        }
-
-        this._updateCurrentItemIndicators();
-
-        if (!doNotNotify)
-            this.set('currentTime', newItem ? newItem.time : undefined);
-
-        this.set('currentItem', newItem);
-        if (this._currentItemLocked)
-            this.set('selectedItem', newItem ? newItem.measurement.id() : null);
-    },
-    _selectedItemChanged: function ()
-    {
-        if (!this._margin)
-            return;
-
-        var selectedId = this.get('selectedItem');
-        var currentItem = this.get('currentItem');
-        if (currentItem &amp;&amp; currentItem.measurement.id() == selectedId)
-            return;
-
-        var series = this._currentTimeSeriesData;
-        var selectedItemIndex = undefined;
-        for (var i = 0; i &lt; series.length; i++) {
-            if (series[i].measurement.id() == selectedId) {
-                this._updateSelection(null);
-                this._currentItemLocked = true;
-                this._setCurrentItem(i);
-                this._updateSelectionToolbar();
-                return;
-            }
-        }
-    }.observes('selectedItem').on('init'),
-    _highlightedItemsChanged: function () {
-        if (!this._margin)
-            return;
-
-        var highlightedItems = this.get('highlightedItems');
-
-        var data = this._currentTimeSeriesData.filter(function (item) { return highlightedItems[item.measurement.id()]; });
-
-        if (this._highlights.length)
-            this._highlights.forEach(function (highlight) { highlight.remove(); });
-
-        this._highlights.push(this._clippedContainer
-            .selectAll(&quot;.highlight&quot;)
-                .data(data)
-            .enter().append(&quot;line&quot;)
-                .attr(&quot;class&quot;, &quot;highlight&quot;));
-
-        this._updateHighlightPositions();
-
-    }.observes('highlightedItems'),
-    _rangesChanged: function ()
-    {
-        if (!this._currentTimeSeries)
-            return;
-
-        function midPoint(firstPoint, secondPoint) {
-            if (firstPoint &amp;&amp; secondPoint)
-                return (+firstPoint.time + +secondPoint.time) / 2;
-            if (firstPoint)
-                return firstPoint.time;
-            return secondPoint.time;
-        }
-        var currentTimeSeries = this._currentTimeSeries;
-        var linkRoute = this.get('rangeRoute');
-        this.set('rangeBars', (this.get('ranges') || []).map(function (range) {
-            var start = currentTimeSeries.findPointByMeasurementId(range.get('startRun'));
-            var end = currentTimeSeries.findPointByMeasurementId(range.get('endRun'));
-            return Ember.Object.create({
-                startTime: midPoint(currentTimeSeries.previousPoint(start), start),
-                endTime: midPoint(end, currentTimeSeries.nextPoint(end)),
-                range: range,
-                left: null,
-                right: null,
-                rowIndex: null,
-                top: null,
-                bottom: null,
-                linkRoute: linkRoute,
-                linkId: range.get('id'),
-                label: range.get('label'),
-            });
-        }));
-
-        this._updateRangeBarRects();
-    }.observes('ranges'),
-    _updateRangeBarRects: function () {
-        var rangeBars = this.get('rangeBars');
-        if (!rangeBars || !rangeBars.length)
-            return;
-
-        var xScale = this._x;
-        var yScale = this._y;
-
-        // Expand the width of each range as needed and sort ranges by the left-edge of ranges.
-        var minWidth = 3;
-        var sortedBars = rangeBars.map(function (bar) {
-            var left = xScale(bar.get('startTime'));
-            var right = xScale(bar.get('endTime'));
-            if (right - left &lt; minWidth) {
-                left -= minWidth / 2;
-                right += minWidth / 2;
-            }
-            bar.set('left', left);
-            bar.set('right', right);
-            return bar;
-        }).sort(function (first, second) { return first.get('left') - second.get('left'); });
-
-        // At this point, left edges of all ranges prior to a range R1 is on the left of R1.
-        // Place R1 into a row in which right edges of all ranges prior to R1 is on the left of R1 to avoid overlapping ranges.
-        var rows = [];
-        sortedBars.forEach(function (bar) {
-            var rowIndex = 0;
-            for (; rowIndex &lt; rows.length; rowIndex++) {
-                var currentRow = rows[rowIndex];
-                if (currentRow[currentRow.length - 1].get('right') &lt; bar.get('left')) {
-                    currentRow.push(bar);
-                    break;
-                }
-            }
-            if (rowIndex &gt;= rows.length)
-                rows.push([bar]);
-            bar.set('rowIndex', rowIndex);
-        });
-        var rowHeight = 0.6 * this._rem;
-        var firstRowTop = this._contentHeight - rows.length * rowHeight;
-        var barHeight = 0.5 * this._rem;
-
-        $(this.get('element')).find('.rangeBarsContainerInlineStyle').css({
-            left: this._margin.left + 'px',
-            top: this._margin.top + firstRowTop + 'px',
-            width: this._contentWidth + 'px',
-            height: rows.length * barHeight + 'px',
-            overflow: 'hidden',
-            position: 'absolute',
-        });
-
-        var margin = this._margin;
-        sortedBars.forEach(function (bar) {
-            var top = bar.get('rowIndex') * rowHeight;
-            var height = barHeight;
-            var left = bar.get('left');
-            var width = bar.get('right') - left;
-            bar.set('inlineStyle', 'left: ' + left + 'px; top: ' + top + 'px; width: ' + width + 'px; height: ' + height + 'px;');
-        });
-    },
-    _updateCurrentItemIndicators: function ()
-    {
-        if (!this._currentItemLine)
-            return;
-
-        var item = this._currentTimeSeriesData[this._currentItemIndex];
-        if (!item) {
-            this._currentItemLine.attr(&quot;x1&quot;, -1000).attr(&quot;x2&quot;, -1000);
-            this._currentItemCircle.attr(&quot;cx&quot;, -1000);
-            return;
-        }
-
-        var x = this._x(item.time);
-        var y = this._y(item.value);
-
-        this._currentItemLine
-            .attr(&quot;x1&quot;, x)
-            .attr(&quot;x2&quot;, x);
-
-        this._currentItemCircle
-            .attr(&quot;cx&quot;, x)
-            .attr(&quot;cy&quot;, y);
-    },
-    _setCurrentSelection: function (newSelection)
-    {
-        if (this._brushExtent === newSelection)
-            return;
-
-        var points = null;
-        if (newSelection) {
-            points = this._currentTimeSeriesData
-                .filter(function (point) { return point.time &gt;= newSelection[0] &amp;&amp; point.time &lt;= newSelection[1]; });
-            if (!points.length)
-                points = null;
-        }
-
-        this._brushExtent = newSelection;
-        this._setCurrentItem(undefined);
-        this._updateSelectionToolbar();
-
-        if (!App.domainsAreEqual(this.get('selection'), newSelection))
-            this.set('selection', newSelection);
-        this.set('selectedPoints', points);
-    },
-    _updateSelectionToolbar: function ()
-    {
-        if (!this.get('interactive'))
-            return;
-
-        var selection = this._currentSelection();
-        var selectionToolbar = $(this.get('element')).children('.selection-toolbar');
-        if (selection) {
-            var left = this._x(selection[0]);
-            var right = this._x(selection[1]);
-            selectionToolbar
-                .css({left: this._margin.left + right, top: this._margin.top + this._contentHeight})
-                .show();
-        } else
-            selectionToolbar.hide();
-    },
-    actions: {
-        zoom: function ()
-        {
-            this.sendAction('zoom', this._currentSelection());
-            this.set('selection', null);
-        },
-        openRange: function (range)
-        {
-            this.sendAction('openRange', range);
-        },
-    },
-});
-
-
-
-App.CommitsViewerComponent = Ember.Component.extend({
-    repository: null,
-    revisionInfo: null,
-    commits: null,
-    commitsChanged: function ()
-    {
-        var revisionInfo = this.get('revisionInfo');
-
-        var to = revisionInfo.get('currentRevision');
-        var from = revisionInfo.get('previousRevision');
-        var repository = this.get('repository');
-        if (!from || !repository || !repository.get('hasReportedCommits'))
-            return;
-
-        var self = this;
-        CommitLogs.fetchForTimeRange(repository.get('id'), from, to).then(function (commits) {
-            if (self.isDestroyed)
-                return;
-            self.set('commits', commits.map(function (commit) {
-                return Ember.Object.create({
-                    repository: repository,
-                    revision: commit.revision,
-                    url: repository.urlForRevision(commit.revision),
-                    author: commit.authorName || commit.authorEmail,
-                    message: commit.message ? commit.message.substr(0, 75) : null,
-                });
-            }));
-        }, function () {
-            if (!self.isDestroyed)
-                self.set('commits', []);
-        })
-    }.observes('repository').observes('revisionInfo').on('init'),
-});
-
-
</del><span class="cx"> App.AnalysisRoute = Ember.Route.extend({
</span><span class="cx">     model: function () {
</span><span class="cx">         return this.store.findAll('analysisTask').then(function (tasks) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2commitsviewerjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v2/commits-viewer.js (0 => 177424)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/commits-viewer.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v2/commits-viewer.js        2014-12-17 02:29:22 UTC (rev 177424)
</span><span class="lines">@@ -0,0 +1,33 @@
</span><ins>+App.CommitsViewerComponent = Ember.Component.extend({
+    repository: null,
+    revisionInfo: null,
+    commits: null,
+    commitsChanged: function ()
+    {
+        var revisionInfo = this.get('revisionInfo');
+
+        var to = revisionInfo.get('currentRevision');
+        var from = revisionInfo.get('previousRevision');
+        var repository = this.get('repository');
+        if (!from || !repository || !repository.get('hasReportedCommits'))
+            return;
+
+        var self = this;
+        CommitLogs.fetchForTimeRange(repository.get('id'), from, to).then(function (commits) {
+            if (self.isDestroyed)
+                return;
+            self.set('commits', commits.map(function (commit) {
+                return Ember.Object.create({
+                    repository: repository,
+                    revision: commit.revision,
+                    url: repository.urlForRevision(commit.revision),
+                    author: commit.authorName || commit.authorEmail,
+                    message: commit.message ? commit.message.substr(0, 75) : null,
+                });
+            }));
+        }, function () {
+            if (!self.isDestroyed)
+                self.set('commits', []);
+        })
+    }.observes('repository').observes('revisionInfo').on('init'),
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2interactivechartjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js (0 => 177424)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js        2014-12-17 02:29:22 UTC (rev 177424)
</span><span class="lines">@@ -0,0 +1,775 @@
</span><ins>+App.InteractiveChartComponent = Ember.Component.extend({
+    chartData: null,
+    showXAxis: true,
+    showYAxis: true,
+    interactive: false,
+    enableSelection: true,
+    classNames: ['chart'],
+    init: function ()
+    {
+        this._super();
+        this._needsConstruction = true;
+        this._eventHandlers = [];
+        $(window).resize(this._updateDimensionsIfNeeded.bind(this));
+    },
+    chartDataDidChange: function ()
+    {
+        var chartData = this.get('chartData');
+        if (!chartData)
+            return;
+        this._needsConstruction = true;
+        this._constructGraphIfPossible(chartData);
+    }.observes('chartData').on('init'),
+    didInsertElement: function ()
+    {
+        var chartData = this.get('chartData');
+        if (chartData)
+            this._constructGraphIfPossible(chartData);
+    },
+    willClearRender: function ()
+    {
+        this._eventHandlers.forEach(function (item) {
+            $(item[0]).off(item[1], item[2]);
+        })
+    },
+    _attachEventListener: function(target, eventName, listener)
+    {
+        this._eventHandlers.push([target, eventName, listener]);
+        $(target).on(eventName, listener);
+    },
+    _constructGraphIfPossible: function (chartData)
+    {
+        if (!this._needsConstruction || !this.get('element'))
+            return;
+
+        var element = this.get('element');
+
+        this._x = d3.time.scale();
+        this._y = d3.scale.linear();
+
+        // FIXME: Tear down the old SVG element.
+        this._svgElement = d3.select(element).append(&quot;svg&quot;)
+                .attr(&quot;width&quot;, &quot;100%&quot;)
+                .attr(&quot;height&quot;, &quot;100%&quot;);
+
+        var svg = this._svg = this._svgElement.append(&quot;g&quot;);
+
+        var clipId = element.id + &quot;-clip&quot;;
+        this._clipPath = svg.append(&quot;defs&quot;).append(&quot;clipPath&quot;)
+            .attr(&quot;id&quot;, clipId)
+            .append(&quot;rect&quot;);
+
+        if (this.get('showXAxis')) {
+            this._xAxis = d3.svg.axis().scale(this._x).orient(&quot;bottom&quot;).ticks(6);
+            this._xAxisLabels = svg.append(&quot;g&quot;)
+                .attr(&quot;class&quot;, &quot;x axis&quot;);
+        }
+
+        if (this.get('showYAxis')) {
+            this._yAxis = d3.svg.axis().scale(this._y).orient(&quot;left&quot;).ticks(6).tickFormat(d3.format(&quot;s&quot;));
+            this._yAxisLabels = svg.append(&quot;g&quot;)
+                .attr(&quot;class&quot;, &quot;y axis&quot;);
+        }
+
+        this._clippedContainer = svg.append(&quot;g&quot;)
+            .attr(&quot;clip-path&quot;, &quot;url(#&quot; + clipId + &quot;)&quot;);
+
+        var xScale = this._x;
+        var yScale = this._y;
+        this._timeLine = d3.svg.line()
+            .x(function(point) { return xScale(point.time); })
+            .y(function(point) { return yScale(point.value); });
+
+        this._confidenceArea = d3.svg.area()
+//            .interpolate(&quot;cardinal&quot;)
+            .x(function(point) { return xScale(point.time); })
+            .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.forEach(function (highlight) { highlight.remove(); });
+        this._highlights = [];
+
+        this._currentTimeSeries = chartData.current.timeSeriesByCommitTime();
+        this._currentTimeSeriesData = this._currentTimeSeries.series();
+        this._baselineTimeSeries = chartData.baseline ? chartData.baseline.timeSeriesByCommitTime() : null;
+        this._targetTimeSeries = chartData.target ? chartData.target.timeSeriesByCommitTime() : null;
+
+        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
+                .append(&quot;path&quot;)
+                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [point.value, largeEnoughValue]}; }))
+                .attr(&quot;class&quot;, &quot;area baseline&quot;));
+        }
+        if (this._targetTimeSeries) {
+            var data = this._targetTimeSeries.series();
+            this._areas.push(this._clippedContainer
+                .append(&quot;path&quot;)
+                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [smallEnoughValue, point.value]}; }))
+                .attr(&quot;class&quot;, &quot;area target&quot;));
+        }
+
+        this._areas.push(this._clippedContainer
+            .append(&quot;path&quot;)
+            .datum(this._currentTimeSeriesData)
+            .attr(&quot;class&quot;, &quot;area&quot;));
+
+        this._paths.push(this._clippedContainer
+            .append(&quot;path&quot;)
+            .datum(this._currentTimeSeriesData)
+            .attr(&quot;class&quot;, &quot;commit-time-line&quot;));
+
+        this._dots.push(this._clippedContainer
+            .selectAll(&quot;.dot&quot;)
+                .data(this._currentTimeSeriesData)
+            .enter().append(&quot;circle&quot;)
+                .attr(&quot;class&quot;, &quot;dot&quot;)
+                .attr(&quot;r&quot;, this.get('chartPointRadius') || 1));
+
+        if (this.get('interactive')) {
+            this._attachEventListener(element, &quot;mousemove&quot;, this._mouseMoved.bind(this));
+            this._attachEventListener(element, &quot;mouseleave&quot;, this._mouseLeft.bind(this));
+            this._attachEventListener(element, &quot;mousedown&quot;, this._mouseDown.bind(this));
+            this._attachEventListener($(element).parents(&quot;[tabindex]&quot;), &quot;keydown&quot;, this._keyPressed.bind(this));
+
+            this._currentItemLine = this._clippedContainer
+                .append(&quot;line&quot;)
+                .attr(&quot;class&quot;, &quot;current-item&quot;);
+
+            this._currentItemCircle = this._clippedContainer
+                .append(&quot;circle&quot;)
+                .attr(&quot;class&quot;, &quot;dot current-item&quot;)
+                .attr(&quot;r&quot;, 3);
+        }
+
+        this._brush = null;
+        if (this.get('enableSelection')) {
+            this._brush = d3.svg.brush()
+                .x(this._x)
+                .on(&quot;brush&quot;, this._brushChanged.bind(this));
+
+            this._brushRect = this._clippedContainer
+                .append(&quot;g&quot;)
+                .attr(&quot;class&quot;, &quot;x brush&quot;);
+        }
+
+        this._updateDomain();
+        this._updateDimensionsIfNeeded();
+
+        // Work around the fact the brush isn't set if we updated it synchronously here.
+        setTimeout(this._selectionChanged.bind(this), 0);
+
+        setTimeout(this._selectedItemChanged.bind(this), 0);
+
+        this._needsConstruction = false;
+
+        this._rangesChanged();
+    },
+    _updateDomain: function ()
+    {
+        var xDomain = this.get('domain');
+        var intrinsicXDomain = this._computeXAxisDomain(this._currentTimeSeriesData);
+        if (!xDomain)
+            xDomain = intrinsicXDomain;
+        var currentDomain = this._x.domain();
+        if (currentDomain &amp;&amp; App.domainsAreEqual(currentDomain, xDomain))
+            return currentDomain;
+
+        var yDomain = this._computeYAxisDomain(xDomain[0], xDomain[1]);
+        this._x.domain(xDomain);
+        this._y.domain(yDomain);
+        return xDomain;
+    },
+    _updateDimensionsIfNeeded: function (newSelection)
+    {
+        var element = $(this.get('element'));
+
+        var newTotalWidth = element.width();
+        var newTotalHeight = element.height();
+        if (this._totalWidth == newTotalWidth &amp;&amp; this._totalHeight == newTotalHeight)
+            return;
+
+        this._totalWidth = newTotalWidth;
+        this._totalHeight = newTotalHeight;
+
+        if (!this._rem)
+            this._rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
+        var rem = this._rem;
+
+        var padding = 0.5 * rem;
+        var margin = {top: padding, right: padding, bottom: padding, left: padding};
+        if (this._xAxis)
+            margin.bottom += rem;
+        if (this._yAxis)
+            margin.left += 3 * rem;
+
+        this._margin = margin;
+        this._contentWidth = this._totalWidth - margin.left - margin.right;
+        this._contentHeight = this._totalHeight - margin.top - margin.bottom;
+
+        this._svg.attr(&quot;transform&quot;, &quot;translate(&quot; + margin.left + &quot;,&quot; + margin.top + &quot;)&quot;);
+
+        this._clipPath
+            .attr(&quot;width&quot;, this._contentWidth)
+            .attr(&quot;height&quot;, this._contentHeight);
+
+        this._x.range([0, this._contentWidth]);
+        this._y.range([this._contentHeight, 0]);
+
+        if (this._xAxis) {
+            this._xAxis.tickSize(-this._contentHeight);
+            this._xAxisLabels.attr(&quot;transform&quot;, &quot;translate(0,&quot; + this._contentHeight + &quot;)&quot;);
+        }
+
+        if (this._yAxis)
+            this._yAxis.tickSize(-this._contentWidth);
+
+        if (this._currentItemLine) {
+            this._currentItemLine
+                .attr(&quot;y1&quot;, 0)
+                .attr(&quot;y2&quot;, margin.top + this._contentHeight);
+        }
+
+        this._relayoutDataAndAxes(this._currentSelection());
+    },
+    _updateBrush: function ()
+    {
+        this._brushRect
+            .call(this._brush)
+        .selectAll(&quot;rect&quot;)
+            .attr(&quot;y&quot;, 1)
+            .attr(&quot;height&quot;, this._contentHeight - 2);
+        this._updateSelectionToolbar();
+    },
+    _relayoutDataAndAxes: function (selection)
+    {
+        var timeline = this._timeLine;
+        this._paths.forEach(function (path) { path.attr(&quot;d&quot;, timeline); });
+
+        var confidenceArea = this._confidenceArea;
+        this._areas.forEach(function (path) { path.attr(&quot;d&quot;, confidenceArea); });
+
+        var xScale = this._x;
+        var yScale = this._y;
+        this._dots.forEach(function (dot) {
+            dot
+                .attr(&quot;cx&quot;, function(measurement) { return xScale(measurement.time); })
+                .attr(&quot;cy&quot;, function(measurement) { return yScale(measurement.value); });
+        });
+        this._updateMarkedDots();
+        this._updateHighlightPositions();
+        this._updateRangeBarRects();
+
+        if (this._brush) {
+            if (selection)
+                this._brush.extent(selection);
+            else
+                this._brush.clear();
+            this._updateBrush();
+        }
+
+        this._updateCurrentItemIndicators();
+
+        if (this._xAxis)
+            this._xAxisLabels.call(this._xAxis);
+        if (!this._yAxis)
+            return;
+
+        this._yAxisLabels.call(this._yAxis);
+        if (this._yAxisUnitContainer)
+            this._yAxisUnitContainer.remove();
+        this._yAxisUnitContainer = this._yAxisLabels.append(&quot;text&quot;)
+            .attr(&quot;x&quot;, 0.5 * this._rem)
+            .attr(&quot;y&quot;, 0.2 * this._rem)
+            .attr(&quot;dy&quot;, 0.8 * this._rem)
+            .style(&quot;text-anchor&quot;, &quot;start&quot;)
+            .style(&quot;z-index&quot;, &quot;100&quot;)
+            .text(this._yAxisUnit);
+    },
+    _updateMarkedDots: function () {
+        var markedPoints = this.get('markedPoints') || {};
+        var defaultDotRadius = this.get('chartPointRadius') || 1;
+        this._dots.forEach(function (dot) {
+            dot.classed('marked', function (point) { return markedPoints[point.measurement.id()]; });
+            dot.attr('r', function (point) {
+                return markedPoints[point.measurement.id()] ? defaultDotRadius * 1.5 : defaultDotRadius; });
+        });
+    }.observes('markedPoints'),
+    _updateHighlightPositions: function () {
+        var xScale = this._x;
+        var yScale = this._y;
+        var y2 = this._margin.top + this._contentHeight;
+        this._highlights.forEach(function (highlight) {
+            highlight
+                .attr(&quot;y1&quot;, 0)
+                .attr(&quot;y2&quot;, y2)
+                .attr(&quot;y&quot;, function(measurement) { return yScale(measurement.value); })
+                .attr(&quot;x1&quot;, function(measurement) { return xScale(measurement.time); })
+                .attr(&quot;x2&quot;, function(measurement) { return xScale(measurement.time); });
+        });
+    },
+    _computeXAxisDomain: function (timeSeries)
+    {
+        var extent = d3.extent(timeSeries, function(point) { return point.time; });
+        var margin = 3600 * 1000; // Use x.inverse to compute the right amount from a margin.
+        return [+extent[0] - margin, +extent[1] + margin];
+    },
+    _computeYAxisDomain: function (startTime, endTime)
+    {
+        var range = this._minMaxForAllTimeSeries(startTime, endTime);
+        var min = range[0];
+        var max = range[1];
+        if (max &lt; min)
+            min = max = 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)
+    {
+        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]),
+        ];
+    },
+    _currentSelection: function ()
+    {
+        return this._brush &amp;&amp; !this._brush.empty() ? this._brush.extent() : null;
+    },
+    _domainChanged: function ()
+    {
+        var selection = this._currentSelection() || this.get('sharedSelection');
+        var newXDomain = this._updateDomain();
+
+        if (selection &amp;&amp; newXDomain &amp;&amp; selection[0] &lt;= newXDomain[0] &amp;&amp; newXDomain[1] &lt;= selection[1])
+            selection = null; // Otherwise the user has no way of clearing the selection.
+
+        this._relayoutDataAndAxes(selection);
+    }.observes('domain'),
+    _selectionChanged: function ()
+    {
+        this._updateSelection(this.get('selection'));
+    }.observes('selection'),
+    _updateSelection: function (newSelection)
+    {
+        if (!this._brush)
+            return;
+
+        var currentSelection = this._currentSelection();
+        if (newSelection &amp;&amp; currentSelection &amp;&amp; App.domainsAreEqual(newSelection, currentSelection))
+            return;
+
+        var domain = this._x.domain();
+        if (!newSelection || App.domainsAreEqual(domain, newSelection))
+            this._brush.clear();
+        else
+            this._brush.extent(newSelection);
+        this._updateBrush();
+
+        this._setCurrentSelection(newSelection);
+    },
+    _brushChanged: function ()
+    {
+        if (this._brush.empty()) {
+            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.
+            this._brushJustChanged = true;
+            var self = this;
+            setTimeout(function () {
+                self._brushJustChanged = false;
+            }, 0);
+
+            return;
+        }
+
+        this.set('selectionIsLocked', true);
+        this._setCurrentSelection(this._brush.extent());
+    },
+    _keyPressed: function (event)
+    {
+        if (!this._currentItemIndex || this._currentSelection())
+            return;
+
+        var newIndex;
+        switch (event.keyCode) {
+        case 37: // Left
+            newIndex = this._currentItemIndex - 1;
+            break;
+        case 39: // Right
+            newIndex = this._currentItemIndex + 1;
+            break;
+        case 38: // Up
+        case 40: // Down
+        default:
+            return;
+        }
+
+        // Unlike mousemove, keydown shouldn't move off the edge.
+        if (this._currentTimeSeriesData[newIndex])
+            this._setCurrentItem(newIndex);
+    },
+    _mouseMoved: function (event)
+    {
+        if (!this._margin || this._currentSelection() || this._currentItemLocked)
+            return;
+
+        var point = this._mousePointInGraph(event);
+
+        this._selectClosestPointToMouseAsCurrentItem(point);
+    },
+    _mouseLeft: function (event)
+    {
+        if (!this._margin || this._currentItemLocked)
+            return;
+
+        this._selectClosestPointToMouseAsCurrentItem(null);
+    },
+    _mouseDown: function (event)
+    {
+        if (!this._margin || this._currentSelection() || this._brushJustChanged)
+            return;
+
+        var point = this._mousePointInGraph(event);
+        if (!point)
+            return;
+
+        if (this._currentItemLocked) {
+            this._currentItemLocked = false;
+            this.set('selectedItem', null);
+            return;
+        }
+
+        this._currentItemLocked = true;
+        this._selectClosestPointToMouseAsCurrentItem(point);
+    },
+    _mousePointInGraph: function (event)
+    {
+        var offset = $(this.get('element')).offset();
+        if (!offset || !$(event.target).closest('svg').length)
+            return null;
+
+        var point = {
+            x: event.pageX - offset.left - this._margin.left,
+            y: event.pageY - offset.top - this._margin.top
+        };
+
+        var xScale = this._x;
+        var yScale = this._y;
+        var xDomain = xScale.domain();
+        var yDomain = yScale.domain();
+        if (point.x &gt;= xScale(xDomain[0]) &amp;&amp; point.x &lt;= xScale(xDomain[1])
+            &amp;&amp; point.y &lt;= yScale(yDomain[0]) &amp;&amp; point.y &gt;= yScale(yDomain[1]))
+            return point;
+
+        return null;
+    },
+    _selectClosestPointToMouseAsCurrentItem: function (point)
+    {
+        var xScale = this._x;
+        var yScale = this._y;
+        var distanceHeuristics = function (m) {
+            var mX = xScale(m.time);
+            var mY = yScale(m.value);
+            var xDiff = mX - point.x;
+            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 &amp;&amp; !this._currentSelection()) {
+            var distances = this._currentTimeSeriesData.map(distanceHeuristics);
+            var minDistance = Number.MAX_VALUE;
+            for (var i = 0; i &lt; distances.length; i++) {
+                if (distances[i] &lt; minDistance) {
+                    newItemIndex = i;
+                    minDistance = distances[i];
+                }
+            }
+        }
+
+        this._setCurrentItem(newItemIndex);
+        this._updateSelectionToolbar();
+    },
+    _currentTimeChanged: function ()
+    {
+        if (!this._margin || this._currentSelection() || this._currentItemLocked)
+            return
+
+        var currentTime = this.get('currentTime');
+        if (currentTime) {
+            for (var i = 0; i &lt; this._currentTimeSeriesData.length; i++) {
+                var point = this._currentTimeSeriesData[i];
+                if (point.time &gt;= currentTime) {
+                    this._setCurrentItem(i, /* doNotNotify */ true);
+                    return;
+                }
+            }
+        }
+        this._setCurrentItem(undefined, /* doNotNotify */ true);
+    }.observes('currentTime'),
+    _setCurrentItem: function (newItemIndex, doNotNotify)
+    {
+        if (newItemIndex === this._currentItemIndex) {
+            if (this._currentItemLocked)
+                this.set('selectedItem', this.get('currentItem') ? this.get('currentItem').measurement.id() : null);
+            return;
+        }
+
+        var newItem = this._currentTimeSeriesData[newItemIndex];
+        this._brushExtent = undefined;
+        this._currentItemIndex = newItemIndex;
+
+        if (!newItem) {
+            this._currentItemLocked = false;
+            this.set('selectedItem', null);
+        }
+
+        this._updateCurrentItemIndicators();
+
+        if (!doNotNotify)
+            this.set('currentTime', newItem ? newItem.time : undefined);
+
+        this.set('currentItem', newItem);
+        if (this._currentItemLocked)
+            this.set('selectedItem', newItem ? newItem.measurement.id() : null);
+    },
+    _selectedItemChanged: function ()
+    {
+        if (!this._margin)
+            return;
+
+        var selectedId = this.get('selectedItem');
+        var currentItem = this.get('currentItem');
+        if (currentItem &amp;&amp; currentItem.measurement.id() == selectedId)
+            return;
+
+        var series = this._currentTimeSeriesData;
+        var selectedItemIndex = undefined;
+        for (var i = 0; i &lt; series.length; i++) {
+            if (series[i].measurement.id() == selectedId) {
+                this._updateSelection(null);
+                this._currentItemLocked = true;
+                this._setCurrentItem(i);
+                this._updateSelectionToolbar();
+                return;
+            }
+        }
+    }.observes('selectedItem').on('init'),
+    _highlightedItemsChanged: function () {
+        if (!this._margin)
+            return;
+
+        var highlightedItems = this.get('highlightedItems');
+
+        var data = this._currentTimeSeriesData.filter(function (item) { return highlightedItems[item.measurement.id()]; });
+
+        if (this._highlights.length)
+            this._highlights.forEach(function (highlight) { highlight.remove(); });
+
+        this._highlights.push(this._clippedContainer
+            .selectAll(&quot;.highlight&quot;)
+                .data(data)
+            .enter().append(&quot;line&quot;)
+                .attr(&quot;class&quot;, &quot;highlight&quot;));
+
+        this._updateHighlightPositions();
+
+    }.observes('highlightedItems'),
+    _rangesChanged: function ()
+    {
+        if (!this._currentTimeSeries)
+            return;
+
+        function midPoint(firstPoint, secondPoint) {
+            if (firstPoint &amp;&amp; secondPoint)
+                return (+firstPoint.time + +secondPoint.time) / 2;
+            if (firstPoint)
+                return firstPoint.time;
+            return secondPoint.time;
+        }
+        var currentTimeSeries = this._currentTimeSeries;
+        var linkRoute = this.get('rangeRoute');
+        this.set('rangeBars', (this.get('ranges') || []).map(function (range) {
+            var start = currentTimeSeries.findPointByMeasurementId(range.get('startRun'));
+            var end = currentTimeSeries.findPointByMeasurementId(range.get('endRun'));
+            return Ember.Object.create({
+                startTime: midPoint(currentTimeSeries.previousPoint(start), start),
+                endTime: midPoint(end, currentTimeSeries.nextPoint(end)),
+                range: range,
+                left: null,
+                right: null,
+                rowIndex: null,
+                top: null,
+                bottom: null,
+                linkRoute: linkRoute,
+                linkId: range.get('id'),
+                label: range.get('label'),
+            });
+        }));
+
+        this._updateRangeBarRects();
+    }.observes('ranges'),
+    _updateRangeBarRects: function () {
+        var rangeBars = this.get('rangeBars');
+        if (!rangeBars || !rangeBars.length)
+            return;
+
+        var xScale = this._x;
+        var yScale = this._y;
+
+        // Expand the width of each range as needed and sort ranges by the left-edge of ranges.
+        var minWidth = 3;
+        var sortedBars = rangeBars.map(function (bar) {
+            var left = xScale(bar.get('startTime'));
+            var right = xScale(bar.get('endTime'));
+            if (right - left &lt; minWidth) {
+                left -= minWidth / 2;
+                right += minWidth / 2;
+            }
+            bar.set('left', left);
+            bar.set('right', right);
+            return bar;
+        }).sort(function (first, second) { return first.get('left') - second.get('left'); });
+
+        // At this point, left edges of all ranges prior to a range R1 is on the left of R1.
+        // Place R1 into a row in which right edges of all ranges prior to R1 is on the left of R1 to avoid overlapping ranges.
+        var rows = [];
+        sortedBars.forEach(function (bar) {
+            var rowIndex = 0;
+            for (; rowIndex &lt; rows.length; rowIndex++) {
+                var currentRow = rows[rowIndex];
+                if (currentRow[currentRow.length - 1].get('right') &lt; bar.get('left')) {
+                    currentRow.push(bar);
+                    break;
+                }
+            }
+            if (rowIndex &gt;= rows.length)
+                rows.push([bar]);
+            bar.set('rowIndex', rowIndex);
+        });
+        var rowHeight = 0.6 * this._rem;
+        var firstRowTop = this._contentHeight - rows.length * rowHeight;
+        var barHeight = 0.5 * this._rem;
+
+        $(this.get('element')).find('.rangeBarsContainerInlineStyle').css({
+            left: this._margin.left + 'px',
+            top: this._margin.top + firstRowTop + 'px',
+            width: this._contentWidth + 'px',
+            height: rows.length * barHeight + 'px',
+            overflow: 'hidden',
+            position: 'absolute',
+        });
+
+        var margin = this._margin;
+        sortedBars.forEach(function (bar) {
+            var top = bar.get('rowIndex') * rowHeight;
+            var height = barHeight;
+            var left = bar.get('left');
+            var width = bar.get('right') - left;
+            bar.set('inlineStyle', 'left: ' + left + 'px; top: ' + top + 'px; width: ' + width + 'px; height: ' + height + 'px;');
+        });
+    },
+    _updateCurrentItemIndicators: function ()
+    {
+        if (!this._currentItemLine)
+            return;
+
+        var item = this._currentTimeSeriesData[this._currentItemIndex];
+        if (!item) {
+            this._currentItemLine.attr(&quot;x1&quot;, -1000).attr(&quot;x2&quot;, -1000);
+            this._currentItemCircle.attr(&quot;cx&quot;, -1000);
+            return;
+        }
+
+        var x = this._x(item.time);
+        var y = this._y(item.value);
+
+        this._currentItemLine
+            .attr(&quot;x1&quot;, x)
+            .attr(&quot;x2&quot;, x);
+
+        this._currentItemCircle
+            .attr(&quot;cx&quot;, x)
+            .attr(&quot;cy&quot;, y);
+    },
+    _setCurrentSelection: function (newSelection)
+    {
+        if (this._brushExtent === newSelection)
+            return;
+
+        var points = null;
+        if (newSelection) {
+            points = this._currentTimeSeriesData
+                .filter(function (point) { return point.time &gt;= newSelection[0] &amp;&amp; point.time &lt;= newSelection[1]; });
+            if (!points.length)
+                points = null;
+        }
+
+        this._brushExtent = newSelection;
+        this._setCurrentItem(undefined);
+        this._updateSelectionToolbar();
+
+        if (!App.domainsAreEqual(this.get('selection'), newSelection))
+            this.set('selection', newSelection);
+        this.set('selectedPoints', points);
+    },
+    _updateSelectionToolbar: function ()
+    {
+        if (!this.get('interactive'))
+            return;
+
+        var selection = this._currentSelection();
+        var selectionToolbar = $(this.get('element')).children('.selection-toolbar');
+        if (selection) {
+            var left = this._x(selection[0]);
+            var right = this._x(selection[1]);
+            selectionToolbar
+                .css({left: this._margin.left + right, top: this._margin.top + this._contentHeight})
+                .show();
+        } else
+            selectionToolbar.hide();
+    },
+    actions: {
+        zoom: function ()
+        {
+            this.sendAction('zoom', this._currentSelection());
+            this.set('selection', null);
+        },
+        openRange: function (range)
+        {
+            this.sendAction('openRange', range);
+        },
+    },
+});
</ins></span></pre>
</div>
</div>

</body>
</html>