95662c4badef1c0f9fa2de62bc49ad8f1f70b016
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / components / chart-pane-base.js
1
2 class ChartPaneBase extends ComponentBase {
3
4     constructor(name)
5     {
6         super(name);
7
8         this._errorMessage = null;
9         this._platformId = null;
10         this._metricId = null;
11         this._platform = null;
12         this._metric = null;
13         this._disableSampling = false;
14         this._showOutliers = false;
15         this._openRepository = null;
16
17         this._overviewChart = null;
18         this._mainChart = null;
19         this._mainChartStatus = null;
20         this._commitLogViewer = null;
21         this._tasksForAnnotations = null;
22         this._renderedAnnotations = false;
23     }
24
25     configure(platformId, metricId)
26     {
27         var result = ChartStyles.resolveConfiguration(platformId, metricId);
28         this._errorMessage = result.error;
29         this._platformId = platformId;
30         this._metricId = metricId;
31         this._platform = result.platform;
32         this._metric = result.metric;
33
34         this._overviewChart = null;
35         this._mainChart = null;
36         this._mainChartStatus = null;
37
38         this._commitLogViewer = this.content().querySelector('commit-log-viewer').component();
39
40         if (result.error)
41             return;
42
43         var formatter = result.metric.makeFormatter(4);
44
45         this._overviewChart = new InteractiveTimeSeriesChart(this._createSourceList(false), ChartStyles.overviewChartOptions(formatter));
46         this._overviewChart.listenToAction('selectionChange', this._overviewSelectionDidChange.bind(this));
47         this.renderReplace(this.content().querySelector('.chart-pane-overview'), this._overviewChart);
48
49         this._mainChart = new InteractiveTimeSeriesChart(this._createSourceList(true), ChartStyles.mainChartOptions(formatter));
50         this._mainChart.listenToAction('dataChange', () => this._didFetchData());
51         this._mainChart.listenToAction('indicatorChange', this._indicatorDidChange.bind(this));
52         this._mainChart.listenToAction('selectionChange', this._mainSelectionDidChange.bind(this));
53         this._mainChart.listenToAction('zoom', this._mainSelectionDidZoom.bind(this));
54         this._mainChart.listenToAction('annotationClick', this._openAnalysisTask.bind(this));
55         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
56
57         this._revisionRange = new ChartRevisionRange(this._mainChart);
58
59         this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart);
60         this._mainChartStatus.listenToAction('openRepository', this.openNewRepository.bind(this));
61         this.renderReplace(this.content().querySelector('.chart-pane-details'), this._mainChartStatus);
62
63         this.content().querySelector('.chart-pane').addEventListener('keydown', this._keyup.bind(this));
64
65         this.fetchAnalysisTasks(false);
66     }
67
68     isSamplingEnabled() { return !this._disableSampling; }
69     setSamplingEnabled(enabled)
70     {
71         this._disableSampling = !enabled;
72         this._updateSourceList();
73     }
74
75     isShowingOutliers() { return this._showOutliers; }
76     setShowOutliers(show)
77     {
78         this._showOutliers = !!show;
79         this._updateSourceList();
80     }
81
82     _createSourceList(isMainChart)
83     {
84         return ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers, isMainChart);
85     }
86
87     _updateSourceList()
88     {
89         this._mainChart.setSourceList(this._createSourceList(true));
90         this._overviewChart.setSourceList(this._createSourceList(false));
91     }
92
93     fetchAnalysisTasks(noCache)
94     {
95         // FIXME: we need to update the annotation bars when the change type of tasks change.
96         var self = this;
97         AnalysisTask.fetchByPlatformAndMetric(this._platformId, this._metricId, noCache).then(function (tasks) {
98             self._tasksForAnnotations = tasks;
99             self._renderedAnnotations = false;
100             self.enqueueToRender();
101         });
102     }
103
104     // FIXME: We should have a mechanism to get notified whenever the set of annotations change.
105     didUpdateAnnotations()
106     {
107         this._renderedAnnotations = false;
108         this.enqueueToRender();
109     }
110
111     platformId() { return this._platformId; }
112     metricId() { return this._metricId; }
113
114     setOverviewDomain(startTime, endTime)
115     {
116         if (this._overviewChart)
117             this._overviewChart.setDomain(startTime, endTime);
118     }
119
120     setMainDomain(startTime, endTime)
121     {
122         if (this._mainChart)
123             this._mainChart.setDomain(startTime, endTime);
124     }
125
126     setMainSelection(selection)
127     {
128         if (this._mainChart)
129             this._mainChart.setSelection(selection);
130     }
131
132     setOpenRepository(repository)
133     {
134         this._openRepository = repository;
135         this._mainChartStatus.setCurrentRepository(repository);
136         this._updateCommitLogViewer();
137     }
138
139     _overviewSelectionDidChange(domain, didEndDrag) { }
140
141     _mainSelectionDidChange(selection, didEndDrag)
142     {
143         this._updateCommitLogViewer();
144     }
145
146     _mainSelectionDidZoom(selection)
147     {
148         this._overviewChart.setSelection(selection, this);
149         this._mainChart.setSelection(null);
150         this.enqueueToRender();
151     }
152
153     _indicatorDidChange(indicatorID, isLocked)
154     {
155         this._updateCommitLogViewer();
156     }
157
158     _didFetchData()
159     {
160         this._updateCommitLogViewer();
161     }
162
163     _updateCommitLogViewer()
164     {
165         const range = this._revisionRange.rangeForRepository(this._openRepository);
166         this._commitLogViewer.view(this._openRepository, range.from, range.to);
167         this.enqueueToRender();
168     }
169
170     _openAnalysisTask(annotation)
171     {
172         var router = this.router();
173         console.assert(router);
174         window.open(router.url(`analysis/task/${annotation.task.id()}`), '_blank');
175     }
176
177     router() { return null; }
178
179     openNewRepository(repository)
180     {
181         this.content().querySelector('.chart-pane').focus();
182         this.setOpenRepository(repository);
183     }
184
185     _keyup(event)
186     {
187         switch (event.keyCode) {
188         case 37: // Left
189             if (!this._mainChart.moveLockedIndicatorWithNotification(false))
190                 return;
191             break;
192         case 39: // Right
193             if (!this._mainChart.moveLockedIndicatorWithNotification(true))
194                 return;
195             break;
196         case 38: // Up
197             if (!this._moveOpenRepository(false))
198                 return;
199             break;
200         case 40: // Down
201             if (!this._moveOpenRepository(true))
202                 return;
203             break;
204         default:
205             return;
206         }
207
208         this.enqueueToRender();
209
210         event.preventDefault();
211         event.stopPropagation();
212     }
213
214     _moveOpenRepository(forward)
215     {
216         const openRepository = this._openRepository;
217         if (!openRepository)
218             return false;
219
220         const revisionList = this._revisionRange.revisionList();
221         if (!revisionList)
222             return false;
223
224         const currentIndex = revisionList.findIndex((info) => info.repository == openRepository);
225         console.assert(currentIndex >= 0);
226
227         const newIndex = currentIndex + (forward ? 1 : -1);
228         if (newIndex < 0 || newIndex >= revisionList.length)
229             return false;
230
231         this.openNewRepository(revisionList[newIndex].repository);
232
233         return true;
234     }
235
236     render()
237     {
238         Instrumentation.startMeasuringTime('ChartPane', 'render');
239
240         super.render();
241
242         if (this._overviewChart)
243             this._overviewChart.enqueueToRender();
244
245         if (this._mainChart)
246             this._mainChart.enqueueToRender();
247
248         if (this._errorMessage) {
249             this.renderReplace(this.content().querySelector('.chart-pane-main'), this._errorMessage);
250             return;
251         }
252
253         this._renderAnnotations();
254
255         if (this._mainChartStatus)
256             this._mainChartStatus.enqueueToRender();
257
258         var body = this.content().querySelector('.chart-pane-body');
259         if (this._openRepository)
260             body.classList.add('has-second-sidebar');
261         else
262             body.classList.remove('has-second-sidebar');
263
264         Instrumentation.endMeasuringTime('ChartPane', 'render');
265     }
266
267     _renderAnnotations()
268     {
269         if (!this._tasksForAnnotations || this._renderedAnnotations)
270             return;
271         this._renderedAnnotations = true;
272
273         var annotations = this._tasksForAnnotations.map(function (task) {
274             var fillStyle = '#fc6';
275             switch (task.changeType()) {
276             case 'inconclusive':
277                 fillStyle = '#fcc';
278                 break;
279             case 'progression':
280                 fillStyle = '#39f';
281                 break;
282             case 'regression':
283                 fillStyle = '#c60';
284                 break;
285             case 'unchanged':
286                 fillStyle = '#ccc';
287                 break;
288             }
289
290             return {
291                 task: task,
292                 startTime: task.startTime(),
293                 endTime: task.endTime(),
294                 label: task.label(),
295                 fillStyle: fillStyle,
296             };
297         });
298         this._mainChart.setAnnotations(annotations);
299     }
300
301     static htmlTemplate()
302     {
303         return `
304             <section class="chart-pane" tabindex="0">
305                 ${this.paneHeaderTemplate()}
306                 <div class="chart-pane-body">
307                     <div class="chart-pane-main"></div>
308                     <div class="chart-pane-sidebar">
309                         <div class="chart-pane-overview"></div>
310                         <div class="chart-pane-details"></div>
311                     </div>
312                     <div class="chart-pane-second-sidebar">
313                         <commit-log-viewer></commit-log-viewer>
314                     </div>
315                 </div>
316             </section>
317             ${this.paneFooterTemplate()}
318         `;
319     }
320
321     static paneHeaderTemplate() { return ''; }
322     static paneFooterTemplate() { return ''; }
323
324     static cssTemplate()
325     {
326         return Toolbar.cssTemplate() + `
327             .chart-pane {
328                 padding: 0rem;
329                 height: 18rem;
330                 outline: none;
331             }
332
333             .chart-pane:focus .chart-pane-header {
334                 background: rgba(204, 153, 51, 0.1);
335             }
336
337             .chart-pane-body {
338                 position: relative;
339                 width: 100%;
340                 height: 100%;
341             }
342
343             .chart-pane-main {
344                 padding-right: 20rem;
345                 height: 100%;
346                 margin: 0;
347                 vertical-align: middle;
348                 text-align: center;
349             }
350
351             .has-second-sidebar .chart-pane-main {
352                 padding-right: 40rem;
353             }
354
355             .chart-pane-main > * {
356                 width: 100%;
357                 height: 100%;
358             }
359
360             .chart-pane-sidebar,
361             .chart-pane-second-sidebar {
362                 position: absolute;
363                 right: 0;
364                 top: 0;
365                 width: 0;
366                 border-left: solid 1px #ccc;
367                 height: 100%;
368             }
369
370             :not(.has-second-sidebar) > .chart-pane-second-sidebar {
371                 border-left: 0;
372             }
373
374             .chart-pane-sidebar {
375                 width: 20rem;
376             }
377
378             .has-second-sidebar .chart-pane-sidebar {
379                 right: 20rem;
380             }
381
382             .has-second-sidebar .chart-pane-second-sidebar {
383                 width: 20rem;
384             }
385
386             .chart-pane-overview {
387                 width: 100%;
388                 height: 5rem;
389                 border-bottom: solid 1px #ccc;
390             }
391
392             .chart-pane-overview > * {
393                 display: block;
394                 width: 100%;
395                 height: 100%;
396             }
397
398             .chart-pane-details {
399                 position: relative;
400                 display: block;
401                 height: calc(100% - 5.5rem - 2px);
402                 overflow-y: scroll;
403                 padding-top: 0.5rem;
404             }
405         `;
406     }
407
408 }