<!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>[212923] 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/212923">212923</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2017-02-23 13:39:55 -0800 (Thu, 23 Feb 2017)</dd>
</dl>

<h3>Log Message</h3>
<pre>Add tests for InteractiveTimeSeriesChart and adopt actions
https://bugs.webkit.org/show_bug.cgi?id=168750

Reviewed by Chris Dumez.

Added tests for InteractiveTimeSeriesChart.

Also replaced selection.onchange, selection.onzoom, indicator.onchange, annotations.onclick callbacks
by &quot;selectionChange&quot;, &quot;zoom&quot;, &quot;indicatorChange&quot;, and &quot;annotationClick&quot; actions respectively.

Also fixed various bugs and bad code I encountered while writing these tests.

* browser-tests/index.html:
(waitForComponentsToRender): Delay the call to enqueueToRender until the next run loop because there
might be outstanding promises that just got resolved. e.g. for fetching measurement sets JSONs. Let
all those promises get resolved first. Otherwise, some tests become racy.
(canvasImageData): Extracted from time-series-chart-tests.js.
(canvasRefTest): Ditto.
(CanvasTest): Ditto.
(CanvasTest.fillCanvasBeforeRedrawCheck): Ditto.
(CanvasTest.hasCanvasBeenRedrawn): Ditto.
(CanvasTest.canvasImageData): Ditto.
(CanvasTest.expectCanvasesMatch): Ditto.
(CanvasTest.expectCanvasesMismatch): Ditto.

* browser-tests/time-series-chart-tests.js: Fixed some test cases where dpr multipler was not doing
the right thing anymore in Safari under a high DPI screen. Also added a lot of test cases for interactive
time series chart and one for rendering annotations.
(scripts): Moved.
(posixTime): Added. A helper function for sampleCluster.
(dayInMilliseconds): Ditto.
(sampleCluster): Moved here. Made the same cluster more artifical for an easier testing.
(createChartWithSampleCluster): Moved out of one of the tests.
(respondWithSampleCluster): Ditto.

* public/v3/components/chart-pane-base.js:
(ChartPaneBase.prototype.configure): Adopted new actions in InteractiveTimeSeriesChart.

* public/v3/components/chart-status-view.js:
(ChartStatusView.prototype.updateStatusIfNeeded): Removed a superflous console.log.

* public/v3/components/chart-styles.js:
(ChartStyles.mainChartOptions): Set zoomButton to true. InteractiveTimeSeriesChart used to determine
whether to show the zoom button or not based on the precense of the zoom callback. We made it explicit.

* public/v3/components/interactive-time-series-chart.js:
(InteractiveTimeSeriesChart.prototype.setIndicator): Explicitly call _notifySelectionChanged with false
instead of relying on undefined to be treated as falsey.
(InteractiveTimeSeriesChart.prototype._createCanvas): Use id instead of selector to find elements.
(InteractiveTimeSeriesChart.htmlTemplate):
(InteractiveTimeSeriesChart.cssTemplate):
(InteractiveTimeSeriesChart.prototype._mouseMove): Explicitly call _startOrContinueDragging with false
instead of relying on undefined treated as falsey. Also added the missing call to enqueueToRender found
by new tests. This was working fine on the dashboard due to other components invoking enqueueToRender
but won't work in a standalone instance of InteractiveTimeSeriesChart.
(InteractiveTimeSeriesChart.prototype._mouseLeave): Ditto, adding the missing call to enqueueToRender.
(InteractiveTimeSeriesChart.prototype._click): Removed the assignment to _forceRender when calling
_mouseMove in an early exist, which does set this flag and invokes enqueueToRender, and added the missing
call to enqueueToRender in the other code path.
(InteractiveTimeSeriesChart.prototype._startOrContinueDragging): Replaced annotations.onclick callback
by the newly added &quot;annotationClick&quot; action, and added the missing call to enqueueToRender.
(InteractiveTimeSeriesChart.prototype._endDragging): Use arrow function.
(InteractiveTimeSeriesChart.prototype._notifyIndicatorChanged): Replaced indicator.onchange callback by
the newly added &quot;indicatorChange&quot; action.
(InteractiveTimeSeriesChart.prototype._notifySelectionChanged): Replaced selection.onchange callback by
the newly added &quot;selectionChange&quot; action.
(InteractiveTimeSeriesChart.prototype._renderChartContent): Show the zoom button when options.zoomButton
is set instead of relying on the presence of selection.onzoom especially now that the callback has been
replaced by the &quot;zoom&quot; action.

* public/v3/components/time-series-chart.js:
(TimeSeriesChart.prototype.setAnnotations): Added the missing call to enqueueToRender.

* public/v3/main.js:</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgbrowsertestsindexhtml">trunk/Websites/perf.webkit.org/browser-tests/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgbrowserteststimeseriescharttestsjs">trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs">trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartstatusviewjs">trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartstylesjs">trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsinteractivetimeserieschartjs">trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstimeserieschartjs">trunk/Websites/perf.webkit.org/public/v3/components/time-series-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 (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -1,3 +1,80 @@
</span><ins>+2017-02-22  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Add tests for InteractiveTimeSeriesChart and adopt actions
+        https://bugs.webkit.org/show_bug.cgi?id=168750
+
+        Reviewed by Chris Dumez.
+
+        Added tests for InteractiveTimeSeriesChart.
+
+        Also replaced selection.onchange, selection.onzoom, indicator.onchange, annotations.onclick callbacks
+        by &quot;selectionChange&quot;, &quot;zoom&quot;, &quot;indicatorChange&quot;, and &quot;annotationClick&quot; actions respectively.
+
+        Also fixed various bugs and bad code I encountered while writing these tests.
+
+        * browser-tests/index.html:
+        (waitForComponentsToRender): Delay the call to enqueueToRender until the next run loop because there
+        might be outstanding promises that just got resolved. e.g. for fetching measurement sets JSONs. Let
+        all those promises get resolved first. Otherwise, some tests become racy.
+        (canvasImageData): Extracted from time-series-chart-tests.js.
+        (canvasRefTest): Ditto.
+        (CanvasTest): Ditto.
+        (CanvasTest.fillCanvasBeforeRedrawCheck): Ditto.
+        (CanvasTest.hasCanvasBeenRedrawn): Ditto.
+        (CanvasTest.canvasImageData): Ditto.
+        (CanvasTest.expectCanvasesMatch): Ditto.
+        (CanvasTest.expectCanvasesMismatch): Ditto.
+
+        * browser-tests/time-series-chart-tests.js: Fixed some test cases where dpr multipler was not doing
+        the right thing anymore in Safari under a high DPI screen. Also added a lot of test cases for interactive
+        time series chart and one for rendering annotations.
+        (scripts): Moved.
+        (posixTime): Added. A helper function for sampleCluster.
+        (dayInMilliseconds): Ditto.
+        (sampleCluster): Moved here. Made the same cluster more artifical for an easier testing.
+        (createChartWithSampleCluster): Moved out of one of the tests.
+        (respondWithSampleCluster): Ditto.
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase.prototype.configure): Adopted new actions in InteractiveTimeSeriesChart.
+
+        * public/v3/components/chart-status-view.js:
+        (ChartStatusView.prototype.updateStatusIfNeeded): Removed a superflous console.log.
+
+        * public/v3/components/chart-styles.js:
+        (ChartStyles.mainChartOptions): Set zoomButton to true. InteractiveTimeSeriesChart used to determine
+        whether to show the zoom button or not based on the precense of the zoom callback. We made it explicit.
+
+        * public/v3/components/interactive-time-series-chart.js:
+        (InteractiveTimeSeriesChart.prototype.setIndicator): Explicitly call _notifySelectionChanged with false
+        instead of relying on undefined to be treated as falsey.
+        (InteractiveTimeSeriesChart.prototype._createCanvas): Use id instead of selector to find elements.
+        (InteractiveTimeSeriesChart.htmlTemplate):
+        (InteractiveTimeSeriesChart.cssTemplate):
+        (InteractiveTimeSeriesChart.prototype._mouseMove): Explicitly call _startOrContinueDragging with false
+        instead of relying on undefined treated as falsey. Also added the missing call to enqueueToRender found
+        by new tests. This was working fine on the dashboard due to other components invoking enqueueToRender
+        but won't work in a standalone instance of InteractiveTimeSeriesChart.
+        (InteractiveTimeSeriesChart.prototype._mouseLeave): Ditto, adding the missing call to enqueueToRender.
+        (InteractiveTimeSeriesChart.prototype._click): Removed the assignment to _forceRender when calling
+        _mouseMove in an early exist, which does set this flag and invokes enqueueToRender, and added the missing
+        call to enqueueToRender in the other code path.
+        (InteractiveTimeSeriesChart.prototype._startOrContinueDragging): Replaced annotations.onclick callback
+        by the newly added &quot;annotationClick&quot; action, and added the missing call to enqueueToRender.
+        (InteractiveTimeSeriesChart.prototype._endDragging): Use arrow function.
+        (InteractiveTimeSeriesChart.prototype._notifyIndicatorChanged): Replaced indicator.onchange callback by
+        the newly added &quot;indicatorChange&quot; action.
+        (InteractiveTimeSeriesChart.prototype._notifySelectionChanged): Replaced selection.onchange callback by
+        the newly added &quot;selectionChange&quot; action.
+        (InteractiveTimeSeriesChart.prototype._renderChartContent): Show the zoom button when options.zoomButton
+        is set instead of relying on the presence of selection.onzoom especially now that the callback has been
+        replaced by the &quot;zoom&quot; action.
+
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart.prototype.setAnnotations): Added the missing call to enqueueToRender.
+
+        * public/v3/main.js:
+
</ins><span class="cx"> 2017-02-21  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Make sampling algorithm more stable and introduce an abstraction for sampled data
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgbrowsertestsindexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/browser-tests/index.html (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/browser-tests/index.html        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/browser-tests/index.html        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -101,7 +101,9 @@
</span><span class="cx">     return new Promise((resolve) =&gt; {
</span><span class="cx">         const instance = new context._dummyComponent(resolve);
</span><span class="cx">         context.document.body.appendChild(instance.element());
</span><del>-        instance.enqueueToRender();
</del><ins>+        setTimeout(() =&gt; {
+            instance.enqueueToRender();
+        }, 0);
</ins><span class="cx">     });
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="lines">@@ -112,8 +114,61 @@
</span><span class="cx">     });
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+function canvasImageData(canvas)
+{
+    return canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
+}
+
+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');
+}
+
+const CanvasTest = {
+    fillCanvasBeforeRedrawCheck(canvas)
+    {
+        const canvasContext = canvas.getContext('2d');
+        canvasContext.fillStyle = 'white';
+        canvasContext.fillRect(0, 0, canvas.width, canvas.height);
+    },
+
+    hasCanvasBeenRedrawn(canvas)
+    {
+        return canvasImageData(canvas).data.some((value) =&gt; value != 255);
+    },
+
+    canvasImageData(canvas) { return canvasImageData(canvas); },
+    expectCanvasesMatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, true); },
+    expectCanvasesMismatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, false); },
+}
+
</ins><span class="cx"> mocha.checkLeaks();
</span><del>-mocha.globals(['expect', 'BrowsingContext', 'wait', 'waitForComponentsToRender']);
</del><ins>+mocha.globals(['expect', 'BrowsingContext', 'CanvasTest', '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="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -1,16 +1,102 @@
</span><ins>+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',
+    'components/interactive-time-series-chart.js'];
</ins><span class="cx"> 
</span><ins>+function posixTime(string) { return +new Date(string); }
+
+const dayInMilliseconds = 24 * 3600 * 1000;
+
+const sampleCluster = {
+    &quot;clusterStart&quot;: posixTime('2016-01-01T00:00:00Z'),
+    &quot;clusterSize&quot;: 7 * dayInMilliseconds,
+    &quot;startTime&quot;: posixTime('2016-01-01T00:00:00Z'),
+    &quot;endTime&quot;: posixTime('2016-01-08T00:00:00Z'),
+    &quot;lastModified&quot;: posixTime('2016-01-18T00:00:00Z'),
+    &quot;clusterCount&quot;: 1,
+    &quot;status&quot;: &quot;OK&quot;,
+    &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;configurations&quot;: {
+        &quot;current&quot;: [
+            [
+                1000, 100, 1, 100, 100, false,
+                [ [ 2000, 1, &quot;4000&quot;, posixTime('2016-01-05T17:35:00Z')] ],
+                posixTime('2016-01-05T17:35:00Z'), 5000, posixTime('2016-01-05T19:23:00Z'), &quot;10&quot;, 7
+            ],
+            [
+                1001, 131, 1, 131, 131, true,
+                [ [ 2001, 1, &quot;4001&quot;, posixTime('2016-01-05T18:43:01Z')] ],
+                posixTime('2016-01-05T18:43:01Z'), 5001, posixTime('2016-01-05T20:58:01Z'), &quot;11&quot;, 7
+            ],
+            [
+                1002, 122, 1, 122, 122, false,
+                [ [ 2002, 1, &quot;4002&quot;, posixTime('2016-01-05T20:01:02Z') ] ],
+                posixTime('2016-01-05T20:01:02Z'), 5002, posixTime('2016-01-05T22:37:02Z'), &quot;12&quot;, 7
+            ],
+            [
+                1003, 113, 1, 113, 113, false,
+                [ [ 2003, 1, &quot;4003&quot;, posixTime('2016-01-05T23:19:03Z') ] ],
+                posixTime('2016-01-05T23:19:03Z'), 5003, posixTime('2016-01-06T23:19:03Z'), &quot;13&quot;, 7
+            ],
+            [
+                1004, 124, 1, 124, 124, false,
+                [ [ 2004, 1, &quot;4004&quot;, posixTime('2016-01-06T01:52:04Z') ] ],
+                posixTime('2016-01-06T01:52:04Z'), 5004, posixTime('2016-01-06T02:42:04Z'), &quot;14&quot;, 7
+            ],
+            [
+                1005, 115, 1, 115, 115, true,
+                [ [ 2005, 1, &quot;4005&quot;, posixTime('2016-01-06T03:22:05Z') ] ],
+                posixTime('2016-01-06T03:22:05Z'), 5005, posixTime('2016-01-06T06:01:05Z'), &quot;15&quot;, 7
+            ],
+            [
+                1006, 116, 1, 116, 116, false,
+                [ [ 2006, 1, &quot;4006&quot;, posixTime('2016-01-06T05:59:06Z') ] ],
+                posixTime('2016-01-06T05:59:06Z'), 5006, posixTime('2016-01-06T08:34:06Z'), &quot;16&quot;, 7
+            ]
+        ]
+    },
+};
+
+function createChartWithSampleCluster(context, chartOptions = {}, options = {})
+{
+    const TimeSeriesChart = context.symbols[options.interactiveChart ? 'InteractiveTimeSeriesChart' : 'TimeSeriesChart'];
+    const MeasurementSet = context.symbols.MeasurementSet;
+
+    const chart = new TimeSeriesChart([
+        {
+            type: 'current',
+            measurementSet: MeasurementSet.findSet(1, 1, 0),
+            interactive: options.interactive || false,
+            includeOutliers: options.includeOutliers || false
+        }], chartOptions);
+    const element = chart.element();
+    element.style.width = options.width || '300px';
+    element.style.height = options.height || '100px';
+    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);
+}
+
</ins><span class="cx"> describe('TimeSeriesChart', () =&gt; {
</span><del>-    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'];
</del><span class="cx"> 
</span><span class="cx">     it('should be constructible with an empty sourec list and an empty options', () =&gt; {
</span><span class="cx">         return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) =&gt; {
</span><span class="lines">@@ -381,83 +467,6 @@
</span><span class="cx"> 
</span><span class="cx">     });
</span><span class="cx"> 
</span><del>-    // 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);
-    }
-
</del><span class="cx">     describe('fetchMeasurementSets', () =&gt; {
</span><span class="cx"> 
</span><span class="cx">         it('should fetch the measurement set and create a canvas element upon receiving the data', () =&gt; {
</span><span class="lines">@@ -474,6 +483,7 @@
</span><span class="cx"> 
</span><span class="cx">                 expect(chart.content().querySelector('canvas')).to.be(null);
</span><span class="cx">                 return waitForComponentsToRender(context).then(() =&gt; {
</span><ins>+                    console.log('done')
</ins><span class="cx">                     expect(chart.content().querySelector('canvas')).to.not.be(null);
</span><span class="cx">                 });
</span><span class="cx">             });
</span><span class="lines">@@ -530,57 +540,94 @@
</span><span class="cx">         });
</span><span class="cx">     });
</span><span class="cx"> 
</span><del>-    function fillCanvasBeforeRedrawCheck(canvas)
-    {
-        const canvasContext = canvas.getContext('2d');
-        canvasContext.fillStyle = 'white';
-        canvasContext.fillRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
-    }
</del><ins>+    describe('sampledTimeSeriesData', () =&gt; {
+        it('should not contain an outlier when includeOutliers is false', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context, {}, {includeOutliers: false});
</ins><span class="cx"> 
</span><del>-    function hasCanvasBeenRedrawn(canvas)
-    {
-        return canvasImageData(canvas).data.some((value) =&gt; value != 255);
-    }
</del><ins>+                chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
</ins><span class="cx"> 
</span><del>-    function canvasImageData(canvas)
-    {
-        return canvas.getContext('2d').getImageData(0, 0, canvas.offsetWidth, canvas.offsetHeight);
-    }
</del><ins>+                return waitForComponentsToRender(context).then(() =&gt; {
+                    const view = chart.sampledTimeSeriesData('current');
+                    expect(view.length()).to.be(5);
+                    for (let point of view)
+                        expect(point.markedOutlier).to.be(false);
+                });
+            });
+        });
</ins><span class="cx"> 
</span><del>-    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);
</del><ins>+        it('should contain every outlier when includeOutliers is true', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
</ins><span class="cx"> 
</span><del>-        let match = true;
-        for (let i = 0; i &lt; data1.length; i++) {
-            if (data1[i] != data2[i]) {
-                match = false;
-                break;
-            }
-        }
</del><ins>+                chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chart.fetchMeasurementSets();
+                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
</ins><span class="cx"> 
</span><del>-        if (match == shouldMatch)
-            return;
</del><ins>+                return waitForComponentsToRender(context).then(() =&gt; {
+                    const view = chart.sampledTimeSeriesData('current');
+                    expect(view.length()).to.be(7);
+                    expect(view.findPointByIndex(1).markedOutlier).to.be(true);
+                    expect(view.findPointByIndex(5).markedOutlier).to.be(true);
+                });
+            });
+        });
</ins><span class="cx"> 
</span><del>-        [canvas1, canvas2].forEach((canvas) =&gt; {
-            let image = document.createElement('img');
-            image.src = canvas.toDataURL();
-            image.style.display = 'block';
-            document.body.appendChild(image);
</del><ins>+        it('should only contain data points in the domain and one preceding point when there are no succeeding points', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
+
+                chart.setDomain(posixTime('2016-01-06T00:00:00Z'), posixTime('2016-01-07T00:00:00Z'));
+                chart.fetchMeasurementSets();
+                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    const view = chart.sampledTimeSeriesData('current');
+                    expect([...view].map((point) =&gt; point.id)).to.be.eql([1003, 1004, 1005, 1006]);
+                });
+            });
</ins><span class="cx">         });
</span><span class="cx"> 
</span><del>-        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); }
</del><ins>+        it('should only contain data points in the domain and one succeeding point when there are no preceding points', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
</ins><span class="cx"> 
</span><ins>+                chart.setDomain(posixTime('2016-01-05T00:00:00Z'), posixTime('2016-01-06T00:00:00Z'));
+                chart.fetchMeasurementSets();
+                chart.fetchMeasurementSets();
+                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    const view = chart.sampledTimeSeriesData('current');
+                    expect([...view].map((point) =&gt; point.id)).to.be.eql([1000, 1001, 1002, 1003, 1004]);
+                });
+            });
+        });
+
+        it('should only contain data points in the domain and one preceding point and one succeeding point', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
+
+                chart.setDomain(posixTime('2016-01-05T21:00:00Z'), posixTime('2016-01-06T02:00:00Z'));
+                chart.fetchMeasurementSets();
+                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    const view = chart.sampledTimeSeriesData('current');
+                    expect([...view].map((point) =&gt; point.id)).to.be.eql([1002, 1003, 1004, 1005]);
+                });
+            });
+        });
+    });
+
</ins><span class="cx">     describe('render', () =&gt; {
</span><del>-
</del><span class="cx">         it('should update the canvas size and its content after the window has been resized', () =&gt; {
</span><span class="cx">             const context = new BrowsingContext();
</span><span class="cx">             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
</span><span class="lines">@@ -602,7 +649,6 @@
</span><span class="cx">                 let canvas;
</span><span class="cx">                 let originalWidth;
</span><span class="cx">                 let originalHeight;
</span><del>-                const dpr = window.devicePixelRatio || 1;
</del><span class="cx">                 return waitForComponentsToRender(context).then(() =&gt; {
</span><span class="cx">                     expect(dataChangeCount).to.be(1);
</span><span class="cx">                     expect(chart.sampledTimeSeriesData('current')).to.not.be(null);
</span><span class="lines">@@ -611,10 +657,10 @@
</span><span class="cx"> 
</span><span class="cx">                     originalWidth = canvas.offsetWidth;
</span><span class="cx">                     originalHeight = canvas.offsetHeight;
</span><del>-                    expect(originalWidth).to.be(dpr * context.document.body.offsetWidth);
-                    expect(originalHeight).to.be(dpr * context.document.body.offsetHeight);
</del><ins>+                    expect(originalWidth).to.be(context.document.body.offsetWidth);
+                    expect(originalHeight).to.be(context.document.body.offsetHeight);
</ins><span class="cx"> 
</span><del>-                    fillCanvasBeforeRedrawCheck(canvas);
</del><ins>+                    CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
</ins><span class="cx">                     context.iframe.style.width = context.iframe.offsetWidth * 2 + 'px';
</span><span class="cx">                     context.global.dispatchEvent(new Event('resize'));
</span><span class="cx"> 
</span><span class="lines">@@ -625,9 +671,9 @@
</span><span class="cx">                 }).then(() =&gt; {
</span><span class="cx">                     expect(dataChangeCount).to.be(2);
</span><span class="cx">                     expect(canvas.offsetWidth).to.be.greaterThan(originalWidth);
</span><del>-                    expect(canvas.offsetWidth).to.be(dpr * context.document.body.offsetWidth);
</del><ins>+                    expect(canvas.offsetWidth).to.be(context.document.body.offsetWidth);
</ins><span class="cx">                     expect(canvas.offsetHeight).to.be(originalHeight);
</span><del>-                    expect(hasCanvasBeenRedrawn(canvas)).to.be(true);
</del><ins>+                    expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
</ins><span class="cx">                 });
</span><span class="cx">             });
</span><span class="cx">         });
</span><span class="lines">@@ -650,7 +696,6 @@
</span><span class="cx"> 
</span><span class="cx">                 let canvas;
</span><span class="cx">                 let data;
</span><del>-                const dpr = window.devicePixelRatio || 1;
</del><span class="cx">                 return waitForComponentsToRender(context).then(() =&gt; {
</span><span class="cx">                     expect(dataChangeCount).to.be(1);
</span><span class="cx">                     data = chart.sampledTimeSeriesData('current');
</span><span class="lines">@@ -658,23 +703,23 @@
</span><span class="cx">                     canvas = chart.content().querySelector('canvas');
</span><span class="cx">                     expect(canvas).to.not.be(null);
</span><span class="cx"> 
</span><del>-                    expect(canvas.offsetWidth).to.be(dpr * 100);
-                    expect(canvas.offsetHeight).to.be(dpr * 100);
</del><ins>+                    expect(canvas.offsetWidth).to.be(100);
+                    expect(canvas.offsetHeight).to.be(100);
</ins><span class="cx"> 
</span><del>-                    fillCanvasBeforeRedrawCheck(canvas);
</del><ins>+                    CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
</ins><span class="cx">                     context.iframe.style.width = context.iframe.offsetWidth * 2 + 'px';
</span><span class="cx">                     context.global.dispatchEvent(new Event('resize'));
</span><span class="cx"> 
</span><del>-                    expect(canvas.offsetWidth).to.be(dpr * 100);
-                    expect(canvas.offsetHeight).to.be(dpr * 100);
</del><ins>+                    expect(canvas.offsetWidth).to.be(100);
+                    expect(canvas.offsetHeight).to.be(100);
</ins><span class="cx"> 
</span><span class="cx">                     return waitForComponentsToRender(context);
</span><span class="cx">                 }).then(() =&gt; {
</span><span class="cx">                     expect(dataChangeCount).to.be(1);
</span><span class="cx">                     expect(chart.sampledTimeSeriesData('current')).to.be(data);
</span><del>-                    expect(canvas.offsetWidth).to.be(dpr * 100);
-                    expect(canvas.offsetHeight).to.be(dpr * 100);
-                    expect(hasCanvasBeenRedrawn(canvas)).to.be(false);
</del><ins>+                    expect(canvas.offsetWidth).to.be(100);
+                    expect(canvas.offsetHeight).to.be(100);
+                    expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
</ins><span class="cx">                 });
</span><span class="cx">             });
</span><span class="cx">         });
</span><span class="lines">@@ -721,11 +766,11 @@
</span><span class="cx">                     let canvasWithoutYAxis = chartWithoutYAxis.content().querySelector('canvas');
</span><span class="cx">                     let canvasWithYAxis1 = chartWithYAxis1.content().querySelector('canvas');
</span><span class="cx">                     let canvasWithYAxis2 = chartWithYAxis2.content().querySelector('canvas');
</span><del>-                    expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
-                    expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
-                    expectCanvasesMismatch(canvasWithYAxis1, canvasWithYAxis2);
</del><ins>+                    CanvasTest.expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
+                    CanvasTest.expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
+                    CanvasTest.expectCanvasesMismatch(canvasWithYAxis1, canvasWithYAxis2);
</ins><span class="cx"> 
</span><del>-                    let content1 = canvasImageData(canvasWithYAxis1);
</del><ins>+                    let content1 = CanvasTest.canvasImageData(canvasWithYAxis1);
</ins><span class="cx">                     let foundGridLine = false;
</span><span class="cx">                     for (let y = 0; y &lt; content1.height; y++) {
</span><span class="cx">                         let endOfY = content1.width * 4 * y;
</span><span class="lines">@@ -741,7 +786,587 @@
</span><span class="cx">                 });
</span><span class="cx">             });
</span><span class="cx">         });
</span><ins>+
+        it('should render annotations', () =&gt; {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+                const options = {annotations: {
+                    textStyle: '#000',
+                    textBackground: '#fff',
+                    minWidth: 3,
+                    barHeight: 7,
+                    barSpacing: 2}};
+                const chartWithoutAnnotations = createChartWithSampleCluster(context, options);
+                const chartWithAnnotations = createChartWithSampleCluster(context, options);
+
+                chartWithoutAnnotations.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chartWithoutAnnotations.fetchMeasurementSets();
+                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+                chartWithAnnotations.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chartWithAnnotations.fetchMeasurementSets();
+
+                let canvasWithAnnotations;
+                return waitForComponentsToRender(context).then(() =&gt; {
+                    const diff = sampleCluster.endTime - sampleCluster.startTime;
+                    chartWithAnnotations.setAnnotations([{
+                        startTime: sampleCluster.startTime + diff / 4,
+                        endTime: sampleCluster.startTime + diff / 2,
+                        label: 'hello, world',
+                        fillStyle: 'rgb(0, 0, 255)',
+                    }]);
+
+                    canvasWithAnnotations = chartWithAnnotations.content().querySelector('canvas');
+                    CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithAnnotations);
+                    return waitForComponentsToRender(context);
+                }).then(() =&gt; {
+                    expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithAnnotations)).to.be(true);
+                
+                    const canvasWithoutAnnotations = chartWithoutAnnotations.content().querySelector('canvas');
+                    CanvasTest.expectCanvasesMismatch(canvasWithAnnotations, canvasWithoutAnnotations);
+                });
+            });
+        });
</ins><span class="cx">     });
</span><span class="cx"> 
</span><span class="cx"> });
</span><span class="cx"> 
</span><ins>+describe('InteractiveTimeSeriesChart', () =&gt; {
+
+    it('should change the indicator to the point closest to the last mouse move position', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const indicatorChangeCalls = [];
+            chart.listenToAction('indicatorChange', (...args) =&gt; indicatorChangeCalls.push(args));
+
+            let selectionChangeCount = 0;
+            chart.listenToAction('selectionChange', () =&gt; selectionChangeCount++);
+
+            let canvas;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([]);
+
+                canvas = chart.content().querySelector('canvas');
+                const rect = canvas.getBoundingClientRect();
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 1, clientY: rect.top + rect.height / 2, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.not.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                const lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
+                expect(chart.currentPoint()).to.be(lastPoint);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false]]);
+
+                expect(selectionChangeCount).to.be(0);
+            });
+        });
+    });
+
+    it('should lock the indicator to the point closest to the clicked position', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const indicatorChangeCalls = [];
+            chart.listenToAction('indicatorChange', (...args) =&gt; indicatorChangeCalls.push(args));
+
+            let selectionChangeCount = 0;
+            chart.listenToAction('selectionChange', () =&gt; selectionChangeCount++);
+
+            let canvas;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([]);
+                canvas = chart.content().querySelector('canvas');
+                const rect = canvas.getBoundingClientRect();
+
+                const x = rect.right - 1;
+                const y = rect.top + rect.height / 2;
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: x, clientY: y, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: x, clientY: y, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: x - 0.5, clientY: y + 0.5, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: x - 0.5, clientY: y + 0.5, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: x - 0.5, clientY: y + 0.5, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                const lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(lastPoint);
+                expect(chart.lockedIndicator()).to.be(lastPoint);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false], [lastPoint.id, true]]);
+
+                expect(selectionChangeCount).to.be(0);
+            });
+        });
+    });
+
+    it('should clear the unlocked indicator when the mouse cursor exits the chart', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const indicatorChangeCalls = [];
+            chart.listenToAction('indicatorChange', (...args) =&gt; indicatorChangeCalls.push(args));
+
+            let selectionChangeCount = 0;
+            chart.listenToAction('selectionChange', () =&gt; selectionChangeCount++);
+
+            let canvas;
+            let rect;
+            let lastPoint;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([]);
+
+                canvas = chart.content().querySelector('canvas');
+                rect = canvas.getBoundingClientRect();
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 1, clientY: rect.top + rect.height / 2, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(lastPoint);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false]]);
+
+                canvas.parentNode.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mouseleave', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false], [null, false]]);
+
+                expect(selectionChangeCount).to.be(0);
+            });
+        });
+    });
+
+    it('should not clear the locked indicator when the mouse cursor exits the chart', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const indicatorChangeCalls = [];
+            chart.listenToAction('indicatorChange', (...args) =&gt; indicatorChangeCalls.push(args));
+
+            let selectionChangeCount = 0;
+            chart.listenToAction('selectionChange', () =&gt; selectionChangeCount++);
+
+            let canvas;
+            let rect;
+            let lastPoint;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([]);
+
+                canvas = chart.content().querySelector('canvas');
+                rect = canvas.getBoundingClientRect();
+                canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 1, clientY: rect.top + rect.height / 2, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(lastPoint);
+                expect(chart.lockedIndicator()).to.be(lastPoint);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true]]);
+
+                canvas.parentNode.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mouseleave', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
+
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(lastPoint);
+                expect(chart.lockedIndicator()).to.be(lastPoint);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true]]);
+
+                expect(selectionChangeCount).to.be(0);
+            })
+        });
+    });
+
+    it('should clear the locked indicator when clicked', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const indicatorChangeCalls = [];
+            chart.listenToAction('indicatorChange', (...args) =&gt; indicatorChangeCalls.push(args));
+
+            let selectionChangeCount = 0;
+            chart.listenToAction('selectionChange', () =&gt; selectionChangeCount++);
+
+            let canvas;
+            let rect;
+            let y;
+            let lastPoint;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([]);
+
+                canvas = chart.content().querySelector('canvas');
+                rect = canvas.getBoundingClientRect();
+                y = rect.top + rect.height / 2;
+                canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 1, clientY: y, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(lastPoint);
+                expect(chart.lockedIndicator()).to.be(lastPoint);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true]]);
+
+                canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.left + 1, clientY: y, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.be(null);
+                const firstPoint = chart.sampledTimeSeriesData('current').firstPoint();
+                expect(chart.currentPoint()).to.be(firstPoint);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true], [firstPoint.id, false]]);
+
+                expect(selectionChangeCount).to.be(0);
+            })
+        });
+    });
+
+    it('should change the selection when the mouse cursor is dragged', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context,
+                {selection: {lineStyle: '#f93', lineWidth: 2, fillStyle: '#ccc'}},
+                {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const indicatorChangeCalls = [];
+            chart.listenToAction('indicatorChange', (...args) =&gt; indicatorChangeCalls.push(args));
+
+            const selectionChangeCalls = [];
+            chart.listenToAction('selectionChange', (...args) =&gt; selectionChangeCalls.push(args));
+
+            const zoomButton = chart.content('zoom-button');
+
+            let canvas;
+            let rect;
+            let y;
+            let firstPoint;
+            let oldRange;
+            let newRange;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(selectionChangeCalls).to.be.eql([]);
+
+                canvas = chart.content().querySelector('canvas');
+                rect = canvas.getBoundingClientRect();
+                y = rect.top + rect.height / 2;
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                firstPoint = chart.sampledTimeSeriesData('current').firstPoint();
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(firstPoint);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(indicatorChangeCalls).to.be.eql([[firstPoint.id, false]]);
+                expect(zoomButton.offsetHeight).to.be(0);
+
+                canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
+
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(firstPoint);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(selectionChangeCalls).to.be.eql([]);
+                expect(indicatorChangeCalls).to.be.eql([[firstPoint.id, false]]);
+                expect(zoomButton.offsetHeight).to.be(0);
+
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 15, clientY: y + 5, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.not.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(selectionChangeCalls.length).to.be(1);
+                oldRange = selectionChangeCalls[0][0];
+                expect(oldRange).to.be.eql(chart.currentSelection());
+                expect(selectionChangeCalls[0][1]).to.be(false);
+                expect(indicatorChangeCalls).to.be.eql([[firstPoint.id, false], [null, false]]);
+                expect(zoomButton.offsetHeight).to.be(0);
+
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 5, clientY: y + 5, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.not.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(selectionChangeCalls.length).to.be(2);
+                newRange = selectionChangeCalls[1][0];
+                expect(newRange).to.be.eql(chart.currentSelection());
+                expect(newRange[0]).to.be(oldRange[0]);
+                expect(newRange[1]).to.be.greaterThan(oldRange[1]);
+                expect(selectionChangeCalls[1][1]).to.be(false);
+                expect(zoomButton.offsetHeight).to.be(0);
+
+                canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: rect.right - 5, clientY: y + 5, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 5, clientY: y + 5, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.be.eql(newRange);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(selectionChangeCalls.length).to.be(3);
+                expect(selectionChangeCalls[2][0]).to.be.eql(newRange);
+                expect(selectionChangeCalls[2][1]).to.be(true);
+                expect(zoomButton.offsetHeight).to.be(0);
+            });
+        });
+    });
+
+    it('should dispatch the &quot;zoom&quot; action when the zoom button is clicked', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context,
+                {selection: {lineStyle: '#f93', lineWidth: 2, fillStyle: '#ccc'}, zoomButton: true},
+                {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const zoomCalls = [];
+            chart.listenToAction('zoom', (...args) =&gt; zoomCalls.push(args));
+            const zoomButton = chart.content('zoom-button');
+
+            let selection;
+            let canvas;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(zoomButton.offsetHeight).to.be(0);
+                canvas = chart.content().querySelector('canvas');
+                const rect = canvas.getBoundingClientRect();
+                const y = rect.top + rect.height / 2;
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                selection = chart.currentSelection();
+                expect(selection).to.not.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+                expect(zoomButton.offsetHeight).to.not.be(0);
+                expect(zoomCalls).to.be.eql([]);
+                zoomButton.click();
+            }).then(() =&gt; {
+                expect(zoomCalls).to.be.eql([[selection]]);
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
+            });
+        });
+    });
+
+    it('should clear the selection when clicked', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const chart = createChartWithSampleCluster(context,
+                {selection: {lineStyle: '#f93', lineWidth: 2, fillStyle: '#ccc'}},
+                {interactiveChart: true, interactive: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            let canvas;
+            let rect;
+            let y;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                canvas = chart.content().querySelector('canvas');
+                rect = canvas.getBoundingClientRect();
+                y = rect.top + rect.height / 2;
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
+                canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.not.be(null);
+                expect(chart.currentPoint()).to.be(null);
+                expect(chart.lockedIndicator()).to.be(null);
+
+                canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.left + 1, clientY: y + 5, composed: true, bubbles: true}));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.currentSelection()).to.be(null);
+                expect(chart.currentPoint()).to.be(chart.sampledTimeSeriesData('current').firstPoint());
+                expect(chart.lockedIndicator()).to.be(null);
+            });
+        });
+    });
+
+    it('should dispatch &quot;annotationClick&quot; action when an annotation is clicked', () =&gt; {
+        const context = new BrowsingContext();
+        return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() =&gt; {
+            const options = {annotations: {
+                textStyle: '#000',
+                textBackground: '#fff',
+                minWidth: 3,
+                barHeight: 10,
+                barSpacing: 1}};
+            const chart = createChartWithSampleCluster(context, options, {interactiveChart: true});
+
+            chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+            chart.fetchMeasurementSets();
+            respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+            const diff = sampleCluster.endTime - sampleCluster.startTime;
+            const annotations = [{
+                startTime: sampleCluster.startTime + diff / 2,
+                endTime: sampleCluster.endTime - diff / 4,
+                label: 'hello, world',
+                fillStyle: 'rgb(0, 0, 255)',
+            }]
+            chart.setAnnotations(annotations);
+
+            const annotationClickCalls = [];
+            chart.listenToAction('annotationClick', (...args) =&gt; annotationClickCalls.push(args));
+
+            let canvas;
+            let init;
+            return waitForComponentsToRender(context).then(() =&gt; {
+                expect(annotationClickCalls).to.be.eql([]);
+                expect(chart.content('annotation-label').textContent).to.not.contain('hello, world');
+
+                canvas = chart.content().querySelector('canvas');
+                const rect = canvas.getBoundingClientRect();
+                init = {target: canvas, clientX: rect.right - rect.width / 4, clientY: rect.bottom - 5, composed: true, bubbles: true};
+                canvas.dispatchEvent(new MouseEvent('mousemove', init));
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
+
+                expect(chart.content('annotation-label').textContent).to.contain('hello, world');
+                expect(annotationClickCalls).to.be.eql([]);
+                canvas.dispatchEvent(new MouseEvent('mousedown', init));
+                canvas.dispatchEvent(new MouseEvent('mouseup', init));
+                canvas.dispatchEvent(new MouseEvent('click', init));
+
+                expect(annotationClickCalls).to.be.eql([[annotations[0]]]);
+
+                CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
+                return waitForComponentsToRender(context);
+            }).then(() =&gt; {
+                expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
+            });
+        });
+    });
+
+});
</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 (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -40,20 +40,17 @@
</span><span class="cx">             return;
</span><span class="cx"> 
</span><span class="cx">         var formatter = result.metric.makeFormatter(4);
</span><del>-        var self = this;
</del><span class="cx"> 
</span><del>-        var overviewOptions = ChartStyles.overviewChartOptions(formatter);
-        overviewOptions.selection.onchange = this._overviewSelectionDidChange.bind(this);
-        this._overviewChart = new InteractiveTimeSeriesChart(this._createSourceList(false), overviewOptions);
</del><ins>+        this._overviewChart = new InteractiveTimeSeriesChart(this._createSourceList(false), ChartStyles.overviewChartOptions(formatter));
+        this._overviewChart.listenToAction('selectionChange', this._overviewSelectionDidChange.bind(this));
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-overview'), this._overviewChart);
</span><span class="cx"> 
</span><del>-        var mainOptions = ChartStyles.mainChartOptions(formatter);
-        mainOptions.indicator.onchange = this._indicatorDidChange.bind(this);
-        mainOptions.selection.onchange = this._mainSelectionDidChange.bind(this);
-        mainOptions.selection.onzoom = this._mainSelectionDidZoom.bind(this);
-        mainOptions.annotations.onclick = this._openAnalysisTask.bind(this);
-        this._mainChart = new InteractiveTimeSeriesChart(this._createSourceList(true), mainOptions);
-        this._mainChart.listenToAction('dataChange', () =&gt; this._didFetchData())
</del><ins>+        this._mainChart = new InteractiveTimeSeriesChart(this._createSourceList(true), ChartStyles.mainChartOptions(formatter));
+        this._mainChart.listenToAction('dataChange', () =&gt; this._didFetchData());
+        this._mainChart.listenToAction('indicatorChange', this._indicatorDidChange.bind(this));
+        this._mainChart.listenToAction('selectionChange', this._mainSelectionDidChange.bind(this));
+        this._mainChart.listenToAction('zoom', this._mainSelectionDidZoom.bind(this));
+        this._mainChart.listenToAction('annotationClick', this._openAnalysisTask.bind(this));
</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="trunkWebsitesperfwebkitorgpublicv3componentschartstatusviewjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -55,7 +55,6 @@
</span><span class="cx">                     return false;
</span><span class="cx"> 
</span><span class="cx">                 if (view &amp;&amp; view.length() &gt; 1) {
</span><del>-                    console.log(view.length(), view.firstPoint(), view.lastPoint())
</del><span class="cx">                     this._usedSelection = selection;
</span><span class="cx">                     currentPoint = view.lastPoint();
</span><span class="cx">                     previousPoint = view.firstPoint();
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartstylesjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -129,6 +129,7 @@
</span><span class="cx">         var options = this.dashboardOptions(valueFormatter);
</span><span class="cx">         options.axis.xAxisEndPadding = 5;
</span><span class="cx">         options.axis.yAxisWidth = 5;
</span><ins>+        options.zoomButton = true;
</ins><span class="cx">         options.selection = {
</span><span class="cx">             lineStyle: '#f93',
</span><span class="cx">             lineWidth: 2,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsinteractivetimeserieschartjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -64,7 +64,6 @@
</span><span class="cx"> 
</span><span class="cx">     lockedIndicator() { return this._indicatorIsLocked ? this.currentPoint() : null; }
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     setIndicator(id, shouldLock)
</span><span class="cx">     {
</span><span class="cx">         var selectionDidChange = !!this._sampledTimeSeriesData;
</span><span class="lines">@@ -77,7 +76,7 @@
</span><span class="cx">         this._forceRender = true;
</span><span class="cx"> 
</span><span class="cx">         if (selectionDidChange)
</span><del>-            this._notifySelectionChanged();
</del><ins>+            this._notifySelectionChanged(false);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     moveLockedIndicatorWithNotification(forward)
</span><span class="lines">@@ -121,14 +120,12 @@
</span><span class="cx">         window.addEventListener('mouseup', this._mouseUp.bind(this));
</span><span class="cx">         canvas.addEventListener('click', this._click.bind(this));
</span><span class="cx"> 
</span><del>-        this._annotationLabel = this.content().querySelector('.time-series-chart-annotation-label');
-        this._zoomButton = this.content().querySelector('.time-series-chart-zoom-button');
</del><ins>+        this._annotationLabel = this.content('annotation-label');
+        this._zoomButton = this.content('zoom-button');
</ins><span class="cx"> 
</span><del>-        var self = this;
-        this._zoomButton.onclick = function (event) {
</del><ins>+        this._zoomButton.onclick = (event) =&gt; {
</ins><span class="cx">             event.preventDefault();
</span><del>-            if (self._options.selection &amp;&amp; self._options.selection.onzoom)
-                self._options.selection.onzoom(self._selectionTimeRange);
</del><ins>+            this.dispatchAction('zoom', this._selectionTimeRange);
</ins><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         return canvas;
</span><span class="lines">@@ -137,7 +134,7 @@
</span><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><del>-            &lt;a href=&quot;#&quot; title=&quot;Zoom&quot; class=&quot;time-series-chart-zoom-button&quot; style=&quot;display:none;&quot;&gt;
</del><ins>+            &lt;a href=&quot;#&quot; title=&quot;Zoom&quot; id=&quot;zoom-button&quot; style=&quot;display:none;&quot;&gt;
</ins><span class="cx">                 &lt;svg viewBox=&quot;0 0 100 100&quot;&gt;
</span><span class="cx">                     &lt;g stroke-width=&quot;0&quot; stroke=&quot;none&quot;&gt;
</span><span class="cx">                         &lt;polygon points=&quot;25,25 5,50 25,75&quot;/&gt;
</span><span class="lines">@@ -146,7 +143,7 @@
</span><span class="cx">                     &lt;line x1=&quot;20&quot; y1=&quot;50&quot; x2=&quot;80&quot; y2=&quot;50&quot; stroke-width=&quot;10&quot;&gt;&lt;/line&gt;
</span><span class="cx">                 &lt;/svg&gt;
</span><span class="cx">             &lt;/a&gt;
</span><del>-            &lt;span class=&quot;time-series-chart-annotation-label&quot; style=&quot;display:none;&quot;&gt;&lt;/span&gt;
</del><ins>+            &lt;span id=&quot;annotation-label&quot; style=&quot;display:none;&quot;&gt;&lt;/span&gt;
</ins><span class="cx">         `;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -153,7 +150,7 @@
</span><span class="cx">     static cssTemplate()
</span><span class="cx">     {
</span><span class="cx">         return TimeSeriesChart.cssTemplate() + `
</span><del>-            .time-series-chart-zoom-button {
</del><ins>+            #zoom-button {
</ins><span class="cx">                 position: absolute;
</span><span class="cx">                 left: 0;
</span><span class="cx">                 top: 0;
</span><span class="lines">@@ -169,7 +166,7 @@
</span><span class="cx">                 z-index: 20;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .time-series-chart-annotation-label {
</del><ins>+            #annotation-label {
</ins><span class="cx">                 position: absolute;
</span><span class="cx">                 left: 0;
</span><span class="cx">                 top: 0;
</span><span class="lines">@@ -192,7 +189,7 @@
</span><span class="cx">     _mouseMove(event)
</span><span class="cx">     {
</span><span class="cx">         var cursorLocation = {x: event.offsetX, y: event.offsetY};
</span><del>-        if (this._startOrContinueDragging(cursorLocation) || this._selectionTimeRange)
</del><ins>+        if (this._startOrContinueDragging(cursorLocation, false) || this._selectionTimeRange)
</ins><span class="cx">             return;
</span><span class="cx"> 
</span><span class="cx">         if (this._indicatorIsLocked)
</span><span class="lines">@@ -208,6 +205,7 @@
</span><span class="cx">             newIndicatorID = this._findClosestPoint(cursorLocation);
</span><span class="cx"> 
</span><span class="cx">         this._forceRender = true;
</span><ins>+        this.enqueueToRender();
</ins><span class="cx"> 
</span><span class="cx">         if (this._currentAnnotation == newAnnotation &amp;&amp; this._indicatorID == newIndicatorID)
</span><span class="cx">             return;
</span><span class="lines">@@ -225,6 +223,7 @@
</span><span class="cx"> 
</span><span class="cx">         this._indicatorID = null;
</span><span class="cx">         this._forceRender = true;
</span><ins>+        this.enqueueToRender();
</ins><span class="cx">         this._notifyIndicatorChanged();
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -245,7 +244,6 @@
</span><span class="cx">             if (!this._didEndDrag) {
</span><span class="cx">                 this._lastMouseDownLocation = null;
</span><span class="cx">                 this._selectionTimeRange = null;
</span><del>-                this._forceRender = true;
</del><span class="cx">                 this._notifySelectionChanged(true);
</span><span class="cx">                 this._mouseMove(event);
</span><span class="cx">             }
</span><span class="lines">@@ -257,8 +255,7 @@
</span><span class="cx">         var cursorLocation = {x: event.offsetX, y: event.offsetY};
</span><span class="cx">         var annotation = this._findAnnotation(cursorLocation);
</span><span class="cx">         if (annotation) {
</span><del>-            if (this._options.annotations.onclick)
-                this._options.annotations.onclick(annotation);
</del><ins>+            this.dispatchAction('annotationClick', annotation);
</ins><span class="cx">             return;
</span><span class="cx">         }
</span><span class="cx"> 
</span><span class="lines">@@ -265,6 +262,7 @@
</span><span class="cx">         this._indicatorIsLocked = !this._indicatorIsLocked;
</span><span class="cx">         this._indicatorID = this._findClosestPoint(cursorLocation);
</span><span class="cx">         this._forceRender = true;
</span><ins>+        this.enqueueToRender();
</ins><span class="cx"> 
</span><span class="cx">         this._notifyIndicatorChanged();
</span><span class="cx">     }
</span><span class="lines">@@ -294,6 +292,7 @@
</span><span class="cx">             this._selectionTimeRange = [metrics.xToTime(selectionStart), metrics.xToTime(selectionEnd)];
</span><span class="cx">         }
</span><span class="cx">         this._forceRender = true;
</span><ins>+        this.enqueueToRender();
</ins><span class="cx"> 
</span><span class="cx">         if (indicatorDidChange)
</span><span class="cx">             this._notifyIndicatorChanged();
</span><span class="lines">@@ -313,20 +312,17 @@
</span><span class="cx">         this._dragStarted = false;
</span><span class="cx">         this._lastMouseDownLocation = null;
</span><span class="cx">         this._didEndDrag = true;
</span><del>-        var self = this;
-        setTimeout(function () { self._didEndDrag = false; }, 0);
</del><ins>+        setTimeout(() =&gt; this._didEndDrag = false, 0);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _notifyIndicatorChanged()
</span><span class="cx">     {
</span><del>-        if (this._options.indicator &amp;&amp; this._options.indicator.onchange)
-            this._options.indicator.onchange(this._indicatorID, this._indicatorIsLocked);
</del><ins>+        this.dispatchAction('indicatorChange', this._indicatorID, this._indicatorIsLocked);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _notifySelectionChanged(didEndDrag)
</span><span class="cx">     {
</span><del>-        if (this._options.selection &amp;&amp; this._options.selection.onchange)
-            this._options.selection.onchange(this._selectionTimeRange, didEndDrag);
</del><ins>+        this.dispatchAction('selectionChange', this._selectionTimeRange, didEndDrag);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _findAnnotation(cursorLocation)
</span><span class="lines">@@ -390,7 +386,7 @@
</span><span class="cx">     {
</span><span class="cx">         if (this._indicatorID)
</span><span class="cx">             excludedPoints.add(this._indicatorID);
</span><del>-        return super._sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints);
</del><ins>+        return super._sampleTimeSeries(data, minimumTimeDiff, excludedPoints);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _renderChartContent(context, metrics)
</span><span class="lines">@@ -467,10 +463,10 @@
</span><span class="cx">             context.fill();
</span><span class="cx">             context.stroke();
</span><span class="cx">         }
</span><del>-    
</del><ins>+
</ins><span class="cx">         if (this._renderedSelection != selectionX2) {
</span><span class="cx">             this._renderedSelection = selectionX2;
</span><del>-            if (this._renderedSelection &amp;&amp; selectionOptions &amp;&amp; selectionOptions.onzoom
</del><ins>+            if (this._renderedSelection &amp;&amp; this._options.zoomButton
</ins><span class="cx">                 &amp;&amp; selectionX2 &gt; 0 &amp;&amp; selectionX2 &lt; metrics.chartX + metrics.chartWidth) {
</span><span class="cx">                 if (this._zoomButton.style.display)
</span><span class="cx">                     this._zoomButton.style.display = null;
</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 (212922 => 212923)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2017-02-23 21:36:13 UTC (rev 212922)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2017-02-23 21:39:55 UTC (rev 212923)
</span><span class="lines">@@ -135,6 +135,8 @@
</span><span class="cx">     {
</span><span class="cx">         this._annotations = annotations;
</span><span class="cx">         this._annotationRows = null;
</span><ins>+
+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     render()
</span></span></pre>
</div>
</div>

</body>
</html>