Extend create-analysis-test API to be able to create with confirming test group.
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / pages / chart-pane.js
1
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();
6         if (!values.length)
7             return Promise.resolve(null);
8
9         var averageValues = callback.call(null, values, ...parameters);
10         if (!averageValues)
11             return Promise.resolve(null);
12
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};
17
18         return Promise.resolve(result);
19     }
20 }
21
22 const ChartTrendLineTypes = [
23     {
24         id: 0,
25         label: 'None',
26     },
27     {
28         id: 5,
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) {
33                 return segmentation;
34             });
35         },
36         parameterList: [
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}
39         ]
40     },
41     {
42         id: 6,
43         label: 'Segmentation with Welch\'s t-test change detection',
44         execute: async function (source, parameters) {
45             const segmentation =  await source.measurementSet.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', parameters,
46                 source.type, source.includeOutliers, source.extendToFuture);
47             if (!segmentation)
48                 return segmentation;
49
50             const metric = Metric.findById(source.measurementSet.metricId());
51             const timeSeries = source.measurementSet.fetchedTimeSeries(source.type, source.includeOutliers, source.extendToFuture);
52             segmentation.analysisAnnotations = Statistics.findRangesForChangeDetectionsWithWelchsTTest(timeSeries.values(),
53                 segmentation, parameters[parameters.length - 1]).map((range) => {
54                 const startPoint = timeSeries.findPointByIndex(range.startIndex);
55                 const endPoint = timeSeries.findPointByIndex(range.endIndex);
56                 const summary = metric.labelForDifference(range.segmentationStartValue, range.segmentationEndValue, 'progression', 'regression');
57                 return {
58                     task: null,
59                     fillStyle: ChartStyles.annotationFillStyleForTask(null),
60                     startTime: startPoint.time,
61                     endTime: endPoint.time,
62                     label: `Potential ${summary.changeLabel}`,
63                 };
64             });
65             return segmentation;
66         },
67         parameterList: [
68             {label: "Segment count weight", value: 2.5, min: 0.01, max: 10, step: 0.01},
69             {label: "Grid size", value: 500, min: 100, max: 10000, step: 10},
70             {label: "t-test significance", value: 0.99, options: Statistics.supportedOneSideTTestProbabilities()},
71         ]
72     },
73     {
74         id: 1,
75         label: 'Simple Moving Average',
76         parameterList: [
77             {label: "Backward window size", value: 8, min: 2, step: 1},
78             {label: "Forward window size", value: 4, min: 0, step: 1}
79         ],
80         execute: createTrendLineExecutableFromAveragingFunction(Statistics.movingAverage.bind(Statistics))
81     },
82     {
83         id: 2,
84         label: 'Cumulative Moving Average',
85         execute: createTrendLineExecutableFromAveragingFunction(Statistics.cumulativeMovingAverage.bind(Statistics))
86     },
87     {
88         id: 3,
89         label: 'Exponential Moving Average',
90         parameterList: [
91             {label: "Smoothing factor", value: 0.01, min: 0.001, max: 0.9, step: 0.001},
92         ],
93         execute: createTrendLineExecutableFromAveragingFunction(Statistics.exponentialMovingAverage.bind(Statistics))
94     },
95 ];
96 ChartTrendLineTypes.DefaultType = ChartTrendLineTypes[1];
97
98
99 class ChartPane extends ChartPaneBase {
100     constructor(chartsPage, platformId, metricId)
101     {
102         super('chart-pane');
103
104         this._mainChartIndicatorWasLocked = false;
105         this._chartsPage = chartsPage;
106         this._lockedPopover = null;
107         this._trendLineType = null;
108         this._trendLineParameters = [];
109         this._trendLineVersion = 0;
110         this._renderedTrendLineOptions = false;
111
112         this.configure(platformId, metricId);
113     }
114
115     didConstructShadowTree()
116     {
117         this.part('close').listenToAction('activate', () => {
118             this._chartsPage.closePane(this);
119         });
120         const createWithTestGroupCheckbox = this.content('create-with-test-group');
121         const repetitionCount = this.content('confirm-repetition');
122         createWithTestGroupCheckbox.onchange = () => repetitionCount.disabled = !createWithTestGroupCheckbox.checked;
123     }
124
125     serializeState()
126     {
127         var state = [this._platformId, this._metricId];
128         if (this._mainChart) {
129             var selection = this._mainChart.currentSelection();
130             const indicator = this._mainChart.currentIndicator();
131             if (selection)
132                 state[2] = selection;
133             else if (indicator && indicator.isLocked)
134                 state[2] = indicator.point.id;
135         }
136
137         var graphOptions = new Set;
138         if (!this.isSamplingEnabled())
139             graphOptions.add('noSampling');
140         if (this.isShowingOutliers())
141             graphOptions.add('showOutliers');
142
143         if (graphOptions.size)
144             state[3] = graphOptions;
145
146         if (this._trendLineType)
147             state[4] = [this._trendLineType.id].concat(this._trendLineParameters);
148
149         return state;
150     }
151
152     updateFromSerializedState(state, isOpen)
153     {
154         if (!this._mainChart)
155             return;
156
157         var selectionOrIndicatedPoint = state[2];
158         if (selectionOrIndicatedPoint instanceof Array)
159             this._mainChart.setSelection([parseFloat(selectionOrIndicatedPoint[0]), parseFloat(selectionOrIndicatedPoint[1])]);
160         else if (typeof(selectionOrIndicatedPoint) == 'number') {
161             this._mainChart.setIndicator(selectionOrIndicatedPoint, true);
162             this._mainChartIndicatorWasLocked = true;
163         } else
164             this._mainChart.setIndicator(null, false);
165
166         // FIXME: This forces sourceList to be set twice. First in configure inside the constructor then here.
167         // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
168         var graphOptions = state[3];
169         if (graphOptions instanceof Set) {
170             this.setSamplingEnabled(!graphOptions.has('nosampling'));
171             this.setShowOutliers(graphOptions.has('showoutliers'));
172         }
173
174         var trendLineOptions = state[4];
175         if (!(trendLineOptions instanceof Array))
176             trendLineOptions = [];
177
178         var trendLineId = trendLineOptions[0];
179         var trendLineType = ChartTrendLineTypes.find(function (type) { return type.id == trendLineId; }) || ChartTrendLineTypes.DefaultType;
180
181         this._trendLineType = trendLineType;
182         this._trendLineParameters = (trendLineType.parameterList || []).map(function (parameter, index) {
183             var specifiedValue = parseFloat(trendLineOptions[index + 1]);
184             return !isNaN(specifiedValue) ? specifiedValue : parameter.value;
185         });
186         this._updateTrendLine();
187         this._renderedTrendLineOptions = false;
188
189         // FIXME: state[5] specifies envelope in v2 UI
190         // FIXME: state[6] specifies change detection algorithm in v2 UI
191     }
192
193     setOverviewSelection(selection)
194     {
195         if (this._overviewChart)
196             this._overviewChart.setSelection(selection);
197     }
198
199     _overviewSelectionDidChange(domain, didEndDrag)
200     {
201         super._overviewSelectionDidChange(domain, didEndDrag);
202         this._chartsPage.setMainDomainFromOverviewSelection(domain, this, didEndDrag);
203     }
204
205     _mainSelectionDidChange(selection, didEndDrag)
206     {
207         super._mainSelectionDidChange(selection, didEndDrag);
208         this._chartsPage.mainChartSelectionDidChange(this, didEndDrag);
209     }
210
211     _mainSelectionDidZoom(selection)
212     {
213         super._mainSelectionDidZoom(selection);
214         this._chartsPage.setMainDomainFromZoom(selection, this);
215     }
216
217     router() { return this._chartsPage.router(); }
218
219     openNewRepository(repository)
220     {
221         this.content().querySelector('.chart-pane').focus();
222         this._chartsPage.setOpenRepository(repository);
223     }
224
225     _indicatorDidChange(indicatorID, isLocked)
226     {
227         this._chartsPage.mainChartIndicatorDidChange(this, isLocked != this._mainChartIndicatorWasLocked);
228         this._mainChartIndicatorWasLocked = isLocked;
229         super._indicatorDidChange(indicatorID, isLocked);
230     }
231
232     async _analyzeRange(startPoint, endPoint)
233     {
234         const router = this._chartsPage.router();
235         const newWindow = window.open(router.url('analysis/task/create', {inProgress: true}), '_blank');
236
237         const name = this.content('task-name').value;
238         const createWithTestGroup = this.content('create-with-test-group').checked;
239         const repetitionCount = this.content('confirm-repetition').value;
240
241         try {
242             const data = await (createWithTestGroup ?
243                 AnalysisTask.create(name, startPoint, endPoint, 'Confirm', repetitionCount) : AnalysisTask.create(name, startPoint, endPoint));
244             newWindow.location.href = router.url('analysis/task/' + data['taskId']);
245             this.fetchAnalysisTasks(true);
246         } catch(error) {
247             newWindow.location.href = router.url('analysis/task/create', {error: error});
248         }
249     }
250
251     _markAsOutlier(markAsOutlier, points)
252     {
253         var self = this;
254         return Promise.all(points.map(function (point) {
255             return PrivilegedAPI.sendRequest('update-run-status', {'run': point.id, 'markedOutlier': markAsOutlier});
256         })).then(function () {
257             self._mainChart.fetchMeasurementSets(true /* noCache */);
258         }, function (error) {
259             alert('Failed to update the outlier status: ' + error);
260         }).catch();
261     }
262
263     render()
264     {
265         if (this._platform && this._metric) {
266             var metric = this._metric;
267             var platform = this._platform;
268
269             this.renderReplace(this.content().querySelector('.chart-pane-title'),
270                 metric.fullName() + ' on ' + platform.name());
271         }
272
273         if (this._mainChartStatus)
274             this._renderActionToolbar();
275
276         super.render();
277     }
278
279     _renderActionToolbar()
280     {
281         var actions = [];
282         var platform = this._platform;
283         var metric = this._metric;
284
285         var element = ComponentBase.createElement;
286         var link = ComponentBase.createLink;
287         var self = this;
288
289         if (this._chartsPage.canBreakdown(platform, metric)) {
290             actions.push(element('li', link('Breakdown', function () {
291                 self._chartsPage.insertBreakdownPanesAfter(platform, metric, self);
292             })));
293         }
294
295         var platformPopover = this.content().querySelector('.chart-pane-alternative-platforms');
296         var alternativePlatforms = this._chartsPage.alternatePlatforms(platform, metric);
297         if (alternativePlatforms.length) {
298             this.renderReplace(platformPopover, Platform.sortByName(alternativePlatforms).map(function (platform) {
299                 return element('li', link(platform.label(), function () {
300                     self._chartsPage.insertPaneAfter(platform, metric, self);
301                 }));
302             }));
303
304             actions.push(this._makePopoverActionItem(platformPopover, 'Other Platforms', true));
305         } else
306             platformPopover.style.display = 'none';
307
308         var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
309         const selectedPoints = this._mainChart.selectedPoints('current');
310         const hasSelectedPoints = selectedPoints && selectedPoints.length();
311         if (hasSelectedPoints) {
312             actions.push(this._makePopoverActionItem(analyzePopover, 'Analyze', false));
313             analyzePopover.onsubmit = this.createEventHandler(() => {
314                 this._analyzeRange(selectedPoints.firstPoint(), selectedPoints.lastPoint());
315             });
316         } else {
317             analyzePopover.style.display = 'none';
318             analyzePopover.onsubmit = this.createEventHandler(() => {});
319         }
320
321         var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
322         actions.push(this._makePopoverActionItem(filteringOptions, 'Filtering', true));
323
324         var trendLineOptions = this.content().querySelector('.chart-pane-trend-line-options');
325         actions.push(this._makePopoverActionItem(trendLineOptions, 'Trend lines', true));
326
327         this._renderFilteringPopover();
328         this._renderTrendLinePopover();
329
330         this._lockedPopover = null;
331         this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
332     }
333
334     _makePopoverActionItem(popover, label, shouldRespondToHover)
335     {
336         var self = this;
337         popover.anchor = ComponentBase.createLink(label, function () {
338             var makeVisible = self._lockedPopover != popover;
339             self._setPopoverVisibility(popover, makeVisible);
340             if (makeVisible)
341                 self._lockedPopover = popover;
342         });
343         if (shouldRespondToHover)
344             this._makePopoverOpenOnHover(popover);
345
346         return ComponentBase.createElement('li', {class: this._lockedPopover == popover ? 'selected' : ''}, popover.anchor);
347     }
348
349     _makePopoverOpenOnHover(popover)
350     {
351         var mouseIsInAnchor = false;
352         var mouseIsInPopover = false;
353
354         var self = this;
355         var closeIfNeeded = function () {
356             setTimeout(function () {
357                 if (self._lockedPopover != popover && !mouseIsInAnchor && !mouseIsInPopover)
358                     self._setPopoverVisibility(popover, false);
359             }, 0);
360         }
361
362         popover.anchor.onmouseenter = function () {
363             if (self._lockedPopover)
364                 return;
365             mouseIsInAnchor = true;
366             self._setPopoverVisibility(popover, true);
367         }
368         popover.anchor.onmouseleave = function () {
369             mouseIsInAnchor = false;
370             closeIfNeeded();         
371         }
372
373         popover.onmouseenter = function () {
374             mouseIsInPopover = true;
375         }
376         popover.onmouseleave = function () {
377             mouseIsInPopover = false;
378             closeIfNeeded();
379         }
380     }
381
382     _setPopoverVisibility(popover, visible)
383     {
384         var anchor = popover.anchor;
385         if (visible) {
386             var width = anchor.offsetParent.offsetWidth;
387             popover.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
388             popover.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
389         }
390         popover.style.display = visible ? null : 'none';
391         anchor.parentNode.className = visible ? 'selected' : '';
392
393         if (this._lockedPopover && this._lockedPopover != popover && visible)
394             this._setPopoverVisibility(this._lockedPopover, false);
395
396         if (this._lockedPopover == popover && !visible)
397             this._lockedPopover = null;
398     }
399
400     _renderFilteringPopover()
401     {
402         var enableSampling = this.content().querySelector('.enable-sampling');
403         enableSampling.checked = this.isSamplingEnabled();
404         enableSampling.onchange = function () {
405             self.setSamplingEnabled(enableSampling.checked);
406             self._chartsPage.graphOptionsDidChange();
407         }
408
409         var showOutliers = this.content().querySelector('.show-outliers');
410         showOutliers.checked = this.isShowingOutliers();
411         showOutliers.onchange = function () {
412             self.setShowOutliers(showOutliers.checked);
413             self._chartsPage.graphOptionsDidChange();
414         }
415
416         var markAsOutlierButton = this.content().querySelector('.mark-as-outlier');
417         const indicator = this._mainChart.currentIndicator();
418         let firstSelectedPoint = indicator && indicator.isLocked ? indicator.point : null;
419         if (!firstSelectedPoint)
420             firstSelectedPoint = this._mainChart.firstSelectedPoint('current');
421         var alreayMarkedAsOutlier = firstSelectedPoint && firstSelectedPoint.markedOutlier;
422
423         var self = this;
424         markAsOutlierButton.textContent = (alreayMarkedAsOutlier ? 'Unmark' : 'Mark') + ' selected points as outlier';
425         markAsOutlierButton.onclick = function () {
426             var selectedPoints = [firstSelectedPoint];
427             if (self._mainChart.currentSelection('current'))
428                 selectedPoints = self._mainChart.selectedPoints('current');
429             self._markAsOutlier(!alreayMarkedAsOutlier, selectedPoints);
430         }
431         markAsOutlierButton.disabled = !firstSelectedPoint;
432     }
433
434     _renderTrendLinePopover()
435     {
436         var element = ComponentBase.createElement;
437         var link = ComponentBase.createLink;
438         var self = this;
439
440         const trendLineTypesContainer = this.content().querySelector('.trend-line-types');
441         if (!trendLineTypesContainer.querySelector('select')) {
442             this.renderReplace(trendLineTypesContainer, [
443                 element('select', {onchange: this._trendLineTypeDidChange.bind(this)},
444                     ChartTrendLineTypes.map((type) => { return element('option', {value: type.id}, type.label); }))
445             ]);
446         }
447         if (this._trendLineType)
448             trendLineTypesContainer.querySelector('select').value = this._trendLineType.id;
449
450         if (this._renderedTrendLineOptions)
451             return;
452         this._renderedTrendLineOptions = true;
453
454         if (this._trendLineParameters.length) {
455             var configuredParameters = this._trendLineParameters;
456             this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), [
457                 element('h3', 'Parameters'),
458                 element('ul', this._trendLineType.parameterList.map(function (parameter, index) {
459                     if (parameter.options) {
460                         const select = element('select', parameter.options.map((option) =>
461                             element('option', {value: option, selected: option == parameter.value}, option)));
462                         select.onchange = self._trendLineParameterDidChange.bind(self);
463                         select.parameterIndex = index;
464                         return element('li', element('label', [parameter.label + ': ', select]));
465                     }
466
467                     var attributes = {type: 'number'};
468                     for (var name in parameter)
469                         attributes[name] = parameter[name];
470
471                     attributes.value = configuredParameters[index];
472                     const input = element('input', attributes);
473                     input.parameterIndex = index;
474                     input.oninput = self._trendLineParameterDidChange.bind(self);
475                     input.onchange = self._trendLineParameterDidChange.bind(self);
476                     return element('li', element('label', [parameter.label + ': ', input]));
477                 }))
478             ]);
479         } else
480             this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), []);
481     }
482
483     _trendLineTypeDidChange(event)
484     {
485         var newType = ChartTrendLineTypes.find(function (type) { return type.id == event.target.value });
486         if (newType == this._trendLineType)
487             return;
488
489         this._trendLineType = newType;
490         this._trendLineParameters = this._defaultParametersForTrendLine(newType);
491         this._renderedTrendLineOptions = false;
492
493         this._updateTrendLine();
494         this._chartsPage.graphOptionsDidChange();
495         this.enqueueToRender();
496     }
497
498     _defaultParametersForTrendLine(type)
499     {
500         return type && type.parameterList ? type.parameterList.map(function (parameter) { return parameter.value; }) : [];
501     }
502
503     _trendLineParameterDidChange(event)
504     {
505         var input = event.target;
506         var index = input.parameterIndex;
507         var newValue = parseFloat(input.value);
508         if (this._trendLineParameters[index] == newValue)
509             return;
510         this._trendLineParameters[index] = newValue;
511         var self = this;
512         setTimeout(function () { // Some trend lines, e.g. sementations, are expensive.
513             if (self._trendLineParameters[index] != newValue)
514                 return;
515             self._updateTrendLine();
516             self._chartsPage.graphOptionsDidChange();
517         }, 500);
518     }
519
520     _didFetchData()
521     {
522         super._didFetchData();
523         this._updateTrendLine();
524     }
525
526     async _updateTrendLine()
527     {
528         if (!this._mainChart.sourceList())
529             return;
530
531         this._trendLineVersion++;
532         var currentTrendLineType = this._trendLineType || ChartTrendLineTypes.DefaultType;
533         var currentTrendLineParameters = this._trendLineParameters || this._defaultParametersForTrendLine(currentTrendLineType);
534         var currentTrendLineVersion = this._trendLineVersion;
535         var sourceList = this._mainChart.sourceList();
536
537         if (!currentTrendLineType.execute) {
538             this._mainChart.clearTrendLines();
539             this.enqueueToRender();
540         } else {
541             // Wait for all trendlines to be ready. Otherwise we might see FOC when the domain is expanded.
542             await Promise.all(sourceList.map(async (source, sourceIndex) => {
543                 const trendlineSeries = await currentTrendLineType.execute.call(null, source, currentTrendLineParameters);
544                 if (this._trendLineVersion == currentTrendLineVersion)
545                     this._mainChart.setTrendLine(sourceIndex, trendlineSeries);
546
547                 if (trendlineSeries && trendlineSeries.analysisAnnotations)
548                     this._detectedAnnotations = trendlineSeries.analysisAnnotations;
549                 else
550                     this._detectedAnnotations = null;
551             }));
552             this.enqueueToRender();
553         }
554     }
555
556     static paneHeaderTemplate()
557     {
558         return `
559             <header class="chart-pane-header">
560                 <h2 class="chart-pane-title">-</h2>
561                 <nav class="chart-pane-actions">
562                     <ul>
563                         <li><close-button id="close"></close-button></li>
564                     </ul>
565                     <ul class="chart-pane-action-buttons buttoned-toolbar"></ul>
566                     <ul class="chart-pane-alternative-platforms popover" style="display:none"></ul>
567                     <form class="chart-pane-analyze-popover popover" style="display:none">
568                         <input type="text" id="task-name" required>
569                         <button>Create</button>
570                         <li>
571                             <label><input type="checkbox" id="create-with-test-group" checked></label>
572                             <label>Confirm with</label>
573                                 <select id="confirm-repetition">
574                                     <option>1</option>
575                                     <option>2</option>
576                                     <option>3</option>
577                                     <option selected>4</option>
578                                     <option>5</option>
579                                     <option>6</option>
580                                     <option>7</option>
581                                     <option>8</option>
582                                     <option>9</option>
583                                     <option>10</option>
584                                 </select>
585                             <label>iterations</label>
586                         </li>
587                     </form>
588                     <ul class="chart-pane-filtering-options popover" style="display:none">
589                         <li><label><input type="checkbox" class="enable-sampling">Sampling</label></li>
590                         <li><label><input type="checkbox" class="show-outliers">Show outliers</label></li>
591                         <li><button class="mark-as-outlier">Mark selected points as outlier</button></li>
592                     </ul>
593                     <ul class="chart-pane-trend-line-options popover" style="display:none">
594                         <div class="trend-line-types"></div>
595                         <div class="trend-line-parameter-list"></div>
596                     </ul>
597                 </nav>
598             </header>
599         `;
600     }
601
602     static cssTemplate()
603     {
604         return ChartPaneBase.cssTemplate() + `
605             .chart-pane {
606                 border: solid 1px #ccc;
607                 border-radius: 0.5rem;
608                 margin: 1rem;
609                 margin-bottom: 2rem;
610             }
611
612             .chart-pane-body {
613                 height: calc(100% - 2rem);
614             }
615
616             .chart-pane-header {
617                 position: relative;
618                 left: 0;
619                 top: 0;
620                 width: 100%;
621                 height: 2rem;
622                 line-height: 2rem;
623                 border-bottom: solid 1px #ccc;
624             }
625
626             .chart-pane-title {
627                 margin: 0 0.5rem;
628                 padding: 0;
629                 padding-left: 1.5rem;
630                 font-size: 1rem;
631                 font-weight: inherit;
632             }
633
634             .chart-pane-actions {
635                 position: absolute;
636                 display: flex;
637                 flex-direction: row;
638                 justify-content: space-between;
639                 align-items: center;
640                 width: 100%;
641                 height: 2rem;
642                 top: 0;
643                 padding: 0 0;
644             }
645
646             .chart-pane-actions ul, form {
647                 display: block;
648                 padding: 0;
649                 margin: 0 0.5rem;
650                 font-size: 1rem;
651                 line-height: 1rem;
652                 list-style: none;
653             }
654
655             .chart-pane-actions .chart-pane-action-buttons {
656                 font-size: 0.9rem;
657                 line-height: 0.9rem;
658             }
659
660             .chart-pane-actions .popover {
661                 position: absolute;
662                 top: 0;
663                 right: 0;
664                 border: solid 1px #ccc;
665                 border-radius: 0.2rem;
666                 z-index: 10;
667                 padding: 0.2rem 0;
668                 margin: 0;
669                 margin-top: -0.2rem;
670                 margin-right: -0.2rem;
671                 background: rgba(255, 255, 255, 0.95);
672             }
673
674             @supports ( -webkit-backdrop-filter: blur(0.5rem) ) {
675                 .chart-pane-actions .popover {
676                     background: rgba(255, 255, 255, 0.6);
677                     -webkit-backdrop-filter: blur(0.5rem);
678                 }
679             }
680
681             .chart-pane-actions .popover li {
682             }
683
684             .chart-pane-actions .popover li a {
685                 display: block;
686                 text-decoration: none;
687                 color: inherit;
688                 font-size: 0.9rem;
689                 padding: 0.2rem 0.5rem;
690             }
691
692             .chart-pane-actions .popover a:hover,
693             .chart-pane-actions .popover input:focus {
694                 background: rgba(204, 153, 51, 0.1);
695             }
696
697             .chart-pane-actions .chart-pane-analyze-popover {
698                 padding: 0.5rem;
699             }
700
701             .chart-pane-actions .popover label {
702                 font-size: 0.9rem;
703             }
704
705             .chart-pane-actions .popover.chart-pane-filtering-options {
706                 padding: 0.2rem;
707             }
708
709             .chart-pane-actions .popover.chart-pane-trend-line-options h3 {
710                 font-size: 0.9rem;
711                 line-height: 0.9rem;
712                 font-weight: inherit;
713                 margin: 0;
714                 padding: 0.2rem;
715                 border-bottom: solid 1px #ccc;
716             }
717
718             .chart-pane-actions .popover.chart-pane-trend-line-options select,
719             .chart-pane-actions .popover.chart-pane-trend-line-options label {
720                 margin: 0.2rem;
721             }
722
723             .chart-pane-actions .popover.chart-pane-trend-line-options label {
724                 font-size: 0.8rem;
725             }
726
727             .chart-pane-actions .popover.chart-pane-trend-line-options input {
728                 width: 2.5rem;
729             }
730
731             .chart-pane-actions .popover input[type=text] {
732                 font-size: 1rem;
733                 width: 15rem;
734                 outline: none;
735                 border: solid 1px #ccc;
736             }
737 `;
738     }
739 }