<!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 <rniwa@webkit.org>
+
+ 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 <rniwa@webkit.org>
</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 && data.length > 1) {
</del><ins>+ if (view && view.length() > 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 < 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 && diff > 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 && 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 && 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 <= point.time && point.time <= endTime; });
- }
-
- firstSampledPointBetweenTime(type, startTime, endTime)
- {
- var data = this.sampledTimeSeriesData(type);
- if (!data)
- return null;
- return data.find(function (point) { return startTime <= point.time && point.time <= 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) => {
+ 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 / <radius-of-points> 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() < 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) => {
+ if (excludedPoints.has(point.id))
+ return true;
+ let previousPoint = view.previousPoint(point) || point;
+ let nextPoint = view.nextPoint(point) || point;
+ return nextPoint.time - previousPoint.time >= 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 <= lastIndex) {
- var startPoint = data[i];
- var j;
- for (j = i; j <= lastIndex; j++) {
- var endPoint = data[j];
- if (excludedPoints.includes(endPoint.id)) {
- j--;
- break;
- }
- if (endPoint.time - startPoint.time >= timePerSample)
- break;
- }
- if (i < 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 >= 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 <= afterEndingIndex);
+ console.assert(afterEndingIndex <= 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) => {
+ if (this._pointIndexMap == null)
+ this._buildPointIndexMap();
+ return this._pointIndexMap.get(point);
+ }
+ } else
+ this._findIndexForPoint = (point) => { return point.seriesIndex; }
+ }
+
+ _buildPointIndexMap()
+ {
+ this._pointIndexMap = new Map;
+ const data = this._data;
+ const length = data.length;
+ for (let i = 0; i < 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 < 0 || index >= 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 < 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 < this._afterEndingIndex; i++) {
+ if (startingIndex == null && data[i].time >= startTime)
+ startingIndex = i;
+ if (data[i].time <= 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 <= endTime);
+ for (let point of this) {
+ if (point.time > endTime)
+ return null;
+ if (point.time >= 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(() => {
+ 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', () => {
+
+ describe('length', () => {
+ it('should return the length', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries);
+ assert.equal(timeSeries.length(), 3);
+ });
+
+ it('should return 0 when there are no points', () => {
+ const timeSeries = new TimeSeries();
+ assert.equal(timeSeries.length(), 0);
+ });
+ });
+
+ describe('firstPoint', () => {
+ it('should return the first point', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries);
+ assert.equal(timeSeries.firstPoint(), threePoints[0]);
+ });
+
+ it('should return null when there are no points', () => {
+ const timeSeries = new TimeSeries();
+ assert.equal(timeSeries.firstPoint(), null);
+ });
+ });
+
+ describe('lastPoint', () => {
+ it('should return the first point', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries);
+ assert.equal(timeSeries.lastPoint(), threePoints[2]);
+ });
+
+ it('should return null when there are no points', () => {
+ const timeSeries = new TimeSeries();
+ assert.equal(timeSeries.lastPoint(), null);
+ });
+ });
+
+ describe('nextPoint', () => {
+ it('should return the next point when called on the first point', () => {
+ 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', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries);
+ assert.equal(timeSeries.nextPoint(threePoints[1]), threePoints[2]);
+ });
+
+ it('should return null when called on the last point', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries);
+ assert.equal(timeSeries.nextPoint(threePoints[2]), null);
+ });
+ });
+
+ describe('previousPoint', () => {
+ it('should return null when called on the first point', () => {
+ 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', () => {
+ 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', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries);
+ assert.equal(timeSeries.previousPoint(threePoints[2]), threePoints[1]);
+ });
+ });
+
+ describe('findPointByIndex', () => {
+ it('should return null the index is less than 0', () => {
+ 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', () => {
+ 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', () => {
+ 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', () => {
+ 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', () => {
+ it('should return the point with the specified ID', () => {
+ 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', () => {
+ 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', () => {
+ it('should return the point at the specified time', () => {
+ 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', () => {
+ 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', () => {
+ 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', () => {
+ 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', () => {
+
+ it('should return a view between two points', () => {
+ 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', () => {
+ 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', () => {
+
+ describe('filter', () => {
+ it('should create a filtered view', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries, fivePoints);
+ const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+ const view = originalView.filter((point) => { 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', () => {
+ it('should create a view filtered by the specified time range', () => {
+ 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', () => {
+ 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', () => {
+ const timeSeries = new TimeSeries();
+ addPointsToSeries(timeSeries, fivePoints);
+ const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+ const prefilteredView = originalView.filter((point) => { 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>