Diff
Modified: trunk/Websites/perf.webkit.org/ChangeLog (179877 => 179878)
--- trunk/Websites/perf.webkit.org/ChangeLog 2015-02-10 20:27:40 UTC (rev 179877)
+++ trunk/Websites/perf.webkit.org/ChangeLog 2015-02-10 21:39:41 UTC (rev 179878)
@@ -1,3 +1,82 @@
+2015-02-10 Ryosuke Niwa <rn...@webkit.org>
+
+ New perf dashboard should have the ability to overlay moving average with an envelope
+ https://bugs.webkit.org/show_bug.cgi?id=141438
+
+ Reviewed by Andreas Kling.
+
+ This patch adds three kinds of moving average strategies and two kinds of enveloping strategies:
+
+ Simple Moving Average - The moving average x̄_i of x_i is computed as the arithmetic mean of values
+ from x_(i - n) though x_(i + m) where n is a non-negative integer and m is a positive integer. It takes
+ n, backward window size, and m, forward window size, as an argument.
+
+ Cumulative Moving Average - x̄_i is computed as the arithmetic mean of all values x_0 though x_i.
+ That is, x̄_1 = x_1 and x̄_i = ((i - 1) * M_(i - 1) + x_i) / i for all i > 1.
+
+ Exponential Moving Average - x̄_i is computed as the weighted average of x_i and x̄_(i - 1) with α as
+ an argument specifying x_i's weight. To be precise, x̄_1 = x_1 and x̄_i = α * x_i + (α - 1) x̄_(i-1).
+
+
+ Average Difference - The enveloping delta d is computed as the arithmetic mean of the difference
+ between each x_i and x̄_i.
+
+ Moving Average Standard Deviation - d is computed like the standard deviation except the deviation
+ for each term is measured from the moving average instead of the sample arithmetic mean. i.e. it uses
+ the average of (x_i - x̄_i)^2 as the "sample variance" instead of the conventional (x_i - x̄)^2 where
+ x̄ is the sample mean of all x_i's. This change was necessary since our time series is non-stationary.
+
+
+ Each strategy is cloned for an App.Pane instance so that its parameterList can be configured per pane.
+ The configuration of the currently chosen strategies is saved in the query string for convenience.
+
+ Also added the "stat pane" to choose a moving average strategy and a enveloping strategy in each pane.
+
+ * public/v2/app.css: Specify the fill color for all SVG groups in the pane toolbar icons.
+
+ * public/v2/app.js:
+ (App.Pane._fetch): Delegate the creation of 'chartData' to _computeChartData.
+ (App.Pane.updateStatisticsTools): Added. Clones moving average and enveloping strategies for this pane.
+ (App.Pane._cloneStrategy): Added. Clones a strategy for a new pane.
+ (App.Pane._configureStrategy): Added. Finds and configures a strategy from the configuration retrieved
+ from the query string via ChartsController.
+ (App.Pane._computeChartData): Added. Creates chartData from fetchedData.
+ (App.Pane._computeMovingAverage): Added. Computes the moving average and the envelope.
+ (App.Pane._executeStrategy): Added.
+ (App.Pane._updateStrategyConfigIfNeeded): Pushes the strategy configurations to the query string via
+ ChartsController.
+ (App.ChartsController._parsePaneList): Merged the query string arguments for the range and point
+ selections, and added two new arguments for the moving average and the enveloping configurations.
+ (App.ChartsController._serializePaneList): Ditto.
+ (App.ChartsController._scheduleQueryStringUpdate): Observes strategy configurations.
+ (App.PaneController.actions.toggleBugsPane): Hides the stat pane.
+ (App.PaneController.actions.toggleSearchPane): Hides the stat pane.
+ (App.PaneController.actions.toggleStatPane): Added.
+
+ * public/v2/chart-pane.css: Added CSS rules for the new stat pane. Also added .foreground class for the
+ current (as opposed to baseline and target) time series for when it's the most foreground graph without
+ moving average and its envelope overlapping on top of it.
+
+ * public/v2/index.html: Added the templates for the stat pane and the corresponding icon (Σ).
+
+ * public/v2/interactive-chart.js:
+ (App.InteractiveChartComponent.chartDataDidChange): Unset _totalWidth and _totalHeight to avoid exiting
+ early inside _updateDimensionsIfNeeded when chartData changes after the initial layout.
+ (App.InteractiveChartComponent.didInsertElement): Attach event listeners here instead of inside
+ _constructGraphIfPossible since that could be called multiple times on the same SVG element.
+ (App.InteractiveChartComponent._constructGraphIfPossible): Clear down the old SVG element we created
+ but don't bother removing individual paths and circles. Added the code to show the moving average time
+ series when there is one. Also add "foreground" class on SVG elements for the current time series when
+ we're not showing the moving average. chart-pane.css has been updated to "dim down" the current time
+ series when "foreground" is not set.
+ (App.InteractiveChartComponent._minMaxForAllTimeSeries): Take the moving average time series into
+ account when computing the y-axis range.
+ (App.InteractiveChartComponent._brushChanged): Removed 'selectionIsLocked' argument as it's useless.
+
+ * public/v2/js/statistics.js:
+ (Statistics.MovingAverageStrategies): Added.
+ (Statistics.EnvelopingStrategies): Added.
+
2015-02-06 Ryosuke Niwa <rn...@webkit.org>
The delta value in the chart pane sometimes doens't show '+' for a positive delta
Modified: trunk/Websites/perf.webkit.org/public/v2/app.css (179877 => 179878)
--- trunk/Websites/perf.webkit.org/public/v2/app.css 2015-02-10 20:27:40 UTC (rev 179877)
+++ trunk/Websites/perf.webkit.org/public/v2/app.css 2015-02-10 21:39:41 UTC (rev 179878)
@@ -121,12 +121,15 @@
}
.icon-button g {
stroke: #ccc;
+ fill: #ccc;
}
.icon-button:hover g {
stroke: #666;
+ fill: #666;
}
.disabled .icon-button:hover g {
stroke: #ccc;
+ fill: #ccc;
}
Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (179877 => 179878)
--- trunk/Websites/perf.webkit.org/public/v2/app.js 2015-02-10 20:27:40 UTC (rev 179877)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js 2015-02-10 21:39:41 UTC (rev 179878)
@@ -351,7 +351,8 @@
App.Manifest.fetchRunsWithPlatformAndMetric(this.get('store'), platformId, metricId).then(function (result) {
self.set('platform', result.platform);
self.set('metric', result.metric);
- self.set('chartData', App.createChartData(result));
+ self.set('fetchedData', result);
+ self._computeChartData();
}, function (result) {
if (!result || typeof(result) === "string")
self.set('failure', 'Failed to fetch the JSON with an error: ' + result);
@@ -431,6 +432,100 @@
return this.computeStatus(lastPoint, chartData.current.previousPoint(lastPoint));
}.property('chartData'),
+ updateStatisticsTools: function ()
+ {
+ var movingAverageStrategies = Statistics.MovingAverageStrategies.map(this._cloneStrategy.bind(this));
+ this.set('movingAverageStrategies', [{label: 'None'}].concat(movingAverageStrategies));
+ this.set('chosenMovingAverageStrategy', this._configureStrategy(movingAverageStrategies, this.get('movingAverageConfig')));
+
+ var envelopingStrategies = Statistics.EnvelopingStrategies.map(this._cloneStrategy.bind(this));
+ this.set('envelopingStrategies', [{label: 'None'}].concat(envelopingStrategies));
+ this.set('chosenEnvelopingStrategy', this._configureStrategy(envelopingStrategies, this.get('envelopingConfig')));
+ }.on('init'),
+ _cloneStrategy: function (strategy)
+ {
+ var parameterList = (strategy.parameterList || []).map(function (param) { return Ember.Object.create(param); });
+ return Ember.Object.create({
+ id: strategy.id,
+ label: strategy.label,
+ description: strategy.description,
+ parameterList: parameterList,
+ execute: strategy.execute,
+ });
+ },
+ _configureStrategy: function (strategies, config)
+ {
+ if (!config || !config[0])
+ return null;
+
+ var id = config[0];
+ var chosenStrategy = strategies.find(function (strategy) { return strategy.id == id });
+ if (!chosenStrategy)
+ return null;
+
+ for (var i = 0; i < chosenStrategy.parameters.length; i++)
+ chosenStrategy.parameters[i] = parseFloat(config[i + 1]);
+
+ return chosenStrategy;
+ },
+ _computeChartData: function ()
+ {
+ if (!this.get('fetchedData'))
+ return;
+
+ var chartData = App.createChartData(this.get('fetchedData'));
+ chartData.movingAverage = this._computeMovingAverage(chartData);
+
+ this._updateStrategyConfigIfNeeded(this.get('chosenMovingAverageStrategy'), 'movingAverageConfig');
+ this._updateStrategyConfigIfNeeded(this.get('chosenEnvelopingStrategy'), 'envelopingConfig');
+
+ this.set('chartData', chartData);
+ }.observes('chosenMovingAverageStrategy', 'chosenMovingAverageStrategy.parameterList.@each.value',
+ 'chosenEnvelopingStrategy', 'chosenEnvelopingStrategy.parameterList.@each.value'),
+ _computeMovingAverage: function (chartData)
+ {
+ var currentTimeSeriesData = chartData.current.series();
+ var movingAverageStrategy = this.get('chosenMovingAverageStrategy');
+ if (!movingAverageStrategy || !movingAverageStrategy.execute)
+ return null;
+
+ var movingAverageValues = this._executeStrategy(movingAverageStrategy, currentTimeSeriesData);
+ if (!movingAverageValues)
+ return null;
+
+ var envelopeDelta = null;
+ var envelopingStrategy = this.get('chosenEnvelopingStrategy');
+ if (envelopingStrategy && envelopingStrategy.execute)
+ envelopeDelta = this._executeStrategy(envelopingStrategy, currentTimeSeriesData, [movingAverageValues]);
+
+ return new TimeSeries(currentTimeSeriesData.map(function (point, index) {
+ var value = movingAverageValues[index];
+ return {
+ measurement: point.measurement,
+ time: point.time,
+ value: value,
+ interval: envelopeDelta !== null ? [value - envelopeDelta, value + envelopeDelta] : null,
+ }
+ }));
+ },
+ _executeStrategy: function (strategy, currentTimeSeriesData, additionalArguments)
+ {
+ var parameters = (strategy.parameterList || []).map(function (param) {
+ var parsed = parseFloat(param.value);
+ return Math.min(param.max || Infinity, Math.max(param.min || -Infinity, isNaN(parsed) ? 0 : parsed));
+ });
+ parameters.push(currentTimeSeriesData.map(function (point) { return point.value }));
+ return strategy.execute.apply(window, parameters.concat(additionalArguments));
+ },
+ _updateStrategyConfigIfNeeded: function (strategy, configName)
+ {
+ var config = null;
+ if (strategy && strategy.execute)
+ config = [strategy.id].concat((strategy.parameterList || []).map(function (param) { return param.value; }));
+
+ if (JSON.stringify(config) != JSON.stringify(this.get(configName)))
+ this.set(configName, config);
+ },
});
App.createChartData = function (data)
@@ -552,26 +647,30 @@
if (!parsedPaneList)
return null;
- // Don't re-create all panes.
+ // FIXME: Don't re-create all panes.
var self = this;
return parsedPaneList.map(function (paneInfo) {
var timeRange = null;
- if (paneInfo[3] && paneInfo[3] instanceof Array) {
- var timeRange = paneInfo[3];
+ var selectedItem = null;
+ if (paneInfo[2] instanceof Array) {
+ var timeRange = paneInfo[2];
try {
timeRange = [new Date(timeRange[0]), new Date(timeRange[1])];
} catch (error) {
console.log("Failed to parse the time range:", timeRange, error);
}
- }
+ } else
+ selectedItem = paneInfo[2];
+
return App.Pane.create({
store: self.store,
info: paneInfo,
platformId: paneInfo[0],
metricId: paneInfo[1],
- selectedItem: paneInfo[2],
+ selectedItem: selectedItem,
timeRange: timeRange,
- timeRangeIsLocked: !!paneInfo[4],
+ movingAverageConfig: paneInfo[3],
+ envelopingConfig: paneInfo[4],
});
});
},
@@ -580,13 +679,14 @@
{
if (!panes.length)
return undefined;
+ var self = this;
return App.encodePrettifiedJSON(panes.map(function (pane) {
return [
pane.get('platformId'),
pane.get('metricId'),
- pane.get('selectedItem'),
- pane.get('timeRange') ? pane.get('timeRange').map(function (date) { return date.getTime() }) : null,
- !!pane.get('timeRangeIsLocked'),
+ pane.get('timeRange') ? pane.get('timeRange').map(function (date) { return date.getTime() }) : pane.get('selectedItem'),
+ pane.get('movingAverageConfig'),
+ pane.get('envelopingConfig'),
];
}));
},
@@ -594,8 +694,8 @@
_scheduleQueryStringUpdate: function ()
{
Ember.run.debounce(this, '_updateQueryString', 1000);
- }.observes('sharedZoom', 'panes.@each.platform', 'panes.@each.metric', 'panes.@each.selectedItem',
- 'panes.@each.timeRange', 'panes.@each.timeRangeIsLocked'),
+ }.observes('sharedZoom', 'panes.@each.platform', 'panes.@each.metric', 'panes.@each.selectedItem', 'panes.@each.timeRange',
+ 'panes.@each.movingAverageConfig', 'panes.@each.envelopingConfig'),
_updateQueryString: function ()
{
@@ -711,8 +811,10 @@
},
toggleBugsPane: function ()
{
- if (this.toggleProperty('showingAnalysisPane'))
+ if (this.toggleProperty('showingAnalysisPane')) {
this.set('showingSearchPane', false);
+ this.set('showingStatPane', false);
+ }
},
createAnalysisTask: function ()
{
@@ -743,13 +845,22 @@
var model = this.get('model');
if (!model.get('commitSearchRepository'))
model.set('commitSearchRepository', App.Manifest.repositoriesWithReportedCommits[0]);
- if (this.toggleProperty('showingSearchPane'))
+ if (this.toggleProperty('showingSearchPane')) {
this.set('showingAnalysisPane', false);
+ this.set('showingStatPane', false);
+ }
},
searchCommit: function () {
var model = this.get('model');
model.searchCommit(model.get('commitSearchRepository'), model.get('commitSearchKeyword'));
},
+ toggleStatPane: function ()
+ {
+ if (this.toggleProperty('showingStatPane')) {
+ this.set('showingSearchPane', false);
+ this.set('showingAnalysisPane', false);
+ }
+ },
zoomed: function (selection)
{
this.set('mainPlotDomain', selection ? selection : this.get('overviewDomain'));
@@ -786,7 +897,7 @@
var newSelection = this.get('parentController').get('sharedZoom');
if (App.domainsAreEqual(newSelection, this.get('mainPlotDomain')))
return;
- this.set('mainPlotDomain', newSelection);
+ this.set('mainPlotDomain', newSelection || this.get('overviewDomain'));
this.set('overviewSelection', newSelection);
}.observes('parentController.sharedZoom').on('init'),
_updateDetails: function ()
Modified: trunk/Websites/perf.webkit.org/public/v2/chart-pane.css (179877 => 179878)
--- trunk/Websites/perf.webkit.org/public/v2/chart-pane.css 2015-02-10 20:27:40 UTC (rev 179877)
+++ trunk/Websites/perf.webkit.org/public/v2/chart-pane.css 2015-02-10 21:39:41 UTC (rev 179878)
@@ -50,6 +50,13 @@
top: 0.55rem;
}
+.chart-pane a.stat-button {
+ display: inline-block;
+ position: absolute;
+ right: 3.15rem;
+ top: 0.55rem;
+}
+
.chart-pane a.bugs-button {
display: inline-block;
position: absolute;
@@ -64,38 +71,77 @@
top: 0.55rem;
}
-.search-pane, .analysis-pane {
+.popup-pane {
position: absolute;
top: 1.7rem;
border: 1px solid #bbb;
- padding: 0;
+ font-size: 0.8rem;
+ padding: 0.2rem;
border-radius: 0.5rem;
display: table;
background: white;
}
-.analysis-pane {
- right: 1.3rem;
+.popup-pane.hidden {
+ display: none;
}
-.analysis-pane table {
+.stat-pane {
+ right: 2.6rem;
+ padding: 0;
+}
+
+.stat-pane fieldset {
+ border: solid 1px #ccc;
+ border-radius: 0.5rem;
margin: 0.2rem;
+ padding: 0;
+}
+
+.stat-option {
+ margin: 0;
+ padding: 0;
font-size: 0.8rem;
}
-.analysis-pane th {
- font-weight: normal;
+.stat-option h1 {
+ font-size: inherit;
+ line-height: 0.8rem;
+ padding: 0.3rem 0.5rem;
+ margin: 0;
+ border-top: solid 1px #ccc;
+ border-bottom: solid 1px #ccc;
}
+.stat-option:first-child h1 {
+ border-top: none;
+}
+
+.stat-option > * {
+ display: block;
+ margin: 0.1rem 0.5rem 0.1rem 1rem;
+}
+
+.stat-option input {
+ width: 4rem;
+}
+
+.stat-option p {
+ max-width: 15rem;
+}
+
+.analysis-pane {
+ right: 1.3rem;
+}
+
+.analysis-pane > * {
+ margin: 0.2rem;
+}
+
.search-pane {
right: 0rem;
}
-.analysis-pane.hidden,
-.search-pane.hidden {
- display: none;
-}
-
.search-pane input {
display: table-cell;
vertical-align: middle;
@@ -103,15 +149,15 @@
border: none;
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
- padding: 0.5rem;
- font-size: 1rem;
+ padding: 0.2rem;
+ font-size: 0.8rem;
margin: 0;
}
.search-pane .repositories {
display: table-cell;
vertical-align: middle;
- padding: 0 0.5rem;
+ padding: 0;
}
.search-pane input:focus {
@@ -303,20 +349,41 @@
}
.chart .dot {
- fill: #666;
+ fill: #ccc;
stroke: none;
}
+.chart .dot.foreground {
+ fill: #666;
+}
.chart path.area {
stroke: none;
fill: #ccc;
opacity: 0.8;
}
+.chart path.area.foreground {
+}
.chart path.current {
+ stroke: #ccc;
+}
+
+.chart path.current.foreground {
stroke: #999;
}
+.chart path.movingAverage {
+ stroke: #363;
+ fill: none;
+ opacity: 0.8;
+}
+
+.chart path.envelope {
+ stroke: none;
+ fill: #6c6;
+ opacity: 0.4;
+}
+
.chart path.baseline {
stroke: #f66;
}
Modified: trunk/Websites/perf.webkit.org/public/v2/index.html (179877 => 179878)
--- trunk/Websites/perf.webkit.org/public/v2/index.html 2015-02-10 20:27:40 UTC (rev 179877)
+++ trunk/Websites/perf.webkit.org/public/v2/index.html 2015-02-10 21:39:41 UTC (rev 179878)
@@ -147,6 +147,9 @@
<header>
<h1 {{action "toggleDetails"}}>{{metric.fullName}} - {{ platform.name}}</h1>
<a href="" title="Close" class="close-button" {{action "close"}}>{{partial "close-button"}}</a>
+ {{#if movingAverageStrategies}}
+ <a href="" title="Statistical Tools" class="stat-button" {{action "toggleStatPane"}}>{{partial "stat-button"}}</a>
+ {{/if}}
{{#if App.Manifest.bugTrackers}}
<a href="" title="Analysis" class="bugs-button" {{action "toggleBugsPane"}}>
{{partial "analysis-button"}}
@@ -173,7 +176,6 @@
rangeRoute="analysisTask"
selection=timeRange
selectedPoints=selectedPoints
- selectionIsLocked=timeRangeIsLocked
markedPoints=markedPoints
zoom="zoomed"}}
{{else}}
@@ -200,7 +202,12 @@
</div>
</div>
- <form {{bind-attr class=":search-pane showingSearchPane::hidden"}}>
+ <div {{bind-attr class=":popup-pane :analysis-pane showingAnalysisPane::hidden"}}>
+ <label>Name: {{input type=text value=newAnalysisTaskName}}</label>
+ <button {{action "createAnalysisTask"}} {{bind-attr disabled=cannotAnalyze}}>Analyze</button>
+ </div>
+
+ <form {{bind-attr class=":popup-pane :search-pane showingSearchPane::hidden"}}>
<span class="repositories">
{{view Ember.Select
content=App.Manifest.repositoriesWithReportedCommits
@@ -211,19 +218,7 @@
{{input action="" placeholder="Name or email" value=commitSearchKeyword}}
</form>
- <div {{bind-attr class=":analysis-pane showingAnalysisPane::hidden"}}>
- <table>
- <tbody>
- <tr>
- <th>
- <label>Name: {{input type=text value=newAnalysisTaskName}}</label>
- <button {{action "createAnalysisTask"}} {{bind-attr disabled=cannotAnalyze}}>Analyze</button>
- </th>
- </tr>
- </tbody>
- </table>
- </div>
-
+ {{partial "stat-pane"}}
</section>
{{/each}}
</script>
@@ -356,12 +351,53 @@
</svg>
</script>
+ <script type="text/x-handlebars" data-template-name="stat-button">
+ <svg class="stat-button icon-button" viewBox="10 0 110 100">
+ <g stroke="none" stroke-width="0" fill="black">
+ <path id="upper-sigma" d="M 5 5 H 95 V 40 h -10 c -5 -20 -5 -20 -25 -20 H 35 L 60 50 l -20 0" />
+ <use xlink:href="" transform="translate(0, 100) scale(1, -1)" />
+ </g>
+ </svg>
+ </script>
+
+ <script type="text/x-handlebars" data-template-name="stat-pane">
+ <section {{bind-attr class=":popup-pane :stat-pane showingStatPane::hidden"}}>
+ <section class="stat-option">
+ <h1>Moving average</h1>
+ <label>Type: {{view Ember.Select
+ content=movingAverageStrategies
+ optionValuePath='content'
+ optionLabelPath='content.label'
+ selection=chosenMovingAverageStrategy}}</label>
+ {{#each chosenMovingAverageStrategy.parameterList}}
+ <label>{{label}}: {{input type="number" value=value min=min max=max step=step}}</label>
+ {{/each}}
+ </section>
+ {{#if chosenMovingAverageStrategy.execute}}
+ <section class="stat-option">
+ <h1>Envelope</h1>
+ <label>Type: {{view Ember.Select
+ content=envelopingStrategies
+ optionValuePath='content'
+ optionLabelPath='content.label'
+ selection=chosenEnvelopingStrategy}}</label>
+ {{#if chosenEnvelopingStrategy.description}}
+ <p class="description">{{chosenEnvelopingStrategy.description}}</p>
+ {{/if}}
+ {{#each chosenEnvelopingStrategy.parameterList}}
+ <label>{{label}}: <input type="number" {{bind-attr value=value min=min max=max step=step}}></label>
+ {{/each}}
+ </section>
+ {{/if}}
+ </section>
+ </script>
+
<script type="text/x-handlebars" data-template-name="analysis-button">
<svg class="analysis-button icon-button" viewBox="0 0 100 100">
- <g stroke="black" stroke-width="15">
+ <g stroke="black" fill="black" stroke-width="15">
<circle cx="50" cy="50" r="40" fill="transparent"/>
<line x1="50" y1="25" x2="50" y2="55"/>
- <circle cx="50" cy="67.5" r="2.5" fill="transparent"/>
+ <circle cx="50" cy="67.5" r="10" stroke="none"/>
</g>
</svg>
</script>
Modified: trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js (179877 => 179878)
--- trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js 2015-02-10 20:27:40 UTC (rev 179877)
+++ trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js 2015-02-10 21:39:41 UTC (rev 179878)
@@ -18,6 +18,8 @@
if (!chartData)
return;
this._needsConstruction = true;
+ this._totalWidth = undefined;
+ this._totalHeight = undefined;
this._constructGraphIfPossible(chartData);
}.observes('chartData').on('init'),
didInsertElement: function ()
@@ -25,6 +27,14 @@
var chartData = this.get('chartData');
if (chartData)
this._constructGraphIfPossible(chartData);
+
+ if (this.get('interactive')) {
+ var element = this.get('element');
+ this._attachEventListener(element, "mousemove", this._mouseMoved.bind(this));
+ this._attachEventListener(element, "mouseleave", this._mouseLeft.bind(this));
+ this._attachEventListener(element, "mousedown", this._mouseDown.bind(this));
+ this._attachEventListener($(element).parents("[tabindex]"), "keydown", this._keyPressed.bind(this));
+ }
},
willClearRender: function ()
{
@@ -47,7 +57,8 @@
this._x = d3.time.scale();
this._y = d3.scale.linear();
- // FIXME: Tear down the old SVG element.
+ if (this._svgElement)
+ this._svgElement.remove();
this._svgElement = d3.select(element).append("svg")
.attr("width", "100%")
.attr("height", "100%");
@@ -86,23 +97,16 @@
.y0(function(point) { return point.interval ? yScale(point.interval[0]) : null; })
.y1(function(point) { return point.interval ? yScale(point.interval[1]) : null; });
- if (this._paths)
- this._paths.forEach(function (path) { path.remove(); });
this._paths = [];
- if (this._areas)
- this._areas.forEach(function (area) { area.remove(); });
this._areas = [];
- if (this._dots)
- this._dots.forEach(function (dot) { dots.remove(); });
this._dots = [];
- if (this._highlights)
- this._highlights.remove();
this._highlights = null;
this._currentTimeSeries = chartData.current;
this._currentTimeSeriesData = this._currentTimeSeries.series();
this._baselineTimeSeries = chartData.baseline;
this._targetTimeSeries = chartData.target;
+ this._movingAverageTimeSeries = chartData.movingAverage;
this._yAxisUnit = chartData.unit;
@@ -119,29 +123,36 @@
.attr("class", "target"));
}
+ var foregroundClass = this._movingAverageTimeSeries ? '' : ' foreground';
this._areas.push(this._clippedContainer
.append("path")
.datum(this._currentTimeSeriesData)
- .attr("class", "area"));
+ .attr("class", "area" + foregroundClass));
this._paths.push(this._clippedContainer
.append("path")
.datum(this._currentTimeSeriesData)
- .attr("class", "current"));
+ .attr("class", "current" + foregroundClass));
this._dots.push(this._clippedContainer
.selectAll(".dot")
.data(this._currentTimeSeriesData)
.enter().append("circle")
- .attr("class", "dot")
+ .attr("class", "dot" + foregroundClass)
.attr("r", this.get('chartPointRadius') || 1));
+ if (this._movingAverageTimeSeries) {
+ this._paths.push(this._clippedContainer
+ .append("path")
+ .datum(this._movingAverageTimeSeries.series())
+ .attr("class", "movingAverage"));
+ this._areas.push(this._clippedContainer
+ .append("path")
+ .datum(this._movingAverageTimeSeries.series())
+ .attr("class", "envelope"));
+ }
+
if (this.get('interactive')) {
- this._attachEventListener(element, "mousemove", this._mouseMoved.bind(this));
- this._attachEventListener(element, "mouseleave", this._mouseLeft.bind(this));
- this._attachEventListener(element, "mousedown", this._mouseDown.bind(this));
- this._attachEventListener($(element).parents("[tabindex]"), "keydown", this._keyPressed.bind(this));
-
this._currentItemLine = this._clippedContainer
.append("line")
.attr("class", "current-item");
@@ -331,9 +342,10 @@
var currentRange = this._currentTimeSeries.minMaxForTimeRange(startTime, endTime);
var baselineRange = this._baselineTimeSeries ? this._baselineTimeSeries.minMaxForTimeRange(startTime, endTime) : [Number.MAX_VALUE, Number.MIN_VALUE];
var targetRange = this._targetTimeSeries ? this._targetTimeSeries.minMaxForTimeRange(startTime, endTime) : [Number.MAX_VALUE, Number.MIN_VALUE];
+ var movingAverageRange = this._movingAverageTimeSeries ? this._movingAverageTimeSeries.minMaxForTimeRange(startTime, endTime) : [Number.MAX_VALUE, Number.MIN_VALUE];
return [
- Math.min(currentRange[0], baselineRange[0], targetRange[0]),
- Math.max(currentRange[1], baselineRange[1], targetRange[1]),
+ Math.min(currentRange[0], baselineRange[0], targetRange[0], movingAverageRange[0]),
+ Math.max(currentRange[1], baselineRange[1], targetRange[1], movingAverageRange[1]),
];
},
_currentSelection: function ()
@@ -378,7 +390,6 @@
if (!this._brushExtent)
return;
- this.set('selectionIsLocked', false);
this._setCurrentSelection(undefined);
// Avoid locking the indicator in _mouseDown when the brush was cleared in the same mousedown event.
@@ -391,7 +402,6 @@
return;
}
- this.set('selectionIsLocked', true);
this._setCurrentSelection(this._brush.extent());
},
_keyPressed: function (event)
Modified: trunk/Websites/perf.webkit.org/public/v2/js/statistics.js (179877 => 179878)
--- trunk/Websites/perf.webkit.org/public/v2/js/statistics.js 2015-02-10 20:27:40 UTC (rev 179877)
+++ trunk/Websites/perf.webkit.org/public/v2/js/statistics.js 2015-02-10 21:39:41 UTC (rev 179878)
@@ -99,6 +99,97 @@
2.368026, 2.367566, 2.367115, 2.366674, 2.366243, 2.365821, 2.365407, 2.365002, 2.364606, 2.364217]
};
+ this.MovingAverageStrategies = [
+ {
+ id: 1,
+ label: 'Simple Moving Average',
+ parameterList: [
+ {label: "Backward window size", value: 5, min: 2, step: 1},
+ {label: "Forward window size", value: 3, min: 0, step: 1}
+ ],
+ execute: function (backwardWindowSize, forwardWindowSize, values) {
+ var averages = new Array(values.length);
+ // We use naive O(n^2) algorithm for simplicy as well as to avoid accumulating round-off errors.
+ for (var i = 0; i < values.length; i++) {
+ var sum = 0;
+ var count = 0;
+ for (var j = i - backwardWindowSize; j < i + backwardWindowSize; j++) {
+ if (j >= 0 && j < values.length) {
+ sum += values[j];
+ count++;
+ }
+ }
+ averages[i] = sum / count;
+ }
+ return averages;
+ },
+
+ },
+ {
+ id: 2,
+ label: 'Cumulative Moving Average',
+ execute: function (values) {
+ var averages = new Array(values.length);
+ var sum = 0;
+ for (var i = 0; i < values.length; i++) {
+ sum += values[i];
+ averages[i] = sum / (i + 1);
+ }
+ return averages;
+ }
+ },
+ {
+ id: 3,
+ label: 'Exponential Moving Average',
+ parameterList: [{label: "Smoothing factor", value: 0.1, min: 0.001, max: 0.9}],
+ execute: function (smoothingFactor, values) {
+ if (!values.length || typeof(smoothingFactor) !== "number")
+ return null;
+
+ var averages = new Array(values.length);
+ var movingAverage = 0;
+ averages[0] = values[0];
+ for (var i = 1; i < values.length; i++)
+ averages[i] = smoothingFactor * values[i] + (1 - smoothingFactor) * averages[i - 1];
+ return averages;
+ }
+ },
+ ];
+
+ this.EnvelopingStrategies = [
+ {
+ id: 100,
+ label: 'Average Difference',
+ description: 'The average difference between consecutive values.',
+ execute: function (values, movingAverages) {
+ if (values.length < 1)
+ return NaN;
+
+ var diff = 0;
+ for (var i = 1; i < values.length; i++)
+ diff += Math.abs(values[i] - values[i - 1]);
+
+ return diff / values.length;
+ }
+ },
+ {
+ id: 101,
+ label: 'Moving Average Standard Deviation',
+ description: 'The square root of the average deviation from the moving average with Bessel\'s correction.',
+ execute: function (values, movingAverages) {
+ if (values.length < 1)
+ return NaN;
+
+ var diffSquareSum = 0;
+ for (var i = 1; i < values.length; i++) {
+ var diff = (values[i] - movingAverages[i]);
+ diffSquareSum += diff * diff;
+ }
+
+ return Math.sqrt(diffSquareSum / (values.length - 1));
+ }
+ },
+ ];
})();
if (typeof module != 'undefined') {