Skip to content

Commit fdeed44

Browse files
authored
Merge pull request #936 from monfera/transforms
[WIP] Transforms [rebase of timelyportfolio's work]
2 parents 7cc1e6b + 4efe6a9 commit fdeed44

16 files changed

+2042
-1070
lines changed

lib/filter.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
module.exports = require('../src/transforms/filter');

lib/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ Plotly.register([
3030
require('./scattermapbox')
3131
]);
3232

33+
// add transforms
34+
Plotly.register([
35+
require('./filter')
36+
]);
37+
3338
module.exports = Plotly;

src/lib/coerce.js

+54
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@
1212
var isNumeric = require('fast-isnumeric');
1313
var tinycolor = require('tinycolor2');
1414
var nestedProperty = require('./nested_property');
15+
var isPlainObject = require('./is_plain_object');
1516

1617
var getColorscale = require('../components/colorscale/get_scale');
1718
var colorscaleNames = Object.keys(require('../components/colorscale/scales'));
1819

1920
var idRegex = /^([2-9]|[1-9][0-9]+)$/;
2021

22+
function isValObject(obj) {
23+
return obj && obj.valType !== undefined;
24+
}
25+
2126
exports.valObjects = {
2227
data_array: {
2328
// You can use *dflt=[] to force said array to exist though.
@@ -355,3 +360,52 @@ exports.validate = function(value, opts) {
355360
valObject.coerceFunction(value, propMock, failed, opts);
356361
return out !== failed;
357362
};
363+
364+
/*
365+
* returns true for a valid value object and false for tree nodes in the attribute hierarchy
366+
*/
367+
exports.isValObject = isValObject;
368+
369+
exports.IS_SUBPLOT_OBJ = '_isSubplotObj';
370+
exports.IS_LINKED_TO_ARRAY = '_isLinkedToArray';
371+
exports.DEPRECATED = '_deprecated';
372+
373+
// list of underscore attributes to keep in schema as is
374+
exports.UNDERSCORE_ATTRS = [exports.IS_SUBPLOT_OBJ, exports.IS_LINKED_TO_ARRAY, exports.DEPRECATED];
375+
376+
/**
377+
* Crawl the attribute tree, recursively calling a callback function
378+
*
379+
* @param {object} attrs
380+
* The node of the attribute tree (e.g. the root) from which recursion originates
381+
* @param {Function} callback
382+
* A callback function with the signature:
383+
* @callback callback
384+
* @param {object} attr an attribute
385+
* @param {String} attrName name string
386+
* @param {object[]} attrs all the attributes
387+
* @param {Number} level the recursion level, 0 at the root
388+
* @param {Number} [specifiedLevel]
389+
* The level in the tree, in order to let the callback function detect descend or backtrack,
390+
* typically unsupplied (implied 0), just used by the self-recursive call.
391+
* The necessity arises because the tree traversal is not controlled by callback return values.
392+
* The decision to not use callback return values for controlling tree pruning arose from
393+
* the goal of keeping the crawler backwards compatible. Observe that one of the pruning conditions
394+
* precedes the callback call.
395+
*
396+
* @return {object} transformOut
397+
* copy of transformIn that contains attribute defaults
398+
*/
399+
exports.crawl = function(attrs, callback, specifiedLevel) {
400+
var level = specifiedLevel || 0;
401+
Object.keys(attrs).forEach(function(attrName) {
402+
var attr = attrs[attrName];
403+
404+
if(exports.UNDERSCORE_ATTRS.indexOf(attrName) !== -1) return;
405+
406+
callback(attr, attrName, attrs, level);
407+
408+
if(isValObject(attr)) return;
409+
if(isPlainObject(attr)) exports.crawl(attr, callback, level + 1);
410+
});
411+
};

src/lib/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ lib.coerce = coerceModule.coerce;
2323
lib.coerce2 = coerceModule.coerce2;
2424
lib.coerceFont = coerceModule.coerceFont;
2525
lib.validate = coerceModule.validate;
26+
lib.isValObject = coerceModule.isValObject;
27+
lib.crawl = coerceModule.crawl;
28+
lib.IS_SUBPLOT_OBJ = coerceModule.IS_SUBPLOT_OBJ;
29+
lib.IS_LINKED_TO_ARRAY = coerceModule.IS_LINKED_TO_ARRAY;
30+
lib.DEPRECATED = coerceModule.DEPRECATED;
31+
lib.UNDERSCORE_ATTRS = coerceModule.UNDERSCORE_ATTRS;
2632

2733
var datesModule = require('./dates');
2834
lib.dateTime2ms = datesModule.dateTime2ms;

src/plot_api/plot_schema.js

+12-31
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,7 @@ var extendDeep = Lib.extendDeep;
2323
var extendDeepAll = Lib.extendDeepAll;
2424

2525
var NESTED_MODULE = '_nestedModules',
26-
COMPOSED_MODULE = '_composedModules',
27-
IS_SUBPLOT_OBJ = '_isSubplotObj',
28-
IS_LINKED_TO_ARRAY = '_isLinkedToArray',
29-
DEPRECATED = '_deprecated';
30-
31-
// list of underscore attributes to keep in schema as is
32-
var UNDERSCORE_ATTRS = [IS_SUBPLOT_OBJ, IS_LINKED_TO_ARRAY, DEPRECATED];
26+
COMPOSED_MODULE = '_composedModules';
3327

3428
var plotSchema = {
3529
traces: {},
@@ -56,22 +50,9 @@ PlotSchema.get = function() {
5650
return plotSchema;
5751
};
5852

59-
PlotSchema.crawl = function(attrs, callback) {
60-
Object.keys(attrs).forEach(function(attrName) {
61-
var attr = attrs[attrName];
62-
63-
if(UNDERSCORE_ATTRS.indexOf(attrName) !== -1) return;
53+
PlotSchema.crawl = Lib.crawl;
6454

65-
callback(attr, attrName, attrs);
66-
67-
if(PlotSchema.isValObject(attr)) return;
68-
if(Lib.isPlainObject(attr)) PlotSchema.crawl(attr, callback);
69-
});
70-
};
71-
72-
PlotSchema.isValObject = function(obj) {
73-
return obj && obj.valType !== undefined;
74-
};
55+
PlotSchema.isValObject = Lib.isValObject;
7556

7657
function getTraceAttributes(type) {
7758
var globalAttributes = Plots.attributes,
@@ -131,13 +112,13 @@ function getLayoutAttributes() {
131112
// FIXME polar layout attributes
132113
layoutAttributes = assignPolarLayoutAttrs(layoutAttributes);
133114

134-
// add IS_SUBPLOT_OBJ attribute
115+
// add crawler.IS_SUBPLOT_OBJ attribute
135116
layoutAttributes = handleSubplotObjs(layoutAttributes);
136117

137118
layoutAttributes = removeUnderscoreAttrs(layoutAttributes);
138119
mergeValTypeAndRole(layoutAttributes);
139120

140-
// generate IS_LINKED_TO_ARRAY structure
121+
// generate crawler.IS_LINKED_TO_ARRAY structure
141122
handleLinkedToArray(layoutAttributes);
142123

143124
plotSchema.layout = { layoutAttributes: layoutAttributes };
@@ -158,7 +139,7 @@ function getTransformAttributes(name) {
158139
function getDefs() {
159140
plotSchema.defs = {
160141
valObjects: Lib.valObjects,
161-
metaKeys: UNDERSCORE_ATTRS.concat(['description', 'role'])
142+
metaKeys: Lib.UNDERSCORE_ATTRS.concat(['description', 'role'])
162143
};
163144
}
164145

@@ -241,7 +222,7 @@ function mergeValTypeAndRole(attrs) {
241222
}
242223
}
243224

244-
PlotSchema.crawl(attrs, callback);
225+
Lib.crawl(attrs, callback);
245226
}
246227

247228
// helper methods
@@ -267,7 +248,7 @@ function getModule(arg) {
267248
function removeUnderscoreAttrs(attributes) {
268249
Object.keys(attributes).forEach(function(k) {
269250
if(k.charAt(0) === '_' &&
270-
UNDERSCORE_ATTRS.indexOf(k) === -1) delete attributes[k];
251+
Lib.UNDERSCORE_ATTRS.indexOf(k) === -1) delete attributes[k];
271252
});
272253
return attributes;
273254
}
@@ -321,7 +302,7 @@ function handleSubplotObjs(layoutAttributes) {
321302
isSubplotObj = subplotRegistry.attrRegex.test(k);
322303
}
323304

324-
if(isSubplotObj) layoutAttributes[k][IS_SUBPLOT_OBJ] = true;
305+
if(isSubplotObj) layoutAttributes[k][Lib.IS_SUBPLOT_OBJ] = true;
325306
});
326307
});
327308

@@ -331,17 +312,17 @@ function handleSubplotObjs(layoutAttributes) {
331312
function handleLinkedToArray(layoutAttributes) {
332313

333314
function callback(attr, attrName, attrs) {
334-
if(attr[IS_LINKED_TO_ARRAY] !== true) return;
315+
if(attr[Lib.IS_LINKED_TO_ARRAY] !== true) return;
335316

336317
// TODO more robust logic
337318
var itemName = attrName.substr(0, attrName.length - 1);
338319

339-
delete attr[IS_LINKED_TO_ARRAY];
320+
delete attr[Lib.IS_LINKED_TO_ARRAY];
340321

341322
attrs[attrName] = { items: {} };
342323
attrs[attrName].items[itemName] = attr;
343324
attrs[attrName].role = 'object';
344325
}
345326

346-
PlotSchema.crawl(layoutAttributes, callback);
327+
Lib.crawl(layoutAttributes, callback);
347328
}

src/plots/plots.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,41 @@ function applyTransforms(fullTrace, fullData, layout) {
786786
var container = fullTrace.transforms,
787787
dataOut = [fullTrace];
788788

789+
var attributeSets = dataOut.map(function(trace) {
790+
791+
var arraySplitAttributes = [];
792+
793+
var stack = [];
794+
795+
/**
796+
* A closure that gathers attribute paths into its enclosed arraySplitAttributes
797+
* Attribute paths are collected iff their leaf node is a splittable attribute
798+
* @callback callback
799+
* @param {object} attr an attribute
800+
* @param {String} attrName name string
801+
* @param {object[]} attrs all the attributes
802+
* @param {Number} level the recursion level, 0 at the root
803+
* @closureVariable {String[][]} arraySplitAttributes the set of gathered attributes
804+
* Example of filled closure variable (expected to be initialized to []):
805+
* [["marker","size"],["marker","line","width"],["marker","line","color"]]
806+
*/
807+
function callback(attr, attrName, attrs, level) {
808+
809+
stack = stack.slice(0, level).concat([attrName]);
810+
811+
var splittableAttr = attr.valType === 'data_array' || attr.arrayOk === true;
812+
if(splittableAttr) {
813+
arraySplitAttributes.push(stack.slice());
814+
}
815+
}
816+
817+
Lib.crawl(trace._module.attributes, callback);
818+
819+
return arraySplitAttributes.map(function(path) {
820+
return path.join('.');
821+
});
822+
});
823+
789824
for(var i = 0; i < container.length; i++) {
790825
var transform = container[i],
791826
type = transform.type,
@@ -796,7 +831,9 @@ function applyTransforms(fullTrace, fullData, layout) {
796831
transform: transform,
797832
fullTrace: fullTrace,
798833
fullData: fullData,
799-
layout: layout
834+
attributeSets: attributeSets,
835+
layout: layout,
836+
transformIndex: i
800837
});
801838
}
802839
}

test/jasmine/assets/transforms/filter.js renamed to src/transforms/filter.js

+63-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
19
'use strict';
210

11+
var isNumeric = require('fast-isnumeric');
12+
313
// var Lib = require('@src/lib');
4-
var Lib = require('../../../../src/lib');
14+
var Lib = require('../lib');
515

616
/* eslint no-unused-vars: 0*/
717

8-
918
// so that Plotly.register knows what to do with it
1019
exports.moduleType = 'transform';
1120

@@ -16,17 +25,21 @@ exports.name = 'filter';
1625
exports.attributes = {
1726
operation: {
1827
valType: 'enumerated',
19-
values: ['=', '<', '>'],
28+
values: ['=', '<', '>', 'within', 'notwithin', 'in', 'notin'],
2029
dflt: '='
2130
},
2231
value: {
23-
valType: 'number',
32+
valType: 'any',
2433
dflt: 0
2534
},
2635
filtersrc: {
2736
valType: 'enumerated',
28-
values: ['x', 'y'],
29-
dflt: 'x'
37+
values: ['x', 'y', 'ids'],
38+
dflt: 'x',
39+
ids: {
40+
valType: 'data_array',
41+
description: 'A list of keys for object constancy of data points during animation'
42+
}
3043
}
3144
};
3245

@@ -54,6 +67,16 @@ exports.supplyDefaults = function(transformIn, fullData, layout) {
5467
coerce('value');
5568
coerce('filtersrc');
5669

70+
// numeric values as character should be converted to numeric
71+
if(Array.isArray(transformOut.value)) {
72+
transformOut.value = transformOut.value.map(function(v) {
73+
if(isNumeric(v)) v = +v;
74+
return v;
75+
});
76+
} else {
77+
if(isNumeric(transformOut.value)) transformOut.value = +transformOut.value;
78+
}
79+
5780
// or some more complex logic using fullData and layout
5881

5982
return transformOut;
@@ -121,6 +144,16 @@ function transformOne(trace, state) {
121144

122145
function getFilterFunc(opts) {
123146
var value = opts.value;
147+
// if value is not array then coerce to
148+
// an array of [value,value] so the
149+
// filter function will work
150+
// but perhaps should just error out
151+
var valueArr = [];
152+
if(!Array.isArray(value)) {
153+
valueArr = [value, value];
154+
} else {
155+
valueArr = value;
156+
}
124157

125158
switch(opts.operation) {
126159
case '=':
@@ -129,6 +162,30 @@ function getFilterFunc(opts) {
129162
return function(v) { return v < value; };
130163
case '>':
131164
return function(v) { return v > value; };
165+
case 'within':
166+
return function(v) {
167+
// if character then ignore with no side effect
168+
function notDateNumber(d) {
169+
return !(isNumeric(d) || Lib.isDateTime(d));
170+
}
171+
if(valueArr.some(notDateNumber)) {
172+
return true;
173+
}
174+
175+
// keep the = ?
176+
return v >= Math.min.apply(null, valueArr) &&
177+
v <= Math.max.apply(null, valueArr);
178+
};
179+
case 'notwithin':
180+
return function(v) {
181+
// keep the = ?
182+
return !(v >= Math.min.apply(null, valueArr) &&
183+
v <= Math.max.apply(null, valueArr));
184+
};
185+
case 'in':
186+
return function(v) { return valueArr.indexOf(v) >= 0; };
187+
case 'notin':
188+
return function(v) { return valueArr.indexOf(v) === -1; };
132189
}
133190
}
134191

0 commit comments

Comments
 (0)