diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 69496df0ee3..e8f9d6f717f 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -28,6 +28,14 @@ module.exports = { '(provided that the legend itself is visible).' ].join(' ') }, + groups: { + valType: 'data_array', + description: [ + 'An array of strings corresponding to each respective datum. These strings are not' + + 'inherently used by plotly for any purpose but may be used, for example, with transforms' + + 'in order to filter or group points by an auxilliary property.' + ].join(' ') + }, showlegend: { valType: 'boolean', role: 'info', diff --git a/src/plots/plots.js b/src/plots/plots.js index 56aa8a9b696..13b5ddeceb7 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -742,6 +742,7 @@ plots.supplyTraceDefaults = function(traceIn, traceIndex, layout) { coerce('type'); coerce('uid'); coerce('name', 'trace ' + traceIndex); + coerce('groups'); // coerce subplot attributes of all registered subplot types var subplotTypes = Object.keys(subplotsRegistry); diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index d9f83dd78ce..dbc7b15cd36 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -67,7 +67,11 @@ module.exports = { }, ids: { valType: 'data_array', - description: 'A list of keys for object constancy of data points during animation' + description: [ + 'A list of unique string ids for object constancy of data points during animation.' + + 'For efficiency, the uniqueness of ids is not validated, but unexpected results may' + + 'occur if ids are not unique. *ids* is equivalent to a *key* in d3.' + ].join(' ') }, text: { valType: 'string', diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index e7df798f1fd..cc91bdd14d6 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -117,7 +117,9 @@ module.exports = function calc(gd, trace) { {x: x[i], y: y[i]} : {x: false, y: false}; if(trace.ids) { - cd[i].id = String(trace.ids[i]); + if(trace.ids[i] !== undefined && trace.ids[i] !== null) { + cd[i].id = String(trace.ids[i]); + } } } diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 042798ac4ef..8699a16684f 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -164,9 +164,13 @@ function getDataToCoordFunc(gd, trace, filtersrc) { // -> use setConvert method if(ax) return ax.d2c; - // special case for 'ids' + // special case for 'ids' or 'groups' // -> cast to String - if(filtersrc === 'ids') return function(v) { return String(v); }; + if(['ids', 'groups'].indexOf(filtersrc) !== -1) { + return function(v) { + return v === undefined ? undefined : String(v); + }; + } // otherwise // -> cast to Number diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 9ac9dda0e98..b4ee7ffd0ee 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -30,6 +30,17 @@ describe('calculated data and points', function() { }); }); + describe('ids', function() { + it('should assign ids to points and cast them to strings in the process', function() { + Plotly.plot(gd, [{ x: [1, 2, 3, 5], y: [1, null, 3, 5], ids: [1, 'a', 'b', undefined]}], {}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({ x: 1, y: 1, id: '1'})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({ x: false, y: false, id: 'a'})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({ x: 3, y: 3, id: 'b'})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({ x: 5, y: 5})); + }); + }); + describe('category ordering', function() { describe('default category ordering reified', function() {