<!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>[201564] 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/201564">201564</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-06-01 12:55:38 -0700 (Wed, 01 Jun 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>v3 UI should support marking and unmarking outliers as well as hiding them
https://bugs.webkit.org/show_bug.cgi?id=158248

Rubber-stamped by Chris Dumez.

Added the support for marking and unmarking a sequence of points as outliers. Unlike v2, we now support marking
multiple points as outliers in a single click. Also fixed a bug that outliers are never explicitly hidden in v3 UI.

This patch splits ChartStyles.createChartSourceList into two functions: resolveConfiguration and createSourceList
to separate the work of resolving platform and metric IDs to their respective model objects, and creating a source
list used by TimeSeriesChart to fetch measurement sets. createSourceList is called again when filtering options are
changed.

It also adds noCache option to TimeSeriesChart's fetchMeasurementSets, MeasurementSet's fetchBetween and
_fetchPrimaryCluster to update the measurement sets after marking or unmarking points as outliers. In addition, it
fixes a bug that the annotation bars for analysis tasks are not updated in charts page after creating an analysis
task by adding noCache option to ChartPaneBase's fetchAnalysisTasks, AnalysisTask's fetchByPlatformAndMetric and
_fetchSubset.

Finally, this patch splits ChartPane._makeAnchorToOpenPane into _makePopoverActionItem, _makePopoverOpenOnHover and
_setPopoverVisibility for clarity.

* public/v3/components/chart-pane-base.js:
(ChartPaneBase): Added _disableSampling and _showOutliers as instance variables.
(ChartPaneBase.prototype.configure):
(ChartPaneBase.prototype.isSamplingEnabled): Added.
(ChartPaneBase.prototype.setSamplingEnabled): Added. When a filtering option is updated, recreate the source list
so that TimeSeriesChart.setSourceList can re-fetch the measurement set JSONs.
(ChartPaneBase.prototype.isShowingOutliers): Added.
(ChartPaneBase.prototype.setShowOutliers): Added. Ditto for calling _updateSourceList.
(ChartPaneBase.prototype._updateSourceList): Added.
(ChartPaneBase.prototype.fetchAnalysisTasks): Renamed from _fetchAnalysisTasks. Now takes noCache as an argument
instead of platform and metric IDs since they're on instance variables.

* public/v3/components/chart-styles.js:
(ChartStyles.resolveConfiguration): Renamed from createChartSourceList. Just resolves platform and metric IDs.
(ChartStyles.createSourceList): Extracted from createChartSourceList since it needs to be called when a filtering
option is changed as well as when ChartPaneBase.prototype.configure is called.
(ChartStyles.baselineStyle): Now takes filtering options.
(ChartStyles.targetStyle): Ditto.
(ChartStyles.currentStyle): Ditto.

* public/v3/components/interactive-time-series-chart.js:
(InteractiveTimeSeriesChart.prototype.currentPoint): Find the point in _fetchedTimeSeries when
_sampledTimeSeriesData hasn't been computed yet as a fallback (e.g. when the chart hasn't been rendered yet).
(InteractiveTimeSeriesChart.prototype.selectedPoints): Added.
(InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Added.
(InteractiveTimeSeriesChart.prototype.lockedIndicator): Added. Returns the current point if it's locked.

* public/v3/components/time-series-chart.js:
(TimeSeriesChart.prototype.setDomain):
(TimeSeriesChart.prototype.setSourceList): Added. Re-create _fetchedTimeSeries when filtering options have changed.
Don't re-fetch measurement set JSONs here since showing outliers can be done entirely in the front end.
(TimeSeriesChart.prototype.fetchMeasurementSets): Extracted out of setDomain. Now takes noCache as an argument.
ChartPane._markAsOutlier
(TimeSeriesChart.prototype.firstSampledPointBetweenTime): Added.

* public/v3/models/analysis-task.js:
(AnalysisTask.fetchByPlatformAndMetric): Added noCache as an argument.
(AnalysisTask._fetchSubset): Ditto.

* public/v3/models/measurement-adaptor.js:
(MeasurementAdaptor.prototype.isOutlier): Added.
(MeasurementAdaptor.prototype.applyToAnalysisResults): Add markedOutlier as a property on each point.

* public/v3/models/measurement-cluster.js:
(MeasurementCluster.prototype.addToSeries): Fixed the bug that filtering outliers was broken as _markedOutlierIndex
is undefined here. Use MeasurementAdaptor's isOutlier instead.

* public/v3/models/measurement-set.js:
(MeasurementSet.prototype.fetchBetween): Added noCache as an argument. Reset _primaryClusterPromise and _allFetches
when noCache is true since we need to re-fetch the primary cluster as well as all secondary clusters now.
(MeasurementSet.prototype._fetchPrimaryCluster): Added noCache as an argument. Directly invoke the JSON API at
/api/measurement-set to re-generate all clusters' JSON files instead of first fetching the cached version.
(MeasurementSet.prototype._fetchSecondaryCluster):
(MeasurementSet.prototype._didFetchJSON): Removed a bogus assertion since this function is called on secondary
clusters as well as primary clusters.
(MeasurementSet.prototype._addFetchedCluster): Reimplemented this function using an insertion sort. Also remove the
existing entry if the fetch cluster should replace it.

* public/v3/models/time-series.js:
(TimeSeries.prototype.dataBetweenPoints): Removed the dead code to filter out outliers. This is done in addToSeries
of MeasurementCluster instead.

* public/v3/pages/chart-pane.js:
(ChartPane): Renamed pane to popover since it was confusing to have a pane inside a pane class. As such, renamed
_paneOpenedByClick to _lockedPopover.
(ChartPane.prototype.serializeState): Added the code to serialize filtering options in the serialized state URL.
(ChartPane.prototype.updateFromSerializedState): Ditto for parsing.
(ChartPane.prototype._analyzeRange): Extracted out of render(). Also fixed a bug that the charts page don't show
the newly created analysis task by invoking fetchAnalysisTasks with noCache set to true.
(ChartPane.prototype._markAsOutlier): Added.
(ChartPane.prototype._renderActionToolbar): A bunch of changes due to pane -&gt; popover rename. Also added a popover
for filtering options.
(ChartPane.prototype._makePopoverActionItem): Extracted from _makeAnchorToOpenPane.
(ChartPane.prototype._makePopoverOpenOnHover): Ditto.
(ChartPane.prototype._setPopoverVisibility): Ditto.
(ChartPane.prototype._renderFilteringPopover): Added.
(ChartPane.htmlTemplate): Added a popover for specifying filtering options. Also added .popover on each popover.
(ChartPane.cssTemplate): Updated the style to make use of .popover.

* public/v3/pages/charts-page.js:
(ChartsPage.prototype.graphOptionsDidChange): Added. Updates the URL state when a filtering option is modified.

* public/v3/pages/dashboard-page.js:
(DashboardPage.prototype._createChartForCell):

* public/v3/pages/page-router.js:
(PageRouter.prototype._serializeHashQueryValue): Serialize a set of strings as | separated tokens.
(PageRouter.prototype._deserializeHashQueryValue): Rewrote the function as the serialized URL can no longer be
parsed as a JSON as | separated tokens can't be converted into a valid JSON construct with a simple regex.

* unit-tests/measurement-set-tests.js: Added a test case for fetchBetween with noCache=true.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs">trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartstylesjs">trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsinteractivetimeserieschartjs">trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstimeserieschartjs">trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsanalysistaskjs">trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmeasurementadaptorjs">trunk/Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmeasurementclusterjs">trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmeasurementsetjs">trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelstimeseriesjs">trunk/Websites/perf.webkit.org/public/v3/models/time-series.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pageschartpanejs">trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pageschartspagejs">trunk/Websites/perf.webkit.org/public/v3/pages/charts-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesdashboardpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagespagerouterjs">trunk/Websites/perf.webkit.org/public/v3/pages/page-router.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsmeasurementsettestsjs">trunk/Websites/perf.webkit.org/unit-tests/measurement-set-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 (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -1,3 +1,119 @@
</span><ins>+2016-05-31  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        v3 UI should support marking and unmarking outliers as well as hiding them
+        https://bugs.webkit.org/show_bug.cgi?id=158248
+
+        Rubber-stamped by Chris Dumez.
+
+        Added the support for marking and unmarking a sequence of points as outliers. Unlike v2, we now support marking
+        multiple points as outliers in a single click. Also fixed a bug that outliers are never explicitly hidden in v3 UI.
+
+        This patch splits ChartStyles.createChartSourceList into two functions: resolveConfiguration and createSourceList
+        to separate the work of resolving platform and metric IDs to their respective model objects, and creating a source
+        list used by TimeSeriesChart to fetch measurement sets. createSourceList is called again when filtering options are
+        changed.
+
+        It also adds noCache option to TimeSeriesChart's fetchMeasurementSets, MeasurementSet's fetchBetween and
+        _fetchPrimaryCluster to update the measurement sets after marking or unmarking points as outliers. In addition, it
+        fixes a bug that the annotation bars for analysis tasks are not updated in charts page after creating an analysis
+        task by adding noCache option to ChartPaneBase's fetchAnalysisTasks, AnalysisTask's fetchByPlatformAndMetric and
+        _fetchSubset.
+
+        Finally, this patch splits ChartPane._makeAnchorToOpenPane into _makePopoverActionItem, _makePopoverOpenOnHover and
+        _setPopoverVisibility for clarity.
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase): Added _disableSampling and _showOutliers as instance variables.
+        (ChartPaneBase.prototype.configure):
+        (ChartPaneBase.prototype.isSamplingEnabled): Added.
+        (ChartPaneBase.prototype.setSamplingEnabled): Added. When a filtering option is updated, recreate the source list
+        so that TimeSeriesChart.setSourceList can re-fetch the measurement set JSONs.
+        (ChartPaneBase.prototype.isShowingOutliers): Added.
+        (ChartPaneBase.prototype.setShowOutliers): Added. Ditto for calling _updateSourceList.
+        (ChartPaneBase.prototype._updateSourceList): Added.
+        (ChartPaneBase.prototype.fetchAnalysisTasks): Renamed from _fetchAnalysisTasks. Now takes noCache as an argument
+        instead of platform and metric IDs since they're on instance variables.
+
+        * public/v3/components/chart-styles.js:
+        (ChartStyles.resolveConfiguration): Renamed from createChartSourceList. Just resolves platform and metric IDs.
+        (ChartStyles.createSourceList): Extracted from createChartSourceList since it needs to be called when a filtering
+        option is changed as well as when ChartPaneBase.prototype.configure is called.
+        (ChartStyles.baselineStyle): Now takes filtering options.
+        (ChartStyles.targetStyle): Ditto.
+        (ChartStyles.currentStyle): Ditto.
+
+        * public/v3/components/interactive-time-series-chart.js:
+        (InteractiveTimeSeriesChart.prototype.currentPoint): Find the point in _fetchedTimeSeries when
+        _sampledTimeSeriesData hasn't been computed yet as a fallback (e.g. when the chart hasn't been rendered yet).
+        (InteractiveTimeSeriesChart.prototype.selectedPoints): Added.
+        (InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Added.
+        (InteractiveTimeSeriesChart.prototype.lockedIndicator): Added. Returns the current point if it's locked.
+
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart.prototype.setDomain):
+        (TimeSeriesChart.prototype.setSourceList): Added. Re-create _fetchedTimeSeries when filtering options have changed.
+        Don't re-fetch measurement set JSONs here since showing outliers can be done entirely in the front end.
+        (TimeSeriesChart.prototype.fetchMeasurementSets): Extracted out of setDomain. Now takes noCache as an argument.
+        ChartPane._markAsOutlier
+        (TimeSeriesChart.prototype.firstSampledPointBetweenTime): Added.
+
+        * public/v3/models/analysis-task.js:
+        (AnalysisTask.fetchByPlatformAndMetric): Added noCache as an argument.
+        (AnalysisTask._fetchSubset): Ditto.
+
+        * public/v3/models/measurement-adaptor.js:
+        (MeasurementAdaptor.prototype.isOutlier): Added.
+        (MeasurementAdaptor.prototype.applyToAnalysisResults): Add markedOutlier as a property on each point.
+
+        * public/v3/models/measurement-cluster.js:
+        (MeasurementCluster.prototype.addToSeries): Fixed the bug that filtering outliers was broken as _markedOutlierIndex
+        is undefined here. Use MeasurementAdaptor's isOutlier instead.
+
+        * public/v3/models/measurement-set.js:
+        (MeasurementSet.prototype.fetchBetween): Added noCache as an argument. Reset _primaryClusterPromise and _allFetches
+        when noCache is true since we need to re-fetch the primary cluster as well as all secondary clusters now.
+        (MeasurementSet.prototype._fetchPrimaryCluster): Added noCache as an argument. Directly invoke the JSON API at
+        /api/measurement-set to re-generate all clusters' JSON files instead of first fetching the cached version.
+        (MeasurementSet.prototype._fetchSecondaryCluster):
+        (MeasurementSet.prototype._didFetchJSON): Removed a bogus assertion since this function is called on secondary
+        clusters as well as primary clusters.
+        (MeasurementSet.prototype._addFetchedCluster): Reimplemented this function using an insertion sort. Also remove the
+        existing entry if the fetch cluster should replace it.
+
+        * public/v3/models/time-series.js:
+        (TimeSeries.prototype.dataBetweenPoints): Removed the dead code to filter out outliers. This is done in addToSeries
+        of MeasurementCluster instead.
+
+        * public/v3/pages/chart-pane.js:
+        (ChartPane): Renamed pane to popover since it was confusing to have a pane inside a pane class. As such, renamed
+        _paneOpenedByClick to _lockedPopover.
+        (ChartPane.prototype.serializeState): Added the code to serialize filtering options in the serialized state URL.
+        (ChartPane.prototype.updateFromSerializedState): Ditto for parsing.
+        (ChartPane.prototype._analyzeRange): Extracted out of render(). Also fixed a bug that the charts page don't show
+        the newly created analysis task by invoking fetchAnalysisTasks with noCache set to true.
+        (ChartPane.prototype._markAsOutlier): Added.
+        (ChartPane.prototype._renderActionToolbar): A bunch of changes due to pane -&gt; popover rename. Also added a popover
+        for filtering options.
+        (ChartPane.prototype._makePopoverActionItem): Extracted from _makeAnchorToOpenPane.
+        (ChartPane.prototype._makePopoverOpenOnHover): Ditto.
+        (ChartPane.prototype._setPopoverVisibility): Ditto.
+        (ChartPane.prototype._renderFilteringPopover): Added.
+        (ChartPane.htmlTemplate): Added a popover for specifying filtering options. Also added .popover on each popover.
+        (ChartPane.cssTemplate): Updated the style to make use of .popover.
+
+        * public/v3/pages/charts-page.js:
+        (ChartsPage.prototype.graphOptionsDidChange): Added. Updates the URL state when a filtering option is modified.
+
+        * public/v3/pages/dashboard-page.js:
+        (DashboardPage.prototype._createChartForCell):
+
+        * public/v3/pages/page-router.js:
+        (PageRouter.prototype._serializeHashQueryValue): Serialize a set of strings as | separated tokens.
+        (PageRouter.prototype._deserializeHashQueryValue): Rewrote the function as the serialized URL can no longer be
+        parsed as a JSON as | separated tokens can't be converted into a valid JSON construct with a simple regex.
+
+        * unit-tests/measurement-set-tests.js: Added a test case for fetchBetween with noCache=true.
+
</ins><span class="cx"> 2016-05-24  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Another build fix after r201307.
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -10,6 +10,8 @@
</span><span class="cx">         this._metricId = null;
</span><span class="cx">         this._platform = null;
</span><span class="cx">         this._metric = null;
</span><ins>+        this._disableSampling = false;
+        this._showOutliers = false;
</ins><span class="cx"> 
</span><span class="cx">         this._overviewChart = null;
</span><span class="cx">         this._mainChart = null;
</span><span class="lines">@@ -20,7 +22,7 @@
</span><span class="cx"> 
</span><span class="cx">     configure(platformId, metricId)
</span><span class="cx">     {
</span><del>-        var result = ChartStyles.createChartSourceList(platformId, metricId);
</del><ins>+        var result = ChartStyles.resolveConfiguration(platformId, metricId);
</ins><span class="cx">         this._errorMessage = result.error;
</span><span class="cx">         this._platformId = platformId;
</span><span class="cx">         this._metricId = metricId;
</span><span class="lines">@@ -39,10 +41,11 @@
</span><span class="cx">         var formatter = result.metric.makeFormatter(4);
</span><span class="cx">         var self = this;
</span><span class="cx"> 
</span><ins>+        var sourceList = ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers);
+
</ins><span class="cx">         var overviewOptions = ChartStyles.overviewChartOptions(formatter);
</span><span class="cx">         overviewOptions.selection.onchange = this._overviewSelectionDidChange.bind(this);
</span><del>-
-        this._overviewChart = new InteractiveTimeSeriesChart(result.sourceList, overviewOptions);
</del><ins>+        this._overviewChart = new InteractiveTimeSeriesChart(sourceList, overviewOptions);
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-overview'), this._overviewChart);
</span><span class="cx"> 
</span><span class="cx">         var mainOptions = ChartStyles.mainChartOptions(formatter);
</span><span class="lines">@@ -51,7 +54,7 @@
</span><span class="cx">         mainOptions.selection.onzoom = this._mainSelectionDidZoom.bind(this);
</span><span class="cx">         mainOptions.annotations.onclick = this._openAnalysisTask.bind(this);
</span><span class="cx">         mainOptions.ondata = this._didFetchData.bind(this);
</span><del>-        this._mainChart = new InteractiveTimeSeriesChart(result.sourceList, mainOptions);
</del><ins>+        this._mainChart = new InteractiveTimeSeriesChart(sourceList, mainOptions);
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
</span><span class="cx"> 
</span><span class="cx">         this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, this._requestOpeningCommitViewer.bind(this));
</span><span class="lines">@@ -59,14 +62,35 @@
</span><span class="cx"> 
</span><span class="cx">         this.content().querySelector('.chart-pane').addEventListener('keyup', this._keyup.bind(this));
</span><span class="cx"> 
</span><del>-        this._fetchAnalysisTasks(platformId, metricId);
</del><ins>+        this.fetchAnalysisTasks(false);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _fetchAnalysisTasks(platformId, metricId)
</del><ins>+    isSamplingEnabled() { return !this._disableSampling; }
+    setSamplingEnabled(enabled)
</ins><span class="cx">     {
</span><ins>+        this._disableSampling = !enabled;
+        this._updateSourceList();
+    }
+
+    isShowingOutliers() { return this._showOutliers; }
+    setShowOutliers(show)
+    {
+        this._showOutliers = !!show;
+        this._updateSourceList();
+    }
+
+    _updateSourceList()
+    {
+        var sourceList = ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers);
+        this._mainChart.setSourceList(sourceList);
+        this._overviewChart.setSourceList(sourceList);
+    }
+
+    fetchAnalysisTasks(noCache)
+    {
</ins><span class="cx">         // FIXME: we need to update the annotation bars when the change type of tasks change.
</span><span class="cx">         var self = this;
</span><del>-        AnalysisTask.fetchByPlatformAndMetric(platformId, metricId).then(function (tasks) {
</del><ins>+        AnalysisTask.fetchByPlatformAndMetric(this._platformId, this._metricId, noCache).then(function (tasks) {
</ins><span class="cx">             self._tasksForAnnotations = tasks;
</span><span class="cx">             self.render();
</span><span class="cx">         });
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartstylesjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -1,6 +1,6 @@
</span><span class="cx"> 
</span><span class="cx"> class ChartStyles {
</span><del>-    static createChartSourceList(platformId, metricId)
</del><ins>+    static resolveConfiguration(platformId, metricId)
</ins><span class="cx">     {
</span><span class="cx">         var platform = Platform.findById(platformId);
</span><span class="cx">         var metric = Metric.findById(metricId);
</span><span class="lines">@@ -11,26 +11,35 @@
</span><span class="cx">         if (!lastModified)
</span><span class="cx">             return {platform: platform, metric: metric, error: `No results on ${platform.name()}`};
</span><span class="cx"> 
</span><del>-        var measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
-        var sourceList = [
-            this.baselineStyle(measurementSet, 'baseline'),
-            this.targetStyle(measurementSet, 'target'),
-            this.currentStyle(measurementSet, 'current'),
-        ];
-
</del><span class="cx">         return {
</span><span class="cx">             platform: platform,
</span><span class="cx">             metric: metric,
</span><del>-            sourceList: sourceList,
</del><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static baselineStyle(measurementSet)
</del><ins>+    static createSourceList(platform, metric, disableSampling, includeOutlier)
</ins><span class="cx">     {
</span><ins>+        console.assert(platform instanceof Platform);
+        console.assert(metric instanceof Metric);
+
+        var lastModified = platform.lastModified(metric);
+        console.assert(lastModified);
+
+        var measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
+        return [
+            this.baselineStyle(measurementSet, disableSampling, includeOutlier),
+            this.targetStyle(measurementSet, disableSampling, includeOutlier),
+            this.currentStyle(measurementSet, disableSampling, includeOutlier),
+        ];
+    }
+
+    static baselineStyle(measurementSet, disableSampling, includeOutlier)
+    {
</ins><span class="cx">         return {
</span><span class="cx">             measurementSet: measurementSet,
</span><span class="cx">             extendToFuture: true,
</span><del>-            sampleData: true,
</del><ins>+            sampleData: !disableSampling,
+            includeOutliers: includeOutlier,
</ins><span class="cx">             type: 'baseline',
</span><span class="cx">             pointStyle: '#f33',
</span><span class="cx">             pointRadius: 2,
</span><span class="lines">@@ -41,12 +50,13 @@
</span><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static targetStyle(measurementSet)
</del><ins>+    static targetStyle(measurementSet, disableSampling, includeOutlier)
</ins><span class="cx">     {
</span><span class="cx">         return {
</span><span class="cx">             measurementSet: measurementSet,
</span><span class="cx">             extendToFuture: true,
</span><del>-            sampleData: true,
</del><ins>+            sampleData: !disableSampling,
+            includeOutliers: includeOutlier,
</ins><span class="cx">             type: 'target',
</span><span class="cx">             pointStyle: '#33f',
</span><span class="cx">             pointRadius: 2,
</span><span class="lines">@@ -57,11 +67,12 @@
</span><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static currentStyle(measurementSet)
</del><ins>+    static currentStyle(measurementSet, disableSampling, includeOutlier)
</ins><span class="cx">     {
</span><span class="cx">         return {
</span><span class="cx">             measurementSet: measurementSet,
</span><del>-            sampleData: true,
</del><ins>+            sampleData: !disableSampling,
+            includeOutliers: includeOutlier,
</ins><span class="cx">             type: 'current',
</span><span class="cx">             pointStyle: '#333',
</span><span class="cx">             pointRadius: 2,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsinteractivetimeserieschartjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -18,13 +18,20 @@
</span><span class="cx"> 
</span><span class="cx">     currentPoint(diff)
</span><span class="cx">     {
</span><del>-        if (!this._sampledTimeSeriesData)
-            return null;
-
</del><span class="cx">         var id = this._indicatorID;
</span><span class="cx">         if (!id)
</span><span class="cx">             return null;
</span><span class="cx"> 
</span><ins>+        if (!this._sampledTimeSeriesData) {
+            this._ensureFetchedTimeSeries();
+            for (var series of this._fetchedTimeSeries) {
+                var point = series.findById(id);
+                if (point)
+                    return point;
+            }
+            return null;
+        }
+
</ins><span class="cx">         for (var data of this._sampledTimeSeriesData) {
</span><span class="cx">             if (!data)
</span><span class="cx">                 continue;
</span><span class="lines">@@ -40,6 +47,21 @@
</span><span class="cx"> 
</span><span class="cx">     currentSelection() { return this._selectionTimeRange; }
</span><span class="cx"> 
</span><ins>+    selectedPoints(type)
+    {
+        var selection = this._selectionTimeRange;
+        return selection ? this.sampledDataBetween(type, selection[0], selection[1]) : null;
+    }
+
+    firstSelectedPoint(type)
+    {
+        var selection = this._selectionTimeRange;
+        return selection ? this.firstSampledPointBetweenTime(type, selection[0], selection[1]) : null;
+    }
+
+    lockedIndicator() { return this._indicatorIsLocked ? this.currentPoint() : null; }
+
+
</ins><span class="cx">     setIndicator(id, shouldLock)
</span><span class="cx">     {
</span><span class="cx">         var selectionDidChange = !!this._sampledTimeSeriesData;
</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 (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -66,9 +66,20 @@
</span><span class="cx">         console.assert(startTime &lt; endTime, 'startTime must be before endTime');
</span><span class="cx">         this._startTime = startTime;
</span><span class="cx">         this._endTime = endTime;
</span><ins>+        this.fetchMeasurementSets(false);
+    }
+
+    setSourceList(sourceList)
+    {
+        this._sourceList = sourceList;
+        this.fetchMeasurementSets(false);
+    }
+
+    fetchMeasurementSets(noCache)
+    {
</ins><span class="cx">         for (var source of this._sourceList) {
</span><span class="cx">             if (source.measurementSet)
</span><del>-                source.measurementSet.fetchBetween(startTime, endTime, this._didFetchMeasurementSet.bind(this, source.measurementSet));
</del><ins>+                source.measurementSet.fetchBetween(this._startTime, this._endTime, this._didFetchMeasurementSet.bind(this, source.measurementSet), noCache);
</ins><span class="cx">         }
</span><span class="cx">         this._sampledTimeSeriesData = null;
</span><span class="cx">         this._valueRangeCache = null;
</span><span class="lines">@@ -103,6 +114,14 @@
</span><span class="cx">         return data.filter(function (point) { return startTime &lt;= point.time &amp;&amp; point.time &lt;= endTime; });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    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; });
+    }
+
</ins><span class="cx">     setAnnotations(annotations)
</span><span class="cx">     {
</span><span class="cx">         this._annotations = annotations;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsanalysistaskjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -166,9 +166,9 @@
</span><span class="cx">         return this._fetchSubset({buildRequest: id}).then(function (tasks) { return tasks[0]; });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static fetchByPlatformAndMetric(platformId, metricId)
</del><ins>+    static fetchByPlatformAndMetric(platformId, metricId, noCache)
</ins><span class="cx">     {
</span><del>-        return this._fetchSubset({platform: platformId, metric: metricId}).then(function (data) {
</del><ins>+        return this._fetchSubset({platform: platformId, metric: metricId}, noCache).then(function (data) {
</ins><span class="cx">             return AnalysisTask.findByPlatformAndMetric(platformId, metricId);
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="lines">@@ -198,11 +198,11 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static _fetchSubset(params)
</del><ins>+    static _fetchSubset(params, noCache)
</ins><span class="cx">     {
</span><span class="cx">         if (this._fetchAllPromise)
</span><span class="cx">             return this._fetchAllPromise;
</span><del>-        return this.cachedFetch('../api/analysis-tasks', params).then(this._constructAnalysisTasksFromRawData.bind(this));
</del><ins>+        return this.cachedFetch('../api/analysis-tasks', params, noCache).then(this._constructAnalysisTasksFromRawData.bind(this));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static fetchAll()
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmeasurementadaptorjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -28,6 +28,11 @@
</span><span class="cx">         return row[this._idIndex];
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    isOutlier(row)
+    {
+        return row[this._markedOutlierIndex];
+    }
+
</ins><span class="cx">     applyToAnalysisResults(row)
</span><span class="cx">     {
</span><span class="cx">         var adaptedRow = this.applyTo(row);
</span><span class="lines">@@ -50,6 +55,7 @@
</span><span class="cx">         var self = this;
</span><span class="cx">         return {
</span><span class="cx">             id: id,
</span><ins>+            markedOutlier: row[this._markedOutlierIndex],
</ins><span class="cx">             buildId: buildId,
</span><span class="cx">             metricId: null,
</span><span class="cx">             configType: null,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmeasurementclusterjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -21,7 +21,7 @@
</span><span class="cx">             var id = self._adaptor.extractId(row);
</span><span class="cx">             if (id in idMap)
</span><span class="cx">                 return;
</span><del>-            if (row[self._markedOutlierIndex] &amp;&amp; !includeOutliers)
</del><ins>+            if (self._adaptor.isOutlier(row) &amp;&amp; !includeOutliers)
</ins><span class="cx">                 return;
</span><span class="cx"> 
</span><span class="cx">             idMap[id] = true;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmeasurementsetjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -55,10 +55,14 @@
</span><span class="cx">         return clusters;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    fetchBetween(startTime, endTime, callback)
</del><ins>+    fetchBetween(startTime, endTime, callback, noCache)
</ins><span class="cx">     {
</span><del>-        if (!this._primaryClusterPromise)
-            this._primaryClusterPromise = this._fetchPrimaryCluster();
</del><ins>+        if (noCache) {
+            this._primaryClusterPromise = null;
+            this._allFetches = {};
+        }
+        if (!this._primaryClusterPromise || noCache)
+            this._primaryClusterPromise = this._fetchPrimaryCluster(noCache);
</ins><span class="cx">         var self = this;
</span><span class="cx">         this._primaryClusterPromise.catch(callback);
</span><span class="cx">         return this._primaryClusterPromise.then(function () {
</span><span class="lines">@@ -86,8 +90,16 @@
</span><span class="cx">         return url;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _fetchPrimaryCluster() {
</del><ins>+    _fetchPrimaryCluster(noCache)
+    {
</ins><span class="cx">         var self = this;
</span><ins>+        if (noCache) {
+            return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null)).then(function (data) {
+                self._didFetchJSON(true, data);
+                self._allFetches[self._primaryClusterEndTime] = self._primaryClusterPromise;
+            });
+        }
+
</ins><span class="cx">         return RemoteAPI.getJSONWithStatus(self._constructUrl(true, null)).then(function (data) {
</span><span class="cx">             if (+data['lastModified'] &lt; self._lastModified)
</span><span class="cx">                 return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
</span><span class="lines">@@ -102,7 +114,8 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _fetchSecondaryCluster(endTime) {
</del><ins>+    _fetchSecondaryCluster(endTime)
+    {
</ins><span class="cx">         var self = this;
</span><span class="cx">         return RemoteAPI.getJSONWithStatus(self._constructUrl(true, endTime)).then(function (data) {
</span><span class="cx">             self._didFetchJSON(false, data);
</span><span class="lines">@@ -111,8 +124,6 @@
</span><span class="cx"> 
</span><span class="cx">     _didFetchJSON(isPrimaryCluster, response, clusterEndTime)
</span><span class="cx">     {
</span><del>-        console.assert(isPrimaryCluster);
-
</del><span class="cx">         if (isPrimaryCluster) {
</span><span class="cx">             this._primaryClusterEndTime = response['endTime'];
</span><span class="cx">             this._clusterCount = response['clusterCount'];
</span><span class="lines">@@ -126,10 +137,14 @@
</span><span class="cx"> 
</span><span class="cx">     _addFetchedCluster(cluster)
</span><span class="cx">     {
</span><ins>+        for (var clusterIndex = 0; clusterIndex &lt; this._sortedClusters.length; clusterIndex++) {
+            var startTime = this._sortedClusters[clusterIndex].startTime();
+            if (cluster.startTime() &lt;= startTime) {
+                this._sortedClusters.splice(clusterIndex, startTime == cluster.startTime() ? 1 : 0, cluster);
+                return;
+            }
+        }
</ins><span class="cx">         this._sortedClusters.push(cluster);
</span><del>-        this._sortedClusters = this._sortedClusters.sort(function (c1, c2) {
-            return c1.startTime() - c2.startTime();
-        });
</del><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     hasFetchedRange(startTime, endTime)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstimeseriesjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/time-series.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/time-series.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -61,13 +61,9 @@
</span><span class="cx"> 
</span><span class="cx">     dataBetweenPoints(firstPoint, lastPoint)
</span><span class="cx">     {
</span><del>-        var data = this._data;
-        var filteredData = [];
-        for (var i = firstPoint.seriesIndex; i &lt;= lastPoint.seriesIndex; i++) {
-            if (!data[i].markedOutlier)
-                filteredData.push(data[i]);
-        }
-        return filteredData;
</del><ins>+        console.assert(firstPoint.series == this);
+        console.assert(lastPoint.series == this);
+        return this._data.slice(firstPoint.seriesIndex, lastPoint.seriesIndex + 1);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx"> };
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pageschartpanejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -6,7 +6,7 @@
</span><span class="cx"> 
</span><span class="cx">         this._mainChartIndicatorWasLocked = false;
</span><span class="cx">         this._chartsPage = chartsPage;
</span><del>-        this._paneOpenedByClick = null;
</del><ins>+        this._lockedPopover = null;
</ins><span class="cx"> 
</span><span class="cx">         this.content().querySelector('close-button').component().setCallback(chartsPage.closePane.bind(chartsPage, this));
</span><span class="cx"> 
</span><span class="lines">@@ -24,6 +24,16 @@
</span><span class="cx">             else if (this._mainChartIndicatorWasLocked &amp;&amp; currentPoint)
</span><span class="cx">                 state[2] = currentPoint.id;
</span><span class="cx">         }
</span><ins>+
+        var graphOptions = new Set;
+        if (!this.isSamplingEnabled())
+            graphOptions.add('noSampling');
+        if (this.isShowingOutliers())
+            graphOptions.add('showOutliers');
+
+        if (graphOptions.size)
+            state[3] = graphOptions;
+
</ins><span class="cx">         return state;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -40,6 +50,18 @@
</span><span class="cx">             this._mainChartIndicatorWasLocked = true;
</span><span class="cx">         } else
</span><span class="cx">             this._mainChart.setIndicator(null, false);
</span><ins>+
+        // FIXME: This forces sourceList to be set twice. First in configure inside the constructor then here.
+        var graphOptions = state[3];
+        if (graphOptions instanceof Set) {
+            this.setSamplingEnabled(!graphOptions.has('nosampling'));
+            this.setShowOutliers(graphOptions.has('showoutliers'));
+        }
+
+        // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
+        // FIXME: state[4] specifies moving average in v2 UI
+        // FIXME: state[5] specifies envelope in v2 UI
+        // FIXME: state[6] specifies change detection algorithm in v2 UI
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     setOverviewSelection(selection)
</span><span class="lines">@@ -90,6 +112,35 @@
</span><span class="cx">         super._indicatorDidChange(indicatorID, isLocked);
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _analyzeRange(pointsRangeForAnalysis)
+    {
+        var router = this._chartsPage.router();
+        var newWindow = window.open(router.url('analysis/task/create'), '_blank');
+
+        var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
+        var name = analyzePopover.querySelector('input').value;
+        var self = this;
+        AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
+            newWindow.location.href = router.url('analysis/task/' + data['taskId']);
+            self.fetchAnalysisTasks(true);
+            // FIXME: Refetch the list of analysis tasks.
+        }, function (error) {
+            newWindow.location.href = router.url('analysis/task/create', {error: error});
+        });
+    }
+
+    _markAsOutlier(markAsOutlier, points)
+    {
+        var self = this;
+        return Promise.all(points.map(function (point) {
+            return PrivilegedAPI.sendRequest('update-run-status', {'run': point.id, 'markedOutlier': markAsOutlier});
+        })).then(function () {
+            self._mainChart.fetchMeasurementSets(true /* noCache */);
+        }, function (error) {
+            alert('Failed to update the outlier status: ' + error);
+        }).catch();
+    }
+
</ins><span class="cx">     render()
</span><span class="cx">     {
</span><span class="cx">         if (this._platform &amp;&amp; this._metric) {
</span><span class="lines">@@ -122,115 +173,140 @@
</span><span class="cx">             })));
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        var platformPane = this.content().querySelector('.chart-pane-alternative-platforms');
</del><ins>+        var platformPopover = this.content().querySelector('.chart-pane-alternative-platforms');
</ins><span class="cx">         var alternativePlatforms = this._chartsPage.alternatePlatforms(platform, metric);
</span><span class="cx">         if (alternativePlatforms.length) {
</span><del>-            this.renderReplace(platformPane, Platform.sortByName(alternativePlatforms).map(function (platform) {
</del><ins>+            this.renderReplace(platformPopover, Platform.sortByName(alternativePlatforms).map(function (platform) {
</ins><span class="cx">                 return element('li', link(platform.label(), function () {
</span><span class="cx">                     self._chartsPage.insertPaneAfter(platform, metric, self);
</span><span class="cx">                 }));
</span><span class="cx">             }));
</span><span class="cx"> 
</span><del>-            actions.push(element('li', {class: this._paneOpenedByClick == platformPane ? 'selected' : ''},
-                this._makeAnchorToOpenPane(platformPane, 'Other Platforms', true)));
-        } else {
-            platformPane.style.display = 'none';
-        }
</del><ins>+            actions.push(this._makePopoverActionItem(platformPopover, 'Other Platforms', true));
+        } else
+            platformPopover.style.display = 'none';
</ins><span class="cx"> 
</span><del>-        var analyzePane = this.content().querySelector('.chart-pane-analyze-pane');
</del><ins>+        var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
</ins><span class="cx">         var pointsRangeForAnalysis = this._mainChartStatus.pointsRangeForAnalysis();
</span><span class="cx">         if (pointsRangeForAnalysis) {
</span><del>-            actions.push(element('li', {class: this._paneOpenedByClick == analyzePane ? 'selected' : ''},
-                this._makeAnchorToOpenPane(analyzePane, 'Analyze', false)));
-
-            var router = this._chartsPage.router();
-            analyzePane.onsubmit = function (event) {
</del><ins>+            actions.push(this._makePopoverActionItem(analyzePopover, 'Analyze', false));
+            analyzePopover.onsubmit = function (event) {
</ins><span class="cx">                 event.preventDefault();
</span><del>-                var newWindow = window.open(router.url('analysis/task/create'), '_blank');
-
-                var name = analyzePane.querySelector('input').value;
-                AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
-                    newWindow.location.href = router.url('analysis/task/' + data['taskId']);
-                    // FIXME: Refetch the list of analysis tasks.
-                }, function (error) {
-                    newWindow.location.href = router.url('analysis/task/create', {error: error});
-                });
</del><ins>+                self._analyzeRange(pointsRangeForAnalysis);
</ins><span class="cx">             }
</span><span class="cx">         } else {
</span><del>-            analyzePane.style.display = 'none';
-            analyzePane.onsubmit = function (event) { event.preventDefault(); }
</del><ins>+            analyzePopover.style.display = 'none';
+            analyzePopover.onsubmit = function (event) { event.preventDefault(); }
</ins><span class="cx">         }
</span><span class="cx"> 
</span><del>-        this._paneOpenedByClick = null;
</del><ins>+        var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
+        actions.push(this._makePopoverActionItem(filteringOptions, 'Filtering', true));
+
+        this._renderFilteringPopover();
+
+        this._lockedPopover = null;
</ins><span class="cx">         this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _makeAnchorToOpenPane(pane, label, shouldRespondToHover)
</del><ins>+    _makePopoverActionItem(popover, label, shouldRespondToHover)
</ins><span class="cx">     {
</span><del>-        var anchor = null;
-        var ignoreMouseLeave = false;
</del><span class="cx">         var self = this;
</span><del>-        var setPaneVisibility = function (pane, shouldShow) {
-            var anchor = pane.anchor;
-            if (shouldShow) {
-                var width = anchor.offsetParent.offsetWidth;
-                pane.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
-                pane.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
-            }
-            pane.style.display = shouldShow ? null : 'none';
-            anchor.parentNode.className = shouldShow ? 'selected' : '';
-            if (self._paneOpenedByClick == pane &amp;&amp; !shouldShow)
-                self._paneOpenedByClick = null;
</del><ins>+        popover.anchor = ComponentBase.createLink(label, function () {
+            var makeVisible = self._lockedPopover != popover;
+            self._setPopoverVisibility(popover, makeVisible);
+            if (makeVisible)
+                self._lockedPopover = popover;
+        });
+        if (shouldRespondToHover)
+            this._makePopoverOpenOnHover(popover);
+
+        return ComponentBase.createElement('li', {class: this._lockedPopover == popover ? 'selected' : ''}, popover.anchor);
+    }
+
+    _makePopoverOpenOnHover(popover)
+    {
+        var mouseIsInAnchor = false;
+        var mouseIsInPopover = false;
+
+        var self = this;
+        var closeIfNeeded = function () {
+            setTimeout(function () {
+                if (self._lockedPopover != popover &amp;&amp; !mouseIsInAnchor &amp;&amp; !mouseIsInPopover)
+                    self._setPopoverVisibility(popover, false);
+            }, 0);
</ins><span class="cx">         }
</span><span class="cx"> 
</span><del>-        var attributes = {
-            href: '#',
-            onclick: function (event) {
-                event.preventDefault();
-                var shouldShowPane = pane.style.display == 'none';
-                if (shouldShowPane) {
-                    if (self._paneOpenedByClick)
-                        setPaneVisibility(self._paneOpenedByClick, false);
-                    self._paneOpenedByClick = pane;
-                }
-                setPaneVisibility(pane, shouldShowPane);
-            },
-        };
-        if (shouldRespondToHover) {
-            var mouseIsInAnchor = false;
-            var mouseIsInPane = false;
</del><ins>+        popover.anchor.onmouseenter = function () {
+            if (self._lockedPopover)
+                return;
+            mouseIsInAnchor = true;
+            self._setPopoverVisibility(popover, true);
+        }
+        popover.anchor.onmouseleave = function () {
+            mouseIsInAnchor = false;
+            closeIfNeeded();         
+        }
</ins><span class="cx"> 
</span><del>-            attributes.onmouseenter = function () {
-                if (self._paneOpenedByClick)
-                    return;
-                mouseIsInAnchor = true;
-                setPaneVisibility(pane, true);
-            }
-            attributes.onmouseleave = function () {
-                setTimeout(function () {
-                    if (!mouseIsInPane)
-                        setPaneVisibility(pane, false);
-                }, 0);
-                mouseIsInAnchor = false;                
-            }
</del><ins>+        popover.onmouseenter = function () {
+            mouseIsInPopover = true;
+        }
+        popover.onmouseleave = function () {
+            mouseIsInPopover = false;
+            closeIfNeeded();
+        }
+    }
</ins><span class="cx"> 
</span><del>-            pane.onmouseleave = function () {
-                setTimeout(function () {
-                    if (!mouseIsInAnchor)
-                        setPaneVisibility(pane, false);
-                }, 0);
-                mouseIsInPane = false;
-            }
-            pane.onmouseenter = function () {
-                mouseIsInPane = true;
-            }
</del><ins>+    _setPopoverVisibility(popover, visible)
+    {
+        var anchor = popover.anchor;
+        if (visible) {
+            var width = anchor.offsetParent.offsetWidth;
+            popover.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
+            popover.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
</ins><span class="cx">         }
</span><ins>+        popover.style.display = visible ? null : 'none';
+        anchor.parentNode.className = visible ? 'selected' : '';
</ins><span class="cx"> 
</span><del>-        var anchor = ComponentBase.createElement('a', attributes, label);
-        pane.anchor = anchor;
-        return anchor;
</del><ins>+        if (this._lockedPopover &amp;&amp; this._lockedPopover != popover &amp;&amp; visible)
+            this._setPopoverVisibility(this._lockedPopover, false);
+
+        if (this._lockedPopover == popover &amp;&amp; !visible)
+            this._lockedPopover = null;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _renderFilteringPopover()
+    {
+        var enableSampling = this.content().querySelector('.enable-sampling');
+        enableSampling.checked = this.isSamplingEnabled();
+        enableSampling.onchange = function () {
+            self.setSamplingEnabled(enableSampling.checked);
+            self._chartsPage.graphOptionsDidChange();
+        }
+
+        var showOutliers = this.content().querySelector('.show-outliers');
+        showOutliers.checked = this.isShowingOutliers();
+        showOutliers.onchange = function () {
+            self.setShowOutliers(showOutliers.checked);
+            self._chartsPage.graphOptionsDidChange();
+        }
+
+        var markAsOutlierButton = this.content().querySelector('.mark-as-outlier');
+        var firstSelectedPoint = this._mainChart.lockedIndicator();
+        if (!firstSelectedPoint)
+            firstSelectedPoint = this._mainChart.firstSelectedPoint('current');
+        var alreayMarkedAsOutlier = firstSelectedPoint &amp;&amp; firstSelectedPoint.markedOutlier;
+
+        var self = this;
+        markAsOutlierButton.textContent = (alreayMarkedAsOutlier ? 'Unmark' : 'Mark') + ' selected points as outlier';
+        markAsOutlierButton.onclick = function () {
+            var selectedPoints = [firstSelectedPoint];
+            if (self._mainChart.currentSelection('current'))
+                selectedPoints = self._mainChart.selectedPoints('current');
+            self._markAsOutlier(!alreayMarkedAsOutlier, selectedPoints);
+        }
+        markAsOutlierButton.disabled = !firstSelectedPoint;
+    }
+
</ins><span class="cx">     static paneHeaderTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><span class="lines">@@ -241,11 +317,16 @@
</span><span class="cx">                         &lt;li class=&quot;close&quot;&gt;&lt;close-button&gt;&lt;/close-button&gt;&lt;/li&gt;
</span><span class="cx">                     &lt;/ul&gt;
</span><span class="cx">                     &lt;ul class=&quot;chart-pane-action-buttons buttoned-toolbar&quot;&gt;&lt;/ul&gt;
</span><del>-                    &lt;ul class=&quot;chart-pane-alternative-platforms&quot; style=&quot;display:none&quot;&gt;&lt;/ul&gt;
-                    &lt;form class=&quot;chart-pane-analyze-pane&quot; style=&quot;display:none&quot;&gt;
</del><ins>+                    &lt;ul class=&quot;chart-pane-alternative-platforms popover&quot; style=&quot;display:none&quot;&gt;&lt;/ul&gt;
+                    &lt;form class=&quot;chart-pane-analyze-popover popover&quot; style=&quot;display:none&quot;&gt;
</ins><span class="cx">                         &lt;input type=&quot;text&quot; required&gt;
</span><span class="cx">                         &lt;button&gt;Create&lt;/button&gt;
</span><span class="cx">                     &lt;/form&gt;
</span><ins>+                    &lt;ul class=&quot;chart-pane-filtering-options popover&quot; style=&quot;display:none&quot;&gt;
+                        &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; class=&quot;enable-sampling&quot;&gt;Sampling&lt;/label&gt;&lt;/li&gt;
+                        &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; class=&quot;show-outliers&quot;&gt;Show outliers&lt;/label&gt;&lt;/li&gt;
+                        &lt;li&gt;&lt;button class=&quot;mark-as-outlier&quot;&gt;Mark selected points as outlier&lt;/button&gt;&lt;/li&gt;
+                    &lt;/ul&gt;
</ins><span class="cx">                 &lt;/nav&gt;
</span><span class="cx">             &lt;/header&gt;
</span><span class="cx">         `;
</span><span class="lines">@@ -309,8 +390,7 @@
</span><span class="cx">                 line-height: 0.9rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-actions .chart-pane-alternative-platforms,
-            .chart-pane-analyze-pane {
</del><ins>+            .chart-pane-actions .popover {
</ins><span class="cx">                 position: absolute;
</span><span class="cx">                 top: 0;
</span><span class="cx">                 right: 0;
</span><span class="lines">@@ -325,10 +405,10 @@
</span><span class="cx">                 margin-right: -0.2rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-alternative-platforms li {
</del><ins>+            .chart-pane-actions .popover li {
</ins><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-alternative-platforms li a {
</del><ins>+            .chart-pane-actions .popover li a {
</ins><span class="cx">                 display: block;
</span><span class="cx">                 text-decoration: none;
</span><span class="cx">                 color: inherit;
</span><span class="lines">@@ -336,16 +416,20 @@
</span><span class="cx">                 padding: 0.2rem 0.5rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-alternative-platforms a:hover,
-            .chart-pane-analyze-pane input:focus {
</del><ins>+            .chart-pane-actions .popover a:hover,
+            .chart-pane-actions .popover input:focus {
</ins><span class="cx">                 background: rgba(204, 153, 51, 0.1);
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-analyze-pane {
</del><ins>+            .chart-pane-actions .chart-pane-analyze-popover {
</ins><span class="cx">                 padding: 0.5rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .chart-pane-analyze-pane input {
</del><ins>+            .chart-pane-actions .popover label {
+                font-size: 0.9rem;
+            }
+
+            .chart-pane-actions .popover input[type=text] {
</ins><span class="cx">                 font-size: 1rem;
</span><span class="cx">                 width: 15rem;
</span><span class="cx">                 outline: none;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pageschartspagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/charts-page.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/charts-page.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/charts-page.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -185,6 +185,11 @@
</span><span class="cx">             this.scheduleUrlStateUpdate();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    graphOptionsDidChange(pane)
+    {
+        this.scheduleUrlStateUpdate();
+    }
+
</ins><span class="cx">     setOpenRepository(repository)
</span><span class="cx">     {
</span><span class="cx">         this._currentRepositoryId = repository ? repository.id() : null;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesdashboardpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -125,13 +125,13 @@
</span><span class="cx">         if (!platformId || !metricId)
</span><span class="cx">             return '';
</span><span class="cx"> 
</span><del>-        var result = ChartStyles.createChartSourceList(platformId, metricId);
</del><ins>+        var result = ChartStyles.resolveConfiguration(platformId, metricId);
</ins><span class="cx">         if (result.error)
</span><span class="cx">             return result.error;
</span><span class="cx"> 
</span><span class="cx">         var options = ChartStyles.dashboardOptions(result.metric.makeFormatter(3));
</span><span class="cx">         options.ondata = this._fetchedData.bind(this);
</span><del>-        var chart = new TimeSeriesChart(result.sourceList, options);
</del><ins>+        var chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false), options);
</ins><span class="cx">         this._charts.push(chart);
</span><span class="cx"> 
</span><span class="cx">         var statusView = new ChartStatusView(result.metric, chart);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagespagerouterjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/page-router.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/page-router.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/page-router.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -127,34 +127,50 @@
</span><span class="cx"> 
</span><span class="cx">     _serializeHashQueryValue(value)
</span><span class="cx">     {
</span><del>-        if (!(value instanceof Array)) {
-            console.assert(value === null || typeof(value) === 'number' || /[A-Za-z0-9]*/.test(value));
-            return value === null ? 'null' : value;
</del><ins>+        if (value instanceof Array) {
+            var serializedItems = [];
+            for (var item of value)
+                serializedItems.push(this._serializeHashQueryValue(item));
+            return '(' + serializedItems.join('-') + ')';
</ins><span class="cx">         }
</span><del>-
-        var serializedItems = [];
-        for (var item of value)
-            serializedItems.push(this._serializeHashQueryValue(item));
-        return '(' + serializedItems.join('-') + ')';
</del><ins>+        if (value instanceof Set)
+            return Array.from(value).sort().join('|');
+        console.assert(value === null || value === undefined || typeof(value) === 'number' || /[0-9]*/.test(value));
+        return value === null || value === undefined ? 'null' : value;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _deserializeHashQueryValue(value)
</span><span class="cx">     {
</span><del>-        var json = value.replace(/\(/g, '[').replace(/\)/g, ']').replace(/-/g, ',');
-        try {
-            return JSON.parse(json);
-        } catch (error) {
-
-            // Some applications don't linkify two consecutive closing parentheses: )).
-            // Try fixing adding one extra parenthesis to see if that works.
-            var missingClosingBrackets = this._countOccurrences(json, /\[/g) - this._countOccurrences(json, /\]/g);
-            var fix = new Array(missingClosingBrackets).fill(']').join('');
-            try {
-                return JSON.parse(json + fix);
-            } catch (newError) { }
-
-            return value;
</del><ins>+        if (value.charAt(0) == '(') {
+            var nestingLevel = 0;
+            var end = 0;
+            var start = 1;
+            var result = [];
+            for (var character of value) {
+                if (character == '(')
+                    nestingLevel++;
+                else if (character == ')') {
+                    nestingLevel--;
+                    if (!nestingLevel)
+                        break;
+                } else if (nestingLevel == 1 &amp;&amp; character == '-') {
+                    result.push(this._deserializeHashQueryValue(value.substring(start, end)));
+                    start = end + 1;
+                }
+                end++;
+            }
+            result.push(this._deserializeHashQueryValue(value.substring(start, end)));
+            return result;
</ins><span class="cx">         }
</span><ins>+        if (value == 'true')
+            return true;
+        if (value == 'false')
+            return true;
+        if (value.match(/^[0-9\.]+$/))
+            return parseFloat(value);
+        if (value.match(/^[A-Za-z][A-Za-z0-9|]*$/))
+            return new Set(value.toLowerCase().split('|'));
+        return null;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _countOccurrences(string, regex)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsmeasurementsettestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js (201563 => 201564)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js        2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js        2016-06-01 19:55:38 UTC (rev 201564)
</span><span class="lines">@@ -290,6 +290,100 @@
</span><span class="cx">             });
</span><span class="cx">         });
</span><span class="cx"> 
</span><ins>+        it('should request the uncached primary cluster when noCache is true', function (done) {
+            var set = MeasurementSet.findSet(1, 1, 3000);
+            var callCount = 0;
+            set.fetchBetween(1000, 3000, function () {
+                callCount++;
+            });
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '../data/measurement-set-1-1.json');
+
+            requests[0].resolve({
+                'clusterStart': 1000,
+                'clusterSize': 1000,
+                'formatMap': [],
+                'configurations': {current: []},
+                'startTime': 2000,
+                'endTime': 3000,
+                'lastModified': 3000,
+                'clusterCount': 2,
+                'status': 'OK'});
+
+            var noCacheFetchCount = 0;
+            waitForMeasurementSet().then(function () {
+                assert.equal(callCount, 1);
+                assert.equal(noCacheFetchCount, 0);
+                assert.equal(set._sortedClusters.length, 1);
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].url, '../data/measurement-set-1-1-2000.json');
+
+                requests[1].resolve({
+                    'clusterStart': 1000,
+                    'clusterSize': 1000,
+                    'formatMap': [],
+                    'configurations': {current: []},
+                    'startTime': 1000,
+                    'endTime': 2000,
+                    'lastModified': 3000,
+                    'clusterCount': 2,
+                    'status': 'OK'});
+
+                set.fetchBetween(1000, 3000, function () {
+                    noCacheFetchCount++;
+                }, true /* noCache */);
+
+                return waitForMeasurementSet();
+            }).then(function () {
+                assert.equal(callCount, 2);
+                assert.equal(noCacheFetchCount, 0);
+                assert.equal(set._sortedClusters.length, 2);
+                assert.equal(requests.length, 3);
+                assert.equal(requests[2].url, '../api/measurement-set?platform=1&amp;metric=1');
+
+                requests[2].resolve({
+                    'clusterStart': 1000,
+                    'clusterSize': 1000,
+                    'formatMap': [],
+                    'configurations': {current: []},
+                    'startTime': 2000,
+                    'endTime': 3000,
+                    'lastModified': 3000,
+                    'clusterCount': 2,
+                    'status': 'OK'});
+
+                return waitForMeasurementSet();
+            }).then(function () {
+                assert.equal(callCount, 2);
+                assert.equal(noCacheFetchCount, 1);
+                assert.equal(set._sortedClusters.length, 2);
+                assert.equal(requests.length, 4);
+                assert.equal(requests[3].url, '../data/measurement-set-1-1-2000.json');
+
+                requests[3].resolve({
+                    'clusterStart': 1000,
+                    'clusterSize': 1000,
+                    'formatMap': [],
+                    'configurations': {current: []},
+                    'startTime': 1000,
+                    'endTime': 2000,
+                    'lastModified': 3000,
+                    'clusterCount': 2,
+                    'status': 'OK'});
+
+                return waitForMeasurementSet();
+            }).then(function () {
+                assert.equal(callCount, 2);
+                assert.equal(noCacheFetchCount, 2);
+                assert.equal(set._sortedClusters.length, 2);
+                assert.equal(requests.length, 4);
+
+                done();
+            }).catch(function (error) {
+                done(error);
+            });
+        });
+
</ins><span class="cx">         it('should not request the primary cluster twice when multiple clients request it but should invoke all callbacks', function (done) {
</span><span class="cx">             var set = MeasurementSet.findSet(1, 1, 3000);
</span><span class="cx">             var callCount = 0;
</span></span></pre>
</div>
</div>

</body>
</html>