<!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>[212542] 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/212542">212542</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2017-02-17 01:15:55 -0800 (Fri, 17 Feb 2017)</dd>
</dl>

<h3>Log Message</h3>
<pre>Add tests for the time series chart and fix bugs I found along the way
https://bugs.webkit.org/show_bug.cgi?id=168499

Reviewed by Antti Koivisto.

Add basic tests for the time series chart.

Replaced the &quot;ondata&quot; callback set in the options by &quot;dataChange&quot; action now that ComponentBase provides
a facility for defining event-like actions.

Also fixed bugs I encountered while writing these tests see below for descriptions.

* browser-tests/editable-text-tests.js:
(waitToRender): Moved to index.html
* browser-tests/index.html:
(waitToRender): Moved from editable-text-tests.js.
(wait): Added.
* browser-tests/time-series-chart-tests.js: Added.
* public/v3/components/chart-pane-base.js:
(ChartPaneBase.prototype.configure):
* public/v3/components/time-series-chart.js:
(TimeSeriesChart): Removed the code to set display and position inline properties. This is now done inside
cssTemplate with :host pseudo class.
(TimeSeriesChart.prototype._ensureCanvas): Don't strech the canvas to 100% of width and height. This was
causing a flush of contents where the canvas is momentarily streched by the browser and the script later
updates with the content with the correct aspect ratio.
(TimeSeriesChart.cssTemplate): Added :host rule to set display: block and position: relative.
(TimeSeriesChart._updateAllCharts): Deleted.
(TimeSeriesChart.prototype.render): Only run the code for axis when options.axis is defined. Also, avoid
setting the fill style because we never fill for axis drawing.
(TimeSeriesChart.prototype._computeHorizontalRenderingMetrics): Ditto. Fallback to sensible values when
options.axis is not defined.
(TimeSeriesChart.prototype._renderYAxis): Now computeValueGrid generates a sequence of {time, label}.
(TimeSeriesChart.prototype._renderTimeSeries): Don't draw the shades for confidence intervals unless its
fill style is defined. Otherwise, we'd end up drawing black shade and mask the actual data points.
(TimeSeriesChart.prototype._ensureSampledTimeSeries): Dispatch newly added &quot;dataChange&quot; action instead of
calling &quot;ondata&quot; callback in options dictionary.
(TimeSeriesChart.computeTimeGrid): Modernized to use const/let. Also fixed the bug that we were emitting
the date even when the entire time range fit within a 24-hour interval.
(TimeSeriesChart.computeValueGrid): Rewritten to make MB/GB use a nice round number instead of 0.98GB.
We were using a power of 10 to round up the stepping value but the value formatter used a power of 1024
to divide byte measurements (e.g. for memory). Use formatter.divisor to find the right scaling factor for
each kind.
* public/v3/models/metric.js:
(Metric.prototype.makeFormatter):
(Metric.makeFormatter): Extracted from the one on the prototype so that tests don't need a metric object
just to test TimeSeriesChart. Added the second argument which specifies the maximum absolute value of the
range we're formatting. This is needed to use the same number of decimal points when the most significant
digit of some value is smaller than that of the biggest one. For example, we were emitting 0.50GB instead
of 0.5G along with 2.0GB. The &quot;adjustment&quot; reduces the number of significant figures in these cases.
* public/v3/pages/dashboard-page.js:
(DashboardPage.prototype._createChartForCell):</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgbrowsertestseditabletexttestsjs">trunk/Websites/perf.webkit.org/browser-tests/editable-text-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgbrowsertestsindexhtml">trunk/Websites/perf.webkit.org/browser-tests/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs">trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstimeserieschartjs">trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmetricjs">trunk/Websites/perf.webkit.org/public/v3/models/metric.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesdashboardpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgbrowserteststimeseriescharttestsjs">trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.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 (212541 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2017-02-17 09:13:58 UTC (rev 212541)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -1,3 +1,58 @@
</span><ins>+2017-02-17  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Add tests for the time series chart and fix bugs I found along the way
+        https://bugs.webkit.org/show_bug.cgi?id=168499
+
+        Reviewed by Antti Koivisto.
+
+        Add basic tests for the time series chart.
+
+        Replaced the &quot;ondata&quot; callback set in the options by &quot;dataChange&quot; action now that ComponentBase provides
+        a facility for defining event-like actions.
+
+        Also fixed bugs I encountered while writing these tests see below for descriptions.
+
+        * browser-tests/editable-text-tests.js:
+        (waitToRender): Moved to index.html
+        * browser-tests/index.html:
+        (waitToRender): Moved from editable-text-tests.js.
+        (wait): Added.
+        * browser-tests/time-series-chart-tests.js: Added.
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase.prototype.configure):
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart): Removed the code to set display and position inline properties. This is now done inside
+        cssTemplate with :host pseudo class.
+        (TimeSeriesChart.prototype._ensureCanvas): Don't strech the canvas to 100% of width and height. This was
+        causing a flush of contents where the canvas is momentarily streched by the browser and the script later
+        updates with the content with the correct aspect ratio.
+        (TimeSeriesChart.cssTemplate): Added :host rule to set display: block and position: relative.
+        (TimeSeriesChart._updateAllCharts): Deleted.
+        (TimeSeriesChart.prototype.render): Only run the code for axis when options.axis is defined. Also, avoid
+        setting the fill style because we never fill for axis drawing.
+        (TimeSeriesChart.prototype._computeHorizontalRenderingMetrics): Ditto. Fallback to sensible values when
+        options.axis is not defined.
+        (TimeSeriesChart.prototype._renderYAxis): Now computeValueGrid generates a sequence of {time, label}.
+        (TimeSeriesChart.prototype._renderTimeSeries): Don't draw the shades for confidence intervals unless its
+        fill style is defined. Otherwise, we'd end up drawing black shade and mask the actual data points.
+        (TimeSeriesChart.prototype._ensureSampledTimeSeries): Dispatch newly added &quot;dataChange&quot; action instead of
+        calling &quot;ondata&quot; callback in options dictionary.
+        (TimeSeriesChart.computeTimeGrid): Modernized to use const/let. Also fixed the bug that we were emitting
+        the date even when the entire time range fit within a 24-hour interval.
+        (TimeSeriesChart.computeValueGrid): Rewritten to make MB/GB use a nice round number instead of 0.98GB.
+        We were using a power of 10 to round up the stepping value but the value formatter used a power of 1024
+        to divide byte measurements (e.g. for memory). Use formatter.divisor to find the right scaling factor for
+        each kind.
+        * public/v3/models/metric.js:
+        (Metric.prototype.makeFormatter):
+        (Metric.makeFormatter): Extracted from the one on the prototype so that tests don't need a metric object
+        just to test TimeSeriesChart. Added the second argument which specifies the maximum absolute value of the
+        range we're formatting. This is needed to use the same number of decimal points when the most significant
+        digit of some value is smaller than that of the biggest one. For example, we were emitting 0.50GB instead
+        of 0.5G along with 2.0GB. The &quot;adjustment&quot; reduces the number of significant figures in these cases.
+        * public/v3/pages/dashboard-page.js:
+        (DashboardPage.prototype._createChartForCell):
+
</ins><span class="cx"> 2017-02-16  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Use expect.js instead of expect in browser tests
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgbrowsertestseditabletexttestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/browser-tests/editable-text-tests.js (212541 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/browser-tests/editable-text-tests.js        2017-02-17 09:13:58 UTC (rev 212541)
+++ trunk/Websites/perf.webkit.org/browser-tests/editable-text-tests.js        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -2,27 +2,6 @@
</span><span class="cx"> describe('EditableText', () =&gt; {
</span><span class="cx">     const scripts = ['instrumentation.js', 'components/base.js', 'components/editable-text.js'];
</span><span class="cx"> 
</span><del>-    function waitToRender(context)
-    {
-        if (!context._dummyComponent) {
-            const ComponentBase = context.symbols.ComponentBase;
-            context._dummyComponent = class SomeComponent extends ComponentBase {
-                constructor(resolve)
-                {
-                    super();
-                    this._resolve = resolve;
-                }
-                render() { setTimeout(this._resolve, 0); }
-            }
-            ComponentBase.defineElement('dummy-component', context._dummyComponent);
-        }
-        return new Promise((resolve) =&gt; {
-            const instance = new context._dummyComponent(resolve);
-            context.document.body.appendChild(instance.element());
-            instance.enqueueToRender();
-        });
-    }
-
</del><span class="cx">     it('show the set text', () =&gt; {
</span><span class="cx">         const context = new BrowsingContext();
</span><span class="cx">         let editableText;
</span><span class="lines">@@ -185,7 +164,7 @@
</span><span class="cx">             return waitForComponentsToRender(context);
</span><span class="cx">         }).then(() =&gt; {
</span><span class="cx">             editableText.content('action-button').dispatchEvent(new MouseEvent('mousedown'));
</span><del>-            return new Promise((resolve) =&gt; setTimeout(resolve, 0));
</del><ins>+            return wait(0);
</ins><span class="cx">         }).then(() =&gt; {
</span><span class="cx">             editableText.content('text-field').blur();
</span><span class="cx">             editableText.content('action-button').dispatchEvent(new MouseEvent('mouseup'));
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgbrowsertestsindexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/browser-tests/index.html (212541 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/browser-tests/index.html        2017-02-17 09:13:58 UTC (rev 212541)
+++ trunk/Websites/perf.webkit.org/browser-tests/index.html        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -10,6 +10,7 @@
</span><span class="cx"> mocha.setup('bdd');
</span><span class="cx"> 
</span><span class="cx"> &lt;/script&gt;
</span><ins>+&lt;script src=&quot;../unit-tests/resources/mock-remote-api.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx"> &lt;/head&gt;
</span><span class="cx"> &lt;body&gt;
</span><span class="cx"> &lt;div id=&quot;mocha&quot;&gt;&lt;/div&gt;
</span><span class="lines">@@ -16,6 +17,7 @@
</span><span class="cx"> &lt;script src=&quot;component-base-tests.js&quot;&gt;&lt;/script&gt;
</span><span class="cx"> &lt;script src=&quot;close-button-tests.js&quot;&gt;&lt;/script&gt;
</span><span class="cx"> &lt;script src=&quot;editable-text-tests.js&quot;&gt;&lt;/script&gt;
</span><ins>+&lt;script src=&quot;time-series-chart-tests.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx"> &lt;script&gt;
</span><span class="cx"> 
</span><span class="cx"> afterEach(() =&gt; {
</span><span class="lines">@@ -33,23 +35,25 @@
</span><span class="cx">         iframe.style.top = '0px';
</span><span class="cx">         BrowsingContext._iframes.push(iframe);
</span><span class="cx"> 
</span><del>-        this._iframe = iframe;
</del><ins>+        this.iframe = iframe;
</ins><span class="cx">         this.symbols = {};
</span><del>-        this.global = this._iframe.contentWindow;
-        this.document = this._iframe.contentDocument;
</del><ins>+        this.global = this.iframe.contentWindow;
+        this.document = this.iframe.contentDocument;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     importScripts(pathList, ...symbolList)
</span><span class="cx">     {
</span><del>-        const doc = this._iframe.contentDocument;
-        const global = this._iframe.contentWindow;
</del><ins>+        const doc = this.iframe.contentDocument;
+        const global = this.iframe.contentWindow;
</ins><span class="cx"> 
</span><del>-        return Promise.all(pathList.map((path) =&gt; {
</del><ins>+        pathList = pathList.map((path) =&gt; `../public/v3/${path}`);
+
+        return Promise.all(['../unit-tests/resources/mock-remote-api.js', ...pathList].map((path) =&gt; {
</ins><span class="cx">             return new Promise((resolve, reject) =&gt; {
</span><span class="cx">                 let script = doc.createElement('script');
</span><span class="cx">                 script.addEventListener('load', resolve);
</span><span class="cx">                 script.addEventListener('error', reject);
</span><del>-                script.src = '../public/v3/' + path;
</del><ins>+                script.src = path;
</ins><span class="cx">                 script.async = false;
</span><span class="cx">                 doc.body.appendChild(script);
</span><span class="cx">             });
</span><span class="lines">@@ -56,6 +60,7 @@
</span><span class="cx">         })).then(() =&gt; {
</span><span class="cx">             const script = doc.createElement('script');
</span><span class="cx">             script.textContent = `window.importedSymbols = [${symbolList.join(', ')}];`;
</span><ins>+            global.RemoteAPI = global.MockRemoteAPI;
</ins><span class="cx">             doc.body.appendChild(script);
</span><span class="cx"> 
</span><span class="cx">             const importedSymbols = global.importedSymbols;
</span><span class="lines">@@ -79,8 +84,36 @@
</span><span class="cx"> }
</span><span class="cx"> BrowsingContext._iframes = [];
</span><span class="cx"> 
</span><ins>+function waitForComponentsToRender(context)
+{
+    if (!context._dummyComponent) {
+        const ComponentBase = context.symbols.ComponentBase;
+        context._dummyComponent = class SomeComponent extends ComponentBase {
+            constructor(resolve)
+            {
+                super();
+                this._resolve = resolve;
+            }
+            render() { setTimeout(this._resolve, 0); }
+        }
+        ComponentBase.defineElement('dummy-component', context._dummyComponent);
+    }
+    return new Promise((resolve) =&gt; {
+        const instance = new context._dummyComponent(resolve);
+        context.document.body.appendChild(instance.element());
+        instance.enqueueToRender();
+    });
+}
+
+function wait(milliseconds)
+{
+    return new Promise((resolve) =&gt; {
+        setTimeout(resolve, milliseconds);
+    });
+}
+
</ins><span class="cx"> mocha.checkLeaks();
</span><del>-mocha.globals(['expect', 'BrowsingContext']);
</del><ins>+mocha.globals(['expect', 'BrowsingContext', 'wait', 'waitForComponentsToRender']);
</ins><span class="cx"> mocha.run();
</span><span class="cx"> 
</span><span class="cx"> &lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgbrowserteststimeseriescharttestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js (0 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -0,0 +1,747 @@
</span><ins>+
+describe('TimeSeriesChart', () =&gt; {
+    const scripts = [
+        '../shared/statistics.js',
+        'instrumentation.js',
+        'models/data-model.js',
+        'models/metric.js',
+        'models/time-series.js',
+        'models/measurement-set.js',
+        'models/measurement-cluster.js',
+        'models/measurement-adaptor.js',
+        'components/base.js',
+        'components/time-series-chart.js'];
+
+    it('should be constructible with an empty sourec list and an empty options', () =&gt; {
+        return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+            new TimeSeriesChart([], {});
+        });
+    });
+
+    describe('computeTimeGrid', () =&gt; {
+        it('should return an empty array when the start and the end times are identical', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const someTime = Date.now();
+                const labels = TimeSeriesChart.computeTimeGrid(someTime, someTime, 0);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(0);
+            });
+        });
+
+        const millisecondsPerHour = 3600 * 1000;
+        const millisecondsPerDay = 24 * millisecondsPerHour;
+
+        it('should return an empty array when maxLabels is 0', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = Date.now();
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - millisecondsPerDay, endTime, 0);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(0);
+            });
+        });
+
+        it('should return an empty array when maxLabels is 0 even when the interval spans multiple months', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = Date.now();
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - 120 * millisecondsPerDay, endTime, 0);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(0);
+            });
+        });
+
+        function checkGridItem(item, label, expectedDate)
+        {
+            expect(item.label).to.be(label);
+            expect(item.time.__proto__.constructor.name).to.be('Date');
+            expect(+item.time).to.be(+new Date(expectedDate));
+        }
+
+        it('should generate one hour label with just day for two hour interval when maxLabels is 1', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T07:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - 2 * millisecondsPerHour, +endTime, 1);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(1);
+                checkGridItem(labels[0], '6AM', '2017-01-15T06:00:00Z');
+            });
+        });
+
+        it('should generate two two-hour labels for four hour interval when maxLabels is 2', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T07:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - 4 * millisecondsPerHour, +endTime, 2);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(2);
+                checkGridItem(labels[0], '4AM', '2017-01-15T04:00:00Z');
+                checkGridItem(labels[1], '6AM', '2017-01-15T06:00:00Z');
+            });
+        });
+
+        it('should generate six two-hour labels for twelve hour interval when maxLabels is 6', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T07:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 12 * millisecondsPerHour, 6);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(6);
+                checkGridItem(labels[0], '8AM', '2017-01-15T08:00:00Z');
+                checkGridItem(labels[1], '10AM', '2017-01-15T10:00:00Z');
+                checkGridItem(labels[2], '12PM', '2017-01-15T12:00:00Z');
+                checkGridItem(labels[3], '2PM', '2017-01-15T14:00:00Z');
+                checkGridItem(labels[4], '4PM', '2017-01-15T16:00:00Z');
+                checkGridItem(labels[5], '6PM', '2017-01-15T18:00:00Z');
+            });
+        });
+
+        it('should generate six two-hour labels with one date label for twelve hour interval that cross a day when maxLabels is 6', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T16:12:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 12 * millisecondsPerHour, 6);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(6);
+                checkGridItem(labels[0], '6PM', '2017-01-15T18:00:00Z');
+                checkGridItem(labels[1], '8PM', '2017-01-15T20:00:00Z');
+                checkGridItem(labels[2], '10PM', '2017-01-15T22:00:00Z');
+                checkGridItem(labels[3], '1/16', '2017-01-16T00:00:00Z');
+                checkGridItem(labels[4], '2AM', '2017-01-16T02:00:00Z');
+                checkGridItem(labels[5], '4AM', '2017-01-16T04:00:00Z');
+            });
+        });
+
+        it('should generate three two-hour labels for six hour interval that cross a year when maxLabels is 5', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2016-12-31T21:37:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 6 * millisecondsPerHour, 5);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(3);
+                checkGridItem(labels[0], '10PM', '2016-12-31T22:00:00Z');
+                checkGridItem(labels[1], '1/1', '2017-01-01T00:00:00Z');
+                checkGridItem(labels[2], '2AM', '2017-01-01T02:00:00Z');
+            });
+        });
+
+        it('should generate one one-day label for one day interval when maxLabels is 1', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T07:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - millisecondsPerDay, +endTime, 1);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(1);
+                checkGridItem(labels[0], '1/15', '2017-01-15T00:00:00Z');
+            });
+        });
+
+        it('should generate two one-day labels for one day interval when maxLabels is 2', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T07:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - millisecondsPerDay, +endTime, 2);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(2);
+                checkGridItem(labels[0], '1/14 12PM', '2017-01-14T12:00:00Z');
+                checkGridItem(labels[1], '1/15', '2017-01-15T00:00:00Z');
+            });
+        });
+
+        it('should generate four half-day labels for two day interval when maxLabels is 5', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T16:12:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 2 * millisecondsPerDay, 5);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(4);
+                checkGridItem(labels[0], '1/16', '2017-01-16T00:00:00Z');
+                checkGridItem(labels[1], '12PM', '2017-01-16T12:00:00Z');
+                checkGridItem(labels[2], '1/17', '2017-01-17T00:00:00Z');
+                checkGridItem(labels[3], '12PM', '2017-01-17T12:00:00Z');
+            });
+        });
+
+        it('should generate four half-day labels for two day interval that cross a year when maxLabels is 5', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2016-12-31T09:12:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 2 * millisecondsPerDay, 5);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(4);
+                checkGridItem(labels[0], '12/31 12PM', '2016-12-31T12:00:00Z');
+                checkGridItem(labels[1], '1/1', '2017-01-01T00:00:00Z');
+                checkGridItem(labels[2], '12PM', '2017-01-01T12:00:00Z');
+                checkGridItem(labels[3], '1/2', '2017-01-02T00:00:00Z');
+            });
+        });
+
+        it('should generate seven per-day labels for one week interval when maxLabels is 10', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T07:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - 7 * millisecondsPerDay, endTime, 10);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(7);
+                checkGridItem(labels[0], '1/9', '2017-01-09T00:00:00Z');
+                checkGridItem(labels[1], '1/10', '2017-01-10T00:00:00Z');
+                checkGridItem(labels[2], '1/11', '2017-01-11T00:00:00Z');
+                checkGridItem(labels[3], '1/12', '2017-01-12T00:00:00Z');
+                checkGridItem(labels[4], '1/13', '2017-01-13T00:00:00Z');
+                checkGridItem(labels[5], '1/14', '2017-01-14T00:00:00Z');
+                checkGridItem(labels[6], '1/15', '2017-01-15T00:00:00Z');
+            });
+        });
+
+        it('should generate three two-day labels for one week interval when maxLabels is 4', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T07:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(endTime - 7 * millisecondsPerDay, endTime, 4);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(3);
+                checkGridItem(labels[0], '1/10', '2017-01-10T00:00:00Z');
+                checkGridItem(labels[1], '1/12', '2017-01-12T00:00:00Z');
+                checkGridItem(labels[2], '1/14', '2017-01-14T00:00:00Z');
+            });
+        });
+
+        it('should generate seven one-day labels for two week interval when maxLabels is 8', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T18:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 14 * millisecondsPerDay, 8);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(7);
+                checkGridItem(labels[0], '1/17', '2017-01-17T00:00:00Z');
+                checkGridItem(labels[1], '1/19', '2017-01-19T00:00:00Z');
+                checkGridItem(labels[2], '1/21', '2017-01-21T00:00:00Z');
+                checkGridItem(labels[3], '1/23', '2017-01-23T00:00:00Z');
+                checkGridItem(labels[4], '1/25', '2017-01-25T00:00:00Z');
+                checkGridItem(labels[5], '1/27', '2017-01-27T00:00:00Z');
+                checkGridItem(labels[6], '1/29', '2017-01-29T00:00:00Z');
+            });
+        });
+
+        it('should generate two one-week labels for two week interval when maxLabels is 3', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
+                const endTime = new Date('2017-01-15T18:53:00Z');
+                const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 14 * millisecondsPerDay, 3);
+                expect(labels).to.be.a('array');
+                expect(labels.length).to.be(2);
+                checkGridItem(labels[0], '1/22', '2017-01-22T00:00:00Z');
+                checkGridItem(labels[1], '1/29', '2017-01-29T00:00:00Z');
+            });
+        });
+    });
+
+    describe('computeValueGrid', () =&gt; {
+
+        function checkValueGrid(actual, expected) {
+            expect(actual).to.be.a('array');
+            expect(JSON.stringify(actual)).to.be(JSON.stringify(expected));
+        }
+
+        function approximate(number)
+        {
+            return Math.round(number * 100000000) / 100000000;
+        }
+
+        it('should generate [0.5, 1.0, 1.5, 2.0] for [0.3, 2.3] when maxLabels is 5', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(0.3, 2.3, 5, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([0.5, 1.0, 1.5, 2.0]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['0.5 pt', '1.0 pt', '1.5 pt', '2.0 pt']);
+            });
+        });
+
+        it('should generate [0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2, 2.2] for [0.3, 2.3] when maxLabels is 10', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(0.3, 2.3, 10, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2, 2.2]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['0.4 pt', '0.6 pt', '0.8 pt', '1.0 pt', '1.2 pt', '1.4 pt', '1.6 pt', '1.8 pt', '2.0 pt', '2.2 pt']);
+            });
+        });
+
+        it('should generate [1, 2] for [0.3, 2.3] when maxLabels is 2', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(0.3, 2.3, 2, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; item.value)).to.eql([1, 2]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['1.0 pt', '2.0 pt']);
+            });
+        });
+
+        it('should generate [0.4, 0.6, 0.8, 1.0, 1.2] for [0.3, 1.3] when maxLabels is 5', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(0.3, 1.3, 5, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([0.4, 0.6, 0.8, 1.0, 1.2]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['0.4 pt', '0.6 pt', '0.8 pt', '1.0 pt', '1.2 pt']);
+            });
+        });
+
+        it('should generate [0.2, 0.4, 0.6, 0.8, 1, 1.2] for [0.2, 1.3] when maxLabels is 10', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(0.2, 1.3, 10, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([0.2, 0.4, 0.6, 0.8, 1, 1.2]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['0.2 pt', '0.4 pt', '0.6 pt', '0.8 pt', '1.0 pt', '1.2 pt']);
+            });
+        });
+
+        it('should generate [0.5, 1.0] for [0.3, 1.3] when maxLabels is 4', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(0.3, 1.3, 4, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([0.5, 1.0]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['0.5 pt', '1.0 pt']);
+            });
+        });
+
+        it('should generate [10, 20, 30] for [4, 35] when maxLabels is 4', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(4, 35, 4, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; item.value)).to.eql([10, 20, 30]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['10 pt', '20 pt', '30 pt']);
+            });
+        });
+
+        it('should generate [10, 20, 30] for [4, 35] when maxLabels is 6', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(4, 35, 6, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; item.value)).to.eql([10, 20, 30]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['10 pt', '20 pt', '30 pt']);
+            });
+        });
+
+        it('should generate [10, 15, 20, 25, 30, 35] for [6, 35] when maxLabels is 6', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(6, 35, 6, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; item.value)).to.eql([10, 15, 20, 25, 30, 35]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['10 pt', '15 pt', '20 pt', '25 pt', '30 pt', '35 pt']);
+            });
+        });
+
+        it('should generate [110, 115, 120, 125, 130] for [107, 134] when maxLabels is 6', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(107, 134, 6, Metric.makeFormatter('pt', 3));
+                expect(grid.map((item) =&gt; item.value)).to.eql([110, 115, 120, 125, 130]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['110 pt', '115 pt', '120 pt', '125 pt', '130 pt']);
+            });
+        });
+
+        it('should generate [5e7, 10e7] for [1e7, 1e8] when maxLabels is 4', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(1e7, 1e8, 4, Metric.makeFormatter('pt', 3));
+                expect(grid.map((item) =&gt; item.value)).to.eql([5e7, 10e7]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['50.0 Mpt', '100 Mpt']);
+            });
+        });
+
+        it('should generate [2e7, 4e7, 6e7, 8e7, 10e7] for [1e7, 1e8] when maxLabels is 5', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(1e7, 1e8, 5, Metric.makeFormatter('pt', 3));
+                expect(grid.map((item) =&gt; item.value)).to.eql([2e7, 4e7, 6e7, 8e7, 10e7]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['20.0 Mpt', '40.0 Mpt', '60.0 Mpt', '80.0 Mpt', '100 Mpt']);
+            });
+        });
+
+        it('should generate [-1.5, -1.0, -0.5, 0.0, 0.5] for [-1.8, 0.7] when maxLabels is 5', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(-1.8, 0.7, 5, Metric.makeFormatter('pt', 2));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([-1.5, -1.0, -0.5, 0.0, 0.5]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['-1.5 pt', '-1.0 pt', '-0.5 pt', '0.0 pt', '0.5 pt']);
+            });
+        });
+
+        it('should generate [200ms, 400ms, 600ms, 800ms, 1.00s, 1.20s] for [0.2, 1.3] when maxLabels is 10 and unit is seconds', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const grid = TimeSeriesChart.computeValueGrid(0.2, 1.3, 10, Metric.makeFormatter('s', 3));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([0.2, 0.4, 0.6, 0.8, 1, 1.2]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['200 ms', '400 ms', '600 ms', '800 ms', '1.00 s', '1.20 s']);
+            });
+        });
+
+        it('should generate [2.0GB, 4.0GB, 6.0GB] for [1.2GB, 7.2GB] when maxLabels is 4 and unit is bytes', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const gigabytes = Math.pow(1024, 3);
+                const grid = TimeSeriesChart.computeValueGrid(1.2 * gigabytes, 7.2 * gigabytes, 4, Metric.makeFormatter('B', 2));
+                expect(grid.map((item) =&gt; approximate(item.value))).to.eql([2 * gigabytes, 4 * gigabytes, 6 * gigabytes]);
+                expect(grid.map((item) =&gt; item.label)).to.eql(['2.0 GB', '4.0 GB', '6.0 GB']);
+            });
+        });
+
+        it('should generate [0.6GB, 0.8GB, 1.0GB, 1.2GB] for [0.53GB, 1.23GB] when maxLabels is 4 and unit is bytes', () =&gt; {
+            return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) =&gt; {
+                const [TimeSeriesChart, Metric] = symbols;
+                const gigabytes = Math.pow(1024, 3);
+                const grid = TimeSeriesChart.computeValueGrid(0.53 * gigabytes, 1.23 * gigabytes, 4, Metric.makeFormatter('B', 2));
+                expect(grid.map((item) =&gt; item.label)).to.eql(['0.6 GB', '0.8 GB', '1.0 GB', '1.2 GB']);
+            });
+        });
+
+    });
+
+    // Data from https://perf.webkit.org/v3/#/charts?paneList=((15-769))&amp;since=1476426488465
+    const sampleCluster = {
+        &quot;clusterStart&quot;: 946684800000,
+        &quot;clusterSize&quot;: 5184000000,
+        &quot;configurations&quot;: {
+            &quot;current&quot;: [
+                [
+                    26530031, 135.26375, 80, 10821.1, 1481628.13, false,
+                    [ [27173, 1, &quot;210096&quot;, 1482398562950] ],
+                    1482398562950, 52999, 1482413222311, &quot;10877&quot;, 7
+                ],
+                [
+                    26530779, 153.2675, 80, 12261.4, 1991987.4, true, // changed to true.
+                    [ [27174,1,&quot;210097&quot;,1482424870729] ],
+                    1482424870729, 53000, 1482424992735, &quot;10878&quot;, 7
+                ],
+                [
+                    26532275, 134.2725, 80, 10741.8, 1458311.88, false,
+                    [ [ 27176, 1, &quot;210102&quot;, 1482431464371 ] ],
+                    1482431464371, 53002, 1482436041865, &quot;10879&quot;, 7
+                ],
+                [
+                    26547226, 150.9625, 80, 12077, 1908614.94, false,
+                    [ [ 27195, 1, &quot;210168&quot;, 1482852412735 ] ],
+                    1482852412735, 53022, 1482852452143, &quot;10902&quot;, 7
+                ],
+                [
+                    26559915, 141.72, 80, 11337.6, 1633126.8, false,
+                    [ [ 27211, 1, &quot;210222&quot;, 1483347732051 ] ],
+                    1483347732051, 53039, 1483347926429, &quot;10924&quot;, 7
+                ],
+                [
+                    26564388, 138.13125, 80, 11050.5, 1551157.93, false,
+                    [ [ 27217, 1, &quot;210231&quot;, 1483412171531 ] ],
+                    1483412171531, 53045, 1483415426049, &quot;10930&quot;, 7
+                ],
+                [
+                    26568867, 144.16, 80, 11532.8, 1694941.1, false,
+                    [ [ 27222, 1, &quot;210240&quot;, 1483469584347 ] ],
+                    1483469584347, 53051, 1483469642993, &quot;10935&quot;, 7
+                ]
+            ]
+        },
+        &quot;formatMap&quot;: [
+            &quot;id&quot;, &quot;mean&quot;, &quot;iterationCount&quot;, &quot;sum&quot;, &quot;squareSum&quot;, &quot;markedOutlier&quot;,
+            &quot;revisions&quot;,
+            &quot;commitTime&quot;, &quot;build&quot;, &quot;buildTime&quot;, &quot;buildNumber&quot;, &quot;builder&quot;
+        ],
+        &quot;startTime&quot;: 1480636800000,
+        &quot;endTime&quot;: 1485820800000,
+        &quot;lastModified&quot;: 1484105738736,
+        &quot;clusterCount&quot;: 1,
+        &quot;elapsedTime&quot;: 56.421995162964,
+        &quot;status&quot;: &quot;OK&quot;
+    };
+
+    function createChartWithSampleCluster(context, chartOptions = {}, options = {width: '500px', height: '150px'})
+    {
+        const TimeSeriesChart = context.symbols.TimeSeriesChart;
+        const MeasurementSet = context.symbols.MeasurementSet;
+
+        const chart = new TimeSeriesChart([{type: 'current', measurementSet: MeasurementSet.findSet(1, 1, 0)}], chartOptions);
+        const element = chart.element();
+        element.style.width = options.width;
+        element.style.height = options.height;
+        context.document.body.appendChild(element);
+
+        return chart;
+    }
+
+    function respondWithSampleCluster(request)
+    {
+        expect(request.url).to.be('../data/measurement-set-1-1.json');
+        expect(request.method).to.be('GET');
+        request.resolve(sampleCluster);
+    }
+
+    describe('fetchMeasurementSets', () =&gt; {
+
+        it('should fetch the measurement set and create a canvas element upon receiving the data', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context);
+
+                chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                respondWithSampleCluster(requests[0]);
+
+                expect(chart.content().querySelector('canvas')).to.be(null);
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    expect(chart.content().querySelector('canvas')).to.not.be(null);
+                });
+            });
+        });
+
+        it('should immediately enqueue to render when the measurement set had already been fetched', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context);
+
+                let set = context.symbols.MeasurementSet.findSet(1, 1, 0);
+                let promise = set.fetchBetween(sampleCluster.startTime, sampleCluster.endTime);
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                respondWithSampleCluster(requests[0]);
+
+                return promise.then(() =&gt; {
+                    expect(chart.content().querySelector('canvas')).to.be(null);
+                    chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                    chart.fetchMeasurementSets();
+                    return waitForComponentsToRender(context);
+                }).then(() =&gt; {
+                    expect(requests.length).to.be(1);
+                    expect(chart.content().querySelector('canvas')).to.not.be(null);
+                });
+            });
+        });
+
+        it('should dispatch &quot;dataChange&quot; action once the fetched data becomes available', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context);
+
+                let dataChangeCount = 0;
+                chart.listenToAction('dataChange', () =&gt; dataChangeCount++);
+
+                chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                respondWithSampleCluster(requests[0]);
+
+                expect(dataChangeCount).to.be(0);
+                expect(chart.sampledTimeSeriesData('current')).to.be(null);
+                expect(chart.content().querySelector('canvas')).to.be(null);
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    expect(dataChangeCount).to.be(1);
+                    expect(chart.sampledTimeSeriesData('current')).to.not.be(null);
+                    expect(chart.content().querySelector('canvas')).to.not.be(null);
+                });
+            });
+        });
+    });
+
+    function fillCanvasBeforeRedrawCheck(canvas)
+    {
+        const canvasContext = canvas.getContext('2d');
+        canvasContext.fillStyle = 'white';
+        canvasContext.fillRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
+    }
+
+    function hasCanvasBeenRedrawn(canvas)
+    {
+        return canvasImageData(canvas).data.some((value) =&gt; value != 255);
+    }
+
+    function canvasImageData(canvas)
+    {
+        return canvas.getContext('2d').getImageData(0, 0, canvas.offsetWidth, canvas.offsetHeight);
+    }
+
+    function canvasRefTest(canvas1, canvas2, shouldMatch)
+    {
+        expect(canvas1.offsetWidth).to.be(canvas2.offsetWidth);
+        expect(canvas2.offsetHeight).to.be(canvas2.offsetHeight);
+        const data1 = canvasImageData(canvas1).data;
+        const data2 = canvasImageData(canvas2).data;
+        expect(data1.length).to.be.a('number');
+        expect(data1.length).to.be(data2.length);
+
+        let match = true;
+        for (let i = 0; i &lt; data1.length; i++) {
+            if (data1[i] != data2[i]) {
+                match = false;
+                break;
+            }
+        }
+
+        if (match == shouldMatch)
+            return;
+
+        [canvas1, canvas2].forEach((canvas) =&gt; {
+            let image = document.createElement('img');
+            image.src = canvas.toDataURL();
+            image.style.display = 'block';
+            document.body.appendChild(image);
+        });
+
+        throw new Error(shouldMatch ? 'Canvas contents were different' : 'Canvas contents were identical');
+    }
+    function expectCanvasesMatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, true); }
+    function expectCanvasesMismatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, false); }
+
+    describe('render', () =&gt; {
+
+        it('should update the canvas size and its content after the window has been resized', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context, {}, {width: '100%', height: '100%'});
+
+                let dataChangeCount = 0;
+                chart.listenToAction('dataChange', () =&gt; dataChangeCount++);
+
+                chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                respondWithSampleCluster(requests[0]);
+
+                expect(dataChangeCount).to.be(0);
+                expect(chart.sampledTimeSeriesData('current')).to.be(null);
+                expect(chart.content().querySelector('canvas')).to.be(null);
+                let canvas;
+                let originalWidth;
+                let originalHeight;
+                const dpr = window.devicePixelRatio || 1;
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    expect(dataChangeCount).to.be(1);
+                    expect(chart.sampledTimeSeriesData('current')).to.not.be(null);
+                    canvas = chart.content().querySelector('canvas');
+                    expect(canvas).to.not.be(null);
+
+                    originalWidth = canvas.offsetWidth;
+                    originalHeight = canvas.offsetHeight;
+                    expect(originalWidth).to.be(dpr * context.document.body.offsetWidth);
+                    expect(originalHeight).to.be(dpr * context.document.body.offsetHeight);
+
+                    fillCanvasBeforeRedrawCheck(canvas);
+                    context.iframe.style.width = context.iframe.offsetWidth * 2 + 'px';
+                    context.global.dispatchEvent(new Event('resize'));
+
+                    expect(canvas.offsetWidth).to.be(originalWidth);
+                    expect(canvas.offsetHeight).to.be(originalHeight);
+
+                    return waitForComponentsToRender(context);
+                }).then(() =&gt; {
+                    expect(dataChangeCount).to.be(2);
+                    expect(canvas.offsetWidth).to.be.greaterThan(originalWidth);
+                    expect(canvas.offsetWidth).to.be(dpr * context.document.body.offsetWidth);
+                    expect(canvas.offsetHeight).to.be(originalHeight);
+                    expect(hasCanvasBeenRedrawn(canvas)).to.be(true);
+                });
+            });
+        });
+
+        it('should not update update the canvas when the window has been resized but its dimensions stays the same', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context, {}, {width: '100px', height: '100px'});
+
+                let dataChangeCount = 0;
+                chart.listenToAction('dataChange', () =&gt; dataChangeCount++);
+
+                chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                respondWithSampleCluster(requests[0]);
+                expect(dataChangeCount).to.be(0);
+
+                let canvas;
+                let data;
+                const dpr = window.devicePixelRatio || 1;
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    expect(dataChangeCount).to.be(1);
+                    data = chart.sampledTimeSeriesData('current');
+                    expect(data).to.not.be(null);
+                    canvas = chart.content().querySelector('canvas');
+                    expect(canvas).to.not.be(null);
+
+                    expect(canvas.offsetWidth).to.be(dpr * 100);
+                    expect(canvas.offsetHeight).to.be(dpr * 100);
+
+                    fillCanvasBeforeRedrawCheck(canvas);
+                    context.iframe.style.width = context.iframe.offsetWidth * 2 + 'px';
+                    context.global.dispatchEvent(new Event('resize'));
+
+                    expect(canvas.offsetWidth).to.be(dpr * 100);
+                    expect(canvas.offsetHeight).to.be(dpr * 100);
+
+                    return waitForComponentsToRender(context);
+                }).then(() =&gt; {
+                    expect(dataChangeCount).to.be(1);
+                    expect(chart.sampledTimeSeriesData('current')).to.be(data);
+                    expect(canvas.offsetWidth).to.be(dpr * 100);
+                    expect(canvas.offsetHeight).to.be(dpr * 100);
+                    expect(hasCanvasBeenRedrawn(canvas)).to.be(false);
+                });
+            });
+        });
+
+        it('should render Y-axis', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI', 'Metric').then(() =&gt; {
+                const chartWithoutYAxis = createChartWithSampleCluster(context, {axis:
+                    {
+                        gridStyle: '#ccc',
+                        fontSize: 1,
+                        valueFormatter: context.symbols.Metric.makeFormatter('ms', 3),
+                    }
+                });
+                const chartWithYAxis1 = createChartWithSampleCluster(context, {axis:
+                    {
+                        yAxisWidth: 4,
+                        gridStyle: '#ccc',
+                        fontSize: 1,
+                        valueFormatter: context.symbols.Metric.makeFormatter('ms', 3),
+                    }
+                });
+                const chartWithYAxis2 = createChartWithSampleCluster(context, {axis:
+                    {
+                        yAxisWidth: 4,
+                        gridStyle: '#ccc',
+                        fontSize: 1,
+                        valueFormatter: context.symbols.Metric.makeFormatter('B', 3),
+                    }
+                });
+
+                chartWithoutYAxis.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chartWithoutYAxis.fetchMeasurementSets();
+                chartWithYAxis1.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chartWithYAxis1.fetchMeasurementSets();
+                chartWithYAxis2.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chartWithYAxis2.fetchMeasurementSets();
+
+                const requests = context.symbols.MockRemoteAPI.requests;
+                expect(requests.length).to.be(1);
+                respondWithSampleCluster(requests[0]);
+
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    let canvasWithoutYAxis = chartWithoutYAxis.content().querySelector('canvas');
+                    let canvasWithYAxis1 = chartWithYAxis1.content().querySelector('canvas');
+                    let canvasWithYAxis2 = chartWithYAxis2.content().querySelector('canvas');
+                    expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
+                    expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
+                    expectCanvasesMismatch(canvasWithYAxis1, canvasWithYAxis2);
+
+                    let content1 = canvasImageData(canvasWithYAxis1);
+                    let foundGridLine = false;
+                    for (let y = 0; y &lt; content1.height; y++) {
+                        let endOfY = content1.width * 4 * y;
+                        let r = content1.data[endOfY - 4];
+                        let g = content1.data[endOfY - 3];
+                        let b = content1.data[endOfY - 2];
+                        if (r == 204 &amp;&amp; g == 204 &amp;&amp; b == 204) {
+                            foundGridLine = true;
+                            break;
+                        }
+                    }
+                    expect(foundGridLine).to.be(true);
+                });
+            });
+        });
+    });
+
+});
+
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (212541 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2017-02-17 09:13:58 UTC (rev 212541)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -52,8 +52,8 @@
</span><span class="cx">         mainOptions.selection.onchange = this._mainSelectionDidChange.bind(this);
</span><span class="cx">         mainOptions.selection.onzoom = this._mainSelectionDidZoom.bind(this);
</span><span class="cx">         mainOptions.annotations.onclick = this._openAnalysisTask.bind(this);
</span><del>-        mainOptions.ondata = this._didFetchData.bind(this);
</del><span class="cx">         this._mainChart = new InteractiveTimeSeriesChart(this._createSourceList(true), mainOptions);
</span><ins>+        this._mainChart.listenToAction('dataChange', () =&gt; this._didFetchData())
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
</span><span class="cx"> 
</span><span class="cx">         this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, this._requestOpeningCommitViewer.bind(this));
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentstimeserieschartjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js (212541 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2017-02-17 09:13:58 UTC (rev 212541)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -3,8 +3,6 @@
</span><span class="cx">     constructor(sourceList, options)
</span><span class="cx">     {
</span><span class="cx">         super();
</span><del>-        this.element().style.display = 'block';
-        this.element().style.position = 'relative';
</del><span class="cx">         this._canvas = null;
</span><span class="cx">         this._sourceList = sourceList;
</span><span class="cx">         this._trendLines = null;
</span><span class="lines">@@ -32,14 +30,23 @@
</span><span class="cx">             this._canvas.style.position = 'absolute';
</span><span class="cx">             this._canvas.style.left = '0px';
</span><span class="cx">             this._canvas.style.top = '0px';
</span><del>-            this._canvas.style.width = '100%';
-            this._canvas.style.height = '100%';
</del><span class="cx">             this.content().appendChild(this._canvas);
</span><span class="cx">         }
</span><span class="cx">         return this._canvas;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static cssTemplate() { return ''; }
</del><ins>+    static cssTemplate()
+    {
+        return `
+
+        :host {
+            display: block !important;
+            position: relative !important;
+        }
+
+        `;
+    }
+
</ins><span class="cx">     static get enqueueToRenderOnResize() { return true; }
</span><span class="cx"> 
</span><span class="cx">     _createCanvas()
</span><span class="lines">@@ -47,11 +54,6 @@
</span><span class="cx">         return document.createElement('canvas');
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static _updateAllCharts()
-    {
-        TimeSeriesChart._chartList.map(function (chart) { chart.render(); });
-    }
-
</del><span class="cx">     setDomain(startTime, endTime)
</span><span class="cx">     {
</span><span class="cx">         console.assert(startTime &lt; endTime, 'startTime must be before endTime');
</span><span class="lines">@@ -156,7 +158,6 @@
</span><span class="cx">         if (!this._startTime || !this._endTime)
</span><span class="cx">             return;
</span><span class="cx"> 
</span><del>-        // FIXME: Also detect horizontal scrolling.
</del><span class="cx">         var canvas = this._ensureCanvas();
</span><span class="cx"> 
</span><span class="cx">         var metrics = this._layout();
</span><span class="lines">@@ -171,13 +172,14 @@
</span><span class="cx">         context.clearRect(0, 0, this._width, this._height);
</span><span class="cx"> 
</span><span class="cx">         context.font = metrics.fontSize + 'px sans-serif';
</span><del>-        context.fillStyle = this._options.axis.fillStyle;
-        context.strokeStyle = this._options.axis.gridStyle;
-        context.lineWidth = 1 / this._contextScaleY;
</del><ins>+        const axis = this._options.axis;
+        if (axis) {
+            context.strokeStyle = axis.gridStyle;
+            context.lineWidth = 1 / this._contextScaleY;
+            this._renderXAxis(context, metrics, this._startTime, this._endTime);
+            this._renderYAxis(context, metrics, this._valueRangeCache[0], this._valueRangeCache[1]);
+        }
</ins><span class="cx"> 
</span><del>-        this._renderXAxis(context, metrics, this._startTime, this._endTime);
-        this._renderYAxis(context, metrics, this._valueRangeCache[0], this._valueRangeCache[1]);
-
</del><span class="cx">         context.save();
</span><span class="cx"> 
</span><span class="cx">         context.beginPath();
</span><span class="lines">@@ -217,14 +219,15 @@
</span><span class="cx">         var timeDiff = this._endTime - this._startTime;
</span><span class="cx">         var startTime = this._startTime;
</span><span class="cx"> 
</span><del>-        var fontSize = this._options.axis.fontSize * this._rem;
-        var chartX = this._options.axis.yAxisWidth * fontSize;
</del><ins>+        const axis = this._options.axis || {};
+        const fontSize = (axis.fontSize || 1) * this._rem;
+        const chartX = (axis.yAxisWidth || 0) * fontSize;
</ins><span class="cx">         var chartY = 0;
</span><span class="cx">         var chartWidth = this._width - chartX;
</span><del>-        var chartHeight = this._height - this._options.axis.xAxisHeight * fontSize;
</del><ins>+        var chartHeight = this._height - (axis.xAxisHeight || 0) * fontSize;
</ins><span class="cx"> 
</span><del>-        if (this._options.axis.xAxisEndPadding)
-            timeDiff += this._options.axis.xAxisEndPadding / (chartWidth / timeDiff);
</del><ins>+        if (axis.xAxisEndPadding)
+            timeDiff += axis.xAxisEndPadding / (chartWidth / timeDiff);
</ins><span class="cx"> 
</span><span class="cx">         return {
</span><span class="cx">             xToTime: function (x)
</span><span class="lines">@@ -356,12 +359,12 @@
</span><span class="cx"> 
</span><span class="cx">     _renderYAxis(context, metrics, minValue, maxValue)
</span><span class="cx">     {
</span><del>-        var maxYAxisLabels = Math.floor(metrics.chartHeight / metrics.fontSize / 2);
-        var yAxisGrid = TimeSeriesChart.computeValueGrid(minValue, maxValue, maxYAxisLabels);
</del><ins>+        const maxYAxisLabels = Math.floor(metrics.chartHeight / metrics.fontSize / 2);
+        const yAxisGrid = TimeSeriesChart.computeValueGrid(minValue, maxValue, maxYAxisLabels, this._options.axis.valueFormatter);
</ins><span class="cx"> 
</span><del>-        for (var value of yAxisGrid) {
</del><ins>+        for (let item of yAxisGrid) {
</ins><span class="cx">             context.beginPath();
</span><del>-            var y = metrics.valueToY(value);
</del><ins>+            const y = metrics.valueToY(item.value);
</ins><span class="cx">             context.moveTo(metrics.chartX, y);
</span><span class="cx">             context.lineTo(metrics.chartX + metrics.chartWidth, y);
</span><span class="cx">             context.stroke();
</span><span class="lines">@@ -370,15 +373,14 @@
</span><span class="cx">         if (!this._options.axis.yAxisWidth)
</span><span class="cx">             return;
</span><span class="cx"> 
</span><del>-        for (var value of yAxisGrid) {
-            var label = this._options.axis.valueFormatter(value);
-            var x = (metrics.chartX - context.measureText(label).width) / 2;
</del><ins>+        for (let item of yAxisGrid) {
+            const x = (metrics.chartX - context.measureText(item.label).width) / 2;
</ins><span class="cx"> 
</span><del>-            var y = metrics.valueToY(value) + metrics.fontSize / 2.5;
</del><ins>+            let y = metrics.valueToY(item.value) + metrics.fontSize / 2.5;
</ins><span class="cx">             if (y &lt; metrics.fontSize)
</span><span class="cx">                 y = metrics.fontSize;
</span><span class="cx"> 
</span><del>-            context.fillText(label, x, y);
</del><ins>+            context.fillText(item.label, x, y);
</ins><span class="cx">         }
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -419,27 +421,29 @@
</span><span class="cx">             point.y = metrics.valueToY(point.value);
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        context.strokeStyle = source.intervalStyle;
-        context.fillStyle = source.intervalStyle;
-        context.lineWidth = source.intervalWidth;
</del><ins>+        if (source.intervalStyle) {
+            context.strokeStyle = source.intervalStyle;
+            context.fillStyle = source.intervalStyle;
+            context.lineWidth = source.intervalWidth;
</ins><span class="cx"> 
</span><del>-        context.beginPath();
-        var width = 1;
-        for (var i = 0; i &lt; series.length; i++) {
-            var point = series[i];
-            var interval = point.interval;
-            var value = interval ? interval[0] : point.value;
-            context.lineTo(point.x - width, metrics.valueToY(value));
-            context.lineTo(point.x + width, metrics.valueToY(value));
</del><ins>+            context.beginPath();
+            var width = 1;
+            for (var i = 0; i &lt; series.length; i++) {
+                var point = series[i];
+                var interval = point.interval;
+                var value = interval ? interval[0] : point.value;
+                context.lineTo(point.x - width, metrics.valueToY(value));
+                context.lineTo(point.x + width, metrics.valueToY(value));
+            }
+            for (var i = series.length - 1; i &gt;= 0; i--) {
+                var point = series[i];
+                var interval = point.interval;
+                var value = interval ? interval[1] : point.value;
+                context.lineTo(point.x + width, metrics.valueToY(value));
+                context.lineTo(point.x - width, metrics.valueToY(value));
+            }
+            context.fill();
</ins><span class="cx">         }
</span><del>-        for (var i = series.length - 1; i &gt;= 0; i--) {
-            var point = series[i];
-            var interval = point.interval;
-            var value = interval ? interval[1] : point.value;
-            context.lineTo(point.x + width, metrics.valueToY(value));
-            context.lineTo(point.x - width, metrics.valueToY(value));
-        }
-        context.fill();
</del><span class="cx"> 
</span><span class="cx">         context.strokeStyle = this._sourceOptionWithFallback(source, layerName + 'LineStyle', 'lineStyle');
</span><span class="cx">         context.lineWidth = this._sourceOptionWithFallback(source, layerName + 'LineWidth', 'lineWidth');
</span><span class="lines">@@ -514,14 +518,13 @@
</span><span class="cx">             var filteredData = timeSeries.dataBetweenPoints(pointBeforeStart, pointAfterEnd);
</span><span class="cx">             if (!source.sampleData)
</span><span class="cx">                 return filteredData;
</span><del>-            else
-                return self._sampleTimeSeries(filteredData, maximumNumberOfPoints, filteredData.slice(-1).map(function (point) { return point.id; }));
</del><ins>+
+            return self._sampleTimeSeries(filteredData, maximumNumberOfPoints, filteredData.slice(-1).map(function (point) { return point.id; }));
</ins><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         Instrumentation.endMeasuringTime('TimeSeriesChart', 'ensureSampledTimeSeries');
</span><span class="cx"> 
</span><del>-        if (this._options.ondata)
-            this._options.ondata();
</del><ins>+        this.dispatchAction('dataChange');
</ins><span class="cx"> 
</span><span class="cx">         return true;
</span><span class="cx">     }
</span><span class="lines">@@ -633,41 +636,43 @@
</span><span class="cx"> 
</span><span class="cx">     static computeTimeGrid(min, max, maxLabels)
</span><span class="cx">     {
</span><del>-        var diffPerLabel = (max - min) / maxLabels;
</del><ins>+        const diffPerLabel = (max - min) / maxLabels;
</ins><span class="cx"> 
</span><del>-        var iterator;
</del><ins>+        let iterator;
</ins><span class="cx">         for (iterator of this._timeIterators()) {
</span><del>-            if (iterator.diff &gt; diffPerLabel)
</del><ins>+            if (iterator.diff &gt;= diffPerLabel)
</ins><span class="cx">                 break;
</span><span class="cx">         }
</span><span class="cx">         console.assert(iterator);
</span><span class="cx"> 
</span><del>-        var currentTime = new Date(min);
</del><ins>+        const currentTime = new Date(min);
</ins><span class="cx">         currentTime.setUTCMilliseconds(0);
</span><span class="cx">         currentTime.setUTCSeconds(0);
</span><span class="cx">         currentTime.setUTCMinutes(0);
</span><span class="cx">         iterator.next(currentTime);
</span><span class="cx"> 
</span><del>-        var result = [];
</del><ins>+        const fitsInOneDay = max - min &lt; 24 * 3600 * 1000;
</ins><span class="cx"> 
</span><del>-        var previousDate = null;
-        var previousMonth = null;
-        var previousHour = null;
-        while (currentTime &lt;= max) {
-            var time = new Date(currentTime);
-            var month = (time.getUTCMonth() + 1);
-            var date = time.getUTCDate();
-            var hour = time.getUTCHours();
-            var hourLabel = (hour &gt; 12 ? hour - 12 : hour) + (hour &gt;= 12 ? 'PM' : 'AM');
</del><ins>+        let result = [];
</ins><span class="cx"> 
</span><ins>+        let previousDate = null;
+        let previousMonth = null;
+        while (currentTime &lt;= max &amp;&amp; result.length &lt; maxLabels) {
+            const time = new Date(currentTime);
+            const month = time.getUTCMonth() + 1;
+            const date = time.getUTCDate();
+            const hour = time.getUTCHours();
+            const hourLabel = ((hour % 12) || 12) + (hour &gt;= 12 ? 'PM' : 'AM');
+
</ins><span class="cx">             iterator.next(currentTime);
</span><span class="cx"> 
</span><del>-            var label;
-            if (date == previousDate &amp;&amp; month == previousMonth)
</del><ins>+            let label;
+            const isMidnight = !hour;
+            if ((date == previousDate &amp;&amp; month == previousMonth) || ((!isMidnight || previousDate == null) &amp;&amp; fitsInOneDay))
</ins><span class="cx">                 label = hourLabel;
</span><span class="cx">             else {
</span><span class="cx">                 label = `${month}/${date}`;
</span><del>-                if (hour &amp;&amp; currentTime.getUTCDate() != date)
</del><ins>+                if (!isMidnight &amp;&amp; currentTime.getUTCDate() != date)
</ins><span class="cx">                     label += ' ' + hourLabel;
</span><span class="cx">             }
</span><span class="cx"> 
</span><span class="lines">@@ -675,9 +680,8 @@
</span><span class="cx"> 
</span><span class="cx">             previousDate = date;
</span><span class="cx">             previousMonth = month;
</span><del>-            previousHour = hour;
</del><span class="cx">         }
</span><del>-        
</del><ins>+
</ins><span class="cx">         console.assert(result.length &lt;= maxLabels);
</span><span class="cx"> 
</span><span class="cx">         return result;
</span><span class="lines">@@ -753,33 +757,44 @@
</span><span class="cx">         ];
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static computeValueGrid(min, max, maxLabels)
</del><ins>+    static computeValueGrid(min, max, maxLabels, formatter)
</ins><span class="cx">     {
</span><del>-        var diff = max - min;
-        var scalingFactor = 1;
-        var diffPerLabel = diff / maxLabels;
-        if (diffPerLabel &lt; 1) {
-            scalingFactor = Math.pow(10, Math.ceil(-Math.log(diffPerLabel) / Math.log(10)));
-            min *= scalingFactor;
-            max *= scalingFactor;
-            diff *= scalingFactor;
-            diffPerLabel *= scalingFactor;
-        }
-        diffPerLabel = Math.ceil(diffPerLabel);
-        var numberOfDigitsToIgnore = Math.ceil(Math.log(diffPerLabel) / Math.log(10));
-        var step = Math.pow(10, numberOfDigitsToIgnore);
</del><ins>+        const diff = max - min;
+        if (!diff)
+            return [];
</ins><span class="cx"> 
</span><del>-        if (diff / (step / 5) &lt; maxLabels) // 0.2, 0.4, etc...
-            step /= 5;
-        else if (diff / (step / 2) &lt; maxLabels) // 0.5, 1, 1.5, etc...
-            step /= 2;
</del><ins>+        const diffPerLabel = diff / maxLabels;
</ins><span class="cx"> 
</span><del>-        var gridValues = [];
-        var currentValue = Math.ceil(min / step) * step;
</del><ins>+        // First, reduce the diff between 1 and 1000 using a power of 1000 or 1024.
+        // FIXME: Share this code with Metric.makeFormatter.
+        const maxAbsValue = Math.max(Math.abs(min), Math.abs(max));
+        let scalingFactor = 1;
+        const divisor = formatter.divisor;
+        while (maxAbsValue * scalingFactor &lt; 1)
+            scalingFactor *= formatter.divisor;
+        while (maxAbsValue * scalingFactor &gt; divisor)
+            scalingFactor /= formatter.divisor;
+        const scaledDiff = diffPerLabel * scalingFactor;
+
+        // Second, compute the smallest number greater than the scaled diff
+        // which is a product of a power of 10, 2, and 5.
+        // These numbers are all factors of the decimal numeral system, 10.
+        const digitsInScaledDiff = Math.ceil(Math.log(scaledDiff) / Math.log(10));
+        let step = Math.pow(10, digitsInScaledDiff);
+        if (step / 5 &gt;= scaledDiff)
+            step /= 5; // The most significant digit is 2
+        else if (step / 2 &gt;= scaledDiff)
+            step /= 2 // The most significant digit is 5
+        step /= scalingFactor;
+
+        const gridValues = [];
+        let currentValue = Math.ceil(min / step) * step;
</ins><span class="cx">         while (currentValue &lt;= max) {
</span><del>-            gridValues.push(currentValue / scalingFactor);
</del><ins>+            let unscaledValue = currentValue;
+            gridValues.push({value: unscaledValue, label: formatter(unscaledValue, maxAbsValue)});
</ins><span class="cx">             currentValue += step;
</span><span class="cx">         }
</span><ins>+
</ins><span class="cx">         return gridValues;
</span><span class="cx">     }
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmetricjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/metric.js (212541 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/metric.js        2017-02-17 09:13:58 UTC (rev 212541)
+++ trunk/Websites/perf.webkit.org/public/v3/models/metric.js        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -60,10 +60,11 @@
</span><span class="cx">     unit() { return RunsData.unitFromMetricName(this.name()); }
</span><span class="cx">     isSmallerBetter() { return RunsData.isSmallerBetter(this.unit()); }
</span><span class="cx"> 
</span><del>-    makeFormatter(sigFig, alwaysShowSign)
</del><ins>+    makeFormatter(sigFig, alwaysShowSign) { return Metric.makeFormatter(this.unit(), sigFig, alwaysShowSign); }
+
+    static makeFormatter(unit, sigFig = 2, alwaysShowSign)
</ins><span class="cx">     {
</span><del>-        var unit = this.unit();
-        var isMiliseconds = false;
</del><ins>+        let isMiliseconds = false;
</ins><span class="cx">         if (unit == 'ms') {
</span><span class="cx">             isMiliseconds = true;
</span><span class="cx">             unit = 's';
</span><span class="lines">@@ -75,16 +76,30 @@
</span><span class="cx">         var divisor = unit == 'B' ? 1024 : 1000;
</span><span class="cx">         var suffix = ['\u03BC', 'm', '', 'K', 'M', 'G', 'T', 'P', 'E'];
</span><span class="cx">         var threshold = sigFig &gt;= 3 ? divisor : (divisor / 10);
</span><del>-        return function (value) {
</del><ins>+        let formatter = function (value, maxAbsValue = 0) {
</ins><span class="cx">             var i;
</span><span class="cx">             var sign = value &gt;= 0 ? (alwaysShowSign ? '+' : '') : '-';
</span><span class="cx">             value = Math.abs(value);
</span><del>-            for (i = isMiliseconds ? 1 : 2; value &lt; 1 &amp;&amp; i &gt; 0; i--)
</del><ins>+            let sigFigForValue = sigFig;
+
+            // The number of sig-figs to reduce in order to match that of maxAbsValue
+            // e.g. 0.5 instead of 0.50 when maxAbsValue is 2.
+            let adjustment = 0;
+            if (maxAbsValue &amp;&amp; value)
+                adjustment = Math.max(0, Math.floor(Math.log(maxAbsValue) / Math.log(10)) - Math.floor(Math.log(value) / Math.log(10)));
+
+            for (i = isMiliseconds ? 1 : 2; value &amp;&amp; value &lt; 1 &amp;&amp; i &gt; 0; i--)
</ins><span class="cx">                 value *= divisor;
</span><span class="cx">             for (; value &gt;= threshold; i++)
</span><span class="cx">                 value /= divisor;
</span><del>-            return sign + value.toPrecision(Math.max(2, sigFig)) + ' ' + suffix[i] + (unit || '');
</del><ins>+
+            if (adjustment) // Make the adjustment only for decimal positions below 1.
+                adjustment = Math.min(adjustment, Math.max(0, -Math.floor(Math.log(value) / Math.log(10))));
+
+            return sign + value.toPrecision(sigFig - adjustment) + ' ' + suffix[i] + (unit || '');
</ins><span class="cx">         }
</span><ins>+        formatter.divisor = divisor;
+        return formatter;
</ins><span class="cx">     };
</span><span class="cx"> }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesdashboardpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js (212541 => 212542)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js        2017-02-17 09:13:58 UTC (rev 212541)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js        2017-02-17 09:15:55 UTC (rev 212542)
</span><span class="lines">@@ -133,8 +133,8 @@
</span><span class="cx">             return result.error;
</span><span class="cx"> 
</span><span class="cx">         var options = ChartStyles.dashboardOptions(result.metric.makeFormatter(3));
</span><del>-        options.ondata = this._fetchedData.bind(this);
-        var chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false, true), options);
</del><ins>+        let chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false, true), options);
+        chart.listenToAction('dataChange', () =&gt; this._fetchedData())
</ins><span class="cx">         this._charts.push(chart);
</span><span class="cx"> 
</span><span class="cx">         var statusView = new ChartStatusView(result.metric, chart);
</span></span></pre>
</div>
</div>

</body>
</html>