diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index 1f060b3b106..a144d91d11f 100644 --- a/src/transforms/aggregate.js +++ b/src/transforms/aggregate.js @@ -11,6 +11,7 @@ var Axes = require('../plots/cartesian/axes'); var Lib = require('../lib'); var PlotSchema = require('../plot_api/plot_schema'); +var pointsAccessorFunction = require('./helpers').pointsAccessorFunction; var BADNUM = require('../constants/numerical').BADNUM; exports.moduleType = 'transform'; @@ -215,20 +216,31 @@ exports.calcTransform = function(gd, trace, opts) { var groupArray = Lib.getTargetArray(trace, {target: groups}); if(!groupArray) return; - var i, vi, groupIndex; + var i, vi, groupIndex, newGrouping; var groupIndices = {}; + var indexToPoints = {}; var groupings = []; + + var originalPointsAccessor = pointsAccessorFunction(trace.transforms, opts); + for(i = 0; i < groupArray.length; i++) { vi = groupArray[i]; groupIndex = groupIndices[vi]; if(groupIndex === undefined) { groupIndices[vi] = groupings.length; - groupings.push([i]); + newGrouping = [i]; + groupings.push(newGrouping); + indexToPoints[groupIndices[vi]] = originalPointsAccessor(i); + } + else { + groupings[groupIndex].push(i); + indexToPoints[groupIndices[vi]] = (indexToPoints[groupIndices[vi]] || []).concat(originalPointsAccessor(i)); } - else groupings[groupIndex].push(i); } + opts._indexToPoints = indexToPoints; + var aggregations = opts.aggregations; for(i = 0; i < aggregations.length; i++) { diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 6e948651558..1fbea13bc31 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -11,6 +11,7 @@ var Lib = require('../lib'); var Registry = require('../registry'); var Axes = require('../plots/cartesian/axes'); +var pointsAccessorFunction = require('./helpers').pointsAccessorFunction; var COMPARISON_OPS = ['=', '!=', '<', '>=', '>', '<=']; var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; @@ -170,6 +171,8 @@ exports.calcTransform = function(gd, trace, opts) { var d2c = Axes.getDataToCoordFunc(gd, trace, target, targetArray); var filterFunc = getFilterFunc(opts, d2c, targetCalendar); var originalArrays = {}; + var indexToPoints = {}; + var index = 0; function forAllAttrs(fn, index) { for(var j = 0; j < arrayAttrs.length; j++) { @@ -203,11 +206,18 @@ exports.calcTransform = function(gd, trace, opts) { // copy all original array attribute values, and clear arrays in trace forAllAttrs(initFn); + var originalPointsAccessor = pointsAccessorFunction(trace.transforms, opts); + // loop through filter array, fill trace arrays if passed for(var i = 0; i < len; i++) { var passed = filterFunc(targetArray[i]); - if(passed) forAllAttrs(fillFn, i); + if(passed) { + forAllAttrs(fillFn, i); + indexToPoints[index++] = originalPointsAccessor(i); + } } + + opts._indexToPoints = indexToPoints; }; function getFilterFunc(opts, d2c, targetCalendar) { diff --git a/src/transforms/helpers.js b/src/transforms/helpers.js new file mode 100644 index 00000000000..4b27d53a430 --- /dev/null +++ b/src/transforms/helpers.js @@ -0,0 +1,24 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +exports.pointsAccessorFunction = function(transforms, opts) { + var tr; + var prevIndexToPoints; + for(var i = 0; i < transforms.length; i++) { + tr = transforms[i]; + if(tr === opts) break; + if(!tr._indexToPoints || tr.enabled === false) continue; + prevIndexToPoints = tr._indexToPoints; + } + var originalPointsAccessor = prevIndexToPoints ? + function(i) {return prevIndexToPoints[i];} : + function(i) {return [i];}; + return originalPointsAccessor; +}; diff --git a/test/jasmine/tests/transform_aggregate_test.js b/test/jasmine/tests/transform_aggregate_test.js index 8e9cc9db8ea..0f7932b2c48 100644 --- a/test/jasmine/tests/transform_aggregate_test.js +++ b/test/jasmine/tests/transform_aggregate_test.js @@ -59,6 +59,10 @@ describe('aggregate', function() { expect(traceOut.marker.opacity).toEqual([0.6, 'boo']); expect(traceOut.marker.line.color).toEqual(['the end', 3.3]); expect(traceOut.marker.line.width).toEqual([4, 1]); + + var transform = traceOut.transforms[0]; + var inverseMapping = transform._indexToPoints; + expect(inverseMapping).toEqual({0: [0, 2, 3, 4], 1: [1]}); }); it('handles all funcs except sum for date data', function() { @@ -163,6 +167,10 @@ describe('aggregate', function() { expect(traceOut.y).toEqual(['b', undefined]); // category average: can result in fractional categories -> rounds (0.5 rounds to 1) expect(traceOut.text).toEqual(['b', 'b']); + + var transform = traceOut.transforms[0]; + var inverseMapping = transform._indexToPoints; + expect(inverseMapping).toEqual({0: [0, 1], 1: [2, 3]}); }); it('can aggregate on an existing data array', function() { @@ -185,10 +193,12 @@ describe('aggregate', function() { expect(traceOut.x).toEqual([8, 7]); expect(traceOut.y).toBeCloseToArray([16 / 3, 7], 5); expect(traceOut.marker.size).toEqual([10, 20]); + + var transform = traceOut.transforms[0]; + var inverseMapping = transform._indexToPoints; + expect(inverseMapping).toEqual({0: [0, 1, 4], 1: [2, 3]}); }); - // Regression test - throws before fix: - // https://github.com/plotly/plotly.js/issues/2024 it('can handle case where aggregation array is missing', function() { Plotly.newPlot(gd, [{ x: [1, 2, 3, 4, 5], @@ -205,6 +215,10 @@ describe('aggregate', function() { expect(traceOut.x).toEqual([1, 3]); expect(traceOut.y).toEqual([2, 6]); expect(traceOut.marker.size).toEqual([10, 20]); + + var transform = traceOut.transforms[0]; + var inverseMapping = transform._indexToPoints; + expect(inverseMapping).toEqual({0: [0, 1, 4], 1: [2, 3]}); }); it('handles median, mode, rms, & stddev for numeric data', function() { @@ -257,7 +271,7 @@ describe('aggregate', function() { aggregations: [ {target: 'x', func: 'sum'}, {target: 'x', func: 'avg'}, - {target: 'y', func: 'avg'}, + {target: 'y', func: 'avg'} ] }] }]); diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index d338251a546..7caee22f0e6 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -62,7 +62,7 @@ describe('filter transforms defaults:', function() { traceIn = { x: [1, 2, 3], transforms: [{ - type: 'filter', + type: 'filter' }, { type: 'filter', target: 0 @@ -143,6 +143,7 @@ describe('filter transforms calc:', function() { expect(out[0].x).toEqual([0, 1]); expect(out[0].y).toEqual([1, 2]); expect(out[0].z).toEqual(['2016-10-21', '2016-12-02']); + expect(out[0].transforms[0]._indexToPoints).toEqual({0: [3], 1: [4]}); }); it('should use the calendar from the target attribute if target is a string', function() { @@ -261,13 +262,14 @@ describe('filter transforms calc:', function() { expect(out[0].x).toEqual([-2, 2, 3]); expect(out[0].y).toEqual([3, 3, 1]); expect(out[0].marker.color).toEqual([0.3, 0.3, 0.4]); + expect(out[0].transforms[0]._indexToPoints).toEqual({0: [2], 1: [5], 2: [6]}); }); it('filters should handle array on base trace attributes', function() { var out = _transform([Lib.extendDeep({}, base, { hoverinfo: ['x', 'y', 'text', 'name', 'none', 'skip', 'all'], hoverlabel: { - bgcolor: ['red', 'green', 'blue', 'black', 'yellow', 'cyan', 'pink'], + bgcolor: ['red', 'green', 'blue', 'black', 'yellow', 'cyan', 'pink'] }, transforms: [{ type: 'filter', @@ -314,6 +316,8 @@ describe('filter transforms calc:', function() { expect(out[0].x).toEqual([1, 2]); expect(out[0].y).toEqual([2, 3]); + expect(out[0].transforms[0]._indexToPoints).toEqual({0: [4], 1: [5], 2: [6]}); + expect(out[0].transforms[1]._indexToPoints).toEqual({0: [4], 1: [5]}); }); it('filters should chain as AND (case 2)', function() { @@ -339,6 +343,8 @@ describe('filter transforms calc:', function() { expect(out[0].x).toEqual([3]); expect(out[0].y).toEqual([1]); + expect(out[0].transforms[0]._indexToPoints).toEqual({0: [4], 1: [5], 2: [6]}); + expect(out[0].transforms[2]._indexToPoints).toEqual({0: [6]}); }); it('should preserve gaps in data when `preservegaps` is turned on', function() { diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index a01b219fa09..94deb0f5505 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -245,7 +245,7 @@ describe('multiple transforms:', function() { groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], styles: [{ target: 'a', - value: {marker: {color: 'red'}}, + value: {marker: {color: 'red'}} }, { target: 'b', value: {marker: {color: 'blue'}} @@ -277,8 +277,60 @@ describe('multiple transforms:', function() { }] }]; + var mockData2 = [{ + x: [1, 2, 3, 4, 5], + y: [2, 3, 1, 7, 9], + marker: {size: [10, 20, 20, 20, 10]}, + transforms: [ + { + type: 'filter', + operation: '>', + value: 2, + target: 'y' + }, + { + type: 'aggregate', + groups: 'marker.size', + aggregations: [ + {target: 'x', func: 'sum'}, // 20: 6, 10: 5 + {target: 'y', func: 'avg'} // 20: 5, 10: 9 + ] + }, + { + type: 'filter', + operation: '<', + value: 6, + target: 'x' + } + ] + }]; + afterEach(destroyGraphDiv); + it('Plotly.plot should plot the transform traces - filter|aggregate|filter', function(done) { + var data = Lib.extendDeep([], mockData2); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + + // this would be the result if we didn't have a second filter - kept for test case overview + // expect(gd._fullData[0].x).toEqual([6, 5]); + // expect(gd._fullData[0].y).toEqual([5, 9]); + // expect(gd._fullData[0].marker.size).toEqual([20, 10]); + + expect(gd._fullData[0].x).toEqual([5]); + expect(gd._fullData[0].y).toEqual([9]); + expect(gd._fullData[0].marker.size).toEqual([10]); + + expect(gd._fullData[0].transforms[0]._indexToPoints).toEqual({0: [1], 1: [3], 2: [4]}); + expect(gd._fullData[0].transforms[1]._indexToPoints).toEqual({0: [1, 3], 1: [4]}); + expect(gd._fullData[0].transforms[2]._indexToPoints).toEqual({0: [4]}); + + done(); + }); + }); + + it('Plotly.plot should plot the transform traces', function(done) { var data = Lib.extendDeep([], mockData0);