Title: [212946] trunk/Websites/perf.webkit.org
Revision
212946
Author
rn...@webkit.org
Date
2017-02-23 22:57:04 -0800 (Thu, 23 Feb 2017)

Log Message

New sampling algorithm shows very few points when zoomed out
https://bugs.webkit.org/show_bug.cgi?id=168813

Reviewed by Saam Barati.

When a chart is zoomed out to a large time interval, the new sampling algorithm introduced in r212853 can
hide most of the data points because the difference between the preceding point's time and the succeeding
point's time of most points will be below the threshold we computed.

Instead, rank each data point based on the aforementioned time interval difference, and pick the first M data
points when M data points are to be shown.

This makes the new algorithm behave like our old algorithm while keeping it stable still. Note that this
algorithm still biases data points without a close neighboring point but this seems to work out in practice
because such a point tends to be an important sample anyway, and we don't have a lot of space between
data points since we aim to show about one point per pixel.

* browser-tests/index.html:
(CanvasTest.canvasContainsColor): Extracted from one of the test cases and generalized. Returns true when
the specified region of the canvas contains a specified color (alpha is optional).
* browser-tests/time-series-chart-tests.js: Added a test case for sampling. It checks that sampling happens
and that we always show some data point even when zoomed out to a large time interval.
(createChartWithSampleCluster):

* public/v3/components/interactive-time-series-chart.js:
(InteractiveTimeSeriesChart.prototype._sampleTimeSeries):
* public/v3/components/time-series-chart.js:
(TimeSeriesChart.prototype._ensureSampledTimeSeries): M, the number of data points we pick must be computed
based on the width of data points we're about to draw constrained by the canvas size. e.g. when the canvas
is only half filled, we shouldn't be showing two points per pixel in the filled region.
(TimeSeriesChart.prototype._sampleTimeSeries): Refined the algorithm. First, compute the time difference or
the rank for each N data points. Sort those ranks in descending order (in the order we prefer), and include
all data points above the M-th rank in the sample.
(TimeSeriesChart.prototype.computeTimeGrid): Revert the inadvertent change in r212935.

* public/v3/models/time-series.js:
(TimeSeriesView.prototype.filter): Fixed a bug that the indices passed onto the callback were shifted by the
starting index.
* unit-tests/time-series-tests.js: Added a test case to ensure callbacks are called with correct data points
and indices.

Modified Paths

Diff

Modified: trunk/Websites/perf.webkit.org/ChangeLog (212945 => 212946)


--- trunk/Websites/perf.webkit.org/ChangeLog	2017-02-24 06:52:02 UTC (rev 212945)
+++ trunk/Websites/perf.webkit.org/ChangeLog	2017-02-24 06:57:04 UTC (rev 212946)
@@ -1,5 +1,48 @@
 2017-02-23  Ryosuke Niwa  <rn...@webkit.org>
 
+        New sampling algorithm shows very few points when zoomed out
+        https://bugs.webkit.org/show_bug.cgi?id=168813
+
+        Reviewed by Saam Barati.
+
+        When a chart is zoomed out to a large time interval, the new sampling algorithm introduced in r212853 can
+        hide most of the data points because the difference between the preceding point's time and the succeeding
+        point's time of most points will be below the threshold we computed.
+
+        Instead, rank each data point based on the aforementioned time interval difference, and pick the first M data
+        points when M data points are to be shown.
+
+        This makes the new algorithm behave like our old algorithm while keeping it stable still. Note that this
+        algorithm still biases data points without a close neighboring point but this seems to work out in practice
+        because such a point tends to be an important sample anyway, and we don't have a lot of space between
+        data points since we aim to show about one point per pixel.
+
+        * browser-tests/index.html:
+        (CanvasTest.canvasContainsColor): Extracted from one of the test cases and generalized. Returns true when
+        the specified region of the canvas contains a specified color (alpha is optional).
+        * browser-tests/time-series-chart-tests.js: Added a test case for sampling. It checks that sampling happens
+        and that we always show some data point even when zoomed out to a large time interval.
+        (createChartWithSampleCluster):
+
+        * public/v3/components/interactive-time-series-chart.js:
+        (InteractiveTimeSeriesChart.prototype._sampleTimeSeries):
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart.prototype._ensureSampledTimeSeries): M, the number of data points we pick must be computed
+        based on the width of data points we're about to draw constrained by the canvas size. e.g. when the canvas
+        is only half filled, we shouldn't be showing two points per pixel in the filled region.
+        (TimeSeriesChart.prototype._sampleTimeSeries): Refined the algorithm. First, compute the time difference or
+        the rank for each N data points. Sort those ranks in descending order (in the order we prefer), and include
+        all data points above the M-th rank in the sample.
+        (TimeSeriesChart.prototype.computeTimeGrid): Revert the inadvertent change in r212935.
+
+        * public/v3/models/time-series.js:
+        (TimeSeriesView.prototype.filter): Fixed a bug that the indices passed onto the callback were shifted by the
+        starting index.
+        * unit-tests/time-series-tests.js: Added a test case to ensure callbacks are called with correct data points
+        and indices.
+
+2017-02-23  Ryosuke Niwa  <rn...@webkit.org>
+
         REGRESSION(r212542): Make TimeSeriesChart.computeTimeGrid stops x-axis grid prematurely
         https://bugs.webkit.org/show_bug.cgi?id=168812
 

Modified: trunk/Websites/perf.webkit.org/browser-tests/index.html (212945 => 212946)


--- trunk/Websites/perf.webkit.org/browser-tests/index.html	2017-02-24 06:52:02 UTC (rev 212945)
+++ trunk/Websites/perf.webkit.org/browser-tests/index.html	2017-02-24 06:57:04 UTC (rev 212946)
@@ -163,6 +163,23 @@
     },
 
     canvasImageData(canvas) { return canvasImageData(canvas); },
+
+    canvasContainsColor(canvas, color, rect = {})
+    {
+        const content = canvas.getContext('2d').getImageData(rect.x || 0, rect.y || 0, rect.width || canvas.width, rect.height || canvas.height);
+        let found = false;
+        const data = ""
+        for (let startOfPixel = 0; startOfPixel < data.length; startOfPixel += 4) {
+            let r = data[startOfPixel];
+            let g = data[startOfPixel + 1];
+            let b = data[startOfPixel + 2];
+            let a = data[startOfPixel + 3];
+            if (r == color.r && g == color.g && b == color.b && (color.a == undefined || a == color.a))
+                return true;
+        }
+        return false;
+    },
+
     expectCanvasesMatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, true); },
     expectCanvasesMismatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, false); },
 }

Modified: trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js (212945 => 212946)


--- trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js	2017-02-24 06:52:02 UTC (rev 212945)
+++ trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js	2017-02-24 06:57:04 UTC (rev 212946)
@@ -77,9 +77,11 @@
     const chart = new TimeSeriesChart([
         {
             type: 'current',
+            lineStyle: options.lineStyle || '#666',
             measurementSet: MeasurementSet.findSet(1, 1, 0),
             interactive: options.interactive || false,
-            includeOutliers: options.includeOutliers || false
+            includeOutliers: options.includeOutliers || false,
+            sampleData: options.sampleData || false,
         }], chartOptions);
     const element = chart.element();
     element.style.width = options.width || '300px';
@@ -541,7 +543,6 @@
 
                 expect(chart.content().querySelector('canvas')).to.be(null);
                 return waitForComponentsToRender(context).then(() => {
-                    console.log('done')
                     expect(chart.content().querySelector('canvas')).to.not.be(null);
                 });
             });
@@ -828,23 +829,54 @@
                     CanvasTest.expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
                     CanvasTest.expectCanvasesMismatch(canvasWithYAxis1, canvasWithYAxis2);
 
-                    let content1 = CanvasTest.canvasImageData(canvasWithYAxis1);
-                    let foundGridLine = false;
-                    for (let y = 0; y < content1.height; y++) {
-                        let endOfY = content1.width * 4 * y;
-                        let r = content1.data[endOfY - 4];
-                        let g = content1.data[endOfY - 3];
-                        let b = content1.data[endOfY - 2];
-                        if (r == 204 && g == 204 && b == 204) {
-                            foundGridLine = true;
-                            break;
-                        }
-                    }
-                    expect(foundGridLine).to.be(true);
+                    expect(CanvasTest.canvasContainsColor(canvasWithYAxis1, {r: 204, g: 204, b: 204},
+                        {x: canvasWithYAxis1.width - 1, width: 1, y: 0, height: canvasWithYAxis1.height})).to.be(true);
                 });
             });
         });
 
+        it('should render the sampled time series', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
+                const chartWithoutSampling = createChartWithSampleCluster(context, {}, {lineStyle: 'rgb(0, 128, 255)', width: '100px', height: '100px', sampleData: false});
+                const chartWithSampling = createChartWithSampleCluster(context, {}, {lineStyle: 'rgb(0, 128, 255)', width: '100px', height: '100px', sampleData: true});
+
+                chartWithoutSampling.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chartWithoutSampling.fetchMeasurementSets();
+                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
+
+                chartWithSampling.setDomain(sampleCluster.startTime, sampleCluster.endTime);
+                chartWithSampling.fetchMeasurementSets();
+
+                let canvasWithSampling;
+                let canvasWithoutSampling;
+                return waitForComponentsToRender(context).then(() => {
+                    canvasWithoutSampling = chartWithoutSampling.content().querySelector('canvas');
+                    canvasWithSampling = chartWithSampling.content().querySelector('canvas');
+
+                    CanvasTest.expectCanvasesMatch(canvasWithSampling, canvasWithoutSampling);
+                    expect(CanvasTest.canvasContainsColor(canvasWithoutSampling, {r: 0, g: 128, b: 255})).to.be(true);
+                    expect(CanvasTest.canvasContainsColor(canvasWithSampling, {r: 0, g: 128, b: 255})).to.be(true);
+
+                    const diff = sampleCluster.endTime - sampleCluster.startTime;
+                    chartWithoutSampling.setDomain(sampleCluster.startTime - 2 * diff, sampleCluster.endTime);
+                    chartWithSampling.setDomain(sampleCluster.startTime - 2 * diff, sampleCluster.endTime);
+
+                    CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithoutSampling);
+                    CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithSampling);
+                    return waitForComponentsToRender(context);
+                }).then(() => {
+                    expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithoutSampling)).to.be(true);
+                    expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithSampling)).to.be(true);
+
+                    expect(CanvasTest.canvasContainsColor(canvasWithoutSampling, {r: 0, g: 128, b: 255})).to.be(true);
+                    expect(CanvasTest.canvasContainsColor(canvasWithSampling, {r: 0, g: 128, b: 255})).to.be(true);
+
+                    CanvasTest.expectCanvasesMismatch(canvasWithSampling, canvasWithoutSampling);
+                });
+            });
+        });
+
         it('should render annotations', () => {
             const context = new BrowsingContext();
             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {

Modified: trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js (212945 => 212946)


--- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js	2017-02-24 06:52:02 UTC (rev 212945)
+++ trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js	2017-02-24 06:57:04 UTC (rev 212946)
@@ -382,11 +382,11 @@
         return metrics;
     }
 
-    _sampleTimeSeries(data, minimumTimeDiff, excludedPoints)
+    _sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints)
     {
         if (this._indicatorID)
             excludedPoints.add(this._indicatorID);
-        return super._sampleTimeSeries(data, minimumTimeDiff, excludedPoints);
+        return super._sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints);
     }
 
     _renderChartContent(context, metrics)

Modified: trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js (212945 => 212946)


--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js	2017-02-24 06:52:02 UTC (rev 212945)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js	2017-02-24 06:57:04 UTC (rev 212946)
@@ -490,9 +490,6 @@
             if (!timeSeries)
                 return null;
 
-            // A chart with X px width shouldn't have more than 2X / <radius-of-points> data points.
-            const maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
-
             const pointAfterStart = timeSeries.findPointAfterTime(startTime);
             const pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
             const pointAfterEnd = timeSeries.findPointAfterTime(endTime) || timeSeries.lastPoint();
@@ -504,7 +501,11 @@
             if (!source.sampleData)
                 return view;
 
-            return this._sampleTimeSeries(view, (endTime - startTime) / maximumNumberOfPoints, new Set);
+            // A chart with X px width shouldn't have more than 2X / <radius-of-points> data points.
+            const viewWidth = Math.min(metrics.chartWidth, metrics.timeToX(pointAfterEnd.time) - metrics.timeToX(pointBeforeStart.time));
+            const maximumNumberOfPoints = 2 * viewWidth / (source.pointRadius || 2);
+
+            return this._sampleTimeSeries(view, maximumNumberOfPoints, new Set);
         });
 
         Instrumentation.endMeasuringTime('TimeSeriesChart', 'ensureSampledTimeSeries');
@@ -514,19 +515,27 @@
         return true;
     }
 
-    _sampleTimeSeries(view, minimumTimeDiff, excludedPoints)
+    _sampleTimeSeries(view, maximumNumberOfPoints, excludedPoints)
     {
-        if (view.length() < 2)
+
+        if (view.length() < 2 || maximumNumberOfPoints >= view.length() || maximumNumberOfPoints < 1)
             return view;
 
         Instrumentation.startMeasuringTime('TimeSeriesChart', 'sampleTimeSeries');
 
-        const sampledData = view.filter((point, i) => {
-            if (excludedPoints.has(point.id))
-                return true;
+        let ranks = new Array(view.length());
+        let i = 0;
+        for (let point of view) {
             let previousPoint = view.previousPoint(point) || point;
             let nextPoint = view.nextPoint(point) || point;
-            return nextPoint.time - previousPoint.time >= minimumTimeDiff;
+            ranks[i] = nextPoint.time - previousPoint.time;
+            i++;
+        }
+
+        const sortedRanks = ranks.slice(0).sort((a, b) => b - a);
+        const minimumRank = sortedRanks[Math.floor(maximumNumberOfPoints)];
+        const sampledData = view.filter((point, i) => {
+            return excludedPoints.has(point.id) || ranks[i] >= minimumRank;
         });
 
         Instrumentation.endMeasuringTime('TimeSeriesChart', 'sampleTimeSeries');
@@ -619,7 +628,7 @@
 
         let previousDate = null;
         let previousMonth = null;
-        while (currentTime <= max) {
+        while (currentTime <= max && result.length < maxLabels) {
             const time = new Date(currentTime);
             const month = time.getUTCMonth() + 1;
             const date = time.getUTCDate();

Modified: trunk/Websites/perf.webkit.org/public/v3/models/time-series.js (212945 => 212946)


--- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js	2017-02-24 06:52:02 UTC (rev 212945)
+++ trunk/Websites/perf.webkit.org/public/v3/models/time-series.js	2017-02-24 06:57:04 UTC (rev 212946)
@@ -167,11 +167,12 @@
 
     filter(callback)
     {
-        const data = ""
         const filteredData = [];
-        for (let i = this._startingIndex; i < this._afterEndingIndex; i++) {
-            if (callback(data[i], i))
-                filteredData.push(data[i]);
+        let i = 0;
+        for (let point of this) {
+            if (callback(point, i))
+                filteredData.push(point);
+            i++;
         }
         return new TimeSeriesView(this._timeSeries, 0, filteredData.length, filteredData);
     }

Modified: trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js (212945 => 212946)


--- trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js	2017-02-24 06:52:02 UTC (rev 212945)
+++ trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js	2017-02-24 06:57:04 UTC (rev 212946)
@@ -276,6 +276,20 @@
 describe('TimeSeriesView', () => {
 
     describe('filter', () => {
+        it('should call callback with an element in the view and its index', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[1], fivePoints[3]);
+            const points = [];
+            const indices = [];
+            const view = originalView.filter((point, index) => {
+                points.push(point);
+                indices.push(index);
+            });
+            assert.deepEqual(points, [fivePoints[1], fivePoints[2], fivePoints[3]]);
+            assert.deepEqual(indices, [0, 1, 2]);
+        });
+
         it('should create a filtered view', () => {
             const timeSeries = new TimeSeries();
             addPointsToSeries(timeSeries, fivePoints);
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to