<!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>[212853] 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/212853">212853</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2017-02-22 14:07:39 -0800 (Wed, 22 Feb 2017)</dd>
</dl>

<h3>Log Message</h3>
<pre>Make sampling algorithm more stable and introduce an abstraction for sampled data
https://bugs.webkit.org/show_bug.cgi?id=168693

Reviewed by Chris Dumez.

Before this patch, TimeSeriesChart's resampling resulted in some points poping up and disappearing as
the width of a chart is changed. e.g. when resizing the browser window. The bug was by caused by
the sample for a given width not always including all points for a smaller width so as the width is
expanded, some point may be dropped.

Fixed this by using a much simpler algorithm of always picking a point when the time interval between
the preceding point and the succeeding point is larger than the minimum space we allow for a given width.

Also introduced a new abstraction around the sample data: TimeSeriesView. A TimeSeriesView provides
a similar API to TimeSeries for a subset of the time series filtered by a time range a custom function.
This paves a way to adding the ability to select baseline, etc... on the chart status view.

TimeSeriesView can be in two modes:
Mode 1. The view represents a contiguous subrange of TimeSeries - In this mode, this._data references
        the underlying TimeSeries's _data directly, and we use _startingIndex to adjust index given to
        find the relative index. Finding the next point or the previous point of a given point is done
        via looking up the point's seriesIndex and doing a simple arithmetic. In general, an index is
        converted to the absolute index in the underlying TimeSeries's _data array.

Mode 2. The view represents a filtered non-contiguous subset of TimeSeries -  In this mode, this._data is
        its own array. Finding the next point or the previous point of a given point requires finding
        a sibling point in the underlying TimeSeries which is in this view. Since this may result in O(n)
        traversal and a hash lookup, we lazily build a map of each point to its position in _data instead.

* public/v3/components/chart-status-view.js:
(ChartStatusView.prototype.updateStatusIfNeeded): Call selectedPoints instead of sampledDataBetween for
clarity. This function now returns a TimeSeriesView instead of a raw array.

* public/v3/components/interactive-time-series-chart.js:
(InteractiveTimeSeriesChart.prototype.currentPoint): Updated now that _sampledTimeSeriesData contains
an array of TimeSeriesView's. Note that diff is either 0, -1, or 1.
(InteractiveTimeSeriesChart.prototype.selectedPoints): Ditto. sampledDataBetween no longer exists since
we can simply call viewTimeRange on TimeSeriesView returned by sampledDataBetween.
(InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Ditto.
(InteractiveTimeSeriesChart.prototype._sampleTimeSeries): Use add since excludedPoints is now a Set.

* public/v3/components/time-series-chart.js:
(TimeSeriesChart.prototype.sampledDataBetween): Deleted.
(TimeSeriesChart.prototype.firstSampledPointBetweenTime): Deleted.
(TimeSeriesChart.prototype._ensureSampledTimeSeries): Modernized the code. Use the the time interval of
the chart divided by the number of allowed points as the time interval used in the new sampling algorithm.
(TimeSeriesChart.prototype._sampleTimeSeries): Rewritten. We also create TimeSeriesView here.
(TimeSeriesChart.prototype._sampleTimeSeries.findMedian): Deleted.
(TimeSeriesChart.prototype._updateCanvasSizeIfClientSizeChanged): Fixed a bug that the canvas size wasn't
set to the correct value on Chrome when a high DPI screen is used.

* public/v3/models/time-series.js:
(TimeSeries.prototype.viewBetweenPoints): Renamed from dataBetweenPoints. Now returns a TimeSeriesView.
(TimeSeriesView): Added. This constructor is to be called by viewBetweenPoints, viewTimeRange, and filter.
(TimeSeriesView.prototype._buildPointIndexMap): Added. Used in mode (2).
(TimeSeriesView.prototype.length): Added.
(TimeSeriesView.prototype.firstPoint): Added.
(TimeSeriesView.prototype.lastPoint): Added.
(TimeSeriesView.prototype.nextPoint): Added. Note index is always a position in this._data. In mode (1),
this is the position of the point in the underlying TimeSeries' _data. In mode (2), this is the position
of the point in this._data which is dictinct from the underlying TimeSeries' _data.
(TimeSeriesView.prototype.previousPoint): Ditto.
(TimeSeriesView.prototype.findPointByIndex): Added. Finds the point using the positional index from the
beginning of this view. findPointByIndex(0) on one view may not be same as findPointByIndex(0) of another.
(TimeSeriesView.prototype.findById): Added. This is O(n).
(TimeSeriesView.prototype.values): Added. Returns the value of each point in this view.
(TimeSeriesView.prototype.filter): Added. Creates a new view with a subset of data points the predicate
function returned true.
(TimeSeriesView.prototype.viewTimeRange): Added. Creates a new view with a subset of data points for the
given time ragne. When the resultant view would include all points of this view, it simply returns itself
as an optimization.
(TimeSeriesView.prototype.firstPointInTimeRange): Added. Returns the first point in the view which lies
within the specified time range.
(TimeSeriesView.prototype.Symbol.iterator): Added. Iterates over each point in the view.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane.prototype.selectedPoints): Use selectedPoints in lieu of getting selection and then
calling sampledDataBetween with that range.

* public/v3/pages/summary-page.js:
(SummaryPageConfigurationGroup.set _medianForTimeRange): Modernized.

* unit-tests/time-series-tests.js: Added tests for TimeSeries and TimeSeriesView. Already caught bugs!
(addPointsToSeries):</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartstatusviewjs">trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.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>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelstimeseriesjs">trunk/Websites/perf.webkit.org/public/v3/models/time-series.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagessummarypagejs">trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgunitteststimeseriestestsjs">trunk/Websites/perf.webkit.org/unit-tests/time-series-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 (212852 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -1,3 +1,90 @@
</span><ins>+2017-02-21  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Make sampling algorithm more stable and introduce an abstraction for sampled data
+        https://bugs.webkit.org/show_bug.cgi?id=168693
+
+        Reviewed by Chris Dumez.
+
+        Before this patch, TimeSeriesChart's resampling resulted in some points poping up and disappearing as
+        the width of a chart is changed. e.g. when resizing the browser window. The bug was by caused by
+        the sample for a given width not always including all points for a smaller width so as the width is
+        expanded, some point may be dropped.
+
+        Fixed this by using a much simpler algorithm of always picking a point when the time interval between
+        the preceding point and the succeeding point is larger than the minimum space we allow for a given width.
+
+        Also introduced a new abstraction around the sample data: TimeSeriesView. A TimeSeriesView provides
+        a similar API to TimeSeries for a subset of the time series filtered by a time range a custom function.
+        This paves a way to adding the ability to select baseline, etc... on the chart status view.
+
+        TimeSeriesView can be in two modes:
+        Mode 1. The view represents a contiguous subrange of TimeSeries - In this mode, this._data references
+                the underlying TimeSeries's _data directly, and we use _startingIndex to adjust index given to
+                find the relative index. Finding the next point or the previous point of a given point is done
+                via looking up the point's seriesIndex and doing a simple arithmetic. In general, an index is
+                converted to the absolute index in the underlying TimeSeries's _data array.
+
+        Mode 2. The view represents a filtered non-contiguous subset of TimeSeries -  In this mode, this._data is
+                its own array. Finding the next point or the previous point of a given point requires finding
+                a sibling point in the underlying TimeSeries which is in this view. Since this may result in O(n)
+                traversal and a hash lookup, we lazily build a map of each point to its position in _data instead.
+
+        * public/v3/components/chart-status-view.js:
+        (ChartStatusView.prototype.updateStatusIfNeeded): Call selectedPoints instead of sampledDataBetween for
+        clarity. This function now returns a TimeSeriesView instead of a raw array.
+
+        * public/v3/components/interactive-time-series-chart.js:
+        (InteractiveTimeSeriesChart.prototype.currentPoint): Updated now that _sampledTimeSeriesData contains
+        an array of TimeSeriesView's. Note that diff is either 0, -1, or 1.
+        (InteractiveTimeSeriesChart.prototype.selectedPoints): Ditto. sampledDataBetween no longer exists since
+        we can simply call viewTimeRange on TimeSeriesView returned by sampledDataBetween.
+        (InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Ditto.
+        (InteractiveTimeSeriesChart.prototype._sampleTimeSeries): Use add since excludedPoints is now a Set.
+
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart.prototype.sampledDataBetween): Deleted.
+        (TimeSeriesChart.prototype.firstSampledPointBetweenTime): Deleted.
+        (TimeSeriesChart.prototype._ensureSampledTimeSeries): Modernized the code. Use the the time interval of
+        the chart divided by the number of allowed points as the time interval used in the new sampling algorithm.
+        (TimeSeriesChart.prototype._sampleTimeSeries): Rewritten. We also create TimeSeriesView here.
+        (TimeSeriesChart.prototype._sampleTimeSeries.findMedian): Deleted.
+        (TimeSeriesChart.prototype._updateCanvasSizeIfClientSizeChanged): Fixed a bug that the canvas size wasn't
+        set to the correct value on Chrome when a high DPI screen is used.
+
+        * public/v3/models/time-series.js:
+        (TimeSeries.prototype.viewBetweenPoints): Renamed from dataBetweenPoints. Now returns a TimeSeriesView.
+        (TimeSeriesView): Added. This constructor is to be called by viewBetweenPoints, viewTimeRange, and filter.
+        (TimeSeriesView.prototype._buildPointIndexMap): Added. Used in mode (2).
+        (TimeSeriesView.prototype.length): Added.
+        (TimeSeriesView.prototype.firstPoint): Added.
+        (TimeSeriesView.prototype.lastPoint): Added.
+        (TimeSeriesView.prototype.nextPoint): Added. Note index is always a position in this._data. In mode (1),
+        this is the position of the point in the underlying TimeSeries' _data. In mode (2), this is the position
+        of the point in this._data which is dictinct from the underlying TimeSeries' _data.
+        (TimeSeriesView.prototype.previousPoint): Ditto.
+        (TimeSeriesView.prototype.findPointByIndex): Added. Finds the point using the positional index from the
+        beginning of this view. findPointByIndex(0) on one view may not be same as findPointByIndex(0) of another.
+        (TimeSeriesView.prototype.findById): Added. This is O(n).
+        (TimeSeriesView.prototype.values): Added. Returns the value of each point in this view.
+        (TimeSeriesView.prototype.filter): Added. Creates a new view with a subset of data points the predicate
+        function returned true.
+        (TimeSeriesView.prototype.viewTimeRange): Added. Creates a new view with a subset of data points for the
+        given time ragne. When the resultant view would include all points of this view, it simply returns itself
+        as an optimization.
+        (TimeSeriesView.prototype.firstPointInTimeRange): Added. Returns the first point in the view which lies
+        within the specified time range.
+        (TimeSeriesView.prototype.Symbol.iterator): Added. Iterates over each point in the view.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane.prototype.selectedPoints): Use selectedPoints in lieu of getting selection and then
+        calling sampledDataBetween with that range.
+
+        * public/v3/pages/summary-page.js:
+        (SummaryPageConfigurationGroup.set _medianForTimeRange): Modernized.
+
+        * unit-tests/time-series-tests.js: Added tests for TimeSeries and TimeSeriesView. Already caught bugs!
+        (addPointsToSeries):
+
</ins><span class="cx"> 2017-02-17  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Add tests for the time series chart and fix bugs I found along the way
</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 (212852 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js        2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -50,14 +50,15 @@
</span><span class="cx">                 return false;
</span><span class="cx"> 
</span><span class="cx">             if (selection) {
</span><del>-                var data = this._chart.sampledDataBetween('current', selection[0], selection[1]);
-                if (!data)
</del><ins>+                const view = this._chart.selectedPoints('current');
+                if (!view)
</ins><span class="cx">                     return false;
</span><span class="cx"> 
</span><del>-                if (data &amp;&amp; data.length &gt; 1) {
</del><ins>+                if (view &amp;&amp; view.length() &gt; 1) {
+                    console.log(view.length(), view.firstPoint(), view.lastPoint())
</ins><span class="cx">                     this._usedSelection = selection;
</span><del>-                    currentPoint = data[data.length - 1];
-                    previousPoint = data[0];
</del><ins>+                    currentPoint = view.lastPoint();
+                    previousPoint = view.firstPoint();
</ins><span class="cx">                 }
</span><span class="cx">             } else  {
</span><span class="cx">                 currentPoint = this._chart.currentPoint();
</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 (212852 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js        2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -23,6 +23,7 @@
</span><span class="cx">             return null;
</span><span class="cx"> 
</span><span class="cx">         if (!this._sampledTimeSeriesData) {
</span><ins>+            // FIXME: Why are we not using diff in this code path?
</ins><span class="cx">             this._ensureFetchedTimeSeries();
</span><span class="cx">             for (var series of this._fetchedTimeSeries) {
</span><span class="cx">                 var point = series.findById(id);
</span><span class="lines">@@ -32,15 +33,15 @@
</span><span class="cx">             return null;
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        for (var data of this._sampledTimeSeriesData) {
-            if (!data)
</del><ins>+        for (var view of this._sampledTimeSeriesData) {
+            if (!view)
</ins><span class="cx">                 continue;
</span><del>-            var index = data.findIndex(function (point) { return point.id == id; });
-            if (index &lt; 0)
</del><ins>+            let point = view.findById(id);
+            if (!point)
</ins><span class="cx">                 continue;
</span><del>-            if (diff)
-                index += diff;
-            return data[Math.min(Math.max(0, index), data.length)];
</del><ins>+            if (!diff)
+                return point;
+            return (point &amp;&amp; diff &gt; 0 ? view.nextPoint(point) : view.previousPoint(point)) || point;
</ins><span class="cx">         }
</span><span class="cx">         return null;
</span><span class="cx">     }
</span><span class="lines">@@ -49,14 +50,16 @@
</span><span class="cx"> 
</span><span class="cx">     selectedPoints(type)
</span><span class="cx">     {
</span><del>-        var selection = this._selectionTimeRange;
-        return selection ? this.sampledDataBetween(type, selection[0], selection[1]) : null;
</del><ins>+        const selection = this._selectionTimeRange;
+        const data = this.sampledTimeSeriesData(type);
+        return selection &amp;&amp; data ? data.viewTimeRange(selection[0], selection[1]) : null;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     firstSelectedPoint(type)
</span><span class="cx">     {
</span><del>-        var selection = this._selectionTimeRange;
-        return selection ? this.firstSampledPointBetweenTime(type, selection[0], selection[1]) : null;
</del><ins>+        const selection = this._selectionTimeRange;
+        const data = this.sampledTimeSeriesData(type);
+        return selection &amp;&amp; data ? data.firstPointInTimeRange(selection[0], selection[1]) : null;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     lockedIndicator() { return this._indicatorIsLocked ? this.currentPoint() : null; }
</span><span class="lines">@@ -383,10 +386,10 @@
</span><span class="cx">         return metrics;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints)
</del><ins>+    _sampleTimeSeries(data, minimumTimeDiff, excludedPoints)
</ins><span class="cx">     {
</span><span class="cx">         if (this._indicatorID)
</span><del>-            excludedPoints.push(this._indicatorID);
</del><ins>+            excludedPoints.add(this._indicatorID);
</ins><span class="cx">         return super._sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints);
</span><span class="cx">     }
</span><span class="cx"> 
</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 (212852 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -131,22 +131,6 @@
</span><span class="cx">         return null;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    sampledDataBetween(type, startTime, endTime)
-    {
-        var data = this.sampledTimeSeriesData(type);
-        if (!data)
-            return null;
-        return data.filter(function (point) { return startTime &lt;= point.time &amp;&amp; point.time &lt;= endTime; });
-    }
-
-    firstSampledPointBetweenTime(type, startTime, endTime)
-    {
-        var data = this.sampledTimeSeriesData(type);
-        if (!data)
-            return null;
-        return data.find(function (point) { return startTime &lt;= point.time &amp;&amp; point.time &lt;= endTime; });
-    }
-
</del><span class="cx">     setAnnotations(annotations)
</span><span class="cx">     {
</span><span class="cx">         this._annotations = annotations;
</span><span class="lines">@@ -497,29 +481,28 @@
</span><span class="cx"> 
</span><span class="cx">         Instrumentation.startMeasuringTime('TimeSeriesChart', 'ensureSampledTimeSeries');
</span><span class="cx"> 
</span><del>-        var self = this;
-        var startTime = this._startTime;
-        var endTime = this._endTime;
-        this._sampledTimeSeriesData = this._sourceList.map(function (source, sourceIndex) {
-            var timeSeries = self._fetchedTimeSeries[sourceIndex];
</del><ins>+        const startTime = this._startTime;
+        const endTime = this._endTime;
+        this._sampledTimeSeriesData = this._sourceList.map((source, sourceIndex) =&gt; {
+            const timeSeries = this._fetchedTimeSeries[sourceIndex];
</ins><span class="cx">             if (!timeSeries)
</span><span class="cx">                 return null;
</span><span class="cx"> 
</span><span class="cx">             // A chart with X px width shouldn't have more than 2X / &lt;radius-of-points&gt; data points.
</span><del>-            var maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
</del><ins>+            const maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
</ins><span class="cx"> 
</span><del>-            var pointAfterStart = timeSeries.findPointAfterTime(startTime);
-            var pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
-            var pointAfterEnd = timeSeries.findPointAfterTime(endTime) || timeSeries.lastPoint();
</del><ins>+            const pointAfterStart = timeSeries.findPointAfterTime(startTime);
+            const pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
+            const pointAfterEnd = timeSeries.findPointAfterTime(endTime) || timeSeries.lastPoint();
</ins><span class="cx">             if (!pointBeforeStart || !pointAfterEnd)
</span><span class="cx">                 return null;
</span><span class="cx"> 
</span><span class="cx">             // FIXME: Move this to TimeSeries.prototype.
</span><del>-            var filteredData = timeSeries.dataBetweenPoints(pointBeforeStart, pointAfterEnd);
</del><ins>+            const view = timeSeries.viewBetweenPoints(pointBeforeStart, pointAfterEnd);
</ins><span class="cx">             if (!source.sampleData)
</span><del>-                return filteredData;
</del><ins>+                return view;
</ins><span class="cx"> 
</span><del>-            return self._sampleTimeSeries(filteredData, maximumNumberOfPoints, filteredData.slice(-1).map(function (point) { return point.id; }));
</del><ins>+            return this._sampleTimeSeries(view, (endTime - startTime) / maximumNumberOfPoints, new Set);
</ins><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         Instrumentation.endMeasuringTime('TimeSeriesChart', 'ensureSampledTimeSeries');
</span><span class="lines">@@ -529,49 +512,24 @@
</span><span class="cx">         return true;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints)
</del><ins>+    _sampleTimeSeries(view, minimumTimeDiff, excludedPoints)
</ins><span class="cx">     {
</span><ins>+        if (view.length() &lt; 2)
+            return view;
+
</ins><span class="cx">         Instrumentation.startMeasuringTime('TimeSeriesChart', 'sampleTimeSeries');
</span><span class="cx"> 
</span><del>-        // FIXME: Do this in O(n) using quickselect: https://en.wikipedia.org/wiki/Quickselect
-        function findMedian(list, startIndex, indexAfterEnd)
-        {
-            var sortedList = list.slice(startIndex, indexAfterEnd).sort(function (a, b) { return a.value - b.value; });
-            return sortedList[Math.floor(sortedList.length / 2)];
-        }
</del><ins>+        const sampledData = view.filter((point, i) =&gt; {
+            if (excludedPoints.has(point.id))
+                return true;
+            let previousPoint = view.previousPoint(point) || point;
+            let nextPoint = view.nextPoint(point) || point;
+            return nextPoint.time - previousPoint.time &gt;= minimumTimeDiff;
+        });
</ins><span class="cx"> 
</span><del>-        var samplingSize = Math.ceil(data.length / maximumNumberOfPoints);
-
-        var totalTimeDiff = data[data.length - 1].time - data[0].time;
-        var timePerSample = totalTimeDiff / maximumNumberOfPoints;
-
-        var sampledData = [];
-        var lastIndex = data.length - 1;
-        var i = 0;
-        while (i &lt;= lastIndex) {
-            var startPoint = data[i];
-            var j;
-            for (j = i; j &lt;= lastIndex; j++) {
-                var endPoint = data[j];
-                if (excludedPoints.includes(endPoint.id)) {
-                    j--;
-                    break;
-                }
-                if (endPoint.time - startPoint.time &gt;= timePerSample)
-                    break;
-            }
-            if (i &lt; j - 1) {
-                sampledData.push(findMedian(data, i, j));
-                i = j;
-            } else {
-                sampledData.push(startPoint);
-                i++;
-            }
-        }
-
</del><span class="cx">         Instrumentation.endMeasuringTime('TimeSeriesChart', 'sampleTimeSeries');
</span><span class="cx"> 
</span><del>-        Instrumentation.reportMeasurement('TimeSeriesChart', 'samplingRatio', '%', sampledData.length / data.length * 100);
</del><ins>+        Instrumentation.reportMeasurement('TimeSeriesChart', 'samplingRatio', '%', sampledData.length() / view.length() * 100);
</ins><span class="cx"> 
</span><span class="cx">         return sampledData;
</span><span class="cx">     }
</span><span class="lines">@@ -624,6 +582,8 @@
</span><span class="cx">         var scale = window.devicePixelRatio;
</span><span class="cx">         canvas.width = newWidth * scale;
</span><span class="cx">         canvas.height = newHeight * scale;
</span><ins>+        canvas.style.width = newWidth + 'px';
+        canvas.style.height = newHeight + 'px';
</ins><span class="cx">         this._contextScaleX = scale;
</span><span class="cx">         this._contextScaleY = scale;
</span><span class="cx">         this._width = newWidth;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstimeseriesjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/time-series.js (212852 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js        2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/models/time-series.js        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -74,14 +74,148 @@
</span><span class="cx"> 
</span><span class="cx">     findPointAfterTime(time) { return this._data.find(function (point) { return point.time &gt;= time; }); }
</span><span class="cx"> 
</span><del>-    dataBetweenPoints(firstPoint, lastPoint)
</del><ins>+    viewBetweenPoints(firstPoint, lastPoint)
</ins><span class="cx">     {
</span><span class="cx">         console.assert(firstPoint.series == this);
</span><span class="cx">         console.assert(lastPoint.series == this);
</span><del>-        return this._data.slice(firstPoint.seriesIndex, lastPoint.seriesIndex + 1);
</del><ins>+        return new TimeSeriesView(this, firstPoint.seriesIndex, lastPoint.seriesIndex + 1);
</ins><span class="cx">     }
</span><del>-
</del><span class="cx"> };
</span><span class="cx"> 
</span><ins>+class TimeSeriesView {
+    constructor(timeSeries, startingIndex, afterEndingIndex, filteredData = null)
+    {
+        console.assert(timeSeries instanceof TimeSeries);
+        console.assert(startingIndex &lt;= afterEndingIndex);
+        console.assert(afterEndingIndex &lt;= timeSeries._data.length);
+        this._timeSeries = timeSeries;
+        this._data = filteredData || timeSeries._data;
+        this._values = null;
+        this._length = afterEndingIndex - startingIndex;
+        this._startingIndex = startingIndex;
+        this._afterEndingIndex = afterEndingIndex;
+        this._pointIndexMap = null;
+
+        if (this._data != timeSeries._data) {
+            this._findIndexForPoint = (point) =&gt; {
+                if (this._pointIndexMap == null)
+                    this._buildPointIndexMap();
+                return this._pointIndexMap.get(point);
+            }
+        } else
+            this._findIndexForPoint = (point) =&gt; { return point.seriesIndex; }
+    }
+
+    _buildPointIndexMap()
+    {
+        this._pointIndexMap = new Map;
+        const data = this._data;
+        const length = data.length;
+        for (let i = 0; i &lt; length; i++)
+            this._pointIndexMap.set(data[i], i);
+    }
+
+    length() { return this._length; }
+
+    firstPoint() { return this._length ? this._data[this._startingIndex] : null; }
+    lastPoint() { return this._length ? this._data[this._afterEndingIndex - 1] : null; }
+
+    nextPoint(point)
+    {
+        let index = this._findIndexForPoint(point);
+        index++;
+        if (index == this._afterEndingIndex)
+            return null;
+        return this._data[index];
+    }
+
+    previousPoint(point)
+    {
+        const index = this._findIndexForPoint(point);
+        if (index == this._startingIndex)
+            return null;
+        return this._data[index - 1];
+    }
+
+    findPointByIndex(index)
+    {
+        index += this._startingIndex;
+        if (index &lt; 0 || index &gt;= this._afterEndingIndex)
+            return null;
+        return this._data[index];
+    }
+
+    findById(id)
+    {
+        for (let point of this) {
+            if (point.id == id)
+                return point;
+        }
+        return null;
+    }
+
+    values()
+    {
+        if (this._values == null) {
+            this._values = new Array(this._length);
+            let i = 0;
+            for (let point of this)
+                this._values[i++] = point.value;
+        }
+        return this._values;
+    }
+
+    filter(callback)
+    {
+        const data = this._data;
+        const filteredData = [];
+        for (let i = this._startingIndex; i &lt; this._afterEndingIndex; i++) {
+            if (callback(data[i], i))
+                filteredData.push(data[i]);
+        }
+        return new TimeSeriesView(this._timeSeries, 0, filteredData.length, filteredData);
+    }
+
+    viewTimeRange(startTime, endTime)
+    {
+        const data = this._data;
+        let startingIndex = null;
+        let endingIndex = null;
+        for (let i = this._startingIndex; i &lt; this._afterEndingIndex; i++) {
+            if (startingIndex == null &amp;&amp; data[i].time &gt;= startTime)
+                startingIndex = i;
+            if (data[i].time &lt;= endTime)
+                endingIndex = i;
+        }
+        if (startingIndex == null || endingIndex == null)
+            return new TimeSeriesView(this._timeSeries, 0, 0, data);
+        return new TimeSeriesView(this._timeSeries, startingIndex, endingIndex + 1, data);
+    }
+
+    firstPointInTimeRange(startTime, endTime)
+    {
+        console.assert(startTime &lt;= endTime);
+        for (let point of this) {
+            if (point.time &gt; endTime)
+                return null;
+            if (point.time &gt;= startTime)
+                return point;
+        }
+        return null;
+    }
+
+    [Symbol.iterator]()
+    {
+        const data = this._data;
+        const end = this._afterEndingIndex;
+        let i = this._startingIndex;
+        return {
+            next() {
+                return {value: data[i], done: i++ == end};
+            }
+        };
+    }
+}
+
</ins><span class="cx"> if (typeof module != 'undefined')
</span><span class="cx">     module.exports.TimeSeries = TimeSeries;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (212852 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -24,11 +24,7 @@
</span><span class="cx"> 
</span><span class="cx">     selectedPoints()
</span><span class="cx">     {
</span><del>-        var selection = this._mainChart ? this._mainChart.currentSelection() : null;
-        if (!selection)
-            return null;
-
-        return this._mainChart.sampledDataBetween('current', selection[0], selection[1]);
</del><ins>+        return this._mainChart ? this._mainChart.selectedPoints('current') : null;
</ins><span class="cx">     }
</span><span class="cx"> }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagessummarypagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js (212852 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js        2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -364,13 +364,12 @@
</span><span class="cx">         if (!timeSeries.firstPoint())
</span><span class="cx">             return NaN;
</span><span class="cx"> 
</span><del>-        var startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
-        var afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
-        var endPoint = timeSeries.previousPoint(afterEndPoint);
</del><ins>+        const startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
+        const afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
+        let endPoint = timeSeries.previousPoint(afterEndPoint);
</ins><span class="cx">         if (!endPoint || startPoint == afterEndPoint)
</span><span class="cx">             endPoint = afterEndPoint;
</span><span class="cx"> 
</span><del>-        var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
-        return Statistics.median(points);
</del><ins>+        return Statistics.median(timeSeries.viewBetweenPoints(startPoint, endPoint).values());
</ins><span class="cx">     }
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunitteststimeseriestestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js (0 => 212853)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js        2017-02-22 22:07:39 UTC (rev 212853)
</span><span class="lines">@@ -0,0 +1,435 @@
</span><ins>+'use strict';
+
+const assert = require('assert');
+if (!assert.almostEqual)
+    assert.almostEqual = require('./resources/almost-equal.js');
+
+const MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI;
+require('../tools/js/v3-models.js');
+
+let threePoints;
+let fivePoints;
+beforeEach(() =&gt; {
+    threePoints = [
+        {id: 910, time: 101, value: 110},
+        {id: 902, time: 220, value: 102},
+        {id: 930, time: 303, value: 130},
+    ];
+    fivePoints = [...threePoints,
+        {id: 904, time: 400, value: 114},
+        {id: 950, time: 505, value: 105},
+        {id: 960, time: 600, value: 116},
+    ];
+});
+
+function addPointsToSeries(timeSeries, list = threePoints)
+{
+    for (let point of list)
+        timeSeries.append(point);
+}
+
+describe('TimeSeries', () =&gt; {
+
+    describe('length', () =&gt; {
+        it('should return the length', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.length(), 3);
+        });
+
+        it('should return 0 when there are no points', () =&gt; {
+            const timeSeries = new TimeSeries();
+            assert.equal(timeSeries.length(), 0);
+        });
+    });
+
+    describe('firstPoint', () =&gt; {
+        it('should return the first point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.firstPoint(), threePoints[0]);
+        });
+
+        it('should return null when there are no points', () =&gt; {
+            const timeSeries = new TimeSeries();
+            assert.equal(timeSeries.firstPoint(), null);
+        });
+    });
+
+    describe('lastPoint', () =&gt; {
+        it('should return the first point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.lastPoint(), threePoints[2]);
+        });
+
+        it('should return null when there are no points', () =&gt; {
+            const timeSeries = new TimeSeries();
+            assert.equal(timeSeries.lastPoint(), null);
+        });
+    });
+
+    describe('nextPoint', () =&gt; {
+        it('should return the next point when called on the first point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.nextPoint(threePoints[0]), threePoints[1]);
+        });
+
+        it('should return the next point when called on a mid-point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.nextPoint(threePoints[1]), threePoints[2]);
+        });
+
+        it('should return null when called on the last point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.nextPoint(threePoints[2]), null);
+        });
+    });
+
+    describe('previousPoint', () =&gt; {
+        it('should return null when called on the first point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.previousPoint(threePoints[0]), null);
+        });
+
+        it('should return the previous point when called on a mid-point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.previousPoint(threePoints[1]), threePoints[0]);
+        });
+
+        it('should return the previous point when called on the last point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.previousPoint(threePoints[2]), threePoints[1]);
+        });
+    });
+
+    describe('findPointByIndex', () =&gt; {
+        it('should return null the index is less than 0', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(-10), null);
+            assert.equal(timeSeries.findPointByIndex(-1), null);
+        });
+
+        it('should return null when the index is greater than or equal to the length', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(10), null);
+            assert.equal(timeSeries.findPointByIndex(3), null);
+        });
+
+        it('should return null when the index is not a number', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(undefined), null);
+            assert.equal(timeSeries.findPointByIndex(NaN), null);
+            assert.equal(timeSeries.findPointByIndex('a'), null);
+        });
+
+        it('should return the point at the specified index when it is in the valid range', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(0), threePoints[0]);
+            assert.equal(timeSeries.findPointByIndex(1), threePoints[1]);
+            assert.equal(timeSeries.findPointByIndex(2), threePoints[2]);
+        });
+    });
+
+    describe('findById', () =&gt; {
+        it('should return the point with the specified ID', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findById(threePoints[0].id), threePoints[0]);
+            assert.equal(timeSeries.findById(threePoints[1].id), threePoints[1]);
+            assert.equal(timeSeries.findById(threePoints[2].id), threePoints[2]);
+        });
+
+        it('should return null for a non-existent ID', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findById(null), null);
+            assert.equal(timeSeries.findById(undefined), null);
+            assert.equal(timeSeries.findById(NaN), null);
+            assert.equal(timeSeries.findById('a'), null);
+            assert.equal(timeSeries.findById(4231563246), null);
+        });
+    });
+
+    describe('findPointAfterTime', () =&gt; {
+        it('should return the point at the specified time', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[0].time), threePoints[0]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[1].time), threePoints[1]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[2].time), threePoints[2]);
+        });
+
+        it('should return the point after the specified time', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[0].time - 0.1), threePoints[0]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[1].time - 0.1), threePoints[1]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[2].time - 0.1), threePoints[2]);
+        });
+
+        it('should return null when there are no points after the specified time', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[2].time + 0.1), null);
+        });
+
+        it('should return the first point when there are multiple points at the specified time', () =&gt; {
+            const timeSeries = new TimeSeries();
+            const points = [
+                {id: 909, time:  99, value: 105},
+                {id: 910, time: 100, value: 110},
+                {id: 902, time: 100, value: 102},
+                {id: 930, time: 101, value: 130},
+            ];
+            addPointsToSeries(timeSeries, points);
+            assert.equal(timeSeries.findPointAfterTime(points[1].time), points[1]);
+        });
+    });
+
+    describe('viewBetweenPoints', () =&gt; {
+
+        it('should return a view between two points', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const view = timeSeries.viewBetweenPoints(fivePoints[1], fivePoints[3]);
+            assert.equal(view.length(), 3);
+            assert.equal(view.firstPoint(), fivePoints[1]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[1]), fivePoints[2]);
+            assert.equal(view.nextPoint(fivePoints[2]), fivePoints[3]);
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+
+            assert.equal(view.previousPoint(fivePoints[1]), null);
+            assert.equal(view.previousPoint(fivePoints[2]), fivePoints[1]);
+            assert.equal(view.previousPoint(fivePoints[3]), fivePoints[2]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[1]);
+            assert.equal(view.findPointByIndex(1), fivePoints[2]);
+            assert.equal(view.findPointByIndex(2), fivePoints[3]);
+            assert.equal(view.findPointByIndex(3), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), fivePoints[1]);
+            assert.equal(view.findById(fivePoints[2].id), fivePoints[2]);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[1].value, fivePoints[2].value, fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], fivePoints.slice(1, 4));
+        });
+
+        it('should return a view with exactly one point for when the starting point is identical to the ending point', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const view = timeSeries.viewBetweenPoints(fivePoints[2], fivePoints[2]);
+            assert.equal(view.length(), 1);
+            assert.equal(view.firstPoint(), fivePoints[2]);
+            assert.equal(view.lastPoint(), fivePoints[2]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[2]);
+            assert.equal(view.findPointByIndex(1), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), null);
+            assert.equal(view.findById(fivePoints[2].id), fivePoints[2]);
+            assert.equal(view.findById(fivePoints[3].id), null);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[2].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[2]]);
+        });
+
+    });
+
+});
+
+describe('TimeSeriesView', () =&gt; {
+
+    describe('filter', () =&gt; {
+        it('should create a filtered view', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const view = originalView.filter((point) =&gt; { return point == fivePoints[1] || point == fivePoints[3]; });
+
+            assert.equal(view.length(), 2);
+            assert.equal(view.firstPoint(), fivePoints[1]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[1]), fivePoints[3]);
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+
+            assert.equal(view.previousPoint(fivePoints[1]), null);
+            assert.equal(view.previousPoint(fivePoints[3]), fivePoints[1]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[1]);
+            assert.equal(view.findPointByIndex(1), fivePoints[3]);
+            assert.equal(view.findPointByIndex(2), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), fivePoints[1]);
+            assert.equal(view.findById(fivePoints[2].id), null);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[1].value, fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[1], fivePoints[3]]);
+        });
+    });
+
+    describe('viewTimeRange', () =&gt; {
+        it('should create a view filtered by the specified time range', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const view = originalView.viewTimeRange(fivePoints[1].time - 0.1, fivePoints[4].time - 0.1);
+
+            assert.equal(view.length(), 3);
+            assert.equal(view.firstPoint(), fivePoints[1]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[1]), fivePoints[2]);
+            assert.equal(view.nextPoint(fivePoints[2]), fivePoints[3]);
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+
+            assert.equal(view.previousPoint(fivePoints[1]), null);
+            assert.equal(view.previousPoint(fivePoints[2]), fivePoints[1]);
+            assert.equal(view.previousPoint(fivePoints[3]), fivePoints[2]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[1]);
+            assert.equal(view.findPointByIndex(1), fivePoints[2]);
+            assert.equal(view.findPointByIndex(2), fivePoints[3]);
+            assert.equal(view.findPointByIndex(3), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), fivePoints[1]);
+            assert.equal(view.findById(fivePoints[2].id), fivePoints[2]);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[1].value, fivePoints[2].value, fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], fivePoints.slice(1, 4));
+        });
+
+        it('should create a view filtered by the specified time range on a view already filtered by a time range', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const prefilteredView = originalView.viewTimeRange(fivePoints[1].time - 0.1, fivePoints[4].time - 0.1);
+            const view = prefilteredView.viewTimeRange(fivePoints[3].time - 0.1, fivePoints[3].time + 0.1);
+
+            assert.equal(view.length(), 1);
+            assert.equal(view.firstPoint(), fivePoints[3]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+            assert.equal(view.previousPoint(fivePoints[3]), null);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[3]);
+            assert.equal(view.findPointByIndex(1), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), null);
+            assert.equal(view.findById(fivePoints[2].id), null);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[3]]);
+        });
+
+        it('should create a view filtered by the specified time range on a view already filtered', () =&gt; {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const prefilteredView = originalView.filter((point) =&gt; { return point == fivePoints[1] || point == fivePoints[3]; });
+            const view = prefilteredView.viewTimeRange(fivePoints[3].time - 0.1, fivePoints[3].time + 0.1);
+
+            assert.equal(view.length(), 1);
+            assert.equal(view.firstPoint(), fivePoints[3]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+            assert.equal(view.previousPoint(fivePoints[3]), null);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[3]);
+            assert.equal(view.findPointByIndex(1), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), null);
+            assert.equal(view.findById(fivePoints[2].id), null);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[3]]);
+        });
+    });
+
+});
</ins></span></pre>
</div>
</div>

</body>
</html>