2 function createTrendLineExecutableFromAveragingFunction(callback) {
3 return function (source, parameters) {
4 var timeSeries = source.measurementSet.fetchedTimeSeries(source.type, source.includeOutliers, source.extendToFuture);
5 var values = timeSeries.values();
7 return Promise.resolve(null);
9 var averageValues = callback.call(null, values, ...parameters);
11 return Promise.resolve(null);
13 var interval = function () { return null; }
14 var result = new Array(averageValues.length);
15 for (var i = 0; i < averageValues.length; i++)
16 result[i] = {time: timeSeries.findPointByIndex(i).time, value: averageValues[i], interval: interval};
18 return Promise.resolve(result);
22 var ChartTrendLineTypes = [
29 label: 'Segmentation',
30 execute: function (source, parameters) {
31 return source.measurementSet.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', parameters,
32 source.type, source.includeOutliers, source.extendToFuture).then(function (segmentation) {
37 {label: "Segment count weight", value: 2.5, min: 0.01, max: 10, step: 0.01},
38 {label: "Grid size", value: 500, min: 100, max: 10000, step: 10}
43 label: 'Simple Moving Average',
45 {label: "Backward window size", value: 8, min: 2, step: 1},
46 {label: "Forward window size", value: 4, min: 0, step: 1}
48 execute: createTrendLineExecutableFromAveragingFunction(Statistics.movingAverage.bind(Statistics))
52 label: 'Cumulative Moving Average',
53 execute: createTrendLineExecutableFromAveragingFunction(Statistics.cumulativeMovingAverage.bind(Statistics))
57 label: 'Exponential Moving Average',
59 {label: "Smoothing factor", value: 0.01, min: 0.001, max: 0.9, step: 0.001},
61 execute: createTrendLineExecutableFromAveragingFunction(Statistics.exponentialMovingAverage.bind(Statistics))
64 ChartTrendLineTypes.DefaultType = ChartTrendLineTypes[1];
67 class ChartPane extends ChartPaneBase {
68 constructor(chartsPage, platformId, metricId)
72 this._mainChartIndicatorWasLocked = false;
73 this._chartsPage = chartsPage;
74 this._lockedPopover = null;
75 this._trendLineType = null;
76 this._trendLineParameters = [];
77 this._trendLineVersion = 0;
78 this._renderedTrendLineOptions = false;
80 this.configure(platformId, metricId);
83 didConstructShadowTree()
85 this.part('close').listenToAction('activate', () => {
86 this._chartsPage.closePane(this);
92 var state = [this._platformId, this._metricId];
93 if (this._mainChart) {
94 var selection = this._mainChart.currentSelection();
95 const indicator = this._mainChart.currentIndicator();
98 else if (indicator && indicator.isLocked)
99 state[2] = indicator.point.id;
102 var graphOptions = new Set;
103 if (!this.isSamplingEnabled())
104 graphOptions.add('noSampling');
105 if (this.isShowingOutliers())
106 graphOptions.add('showOutliers');
108 if (graphOptions.size)
109 state[3] = graphOptions;
111 if (this._trendLineType)
112 state[4] = [this._trendLineType.id].concat(this._trendLineParameters);
117 updateFromSerializedState(state, isOpen)
119 if (!this._mainChart)
122 var selectionOrIndicatedPoint = state[2];
123 if (selectionOrIndicatedPoint instanceof Array)
124 this._mainChart.setSelection([parseFloat(selectionOrIndicatedPoint[0]), parseFloat(selectionOrIndicatedPoint[1])]);
125 else if (typeof(selectionOrIndicatedPoint) == 'number') {
126 this._mainChart.setIndicator(selectionOrIndicatedPoint, true);
127 this._mainChartIndicatorWasLocked = true;
129 this._mainChart.setIndicator(null, false);
131 // FIXME: This forces sourceList to be set twice. First in configure inside the constructor then here.
132 // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
133 var graphOptions = state[3];
134 if (graphOptions instanceof Set) {
135 this.setSamplingEnabled(!graphOptions.has('nosampling'));
136 this.setShowOutliers(graphOptions.has('showoutliers'));
139 var trendLineOptions = state[4];
140 if (!(trendLineOptions instanceof Array))
141 trendLineOptions = [];
143 var trendLineId = trendLineOptions[0];
144 var trendLineType = ChartTrendLineTypes.find(function (type) { return type.id == trendLineId; }) || ChartTrendLineTypes.DefaultType;
146 this._trendLineType = trendLineType;
147 this._trendLineParameters = (trendLineType.parameterList || []).map(function (parameter, index) {
148 var specifiedValue = parseFloat(trendLineOptions[index + 1]);
149 return !isNaN(specifiedValue) ? specifiedValue : parameter.value;
151 this._updateTrendLine();
152 this._renderedTrendLineOptions = false;
154 // FIXME: state[5] specifies envelope in v2 UI
155 // FIXME: state[6] specifies change detection algorithm in v2 UI
158 setOverviewSelection(selection)
160 if (this._overviewChart)
161 this._overviewChart.setSelection(selection);
164 _overviewSelectionDidChange(domain, didEndDrag)
166 super._overviewSelectionDidChange(domain, didEndDrag);
167 this._chartsPage.setMainDomainFromOverviewSelection(domain, this, didEndDrag);
170 _mainSelectionDidChange(selection, didEndDrag)
172 super._mainSelectionDidChange(selection, didEndDrag);
173 this._chartsPage.mainChartSelectionDidChange(this, didEndDrag);
176 _mainSelectionDidZoom(selection)
178 super._mainSelectionDidZoom(selection);
179 this._chartsPage.setMainDomainFromZoom(selection, this);
182 router() { return this._chartsPage.router(); }
184 _requestOpeningCommitViewer(repository, from, to)
186 super._requestOpeningCommitViewer(repository, from, to);
187 this._chartsPage.setOpenRepository(repository);
190 setOpenRepository(repository)
192 if (repository != this._commitLogViewer.currentRepository()) {
193 var range = this._mainChartStatus.setCurrentRepository(repository);
194 this._commitLogViewer.view(repository, range.from, range.to).then(() => { this.enqueueToRender(); });
195 this.enqueueToRender();
199 _indicatorDidChange(indicatorID, isLocked)
201 this._chartsPage.mainChartIndicatorDidChange(this, isLocked != this._mainChartIndicatorWasLocked);
202 this._mainChartIndicatorWasLocked = isLocked;
203 super._indicatorDidChange(indicatorID, isLocked);
206 _analyzeRange(pointsRangeForAnalysis)
208 var router = this._chartsPage.router();
209 var newWindow = window.open(router.url('analysis/task/create'), '_blank');
211 var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
212 var name = analyzePopover.querySelector('input').value;
214 AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
215 newWindow.location.href = router.url('analysis/task/' + data['taskId']);
216 self.fetchAnalysisTasks(true);
217 }, function (error) {
218 newWindow.location.href = router.url('analysis/task/create', {error: error});
222 _markAsOutlier(markAsOutlier, points)
225 return Promise.all(points.map(function (point) {
226 return PrivilegedAPI.sendRequest('update-run-status', {'run': point.id, 'markedOutlier': markAsOutlier});
227 })).then(function () {
228 self._mainChart.fetchMeasurementSets(true /* noCache */);
229 }, function (error) {
230 alert('Failed to update the outlier status: ' + error);
236 if (this._platform && this._metric) {
237 var metric = this._metric;
238 var platform = this._platform;
240 this.renderReplace(this.content().querySelector('.chart-pane-title'),
241 metric.fullName() + ' on ' + platform.name());
244 if (this._mainChartStatus)
245 this._renderActionToolbar();
250 _renderActionToolbar()
253 var platform = this._platform;
254 var metric = this._metric;
256 var element = ComponentBase.createElement;
257 var link = ComponentBase.createLink;
260 this.part('close').enqueueToRender();
262 if (this._chartsPage.canBreakdown(platform, metric)) {
263 actions.push(element('li', link('Breakdown', function () {
264 self._chartsPage.insertBreakdownPanesAfter(platform, metric, self);
268 var platformPopover = this.content().querySelector('.chart-pane-alternative-platforms');
269 var alternativePlatforms = this._chartsPage.alternatePlatforms(platform, metric);
270 if (alternativePlatforms.length) {
271 this.renderReplace(platformPopover, Platform.sortByName(alternativePlatforms).map(function (platform) {
272 return element('li', link(platform.label(), function () {
273 self._chartsPage.insertPaneAfter(platform, metric, self);
277 actions.push(this._makePopoverActionItem(platformPopover, 'Other Platforms', true));
279 platformPopover.style.display = 'none';
281 var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
282 var pointsRangeForAnalysis = this._mainChartStatus.pointsRangeForAnalysis();
283 if (pointsRangeForAnalysis) {
284 actions.push(this._makePopoverActionItem(analyzePopover, 'Analyze', false));
285 analyzePopover.onsubmit = function (event) {
286 event.preventDefault();
287 self._analyzeRange(pointsRangeForAnalysis);
290 analyzePopover.style.display = 'none';
291 analyzePopover.onsubmit = function (event) { event.preventDefault(); }
294 var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
295 actions.push(this._makePopoverActionItem(filteringOptions, 'Filtering', true));
297 var trendLineOptions = this.content().querySelector('.chart-pane-trend-line-options');
298 actions.push(this._makePopoverActionItem(trendLineOptions, 'Trend lines', true));
300 this._renderFilteringPopover();
301 this._renderTrendLinePopover();
303 this._lockedPopover = null;
304 this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
307 _makePopoverActionItem(popover, label, shouldRespondToHover)
310 popover.anchor = ComponentBase.createLink(label, function () {
311 var makeVisible = self._lockedPopover != popover;
312 self._setPopoverVisibility(popover, makeVisible);
314 self._lockedPopover = popover;
316 if (shouldRespondToHover)
317 this._makePopoverOpenOnHover(popover);
319 return ComponentBase.createElement('li', {class: this._lockedPopover == popover ? 'selected' : ''}, popover.anchor);
322 _makePopoverOpenOnHover(popover)
324 var mouseIsInAnchor = false;
325 var mouseIsInPopover = false;
328 var closeIfNeeded = function () {
329 setTimeout(function () {
330 if (self._lockedPopover != popover && !mouseIsInAnchor && !mouseIsInPopover)
331 self._setPopoverVisibility(popover, false);
335 popover.anchor.onmouseenter = function () {
336 if (self._lockedPopover)
338 mouseIsInAnchor = true;
339 self._setPopoverVisibility(popover, true);
341 popover.anchor.onmouseleave = function () {
342 mouseIsInAnchor = false;
346 popover.onmouseenter = function () {
347 mouseIsInPopover = true;
349 popover.onmouseleave = function () {
350 mouseIsInPopover = false;
355 _setPopoverVisibility(popover, visible)
357 var anchor = popover.anchor;
359 var width = anchor.offsetParent.offsetWidth;
360 popover.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
361 popover.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
363 popover.style.display = visible ? null : 'none';
364 anchor.parentNode.className = visible ? 'selected' : '';
366 if (this._lockedPopover && this._lockedPopover != popover && visible)
367 this._setPopoverVisibility(this._lockedPopover, false);
369 if (this._lockedPopover == popover && !visible)
370 this._lockedPopover = null;
373 _renderFilteringPopover()
375 var enableSampling = this.content().querySelector('.enable-sampling');
376 enableSampling.checked = this.isSamplingEnabled();
377 enableSampling.onchange = function () {
378 self.setSamplingEnabled(enableSampling.checked);
379 self._chartsPage.graphOptionsDidChange();
382 var showOutliers = this.content().querySelector('.show-outliers');
383 showOutliers.checked = this.isShowingOutliers();
384 showOutliers.onchange = function () {
385 self.setShowOutliers(showOutliers.checked);
386 self._chartsPage.graphOptionsDidChange();
389 var markAsOutlierButton = this.content().querySelector('.mark-as-outlier');
390 const indicator = this._mainChart.currentIndicator();
391 let firstSelectedPoint = indicator && indicator.isLocked ? indicator.point : null;
392 if (!firstSelectedPoint)
393 firstSelectedPoint = this._mainChart.firstSelectedPoint('current');
394 var alreayMarkedAsOutlier = firstSelectedPoint && firstSelectedPoint.markedOutlier;
397 markAsOutlierButton.textContent = (alreayMarkedAsOutlier ? 'Unmark' : 'Mark') + ' selected points as outlier';
398 markAsOutlierButton.onclick = function () {
399 var selectedPoints = [firstSelectedPoint];
400 if (self._mainChart.currentSelection('current'))
401 selectedPoints = self._mainChart.selectedPoints('current');
402 self._markAsOutlier(!alreayMarkedAsOutlier, selectedPoints);
404 markAsOutlierButton.disabled = !firstSelectedPoint;
407 _renderTrendLinePopover()
409 var element = ComponentBase.createElement;
410 var link = ComponentBase.createLink;
413 const trendLineTypesContainer = this.content().querySelector('.trend-line-types');
414 if (!trendLineTypesContainer.querySelector('select')) {
415 this.renderReplace(trendLineTypesContainer, [
416 element('select', {onchange: this._trendLineTypeDidChange.bind(this)},
417 ChartTrendLineTypes.map((type) => { return element('option', {value: type.id}, type.label); }))
420 if (this._trendLineType)
421 trendLineTypesContainer.querySelector('select').value = this._trendLineType.id;
423 if (this._renderedTrendLineOptions)
425 this._renderedTrendLineOptions = true;
427 if (this._trendLineParameters.length) {
428 var configuredParameters = this._trendLineParameters;
429 this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), [
430 element('h3', 'Parameters'),
431 element('ul', this._trendLineType.parameterList.map(function (parameter, index) {
432 var attributes = {type: 'number'};
433 for (var name in parameter)
434 attributes[name] = parameter[name];
435 attributes.value = configuredParameters[index];
436 var input = element('input', attributes);
437 input.parameterIndex = index;
438 input.oninput = self._trendLineParameterDidChange.bind(self);
439 input.onchange = self._trendLineParameterDidChange.bind(self);
440 return element('li', element('label', [parameter.label + ': ', input]));
444 this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), []);
447 _trendLineTypeDidChange(event)
449 var newType = ChartTrendLineTypes.find(function (type) { return type.id == event.target.value });
450 if (newType == this._trendLineType)
453 this._trendLineType = newType;
454 this._trendLineParameters = this._defaultParametersForTrendLine(newType);
455 this._renderedTrendLineOptions = false;
457 this._updateTrendLine();
458 this._chartsPage.graphOptionsDidChange();
459 this.enqueueToRender();
462 _defaultParametersForTrendLine(type)
464 return type && type.parameterList ? type.parameterList.map(function (parameter) { return parameter.value; }) : [];
467 _trendLineParameterDidChange(event)
469 var input = event.target;
470 var index = input.parameterIndex;
471 var newValue = parseFloat(input.value);
472 if (this._trendLineParameters[index] == newValue)
474 this._trendLineParameters[index] = newValue;
476 setTimeout(function () { // Some trend lines, e.g. sementations, are expensive.
477 if (self._trendLineParameters[index] != newValue)
479 self._updateTrendLine();
480 self._chartsPage.graphOptionsDidChange();
486 super._didFetchData();
487 this._updateTrendLine();
492 if (!this._mainChart.sourceList())
495 this._trendLineVersion++;
496 var currentTrendLineType = this._trendLineType || ChartTrendLineTypes.DefaultType;
497 var currentTrendLineParameters = this._trendLineParameters || this._defaultParametersForTrendLine(currentTrendLineType);
498 var currentTrendLineVersion = this._trendLineVersion;
500 var sourceList = this._mainChart.sourceList();
502 if (!currentTrendLineType.execute) {
503 this._mainChart.clearTrendLines();
504 this.enqueueToRender();
506 // Wait for all trendlines to be ready. Otherwise we might see FOC when the domain is expanded.
507 Promise.all(sourceList.map(function (source, sourceIndex) {
508 return currentTrendLineType.execute.call(null, source, currentTrendLineParameters).then(function (trendlineSeries) {
509 if (self._trendLineVersion == currentTrendLineVersion)
510 self._mainChart.setTrendLine(sourceIndex, trendlineSeries);
512 })).then(function () {
513 self.enqueueToRender();
518 static paneHeaderTemplate()
521 <header class="chart-pane-header">
522 <h2 class="chart-pane-title">-</h2>
523 <nav class="chart-pane-actions">
525 <li><close-button id="close"></close-button></li>
527 <ul class="chart-pane-action-buttons buttoned-toolbar"></ul>
528 <ul class="chart-pane-alternative-platforms popover" style="display:none"></ul>
529 <form class="chart-pane-analyze-popover popover" style="display:none">
530 <input type="text" required>
531 <button>Create</button>
533 <ul class="chart-pane-filtering-options popover" style="display:none">
534 <li><label><input type="checkbox" class="enable-sampling">Sampling</label></li>
535 <li><label><input type="checkbox" class="show-outliers">Show outliers</label></li>
536 <li><button class="mark-as-outlier">Mark selected points as outlier</button></li>
538 <ul class="chart-pane-trend-line-options popover" style="display:none">
539 <div class="trend-line-types"></div>
540 <div class="trend-line-parameter-list"></div>
549 return ChartPaneBase.cssTemplate() + `
551 border: solid 1px #ccc;
552 border-radius: 0.5rem;
558 height: calc(100% - 2rem);
568 border-bottom: solid 1px #ccc;
574 padding-left: 1.5rem;
576 font-weight: inherit;
579 .chart-pane-actions {
583 justify-content: space-between;
591 .chart-pane-actions ul {
600 .chart-pane-actions .chart-pane-action-buttons {
605 .chart-pane-actions .popover {
609 border: solid 1px #ccc;
610 border-radius: 0.2rem;
615 margin-right: -0.2rem;
616 background: rgba(255, 255, 255, 0.95);
619 @supports ( -webkit-backdrop-filter: blur(0.5rem) ) {
620 .chart-pane-actions .popover {
621 background: rgba(255, 255, 255, 0.6);
622 -webkit-backdrop-filter: blur(0.5rem);
626 .chart-pane-actions .popover li {
629 .chart-pane-actions .popover li a {
631 text-decoration: none;
634 padding: 0.2rem 0.5rem;
637 .chart-pane-actions .popover a:hover,
638 .chart-pane-actions .popover input:focus {
639 background: rgba(204, 153, 51, 0.1);
642 .chart-pane-actions .chart-pane-analyze-popover {
646 .chart-pane-actions .popover label {
650 .chart-pane-actions .popover.chart-pane-filtering-options {
654 .chart-pane-actions .popover.chart-pane-trend-line-options h3 {
657 font-weight: inherit;
660 border-bottom: solid 1px #ccc;
663 .chart-pane-actions .popover.chart-pane-trend-line-options select,
664 .chart-pane-actions .popover.chart-pane-trend-line-options label {
668 .chart-pane-actions .popover.chart-pane-trend-line-options label {
672 .chart-pane-actions .popover.chart-pane-trend-line-options input {
676 .chart-pane-actions .popover input[type=text] {
680 border: solid 1px #ccc;