Added UI to show potential regressions in chart with t-testing against segmentations.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 5 Apr 2018 04:49:21 +0000 (04:49 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 5 Apr 2018 04:49:21 +0000 (04:49 +0000)
https://bugs.webkit.org/show_bug.cgi?id=184131

Reviewed by Ryosuke Niwa.

Added UI in the chart-pane so that user can use new option in trendline which not only
shows the segmentation, but also t-test against potential changes indicated by segmentation.

Fix a bug in AnalysisTaskPage that chart is not updated when change type of task changes.

* public/shared/statistics.js: Added a function to t-test certain range based on segmentation results.
(Statistics.supportedOneSideTTestProbabilities):
(Statistics.findRangesForChangeDetectionsWithWelchsTTest): The argument `segmentations`, every 2 items in the list defines
segmentation, that is why the index incremental is 2 in this funcion.
* public/v3/components/chart-pane-base.js: Will select the range if user clicks on a suggested annotation.
(ChartPaneBase.prototype.configure):
(ChartPaneBase.prototype._didClickAnnotation):
* public/v3/components/chart-styles.js:
(ChartStyles.annotationFillStyleForTask): Added 'annotationFillStyleForTask' to determine the fillStyle for annotation based on change type of a analysis task.
* public/v3/components/interactive-time-series-chart.js:
(InteractiveTimeSeriesChart.prototype._findAnnotation): Also need to search among suggested annotaions.
* public/v3/components/time-series-chart.js: Introduced 'suggested annotaion' which does not have an existing task and is suggested by t-test based on segmentation.
(TimeSeriesChart):
(TimeSeriesChart.prototype.setSuggestedAnnotations):
(TimeSeriesChart.prototype.allAnnotations): Returns both annotations with and without analysis task.
(TimeSeriesChart.prototype._layoutAnnotationBars): Should take all annotations in the calculation.
* public/v3/models/measurement-set.js:
(MeasurementSet.prototype.metricId): Returns metric id.
* public/v3/models/metric.js:
(Metric.prototype.summarizeForValues): Added helper function to summarize a given value
* public/v3/models/test-group.js:
(TestGroup.prototype.compareTestResults): Adapted to use 'Metric.summarizeForValues'.
* public/v3/pages/chart-pane.js: Added 'Segmentation with t-test analysis' to 'ChartTrendLineTypes'.
(ChartPane.prototype._renderTrendLinePopover):
(ChartPane.prototype.async._updateTrendLine): make it an async function.
* unit-tests/statistics-tests.js: Added unit tests for 'findRangesForChangeDetectionsWithWelchsTTest'.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@230295 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/browser-tests/interactive-time-series-chart-tests.js
Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js
Websites/perf.webkit.org/public/shared/statistics.js
Websites/perf.webkit.org/public/v3/components/chart-pane-base.js
Websites/perf.webkit.org/public/v3/components/chart-styles.js
Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js
Websites/perf.webkit.org/public/v3/models/measurement-set.js
Websites/perf.webkit.org/public/v3/models/metric.js
Websites/perf.webkit.org/public/v3/models/test-group.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/public/v3/pages/chart-pane.js
Websites/perf.webkit.org/unit-tests/statistics-tests.js

index ed269f9..e5929ba 100644 (file)
@@ -1,3 +1,44 @@
+2018-03-29  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Added UI to show potential regressions in chart with t-testing against segmentations.
+        https://bugs.webkit.org/show_bug.cgi?id=184131
+
+        Reviewed by Ryosuke Niwa.
+
+        Added UI in the chart-pane so that user can use new option in trendline which not only
+        shows the segmentation, but also t-test against potential changes indicated by segmentation.
+
+        Fix a bug in AnalysisTaskPage that chart is not updated when change type of task changes.
+
+        * browser-tests/interactive-time-series-chart-tests.js: Fix a unit tests.
+        * browser-tests/time-series-chart-tests.js: Fix a unit tests.
+        * public/shared/statistics.js: Added a function to t-test certain range based on segmentation results.
+        (Statistics.supportedOneSideTTestProbabilities):
+        (Statistics.findRangesForChangeDetectionsWithWelchsTTest): The argument `segmentations`, every 2 items in the list defines 
+        segmentation, that is why the index incremental is 2 in this funcion.
+        * public/v3/components/chart-pane-base.js: Will select the range if user clicks on a suggested annotation.
+        (ChartPaneBase.prototype.configure):
+        (ChartPaneBase.prototype._didClickAnnotation):
+        * public/v3/components/chart-styles.js:
+        (ChartStyles.annotationFillStyleForTask): Added 'annotationFillStyleForTask' to determine the fillStyle for annotation based on change type of a analysis task.
+        * public/v3/components/interactive-time-series-chart.js:
+        (InteractiveTimeSeriesChart.prototype._findAnnotation): Also need to search among suggested annotaions.
+        * public/v3/components/time-series-chart.js: Introduced 'suggested annotaion' which does not have an existing task and is suggested by t-test based on segmentation.
+        (TimeSeriesChart):
+        (TimeSeriesChart.prototype.setSuggestedAnnotations):
+        (TimeSeriesChart.prototype.allAnnotations): Returns both annotations with and without analysis task.
+        (TimeSeriesChart.prototype._layoutAnnotationBars): Should take all annotations in the calculation.
+        * public/v3/models/measurement-set.js:
+        (MeasurementSet.prototype.metricId): Returns metric id.
+        * public/v3/models/metric.js:
+        (Metric.prototype.summarizeForValues): Added helper function to summarize a given value
+        * public/v3/models/test-group.js:
+        (TestGroup.prototype.compareTestResults): Adapted to use 'Metric.summarizeForValues'.
+        * public/v3/pages/chart-pane.js: Added 'Segmentation with t-test analysis' to 'ChartTrendLineTypes'.
+        (ChartPane.prototype._renderTrendLinePopover):
+        (ChartPane.prototype.async._updateTrendLine): make it an async function.
+        * unit-tests/statistics-tests.js: Added unit tests for 'findRangesForChangeDetectionsWithWelchsTTest'.
+
 2018-04-02  Aakash Jain  <aakash_jain@apple.com>
 
         Remove deprecated Buildbot 0.8 code from Perf syncing scripts
index 3856d21..b803a7e 100644 (file)
@@ -494,7 +494,7 @@ describe('InteractiveTimeSeriesChart', () => {
         const context = new BrowsingContext();
         return ChartTest.importChartScripts(context).then(() => {
             const chart = ChartTest.createInteractiveChartWithSampleCluster(context, null,
-                {annotations: { textStyle: '#000', textBackground: '#fff', minWidth: 3, barHeight: 10, barSpacing: 1}});
+                {annotations: { textStyle: '#000', textBackground: '#fff', minWidth: 3, barHeight: 10, barSpacing: 1, fillStyle: '#ccc'}});
 
             chart.setDomain(ChartTest.sampleCluster.startTime, ChartTest.sampleCluster.endTime);
             chart.fetchMeasurementSets();
@@ -505,8 +505,7 @@ describe('InteractiveTimeSeriesChart', () => {
                 startTime: ChartTest.sampleCluster.startTime + diff / 2,
                 endTime: ChartTest.sampleCluster.endTime - diff / 4,
                 label: 'hello, world',
-                fillStyle: 'rgb(0, 0, 255)',
-            }]
+            }];
             chart.setAnnotations(annotations);
 
             const annotationClickCalls = [];
index deef1c1..2759a0c 100644 (file)
@@ -800,7 +800,7 @@ describe('TimeSeriesChart', () => {
         it('should render annotations', () => {
             const context = new BrowsingContext();
             return ChartTest.importChartScripts(context).then(() => {
-                const options = {annotations: { textStyle: '#000', textBackground: '#fff', minWidth: 3, barHeight: 7, barSpacing: 2}};
+                const options = {annotations: { textStyle: '#000', textBackground: '#fff', minWidth: 3, barHeight: 7, barSpacing: 2, fillStyle: '#ccc'}};
                 const chartWithoutAnnotations = ChartTest.createChartWithSampleCluster(context, null, options);
                 const chartWithAnnotations = ChartTest.createChartWithSampleCluster(context, null, options);
 
@@ -818,7 +818,6 @@ describe('TimeSeriesChart', () => {
                         startTime: ChartTest.sampleCluster.startTime + diff / 4,
                         endTime: ChartTest.sampleCluster.startTime + diff / 2,
                         label: 'hello, world',
-                        fillStyle: 'rgb(0, 0, 255)',
                     }]);
 
                     canvasWithAnnotations = chartWithAnnotations.content().querySelector('canvas');
index c231a1e..a3164cf 100644 (file)
@@ -39,6 +39,10 @@ var Statistics = new (function () {
         return supportedProbabilities
     }
 
+    this.supportedOneSideTTestProbabilities = function () {
+        return Object.keys(tDistributionByOneSidedProbability);
+    }
+
     // Computes the delta d s.t. (mean - d, mean + d) is the confidence interval with the specified probability in O(1).
     this.confidenceIntervalDelta = function (probability, numberOfSamples, sum, squareSum) {
         var oneSidedProbability = twoSidedToOneSidedProbability(probability);
@@ -106,6 +110,42 @@ var Statistics = new (function () {
         };
     }
 
+    this.findRangesForChangeDetectionsWithWelchsTTest = function (values, segmentations, oneSidedPossibility) {
+        if (!values.length)
+            return [];
+
+        const selectedRanges = [];
+        const twoSidedFromOneSidedPossibility = 2 * oneSidedPossibility - 1;
+
+        for (let i = 1; i + 2 < segmentations.length; i += 2) {
+            let found = false;
+            const previousMean = segmentations[i].value;
+            const currentMean = segmentations[i + 1].value;
+            console.assert(currentMean != previousMean);
+            const currentChangePoint = segmentations[i].seriesIndex;
+            const start = segmentations[i - 1].seriesIndex;
+            const end = segmentations[i + 2].seriesIndex;
+
+            for (let leftEdge = currentChangePoint - 2, rightEdge = currentChangePoint + 2; leftEdge >= start && rightEdge <= end; leftEdge--, rightEdge++) {
+                const result = this.computeWelchsT(values, leftEdge, currentChangePoint - leftEdge, values, currentChangePoint, rightEdge - currentChangePoint, twoSidedFromOneSidedPossibility);
+                if (result.significantlyDifferent) {
+                    selectedRanges.push({
+                        startIndex: leftEdge,
+                        endIndex: rightEdge - 1,
+                        segmentationStartValue: previousMean,
+                        segmentationEndValue: currentMean
+                    });
+                    found = true;
+                    break;
+                }
+            }
+            if (!found && Statistics.debuggingTestingRangeNomination)
+                console.log('Failed to find a testing range at', currentChangePoint, 'changing from', previousMean, 'to', currentMean);
+        }
+
+        return selectedRanges;
+    };
+
     function sampleMeanAndVarianceForValues(values, startIndex, length) {
         var sum = 0;
         for (var i = 0; i < length; i++)
index 16ce207..8d991be 100644 (file)
@@ -19,7 +19,8 @@ class ChartPaneBase extends ComponentBase {
         this._mainChartStatus = null;
         this._commitLogViewer = null;
         this._tasksForAnnotations = null;
-        this._renderedAnnotations = false;
+        this._detectedAnnotations = null;
+        this._renderAnnotationsLazily = new LazilyEvaluatedFunction(this._renderAnnotations.bind(this));
     }
 
     configure(platformId, metricId)
@@ -51,7 +52,7 @@ class ChartPaneBase extends ComponentBase {
         this._mainChart.listenToAction('indicatorChange', this._indicatorDidChange.bind(this));
         this._mainChart.listenToAction('selectionChange', this._mainSelectionDidChange.bind(this));
         this._mainChart.listenToAction('zoom', this._mainSelectionDidZoom.bind(this));
-        this._mainChart.listenToAction('annotationClick', this._openAnalysisTask.bind(this));
+        this._mainChart.listenToAction('annotationClick', this._didClickAnnotation.bind(this));
         this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
 
         this._revisionRange = new ChartRevisionRange(this._mainChart);
@@ -97,7 +98,6 @@ class ChartPaneBase extends ComponentBase {
         var self = this;
         AnalysisTask.fetchByPlatformAndMetric(this._platformId, this._metricId, noCache).then(function (tasks) {
             self._tasksForAnnotations = tasks;
-            self._renderedAnnotations = false;
             self.enqueueToRender();
         });
     }
@@ -105,7 +105,7 @@ class ChartPaneBase extends ComponentBase {
     // FIXME: We should have a mechanism to get notified whenever the set of annotations change.
     didUpdateAnnotations()
     {
-        this._renderedAnnotations = false;
+        this._tasksForAnnotations = [...this._tasksForAnnotations];
         this.enqueueToRender();
     }
 
@@ -171,6 +171,18 @@ class ChartPaneBase extends ComponentBase {
         this.enqueueToRender();
     }
 
+    _didClickAnnotation(annotation)
+    {
+        if (annotation.task)
+            this._openAnalysisTask(annotation);
+        else {
+            const newSelection = [annotation.startTime, annotation.endTime];
+            this._mainChart.setSelection(newSelection);
+            this._overviewChart.setSelection(newSelection, this);
+            this.enqueueToRender();
+        }
+    }
+
     _openAnalysisTask(annotation)
     {
         var router = this.router();
@@ -246,15 +258,16 @@ class ChartPaneBase extends ComponentBase {
         if (this._overviewChart)
             this._overviewChart.enqueueToRender();
 
-        if (this._mainChart)
+        if (this._mainChart) {
             this._mainChart.enqueueToRender();
+            this._renderAnnotationsLazily.evaluate(this._tasksForAnnotations, this._detectedAnnotations);
+        }
 
         if (this._errorMessage) {
             this.renderReplace(this.content().querySelector('.chart-pane-main'), this._errorMessage);
             return;
         }
 
-        this._renderAnnotations();
 
         if (this._mainChartStatus)
             this._mainChartStatus.enqueueToRender();
@@ -268,37 +281,20 @@ class ChartPaneBase extends ComponentBase {
         Instrumentation.endMeasuringTime('ChartPane', 'render');
     }
 
-    _renderAnnotations()
+    _renderAnnotations(taskForAnnotations, detectedAnnotations)
     {
-        if (!this._tasksForAnnotations || this._renderedAnnotations)
-            return;
-        this._renderedAnnotations = true;
-
-        var annotations = this._tasksForAnnotations.map(function (task) {
-            var fillStyle = '#fc6';
-            switch (task.changeType()) {
-            case 'inconclusive':
-                fillStyle = '#fcc';
-                break;
-            case 'progression':
-                fillStyle = '#39f';
-                break;
-            case 'regression':
-                fillStyle = '#c60';
-                break;
-            case 'unchanged':
-                fillStyle = '#ccc';
-                break;
-            }
-
+        let annotations = (taskForAnnotations || []).map((task) => {
             return {
-                task: task,
+                task,
+                fillStyle: ChartStyles.annotationFillStyleForTask(task),
                 startTime: task.startTime(),
                 endTime: task.endTime(),
-                label: task.label(),
-                fillStyle: fillStyle,
+                label: task.label()
             };
         });
+
+        annotations = annotations.concat(detectedAnnotations || []);
+
         this._mainChart.setAnnotations(annotations);
     }
 
index cd13078..042a70a 100644 (file)
@@ -152,8 +152,26 @@ class ChartStyles {
             textBackground: '#fff',
             minWidth: 3,
             barHeight: 7,
-            barSpacing: 2,
+            barSpacing: 2
         };
         return options;
     }
+
+    static annotationFillStyleForTask(task) {
+        if (!task)
+            return '#888';
+
+        switch (task.changeType()) {
+            case 'inconclusive':
+                return '#fcc';
+            case 'progression':
+                return '#39f';
+            case 'regression':
+                return '#c60';
+            case 'unchanged':
+                return '#ccc';
+        }
+        return '#fc6';
+
+    }
 }
index cf4aef1..eba5244 100644 (file)
@@ -341,7 +341,7 @@ class InteractiveTimeSeriesChart extends TimeSeriesChart {
         if (!this._annotations)
             return null;
 
-        for (var item of this._annotations) {
+        for (const item of this._annotations) {
             if (item.x <= cursorLocation.x && cursorLocation.x <= item.x + item.width
                 && item.y <= cursorLocation.y && cursorLocation.y <= item.y + item.height)
                 return item;
index a2bd6cb..ac4b245 100644 (file)
@@ -22,6 +22,7 @@ class MeasurementSet {
     }
 
     platformId() { return this._platformId; }
+    metricId() { return this._metricId; }
 
     static findSet(platformId, metricId, lastModified)
     {
@@ -225,8 +226,8 @@ class MeasurementSet {
             var segmentationSeries = [];
             var addSegment = function (startingPoint, endingPoint) {
                 var value = Statistics.mean(timeSeries.valuesBetweenRange(startingPoint.seriesIndex, endingPoint.seriesIndex));
-                segmentationSeries.push({value: value, time: startingPoint.time, interval: function () { return null; }});
-                segmentationSeries.push({value: value, time: endingPoint.time, interval: function () { return null; }});
+                segmentationSeries.push({value: value, time: startingPoint.time, seriesIndex: startingPoint.seriesIndex, interval: function () { return null; }});
+                segmentationSeries.push({value: value, time: endingPoint.time, seriesIndex: endingPoint.seriesIndex, interval: function () { return null; }});
             };
 
             var startingIndex = 0;
index e43af2c..f0a2329 100644 (file)
@@ -85,6 +85,15 @@ class Metric extends LabeledObject {
         return unit != 'fps' && unit != '/s' && unit != 'pt';
     }
 
+    labelForDifference(beforeValue, afterValue, progressionTypeName, regressionTypeName)
+    {
+        const diff = afterValue - beforeValue;
+        const changeType = diff < 0 == this.isSmallerBetter() ? progressionTypeName : regressionTypeName;
+        const relativeChange = diff / beforeValue * 100;
+        const changeLabel = Math.abs(relativeChange).toFixed(2) + '% ' + changeType;
+        return {changeType, relativeChange, changeLabel};
+    }
+
     makeFormatter(sigFig, alwaysShowSign) { return Metric.makeFormatter(this.unit(), sigFig, alwaysShowSign); }
 
     static makeFormatter(unit, sigFig = 2, alwaysShowSign = false)
index 0c0dfad..cf8f296 100644 (file)
@@ -144,15 +144,12 @@ class TestGroup extends LabeledObject {
         }
 
         if (beforeValues.length && afterValues.length) {
-            var diff = afterMean - beforeMean;
-            var smallerIsBetter = metric.isSmallerBetter();
-            var changeType = diff < 0 == smallerIsBetter ? 'better' : 'worse';
-            var changeLabel = Math.abs(diff / beforeMean * 100).toFixed(2) + '% ' + changeType;
+            const summary = metric.labelForDifference(beforeMean, afterMean, 'better', 'worse');
+            result.changeType = summary.changeType;
+            result.changeLabel = summary.changeLabel;
             var isSignificant = Statistics.testWelchsT(beforeValues, afterValues);
             var significanceLabel = isSignificant ? 'significant' : 'insignificant';
 
-            result.changeType = changeType;
-            result.label = changeLabel;
             if (hasCompleted)
                 result.status = isSignificant ? result.changeType : 'unchanged';
             result.fullLabel = `${result.label} (statistically ${significanceLabel})`;
index 0a6a02e..6337127 100644 (file)
@@ -751,7 +751,7 @@ class AnalysisTaskPage extends PageWithHeading {
             newChangeType = null;
 
         const updateRendering = () => {
-            this._chartPane.didUpdateAnnotations();
+            this.part('chart-pane').didUpdateAnnotations();
             this.enqueueToRender();
         };
         return this._task.updateChangeType(newChangeType).then(updateRendering, (error) => {
index c791486..f32bdec 100644 (file)
@@ -39,6 +39,38 @@ const ChartTrendLineTypes = [
         ]
     },
     {
+        id: 6,
+        label: 'Segmentation with Welch\'s t-test change detection',
+        execute: async function (source, parameters) {
+            const segmentation =  await source.measurementSet.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', parameters,
+                source.type, source.includeOutliers, source.extendToFuture);
+            if (!segmentation)
+                return segmentation;
+
+            const metric = Metric.findById(source.measurementSet.metricId());
+            const timeSeries = source.measurementSet.fetchedTimeSeries(source.type, source.includeOutliers, source.extendToFuture);
+            segmentation.analysisAnnotations = Statistics.findRangesForChangeDetectionsWithWelchsTTest(timeSeries.values(),
+                segmentation, parameters[parameters.length - 1]).map((range) => {
+                const startPoint = timeSeries.findPointByIndex(range.startIndex);
+                const endPoint = timeSeries.findPointByIndex(range.endIndex);
+                const summary = metric.labelForDifference(range.segmentationStartValue, range.segmentationEndValue, 'progression', 'regression');
+                return {
+                    task: null,
+                    fillStyle: ChartStyles.annotationFillStyleForTask(null),
+                    startTime: startPoint.time,
+                    endTime: endPoint.time,
+                    label: `Potential ${summary.changeLabel}`,
+                };
+            });
+            return segmentation;
+        },
+        parameterList: [
+            {label: "Segment count weight", value: 2.5, min: 0.01, max: 10, step: 0.01},
+            {label: "Grid size", value: 500, min: 100, max: 10000, step: 10},
+            {label: "t-test significance", value: 0.99, options: Statistics.supportedOneSideTTestProbabilities()},
+        ]
+    },
+    {
         id: 1,
         label: 'Simple Moving Average',
         parameterList: [
@@ -417,11 +449,20 @@ class ChartPane extends ChartPaneBase {
             this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), [
                 element('h3', 'Parameters'),
                 element('ul', this._trendLineType.parameterList.map(function (parameter, index) {
+                    if (parameter.options) {
+                        const select = element('select', parameter.options.map((option) =>
+                            element('option', {value: option, selected: option == parameter.value}, option)));
+                        select.onchange = self._trendLineParameterDidChange.bind(self);
+                        select.parameterIndex = index;
+                        return element('li', element('label', [parameter.label + ': ', select]));
+                    }
+
                     var attributes = {type: 'number'};
                     for (var name in parameter)
                         attributes[name] = parameter[name];
+
                     attributes.value = configuredParameters[index];
-                    var input = element('input', attributes);
+                    const input = element('input', attributes);
                     input.parameterIndex = index;
                     input.oninput = self._trendLineParameterDidChange.bind(self);
                     input.onchange = self._trendLineParameterDidChange.bind(self);
@@ -475,7 +516,7 @@ class ChartPane extends ChartPaneBase {
         this._updateTrendLine();
     }
 
-    _updateTrendLine()
+    async _updateTrendLine()
     {
         if (!this._mainChart.sourceList())
             return;
@@ -484,7 +525,6 @@ class ChartPane extends ChartPaneBase {
         var currentTrendLineType = this._trendLineType || ChartTrendLineTypes.DefaultType;
         var currentTrendLineParameters = this._trendLineParameters || this._defaultParametersForTrendLine(currentTrendLineType);
         var currentTrendLineVersion = this._trendLineVersion;
-        var self = this;
         var sourceList = this._mainChart.sourceList();
 
         if (!currentTrendLineType.execute) {
@@ -492,14 +532,17 @@ class ChartPane extends ChartPaneBase {
             this.enqueueToRender();
         } else {
             // Wait for all trendlines to be ready. Otherwise we might see FOC when the domain is expanded.
-            Promise.all(sourceList.map(function (source, sourceIndex) {
-                return currentTrendLineType.execute.call(null, source, currentTrendLineParameters).then(function (trendlineSeries) {
-                    if (self._trendLineVersion == currentTrendLineVersion)
-                        self._mainChart.setTrendLine(sourceIndex, trendlineSeries);
-                });
-            })).then(function () {
-                self.enqueueToRender();
-            });
+            await Promise.all(sourceList.map(async (source, sourceIndex) => {
+                const trendlineSeries = await currentTrendLineType.execute.call(null, source, currentTrendLineParameters);
+                if (this._trendLineVersion == currentTrendLineVersion)
+                    this._mainChart.setTrendLine(sourceIndex, trendlineSeries);
+
+                if (trendlineSeries && trendlineSeries.analysisAnnotations)
+                    this._detectedAnnotations = trendlineSeries.analysisAnnotations;
+                else
+                    this._detectedAnnotations = null;
+            }));
+            this.enqueueToRender();
         }
     }
 
index d564471..d910422 100644 (file)
@@ -388,4 +388,96 @@ describe('Statistics', function () {
             assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [ 0, 6, 16, 26, 37 ]);
         });
     });
+
+    describe('findRangesForChangeDetectionsWithWelchsTTest', () => {
+        it('should return an empty array if the value is empty list', () => {
+            assert.deepEqual(Statistics.findRangesForChangeDetectionsWithWelchsTTest([], [], 0.975), []);
+        });
+
+        it('should return an empty array if segmentation is empty list', () => {
+            assert.deepEqual(Statistics.findRangesForChangeDetectionsWithWelchsTTest([1,2,3], [], 0.975), []);
+        });
+
+        it('should return the range if computeWelchsT shows a significant change', () => {
+            const values = [
+                747.30337423744,
+                731.47392585276,
+                743.66763513161,
+                738.02055323487,
+                738.25426340842,
+                742.38680046471,
+                733.13921703284,
+                739.22069966147,
+                735.69295749633,
+                743.01705472504,
+                745.45778145306,
+                731.04841157169,
+                729.4372674973,
+                735.4497416527,
+                739.0230668644,
+                730.91782989909,
+                722.18725411279,
+                731.96223451728,
+                730.04119216192,
+                730.78087646284,
+                729.63155210365,
+                730.17585200878,
+                733.93766054706,
+                740.74920717197,
+                752.14718023647,
+                764.49990164847,
+                766.36100828473,
+                756.2291883252,
+                750.14522451097,
+                749.57595092266,
+                748.03624881866,
+                769.41522176386,
+                744.04660430456,
+                751.17927808265,
+                753.29996854062,
+                757.01813756936,
+                746.62413820741,
+                742.64420062736,
+                758.12726352772,
+                778.2278439089,
+                775.11818554541,
+                775.11818554541];
+            const segmentation = [{
+                    seriesIndex: 0,
+                    time: 1505176030671,
+                    value: 736.5366704896555,
+                    x: 370.4571789404566,
+                    y: 185.52613334520248,
+                },
+                {
+                    seriesIndex: 18,
+                    time: 1515074391534,
+                    value: 736.5366704896555,
+                    x: 919.4183852714947,
+                    y: 185.52613334520248
+                },
+                {
+                    seriesIndex: 18,
+                    time: 1515074391534,
+                    value: 750.3483428383142,
+                    x: 919.4183852714947,
+                    y: 177.9710953409673,
+                },
+                {
+                    seriesIndex: 41,
+                    time: 1553851695869,
+                    value: 750.3483428383142,
+                    x: 3070.000290764446,
+                    y: 177.9710953409673,
+                }];
+            assert.deepEqual(Statistics.findRangesForChangeDetectionsWithWelchsTTest(values, segmentation, 0.975), [
+                {
+                  "endIndex": 29,
+                  "segmentationEndValue": 750.3483428383142,
+                  "segmentationStartValue": 736.5366704896555,
+                  "startIndex": 6
+                }
+            ]);
+        })
+    });
 });