v3 UI should use four sig-figs to label y-axis of the main charts
[WebKit.git] / Websites / perf.webkit.org / public / v3 / pages / chart-pane.js
1
2 class ChartPane extends ComponentBase {
3     constructor(chartsPage, platformId, metricId)
4     {
5         super('chart-pane');
6
7         this._chartsPage = chartsPage;
8         this._platformId = platformId;
9         this._metricId = metricId;
10
11         var result = ChartsPage.createChartSourceList(platformId, metricId);
12         this._errorMessage = result.error;
13         this._platform = result.platform;
14         this._metric = result.metric;
15
16         this._overviewChart = null;
17         this._mainChart = null;
18         this._mainChartStatus = null;
19         this._mainSelection = null;
20         this._mainChartIndicatorWasLocked = false;
21         this._status = null;
22         this._revisions = null;
23
24         this._paneOpenedByClick = null;
25
26         this._commitLogViewer = this.content().querySelector('commit-log-viewer').component();
27         this.content().querySelector('close-button').component().setCallback(chartsPage.closePane.bind(chartsPage, this));
28
29         if (result.error)
30             return;
31
32         var formatter = result.metric.makeFormatter(4);
33         var self = this;
34
35         var overviewOptions = ChartsPage.overviewChartOptions(formatter);
36         overviewOptions.selection.onchange = function (domain, didEndDrag) {
37             self._chartsPage.setMainDomainFromOverviewSelection(domain, self, didEndDrag);
38         }
39
40         this._overviewChart = new InteractiveTimeSeriesChart(result.sourceList, overviewOptions);
41         this.renderReplace(this.content().querySelector('.chart-pane-overview'), this._overviewChart);
42
43         var mainOptions = ChartsPage.mainChartOptions(formatter);
44         mainOptions.indicator.onchange = this._indicatorDidChange.bind(this);
45         mainOptions.selection.onchange = this._mainSelectionDidChange.bind(this);
46         mainOptions.selection.onzoom = this._mainSelectionDidZoom.bind(this);
47         mainOptions.annotations.onclick = this._openAnalysisTask.bind(this);
48         mainOptions.ondata = this._didFetchData.bind(this);
49         this._mainChart = new InteractiveTimeSeriesChart(result.sourceList, mainOptions);
50         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
51
52         this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, chartsPage.router(), this._openCommitViewer.bind(this));
53         this.renderReplace(this.content().querySelector('.chart-pane-details'), this._mainChartStatus);
54
55         this.content().querySelector('.chart-pane').addEventListener('keyup', this._keyup.bind(this));
56         this._fetchAnalysisTasks();
57     }
58
59     _fetchAnalysisTasks()
60     {
61         var self = this;
62         AnalysisTask.fetchByPlatformAndMetric(this._platformId, this._metricId).then(function (tasks) {
63             self._mainChart.setAnnotations(tasks.map(function (task) {
64                 var fillStyle = '#fc6';
65                 switch (task.changeType()) {
66                 case 'inconclusive':
67                     fillStyle = '#fcc';
68                 case 'progression':
69                     fillStyle = '#39f';
70                     break;
71                 case 'regression':
72                     fillStyle = '#c60';
73                     break;
74                 case 'unchanged':
75                     fillStyle = '#ccc';
76                     break;
77                 }
78
79                 return {
80                     task: task,
81                     startTime: task.startTime(),
82                     endTime: task.endTime(),
83                     label: task.label(),
84                     fillStyle: fillStyle,
85                 };
86             }));
87         });
88     }
89
90     platformId() { return this._platformId; }
91     metricId() { return this._metricId; }
92
93     serializeState()
94     {
95         var selection = this._mainChart ? this._mainChart.currentSelection() : null;
96         var point = this._mainChart ? this._mainChart.currentPoint() : null;
97         return [
98             this._platformId,
99             this._metricId,
100             selection || (point && this._mainChartIndicatorWasLocked ? point.id : null),
101         ];
102     }
103
104     updateFromSerializedState(state, isOpen)
105     {
106         if (!this._mainChart)
107             return;
108
109         var selectionOrIndicatedPoint = state[2];
110         if (selectionOrIndicatedPoint instanceof Array)
111             this._mainChart.setSelection([parseFloat(selectionOrIndicatedPoint[0]), parseFloat(selectionOrIndicatedPoint[1])]);
112         else if (typeof(selectionOrIndicatedPoint) == 'number') {
113             this._mainChart.setIndicator(selectionOrIndicatedPoint, true);
114             this._mainChartIndicatorWasLocked = true;
115         } else
116             this._mainChart.setIndicator(null, false);
117     }
118
119     setOverviewDomain(startTime, endTime)
120     {
121         if (this._overviewChart)
122             this._overviewChart.setDomain(startTime, endTime);
123     }
124
125     setOverviewSelection(selection)
126     {
127         if (this._overviewChart)
128             this._overviewChart.setSelection(selection);
129     }
130
131     setMainDomain(startTime, endTime)
132     {
133         if (this._mainChart)
134             this._mainChart.setDomain(startTime, endTime);
135     }
136
137     _mainSelectionDidChange(selection, didEndDrag)
138     {
139         this._chartsPage.mainChartSelectionDidChange(this, didEndDrag);
140         this.render();
141     }
142
143     _mainSelectionDidZoom(selection)
144     {
145         this._overviewChart.setSelection(selection, this);
146         this._mainChart.setSelection(null);
147         this._chartsPage.setMainDomainFromZoom(selection, this);
148         this.render();
149     }
150
151     _openAnalysisTask(annotation)
152     {
153         window.open(this._chartsPage.router().url(`analysis/task/${annotation.task.id()}`), '_blank');
154     }
155
156     _indicatorDidChange(indicatorID, isLocked)
157     {
158         this._chartsPage.mainChartIndicatorDidChange(this, isLocked || this._mainChartIndicatorWasLocked);
159         this._mainChartIndicatorWasLocked = isLocked;
160         this._mainChartStatus.updateRevisionListWithNotification();
161         this.render();
162     }
163
164     _openCommitViewer(repository, from, to)
165     {
166         var self = this;
167         this._commitLogViewer.view(repository, from, to).then(function () {
168             self._mainChartStatus.setCurrentRepository(self._commitLogViewer.currentRepository());
169             self.render();
170         });
171     }
172     
173     _didFetchData()
174     {
175         this._mainChartStatus.updateRevisionListWithNotification();
176         this.render();
177     }
178
179     _keyup(event)
180     {
181         switch (event.keyCode) {
182         case 37: // Left
183             if (!this._mainChart.moveLockedIndicatorWithNotification(false))
184                 return;
185             break;
186         case 39: // Right
187             if (!this._mainChart.moveLockedIndicatorWithNotification(true))
188                 return;
189             break;
190         case 38: // Up
191             if (!this._mainChartStatus.moveRepositoryWithNotification(false))
192                 return;
193         case 40: // Down
194             if (!this._mainChartStatus.moveRepositoryWithNotification(true))
195                 return;
196         default:
197             return;
198         }
199
200         event.preventDefault();
201         event.stopPropagation();
202     }
203
204     render()
205     {
206         Instrumentation.startMeasuringTime('ChartPane', 'render');
207
208         super.render();
209
210         if (this._platform && this._metric) {
211             var metric = this._metric;
212             var platform = this._platform;
213
214             this.renderReplace(this.content().querySelector('.chart-pane-title'),
215                 metric.fullName() + ' on ' + platform.name());
216         }
217
218         if (this._errorMessage) {
219             this.renderReplace(this.content().querySelector('.chart-pane-main'), this._errorMessage);
220             return;
221         }
222
223         if (this._mainChartStatus) {
224             this._mainChartStatus.render();
225             this._renderActionToolbar(this._mainChartStatus.analyzeData());
226         }
227
228         var body = this.content().querySelector('.chart-pane-body');
229         if (this._commitLogViewer.currentRepository()) {
230             body.classList.add('has-second-sidebar');
231             this._commitLogViewer.render();
232         } else
233             body.classList.remove('has-second-sidebar');
234
235         Instrumentation.endMeasuringTime('ChartPane', 'render');
236     }
237
238     _renderActionToolbar(analyzeData)
239     {
240         var actions = [];
241         var platform = this._platform;
242         var metric = this._metric;
243
244         var element = ComponentBase.createElement;
245         var link = ComponentBase.createLink;
246         var self = this;
247
248         if (this._chartsPage.canBreakdown(platform, metric)) {
249             actions.push(element('li', link('Breakdown', function () {
250                 self._chartsPage.insertBreakdownPanesAfter(platform, metric, self);
251             })));
252         }
253
254         var platformPane = this.content().querySelector('.chart-pane-alternative-platforms');
255         var alternativePlatforms = this._chartsPage.alternatePlatforms(platform, metric);
256         if (alternativePlatforms.length) {
257             this.renderReplace(platformPane, Platform.sortByName(alternativePlatforms).map(function (platform) {
258                 return element('li', link(platform.label(), function () {
259                     self._chartsPage.insertPaneAfter(platform, metric, self);
260                 }));
261             }));
262
263             actions.push(element('li', {class: this._paneOpenedByClick == platformPane ? 'selected' : ''},
264                 this._makeAnchorToOpenPane(platformPane, 'Other Platforms', true)));
265         } else {
266             platformPane.style.display = 'none';
267         }
268
269         var analyzePane = this.content().querySelector('.chart-pane-analyze-pane');
270         if (analyzeData) {
271             actions.push(element('li', {class: this._paneOpenedByClick == analyzePane ? 'selected' : ''},
272                 this._makeAnchorToOpenPane(analyzePane, 'Analyze', false)));
273
274             var router = this._chartsPage.router();
275             analyzePane.onsubmit = function (event) {
276                 event.preventDefault();
277                 var newWindow = window.open(router.url('analysis/task/create'), '_blank');
278
279                 var name = analyzePane.querySelector('input').value;
280                 AnalysisTask.create(name, analyzeData.startPointId, analyzeData.endPointId).then(function (data) {
281                     newWindow.location.href = router.url('analysis/task/' + data['taskId']);
282                     // FIXME: Refetch the list of analysis tasks.
283                 }, function (error) {
284                     newWindow.location.href = router.url('analysis/task/create', {error: error});
285                 });
286             }
287         } else {
288             analyzePane.style.display = 'none';
289             analyzePane.onsubmit = function (event) { event.preventDefault(); }
290         }
291
292         this._paneOpenedByClick = null;
293         this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
294     }
295
296     _makeAnchorToOpenPane(pane, label, shouldRespondToHover)
297     {
298         var anchor = null;
299         var ignoreMouseLeave = false;
300         var self = this;
301         var setPaneVisibility = function (pane, shouldShow) {
302             var anchor = pane.anchor;
303             if (shouldShow) {
304                 var width = anchor.offsetParent.offsetWidth;
305                 pane.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
306                 pane.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
307             }
308             pane.style.display = shouldShow ? null : 'none';
309             anchor.parentNode.className = shouldShow ? 'selected' : '';
310             if (self._paneOpenedByClick == pane && !shouldShow)
311                 self._paneOpenedByClick = null;
312         }
313
314         var attributes = {
315             href: '#',
316             onclick: function (event) {
317                 event.preventDefault();
318                 var shouldShowPane = pane.style.display == 'none';
319                 if (shouldShowPane) {
320                     if (self._paneOpenedByClick)
321                         setPaneVisibility(self._paneOpenedByClick, false);
322                     self._paneOpenedByClick = pane;
323                 }
324                 setPaneVisibility(pane, shouldShowPane);
325             },
326         };
327         if (shouldRespondToHover) {
328             var mouseIsInAnchor = false;
329             var mouseIsInPane = false;
330
331             attributes.onmouseenter = function () {
332                 if (self._paneOpenedByClick)
333                     return;
334                 mouseIsInAnchor = true;
335                 setPaneVisibility(pane, true);
336             }
337             attributes.onmouseleave = function () {
338                 setTimeout(function () {
339                     if (!mouseIsInPane)
340                         setPaneVisibility(pane, false);
341                 }, 0);
342                 mouseIsInAnchor = false;                
343             }
344
345             pane.onmouseleave = function () {
346                 setTimeout(function () {
347                     if (!mouseIsInAnchor)
348                         setPaneVisibility(pane, false);
349                 }, 0);
350                 mouseIsInPane = false;
351             }
352             pane.onmouseenter = function () {
353                 mouseIsInPane = true;
354             }
355         }
356
357         var anchor = ComponentBase.createElement('a', attributes, label);
358         pane.anchor = anchor;
359         return anchor;
360     }
361
362     static htmlTemplate()
363     {
364         return `
365             <section class="chart-pane" tabindex="0">
366                 <header class="chart-pane-header">
367                     <h2 class="chart-pane-title">-</h2>
368                     <nav class="chart-pane-actions">
369                         <ul>
370                             <li class="close"><close-button></close-button></li>
371                         </ul>
372                         <ul class="chart-pane-action-buttons buttoned-toolbar"></ul>
373                         <ul class="chart-pane-alternative-platforms" style="display:none"></ul>
374                         <form class="chart-pane-analyze-pane" style="display:none">
375                             <input type="text" required>
376                             <button>Create</button>
377                         </form>
378                     </nav>
379                 </header>
380                 <div class="chart-pane-body">
381                     <div class="chart-pane-main"></div>
382                     <div class="chart-pane-sidebar">
383                         <div class="chart-pane-overview"></div>
384                         <div class="chart-pane-details"></div>
385                     </div>
386                     <div class="chart-pane-second-sidebar">
387                         <commit-log-viewer></commit-log-viewer>
388                     </div>
389                 </div>
390             </section>
391 `;
392     }
393
394     static cssTemplate()
395     {
396         return Toolbar.cssTemplate() + `
397             .chart-pane {
398                 margin: 1rem;
399                 margin-bottom: 2rem;
400                 padding: 0rem;
401                 height: 18rem;
402                 border: solid 1px #ccc;
403                 border-radius: 0.5rem;
404                 outline: none;
405             }
406
407             .chart-pane:focus .chart-pane-header {
408                 background: rgba(204, 153, 51, 0.1);
409             }
410
411             .chart-pane-header {
412                 position: relative;
413                 left: 0;
414                 top: 0;
415                 width: 100%;
416                 height: 2rem;
417                 line-height: 2rem;
418                 border-bottom: solid 1px #ccc;
419             }
420
421             .chart-pane-title {
422                 margin: 0 0.5rem;
423                 padding: 0;
424                 padding-left: 1.5rem;
425                 font-size: 1rem;
426                 font-weight: inherit;
427             }
428
429             .chart-pane-actions {
430                 position: absolute;
431                 display: flex;
432                 flex-direction: row;
433                 justify-content: space-between;
434                 align-items: center;
435                 width: 100%;
436                 height: 2rem;
437                 top: 0;
438                 padding: 0 0;
439             }
440
441             .chart-pane-actions ul {
442                 display: block;
443                 padding: 0;
444                 margin: 0 0.5rem;
445                 font-size: 1rem;
446                 line-height: 1rem;
447                 list-style: none;
448             }
449
450             .chart-pane-actions .chart-pane-action-buttons {
451                 font-size: 0.9rem;
452                 line-height: 0.9rem;
453             }
454
455             .chart-pane-actions .chart-pane-alternative-platforms,
456             .chart-pane-analyze-pane {
457                 position: absolute;
458                 top: 0;
459                 right: 0;
460                 border: solid 1px #ccc;
461                 border-radius: 0.2rem;
462                 z-index: 10;
463                 background: rgba(255, 255, 255, 0.8);
464                 -webkit-backdrop-filter: blur(0.5rem);
465                 padding: 0.2rem 0;
466                 margin: 0;
467                 margin-top: -0.2rem;
468                 margin-right: -0.2rem;
469             }
470
471             .chart-pane-alternative-platforms li {
472             }
473
474             .chart-pane-alternative-platforms li a {
475                 display: block;
476                 text-decoration: none;
477                 color: inherit;
478                 font-size: 0.9rem;
479                 padding: 0.2rem 0.5rem;
480             }
481
482             .chart-pane-alternative-platforms a:hover,
483             .chart-pane-analyze-pane input:focus {
484                 background: rgba(204, 153, 51, 0.1);
485             }
486
487             .chart-pane-analyze-pane {
488                 padding: 0.5rem;
489             }
490
491             .chart-pane-analyze-pane input {
492                 font-size: 1rem;
493                 width: 15rem;
494                 outline: none;
495                 border: solid 1px #ccc;
496             }
497
498             .chart-pane-body {
499                 position: relative;
500                 width: 100%;
501                 height: calc(100% - 2rem);
502             }
503
504             .chart-pane-main {
505                 padding-right: 20rem;
506                 height: 100%;
507                 margin: 0;
508                 vertical-align: middle;
509                 text-align: center;
510             }
511
512             .has-second-sidebar .chart-pane-main {
513                 padding-right: 40rem;
514             }
515
516             .chart-pane-main > * {
517                 width: 100%;
518                 height: 100%;
519             }
520
521             .chart-pane-sidebar,
522             .chart-pane-second-sidebar {
523                 position: absolute;
524                 right: 0;
525                 top: 0;
526                 width: 0;
527                 border-left: solid 1px #ccc;
528                 height: 100%;
529             }
530
531             :not(.has-second-sidebar) > .chart-pane-second-sidebar {
532                 border-left: 0;
533             }
534
535             .chart-pane-sidebar {
536                 width: 20rem;
537             }
538
539             .has-second-sidebar .chart-pane-sidebar {
540                 right: 20rem;
541             }
542
543             .has-second-sidebar .chart-pane-second-sidebar {
544                 width: 20rem;
545             }
546
547             .chart-pane-overview {
548                 width: 100%;
549                 height: 5rem;
550                 border-bottom: solid 1px #ccc;
551             }
552
553             .chart-pane-overview > * {
554                 display: block;
555                 width: 100%;
556                 height: 100%;
557             }
558
559             .chart-pane-details {
560                 position: relative;
561                 display: block;
562                 height: calc(100% - 5.5rem - 2px);
563                 overflow-y: scroll;
564                 padding-top: 0.5rem;
565             }
566 `;
567     }
568 }