From aef38e556db4c93fe1b3d43e2fef82c508a7f451 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 27 Jul 2017 15:17:35 -0700 Subject: [PATCH 01/19] Add ability to rename grouped traces --- src/components/legend/draw.js | 48 +++++++++++--- src/lib/index.js | 1 + src/lib/keyed_container.js | 110 +++++++++++++++++++++++++++++++++ src/plots/plots.js | 5 ++ src/transforms/groupby.js | 43 ++++++++++++- test/image/mocks/groupby.json | 15 +++++ test/jasmine/tests/lib_test.js | 96 ++++++++++++++++++++++++++++ 7 files changed, 310 insertions(+), 8 deletions(-) create mode 100644 src/lib/keyed_container.js create mode 100644 test/image/mocks/groupby.json diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 807f0c97b41..e29bfaf4d5f 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -31,6 +31,7 @@ var anchorUtils = require('./anchor_utils'); var SHOWISOLATETIP = true; var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY; +var BLANK_STRING_REGEX = /^[\s\r]*$/; module.exports = function draw(gd) { var fullLayout = gd._fullLayout; @@ -392,24 +393,57 @@ function drawTexts(g, gd) { this.text(text) .call(textLayout); + var origText = text; + if(!this.text()) text = ' \u0020\u0020 '; - var fullInput = legendItem.trace._fullInput || {}, - astr; + var i, transforms, direction; + var fullInput = legendItem.trace._fullInput || {}; + var needsRedraw = false; + var update = {}; // N.B. this block isn't super clean, // is unfortunately untested at the moment, // and only works for for 'ohlc' and 'candlestick', // but should be generalized for other one-to-many transforms if(['ohlc', 'candlestick'].indexOf(fullInput.type) !== -1) { - var transforms = legendItem.trace.transforms, - direction = transforms[transforms.length - 1].direction; + transforms = legendItem.trace.transforms; + direction = transforms[transforms.length - 1].direction; + + update[direction + '.name'] = [text]; + } else { + if(fullInput.transforms) { + for(i = fullInput.transforms.length - 1; i >= 0; i--) { + if(fullInput.transforms[i].type === 'groupby') { + break; + } + } + + var carr = Lib.keyedContainer(fullInput, 'transforms[' + i + '].groupnames'); + + if(BLANK_STRING_REGEX.test(origText)) { + carr.remove(legendItem.trace._group); + needsRedraw = true; + } else { + carr.set(legendItem.trace._group, [text]); + } + + update = carr.constructUpdate(); + } + } + + var p = Plotly.restyle(gd, update, traceIndex); - astr = direction + '.name'; + // If a groupby label is deleted, it seems like we need another redraw in order + // to restore the label. Otherwise it simply sets this property and the blank + // string is retained. + if(needsRedraw) { + p = p.then(function() { + return Plotly.redraw(gd); + }); } - else astr = 'name'; - Plotly.restyle(gd, astr, text, traceIndex); + return p; }); } else text.call(textLayout); diff --git a/src/lib/index.js b/src/lib/index.js index 4e22ac3a4f7..967090583f8 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -19,6 +19,7 @@ var BADNUM = numConstants.BADNUM; var lib = module.exports = {}; lib.nestedProperty = require('./nested_property'); +lib.keyedContainer = require('./keyed_container'); lib.isPlainObject = require('./is_plain_object'); lib.isArray = require('./is_array'); lib.mod = require('./mod'); diff --git a/src/lib/keyed_container.js b/src/lib/keyed_container.js new file mode 100644 index 00000000000..5814bc2dbac --- /dev/null +++ b/src/lib/keyed_container.js @@ -0,0 +1,110 @@ +/** +* 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'; + +var nestedProperty = require('./nested_property'); + +// bitmask for deciding what's updated: +var NONE = 0; +var NAME = 1; +var VALUE = 2; +var BOTH = 3; + +module.exports = function keyedContainer(baseObj, path) { + var i, arr; + var changeTypes = {}; + + if(path && path.length) { arr = nestedProperty(baseObj, path).get(); + } else { + arr = baseObj; + } + + path = path || ''; + arr = arr || []; + + // Construct an index: + var indexLookup = {}; + for(i = 0; i < arr.length; i++) { + indexLookup[arr[i].name] = i; + } + + var obj = { + // NB: this does not actually modify the baseObj + set: function(name, value) { + var changeType = NONE; + var idx = indexLookup[name]; + if(idx === undefined) { + changeType = BOTH; + idx = arr.length; + indexLookup[name] = idx; + } else if(value !== arr[idx].value) { + changeType = VALUE; + } + arr[idx] = {name: name, value: value}; + + changeTypes[idx] = changeTypes[idx] | changeType; + + return obj; + }, + get: function(name) { + var idx = indexLookup[name]; + return idx === undefined ? undefined : arr[idx].value; + }, + rename: function(name, newName) { + var idx = indexLookup[name]; + + if(idx === undefined) return obj; + changeTypes[idx] = changeTypes[idx] | NAME; + + indexLookup[newName] = idx; + delete indexLookup[name]; + + arr[idx].name = newName; + + return obj; + }, + remove: function(name) { + var idx = indexLookup[name]; + if(idx === undefined) return obj; + for(i = idx; i < arr.length; i++) { + changeTypes[i] = changeTypes[i] | BOTH; + } + for(i = idx; i < arr.length; i++) { + indexLookup[arr[i].name]--; + } + arr.splice(idx, 1); + delete(indexLookup[name]); + + return obj; + }, + constructUpdate: function() { + var astr, idx; + var update = {}; + var changed = Object.keys(changeTypes); + for(var i = 0; i < changed.length; i++) { + idx = changed[i]; + astr = path + '[' + idx + ']'; + if(arr[idx]) { + if(changeTypes[idx] & NAME) { + update[astr + '.name'] = arr[idx].name; + } + if(changeTypes[idx] & VALUE) { + update[astr + '.value'] = arr[idx].value; + } + } else { + update[astr] = null; + } + } + + return update; + } + }; + + return obj; +}; diff --git a/src/plots/plots.js b/src/plots/plots.js index 17b22b4f230..99eb5181296 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -811,6 +811,11 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var expandedTrace = expandedTraces[j]; var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); + // The group key gets cleared. If set, pass it forward + if(expandedTrace._group) { + fullExpandedTrace._group = expandedTrace._group; + } + // mutate uid here using parent uid and expanded index // to promote consistency between update calls expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 92e00d17fb3..ae9b601f318 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -35,6 +35,24 @@ exports.attributes = { 'with `x` [1, 3] and one trace with `x` [2, 4].' ].join(' ') }, + namepattern: { + valType: 'string', + dflt: '%g (%t)', + description: [ + 'Pattern by which grouped traces are named. Available special escape', + 'sequences are `%g`, which inserts the group name, and `%t`, which', + 'inserts the trace name. If grouping GDP data by country, for example', + 'The default "%g (%t)" would return "Monaco (GDP per capita)".' + ].join(' ') + }, + groupnames: { + valType: 'any', + description: [ + 'An array of trace names based on group name. Each entry must be an', + 'object `{name: "group", value: "trace name"}` which is then applied', + 'to the particular group, overriding the name derived from `namepattern`.' + ].join(' ') + }, styles: { _isLinkedToArray: 'style', target: { @@ -83,6 +101,8 @@ exports.supplyDefaults = function(transformIn) { if(!enabled) return transformOut; coerce('groups'); + coerce('groupnames'); + coerce('namepattern'); var styleIn = transformIn.styles; var styleOut = transformOut.styles = []; @@ -130,9 +150,15 @@ exports.transform = function(data, state) { return newData; }; +function computeName(pattern, traceName, groupName) { + return pattern.replace(/%g/g, groupName) + .replace(/%t/g, traceName); +} + function transformOne(trace, state) { var i, j, k, attr, srcArray, groupName, newTrace, transforms, arrayLookup; + var groupNameObj; var opts = state.transform; var groups = trace.transforms[state.transformIndex].groups; @@ -153,6 +179,10 @@ function transformOne(trace, state) { styleLookup[styles[i].target] = styles[i].value; } + if(opts.groupnames) { + groupNameObj = Lib.keyedContainer(opts, 'groupnames'); + } + // An index to map group name --> expanded trace index var indexLookup = {}; @@ -162,7 +192,18 @@ function transformOne(trace, state) { // Start with a deep extend that just copies array references. newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); - newTrace.name = groupName; + newTrace._group = groupName; + + var suppliedName = null; + if(groupNameObj) { + suppliedName = groupNameObj.get(groupName); + } + + if(suppliedName) { + newTrace.name = suppliedName; + } else { + newTrace.name = computeName(opts.namepattern, trace.name, groupName); + } // In order for groups to apply correctly to other transform data (e.g. // a filter transform), we have to break the connection and clone the diff --git a/test/image/mocks/groupby.json b/test/image/mocks/groupby.json new file mode 100644 index 00000000000..805a114a39a --- /dev/null +++ b/test/image/mocks/groupby.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + "y": [1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 7, 9], + "transforms": [{ + "type": "groupby", + "groups": [1, 2, 1, 2, 3, 4, 3, 4, 5, 6, 5, 6, 7, 8] + }] + } + ], + "config": { + "editable": true + } +} diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 69573e1aa85..444cdb81372 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1605,6 +1605,102 @@ describe('Test lib.js:', function() { expect(c.MESSAGES).toEqual([]); }); }); + + describe('keyedContainer', function() { + describe('with a filled container', function() { + var container, carr; + + beforeEach(function() { + container = { + styles: [ + {name: 'name1', value: 'value1'}, + {name: 'name2', value: 'value2'} + ] + }; + + carr = Lib.keyedContainer(container, 'styles'); + }); + + describe('modifying the object', function() { + it('adds and updates items', function() { + carr.set('foo', 'bar'); + carr.set('name1', 'value3'); + + expect(container).toEqual({styles: [ + {name: 'name1', value: 'value3'}, + {name: 'name2', value: 'value2'}, + {name: 'foo', value: 'bar'} + ]}); + }); + + it('removes items', function() { + carr.set('foo', 'bar'); + carr.remove('name1'); + + expect(container).toEqual({styles: [ + {name: 'name2', value: 'value2'}, + {name: 'foo', value: 'bar'} + ]}); + }); + + it('gets items', function() { + expect(carr.get('foo')).toBe(undefined); + expect(carr.get('name1')).toEqual('value1'); + + carr.remove('name1'); + + expect(carr.get('name1')).toBe(undefined); + + carr.rename('name2', 'name3'); + + expect(carr.get('name3')).toEqual('value2'); + }); + + it('renames items', function() { + carr.rename('name2', 'name3'); + + expect(container).toEqual({styles: [ + {name: 'name1', value: 'value1'}, + {name: 'name3', value: 'value2'} + ]}); + }); + }); + + describe('constrcting updates', function() { + it('constructs updates for addition and modification', function() { + carr.set('foo', 'bar'); + carr.set('name1', 'value3'); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].value': 'value3', + 'styles[2].name': 'foo', + 'styles[2].value': 'bar' + }); + }); + + it('constructs updates for removal', function() { + carr.set('foo', 'bar'); + carr.remove('name1'); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].name': 'name2', + 'styles[0].value': 'value2', + 'styles[1].name': 'foo', + 'styles[1].value': 'bar', + 'styles[2]': null + }); + }); + + it('constructs updates for renaming', function() { + carr.rename('name2', 'name3'); + + expect(carr.constructUpdate()).toEqual({ + 'styles[1].name': 'name3' + }); + }); + }); + }); + }); }); describe('Queue', function() { From d886c9e84b49f7d81d40f207eac1150b63e5c42b Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 27 Jul 2017 15:46:52 -0700 Subject: [PATCH 02/19] Fix l egend naming --- src/components/legend/draw.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index e29bfaf4d5f..0317abf0b00 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -410,26 +410,26 @@ function drawTexts(g, gd) { transforms = legendItem.trace.transforms; direction = transforms[transforms.length - 1].direction; - update[direction + '.name'] = [text]; - } else { - if(fullInput.transforms) { - for(i = fullInput.transforms.length - 1; i >= 0; i--) { - if(fullInput.transforms[i].type === 'groupby') { - break; - } + update[direction + '.name'] = text; + } else if(fullInput.transforms) { + for(i = fullInput.transforms.length - 1; i >= 0; i--) { + if(fullInput.transforms[i].type === 'groupby') { + break; } + } - var carr = Lib.keyedContainer(fullInput, 'transforms[' + i + '].groupnames'); + var carr = Lib.keyedContainer(fullInput, 'transforms[' + i + '].groupnames'); - if(BLANK_STRING_REGEX.test(origText)) { - carr.remove(legendItem.trace._group); - needsRedraw = true; - } else { - carr.set(legendItem.trace._group, [text]); - } - - update = carr.constructUpdate(); + if(BLANK_STRING_REGEX.test(origText)) { + carr.remove(legendItem.trace._group); + needsRedraw = true; + } else { + carr.set(legendItem.trace._group, text); } + + update = carr.constructUpdate(); + } else { + update.name = text; } var p = Plotly.restyle(gd, update, traceIndex); From e6f5e13bd96fa5fd33990f13490ef5d216c57e94 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 27 Jul 2017 15:58:59 -0700 Subject: [PATCH 03/19] Rename namepattern --> nameformat --- src/transforms/groupby.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index ae9b601f318..fad5a43e426 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -35,7 +35,7 @@ exports.attributes = { 'with `x` [1, 3] and one trace with `x` [2, 4].' ].join(' ') }, - namepattern: { + nameformat: { valType: 'string', dflt: '%g (%t)', description: [ @@ -50,7 +50,7 @@ exports.attributes = { description: [ 'An array of trace names based on group name. Each entry must be an', 'object `{name: "group", value: "trace name"}` which is then applied', - 'to the particular group, overriding the name derived from `namepattern`.' + 'to the particular group, overriding the name derived from `nameformat`.' ].join(' ') }, styles: { @@ -102,7 +102,7 @@ exports.supplyDefaults = function(transformIn) { coerce('groups'); coerce('groupnames'); - coerce('namepattern'); + coerce('nameformat'); var styleIn = transformIn.styles; var styleOut = transformOut.styles = []; @@ -202,7 +202,7 @@ function transformOne(trace, state) { if(suppliedName) { newTrace.name = suppliedName; } else { - newTrace.name = computeName(opts.namepattern, trace.name, groupName); + newTrace.name = computeName(opts.nameformat, trace.name, groupName); } // In order for groups to apply correctly to other transform data (e.g. From 7a2316d547c8ea9841375008e589f49c5ec09b87 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 27 Jul 2017 16:00:46 -0700 Subject: [PATCH 04/19] Remove unused mock --- test/image/mocks/groupby.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 test/image/mocks/groupby.json diff --git a/test/image/mocks/groupby.json b/test/image/mocks/groupby.json deleted file mode 100644 index 805a114a39a..00000000000 --- a/test/image/mocks/groupby.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "data": [ - { - "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], - "y": [1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 7, 9], - "transforms": [{ - "type": "groupby", - "groups": [1, 2, 1, 2, 3, 4, 3, 4, 5, 6, 5, 6, 7, 8] - }] - } - ], - "config": { - "editable": true - } -} From 4e6a0aae25824e7613ec1a55193e00207e2d4a16 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 27 Jul 2017 18:17:50 -0700 Subject: [PATCH 05/19] Add custom prop names to keyedContainer --- src/components/legend/draw.js | 2 +- src/lib/keyed_container.js | 24 +++++++++++++++--------- src/transforms/groupby.js | 2 +- test/jasmine/tests/lib_test.js | 29 ++++++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 0317abf0b00..fc4e35bc961 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -418,7 +418,7 @@ function drawTexts(g, gd) { } } - var carr = Lib.keyedContainer(fullInput, 'transforms[' + i + '].groupnames'); + var carr = Lib.keyedContainer(fullInput, 'transforms[' + i + '].groupnames', 'group', 'name'); if(BLANK_STRING_REGEX.test(origText)) { carr.remove(legendItem.trace._group); diff --git a/src/lib/keyed_container.js b/src/lib/keyed_container.js index 5814bc2dbac..8d2f58e692f 100644 --- a/src/lib/keyed_container.js +++ b/src/lib/keyed_container.js @@ -16,7 +16,10 @@ var NAME = 1; var VALUE = 2; var BOTH = 3; -module.exports = function keyedContainer(baseObj, path) { +module.exports = function keyedContainer(baseObj, path, keyName, valueName) { + + keyName = keyName || 'name'; + valueName = valueName || 'value'; var i, arr; var changeTypes = {}; @@ -31,7 +34,7 @@ module.exports = function keyedContainer(baseObj, path) { // Construct an index: var indexLookup = {}; for(i = 0; i < arr.length; i++) { - indexLookup[arr[i].name] = i; + indexLookup[arr[i][keyName]] = i; } var obj = { @@ -43,10 +46,13 @@ module.exports = function keyedContainer(baseObj, path) { changeType = BOTH; idx = arr.length; indexLookup[name] = idx; - } else if(value !== arr[idx].value) { + } else if(value !== arr[idx][valueName]) { changeType = VALUE; } - arr[idx] = {name: name, value: value}; + var newValue = {}; + newValue[keyName] = name; + newValue[valueName] = value; + arr[idx] = newValue; changeTypes[idx] = changeTypes[idx] | changeType; @@ -54,7 +60,7 @@ module.exports = function keyedContainer(baseObj, path) { }, get: function(name) { var idx = indexLookup[name]; - return idx === undefined ? undefined : arr[idx].value; + return idx === undefined ? undefined : arr[idx][valueName]; }, rename: function(name, newName) { var idx = indexLookup[name]; @@ -65,7 +71,7 @@ module.exports = function keyedContainer(baseObj, path) { indexLookup[newName] = idx; delete indexLookup[name]; - arr[idx].name = newName; + arr[idx][keyName] = newName; return obj; }, @@ -76,7 +82,7 @@ module.exports = function keyedContainer(baseObj, path) { changeTypes[i] = changeTypes[i] | BOTH; } for(i = idx; i < arr.length; i++) { - indexLookup[arr[i].name]--; + indexLookup[arr[i][keyName]]--; } arr.splice(idx, 1); delete(indexLookup[name]); @@ -92,10 +98,10 @@ module.exports = function keyedContainer(baseObj, path) { astr = path + '[' + idx + ']'; if(arr[idx]) { if(changeTypes[idx] & NAME) { - update[astr + '.name'] = arr[idx].name; + update[astr + '.' + keyName] = arr[idx][keyName]; } if(changeTypes[idx] & VALUE) { - update[astr + '.value'] = arr[idx].value; + update[astr + '.' + valueName] = arr[idx][valueName]; } } else { update[astr] = null; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index fad5a43e426..114fab45d05 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -180,7 +180,7 @@ function transformOne(trace, state) { } if(opts.groupnames) { - groupNameObj = Lib.keyedContainer(opts, 'groupnames'); + groupNameObj = Lib.keyedContainer(opts, 'groupnames', 'group', 'name'); } // An index to map group name --> expanded trace index diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 444cdb81372..dde9f9a3eb0 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1666,7 +1666,7 @@ describe('Test lib.js:', function() { }); }); - describe('constrcting updates', function() { + describe('constructing updates', function() { it('constructs updates for addition and modification', function() { carr.set('foo', 'bar'); carr.set('name1', 'value3'); @@ -1700,6 +1700,33 @@ describe('Test lib.js:', function() { }); }); }); + + describe('with custom named properties', function() { + it('performs standard operations', function() { + var container = {styles: [ + {foo: 'name1', bar: 'value1'}, + {foo: 'name2', bar: 'value2'} + ]}; + + var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar'); + + carr.set('name3', 'value3'); + carr.remove('name2'); + carr.rename('name1', 'name2'); + + expect(container).toEqual({styles: [ + {foo: 'name2', bar: 'value1'}, + {foo: 'name3', bar: 'value3'} + ]}); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].foo': 'name2', + 'styles[1].foo': 'name3', + 'styles[1].bar': 'value3', + 'styles[2]': null + }); + }); + }); }); }); From 952e4eadfe7b6494c4766974aeca63126672b293 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 27 Jul 2017 19:10:38 -0700 Subject: [PATCH 06/19] Fix groupby groupnames supplyDefaults --- src/transforms/groupby.js | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 114fab45d05..10cdd006057 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -46,12 +46,22 @@ exports.attributes = { ].join(' ') }, groupnames: { - valType: 'any', - description: [ - 'An array of trace names based on group name. Each entry must be an', - 'object `{name: "group", value: "trace name"}` which is then applied', - 'to the particular group, overriding the name derived from `nameformat`.' - ].join(' ') + _isLinkedToArray: 'groupname', + group: { + valType: 'string', + role: 'info', + description: [ + 'An group to which this name applies. If a `group` and `name` are specified,', + 'that name overrides the `nameformat` for that group\'s trace.' + ].join(' ') + }, + name: { + valType: 'string', + role: 'info', + description: [ + 'Trace names assigned to the grouped traces of the corresponding `group`.' + ].join(' ') + } }, styles: { _isLinkedToArray: 'style', @@ -90,6 +100,7 @@ exports.attributes = { * copy of transformIn that contains attribute defaults */ exports.supplyDefaults = function(transformIn) { + var i; var transformOut = {}; function coerce(attr, dflt) { @@ -101,14 +112,24 @@ exports.supplyDefaults = function(transformIn) { if(!enabled) return transformOut; coerce('groups'); - coerce('groupnames'); coerce('nameformat'); + var nameFormatIn = transformIn.groupnames; + var nameFormatOut = transformOut.groupnames = []; + + if(nameFormatIn) { + for(i = 0; i < nameFormatIn.length; i++) { + nameFormatOut[i] = {}; + Lib.coerce(nameFormatIn[i], nameFormatOut[i], exports.attributes.groupnames, 'group'); + Lib.coerce(nameFormatIn[i], nameFormatOut[i], exports.attributes.groupnames, 'name'); + } + } + var styleIn = transformIn.styles; var styleOut = transformOut.styles = []; if(styleIn) { - for(var i = 0; i < styleIn.length; i++) { + for(i = 0; i < styleIn.length; i++) { styleOut[i] = {}; Lib.coerce(styleIn[i], styleOut[i], exports.attributes.styles, 'target'); Lib.coerce(styleIn[i], styleOut[i], exports.attributes.styles, 'value'); From 8f37fc7169b8dd2416d603f4d0e59f25793b84d0 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 27 Jul 2017 19:24:31 -0700 Subject: [PATCH 07/19] Make groupby nameformat dependent on number of traces --- src/plots/plots.js | 12 ++++++------ src/transforms/groupby.js | 15 ++++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 99eb5181296..117fc16c5c3 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -798,7 +798,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { for(i = 0; i < dataIn.length; i++) { trace = dataIn[i]; - fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i); + fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i, dataIn.length); fullTrace.index = i; fullTrace._input = trace; @@ -809,7 +809,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { for(var j = 0; j < expandedTraces.length; j++) { var expandedTrace = expandedTraces[j]; - var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); + var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i, dataIn.length); // The group key gets cleared. If set, pass it forward if(expandedTrace._group) { @@ -945,7 +945,7 @@ plots.supplyFrameDefaults = function(frameIn) { return frameOut; }; -plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInIndex) { +plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInIndex, inputTraceCount) { var traceOut = {}, defaultColor = Color.defaults[traceOutIndex % Color.defaults.length]; @@ -1018,13 +1018,13 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde traceOut.visible = !!traceOut.visible; } - plots.supplyTransformDefaults(traceIn, traceOut, layout); + plots.supplyTransformDefaults(traceIn, traceOut, layout, inputTraceCount); } return traceOut; }; -plots.supplyTransformDefaults = function(traceIn, traceOut, layout) { +plots.supplyTransformDefaults = function(traceIn, traceOut, layout, inputTraceCount) { var globalTransforms = layout._globalTransforms || []; var transformModules = layout._transformModules || []; @@ -1043,7 +1043,7 @@ plots.supplyTransformDefaults = function(traceIn, traceOut, layout) { if(!_module) Lib.warn('Unrecognized transform type ' + type + '.'); if(_module && _module.supplyDefaults) { - transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn); + transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn, inputTraceCount); transformOut.type = type; transformOut._module = _module; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 10cdd006057..c6317c12526 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -37,12 +37,13 @@ exports.attributes = { }, nameformat: { valType: 'string', - dflt: '%g (%t)', description: [ - 'Pattern by which grouped traces are named. Available special escape', - 'sequences are `%g`, which inserts the group name, and `%t`, which', - 'inserts the trace name. If grouping GDP data by country, for example', - 'The default "%g (%t)" would return "Monaco (GDP per capita)".' + 'Pattern by which grouped traces are named. If only one trace is present,', + 'defaults to the group name (`"%g"`), otherwise defaults to the group name', + 'with trace name (`"%g (%t)"`). Available escape sequences are `%g`, which', + 'inserts the group name, and `%t`, which inserts the trace name. If grouping', + 'GDP data by country when more than one trace is present, for example, the', + 'default "%g (%t)" would return "Monaco (GDP per capita)".' ].join(' ') }, groupnames: { @@ -99,7 +100,7 @@ exports.attributes = { * @return {object} transformOut * copy of transformIn that contains attribute defaults */ -exports.supplyDefaults = function(transformIn) { +exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn, inputTraceCount) { var i; var transformOut = {}; @@ -112,7 +113,7 @@ exports.supplyDefaults = function(transformIn) { if(!enabled) return transformOut; coerce('groups'); - coerce('nameformat'); + coerce('nameformat', inputTraceCount > 1 ? '%g (%t)' : '%g'); var nameFormatIn = transformIn.groupnames; var nameFormatOut = transformOut.groupnames = []; From dd035efe18df3263e1fb1ccddae2dc892c66047c Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 1 Aug 2017 13:05:55 -0700 Subject: [PATCH 08/19] Add registry methods for transforms --- src/components/legend/draw.js | 13 +++----- src/registry.js | 42 ++++++++++++++++++++++++ test/jasmine/tests/register_test.js | 50 +++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index fc4e35bc961..8fbdb238cb6 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -397,7 +397,7 @@ function drawTexts(g, gd) { if(!this.text()) text = ' \u0020\u0020 '; - var i, transforms, direction; + var transforms, direction; var fullInput = legendItem.trace._fullInput || {}; var needsRedraw = false; var update = {}; @@ -411,14 +411,11 @@ function drawTexts(g, gd) { direction = transforms[transforms.length - 1].direction; update[direction + '.name'] = text; - } else if(fullInput.transforms) { - for(i = fullInput.transforms.length - 1; i >= 0; i--) { - if(fullInput.transforms[i].type === 'groupby') { - break; - } - } + } else if(Registry.hasTransform(fullInput, 'groupby')) { + var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); + var index = groupbyIndices[groupbyIndices.length - 1]; - var carr = Lib.keyedContainer(fullInput, 'transforms[' + i + '].groupnames', 'group', 'name'); + var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].groupnames', 'group', 'name'); if(BLANK_STRING_REGEX.test(origText)) { carr.remove(legendItem.trace._group); diff --git a/src/registry.js b/src/registry.js index 2952742f630..163bdda74b8 100644 --- a/src/registry.js +++ b/src/registry.js @@ -166,6 +166,48 @@ exports.traceIs = function(traceType, category) { return !!_module.categories[category]; }; +/** + * Determine if this trace has a transform of the given type and return + * array of matching indices. + * + * @param {object} data + * a trace object (member of data or fullData) + * @param {string} type + * type of trace to test + * @return {array} + * array of matching indices. If none found, returns [] + */ +exports.getTransformIndices = function(data, type) { + var indices = []; + var transforms = data.transforms || []; + for(var i = 0; i < transforms.length; i++) { + if(transforms[i].type === type) { + indices.push(i); + } + } + return indices; +}; + +/** + * Determine if this trace has a transform of the given type + * + * @param {object} data + * a trace object (member of data or fullData) + * @param {string} type + * type of trace to test + * @return {boolean} + */ +exports.hasTransform = function(data, type) { + var transforms = data.transforms || []; + for(var i = 0; i < transforms.length; i++) { + if(transforms[i].type === type) { + return true; + } + } + return false; + +}; + /** * Retrieve component module method. Falls back on noop if either the * module or the method is missing, so the result can always be safely called diff --git a/test/jasmine/tests/register_test.js b/test/jasmine/tests/register_test.js index 6fa534a2e42..2e436659878 100644 --- a/test/jasmine/tests/register_test.js +++ b/test/jasmine/tests/register_test.js @@ -277,4 +277,54 @@ describe('the register function', function() { expect(Registry.transformsRegistry['mah-transform']).toBeDefined(); }); + + describe('getTransformIndices', function() { + it('returns an empty array if no transforms present', function() { + expect(Registry.getTransformIndices({}, 'groupby')).toEqual([]); + }); + + it('returns an empty array if none present', function() { + expect(Registry.getTransformIndices({ + transforms: [ + {type: 'filter'}, + {type: 'groupby'} + ] + }, 'degauss')).toEqual([]); + }); + + it('returns an empty array if none present', function() { + expect(Registry.getTransformIndices({ + transforms: [ + {type: 'filter'}, + {type: 'groupby'}, + {type: 'groupby'} + ] + }, 'groupby')).toEqual([1, 2]); + }); + }); + + describe('hasTransform', function() { + it('returns an false array if no transforms present', function() { + expect(Registry.hasTransform({}, 'groupby')).toBe(false); + }); + + it('returns an empty array if none present', function() { + expect(Registry.hasTransform({ + transforms: [ + {type: 'filter'}, + {type: 'groupby'} + ] + }, 'degauss')).toBe(false); + }); + + it('returns an empty array if none present', function() { + expect(Registry.hasTransform({ + transforms: [ + {type: 'filter'}, + {type: 'groupby'}, + {type: 'groupby'} + ] + }, 'groupby')).toBe(true); + }); + }); }); From 7298a0cd6bb0904334b5f8327866427c76b27c45 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 1 Aug 2017 13:38:01 -0700 Subject: [PATCH 09/19] Cut out unnecessary redraw in legend update --- src/components/legend/draw.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 8fbdb238cb6..6ea40e60e6b 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -399,7 +399,6 @@ function drawTexts(g, gd) { var transforms, direction; var fullInput = legendItem.trace._fullInput || {}; - var needsRedraw = false; var update = {}; // N.B. this block isn't super clean, @@ -419,7 +418,6 @@ function drawTexts(g, gd) { if(BLANK_STRING_REGEX.test(origText)) { carr.remove(legendItem.trace._group); - needsRedraw = true; } else { carr.set(legendItem.trace._group, text); } @@ -429,18 +427,7 @@ function drawTexts(g, gd) { update.name = text; } - var p = Plotly.restyle(gd, update, traceIndex); - - // If a groupby label is deleted, it seems like we need another redraw in order - // to restore the label. Otherwise it simply sets this property and the blank - // string is retained. - if(needsRedraw) { - p = p.then(function() { - return Plotly.redraw(gd); - }); - } - - return p; + return Plotly.restyle(gd, update, traceIndex); }); } else text.call(textLayout); From 64d2bcd88c8a892447eb99b42cf4dbad260f911b Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 1 Aug 2017 13:44:47 -0700 Subject: [PATCH 10/19] Remove unneeded inputTraceCount parameter --- src/plots/plots.js | 12 ++++++------ src/transforms/groupby.js | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 117fc16c5c3..99eb5181296 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -798,7 +798,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { for(i = 0; i < dataIn.length; i++) { trace = dataIn[i]; - fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i, dataIn.length); + fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i); fullTrace.index = i; fullTrace._input = trace; @@ -809,7 +809,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { for(var j = 0; j < expandedTraces.length; j++) { var expandedTrace = expandedTraces[j]; - var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i, dataIn.length); + var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); // The group key gets cleared. If set, pass it forward if(expandedTrace._group) { @@ -945,7 +945,7 @@ plots.supplyFrameDefaults = function(frameIn) { return frameOut; }; -plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInIndex, inputTraceCount) { +plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInIndex) { var traceOut = {}, defaultColor = Color.defaults[traceOutIndex % Color.defaults.length]; @@ -1018,13 +1018,13 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde traceOut.visible = !!traceOut.visible; } - plots.supplyTransformDefaults(traceIn, traceOut, layout, inputTraceCount); + plots.supplyTransformDefaults(traceIn, traceOut, layout); } return traceOut; }; -plots.supplyTransformDefaults = function(traceIn, traceOut, layout, inputTraceCount) { +plots.supplyTransformDefaults = function(traceIn, traceOut, layout) { var globalTransforms = layout._globalTransforms || []; var transformModules = layout._transformModules || []; @@ -1043,7 +1043,7 @@ plots.supplyTransformDefaults = function(traceIn, traceOut, layout, inputTraceCo if(!_module) Lib.warn('Unrecognized transform type ' + type + '.'); if(_module && _module.supplyDefaults) { - transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn, inputTraceCount); + transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn); transformOut.type = type; transformOut._module = _module; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index c6317c12526..30394bbc3c2 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -100,7 +100,7 @@ exports.attributes = { * @return {object} transformOut * copy of transformIn that contains attribute defaults */ -exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn, inputTraceCount) { +exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn) { var i; var transformOut = {}; @@ -113,7 +113,7 @@ exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn, inputT if(!enabled) return transformOut; coerce('groups'); - coerce('nameformat', inputTraceCount > 1 ? '%g (%t)' : '%g'); + coerce('nameformat', layout._dataLength > 1 ? '%g (%t)' : '%g'); var nameFormatIn = transformIn.groupnames; var nameFormatOut = transformOut.groupnames = []; From 57dfe4ac7ceb2281c71868f829a04e0a2ac994de Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 1 Aug 2017 14:15:09 -0700 Subject: [PATCH 11/19] Add templateString method to perform substitutions --- src/lib/index.js | 30 ++++++++++++++++++++++++++++++ src/transforms/groupby.js | 23 ++++++++++------------- test/jasmine/tests/lib_test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index 967090583f8..b3335928f24 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -728,3 +728,33 @@ lib.numSeparate = function(value, separators, separatethousands) { return x1 + x2; }; + +var TEMPLATE_STRING_REGEX = /%{([^\s%{}]*)}/g; +var SIMPLE_PROPERTY_REGEX = /^\w*$/; + +/* + * Substitute values from an object into a string + * + * Examples: + * Lib.templateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf' + * Lib.templateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf' + * + * @param {string} input string containing %{...} template strings + * @param {obj} data object containing substitution values + * + * @return {string} templated string + */ + +lib.templateString = function(string, obj) { + // Not all that useful, but cache nestedProperty instantiation + // just in case it speeds things up *slightly*: + var getterCache = {}; + + return string.replace(TEMPLATE_STRING_REGEX, function(dummy, key) { + if(SIMPLE_PROPERTY_REGEX.test(key)) { + return obj[key] || ''; + } + getterCache[key] = getterCache[key] || lib.nestedProperty(obj, key).get; + return getterCache[key]() || ''; + }); +}; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 30394bbc3c2..fa400456c91 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -39,11 +39,11 @@ exports.attributes = { valType: 'string', description: [ 'Pattern by which grouped traces are named. If only one trace is present,', - 'defaults to the group name (`"%g"`), otherwise defaults to the group name', - 'with trace name (`"%g (%t)"`). Available escape sequences are `%g`, which', - 'inserts the group name, and `%t`, which inserts the trace name. If grouping', + 'defaults to the group name (`"%{group}"`), otherwise defaults to the group name', + 'with trace name (`"%{group} (%{trace})"`). Available escape sequences are `%{group}`, which', + 'inserts the group name, and `%{trace}`, which inserts the trace name. If grouping', 'GDP data by country when more than one trace is present, for example, the', - 'default "%g (%t)" would return "Monaco (GDP per capita)".' + 'default "%{group} (%{trace})" would return "Monaco (GDP per capita)".' ].join(' ') }, groupnames: { @@ -100,7 +100,7 @@ exports.attributes = { * @return {object} transformOut * copy of transformIn that contains attribute defaults */ -exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn) { +exports.supplyDefaults = function(transformIn, traceOut, layout) { var i; var transformOut = {}; @@ -113,7 +113,7 @@ exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn) { if(!enabled) return transformOut; coerce('groups'); - coerce('nameformat', layout._dataLength > 1 ? '%g (%t)' : '%g'); + coerce('nameformat', layout._dataLength > 1 ? '%{group} (%{trace})' : '%{group}'); var nameFormatIn = transformIn.groupnames; var nameFormatOut = transformOut.groupnames = []; @@ -172,12 +172,6 @@ exports.transform = function(data, state) { return newData; }; -function computeName(pattern, traceName, groupName) { - return pattern.replace(/%g/g, groupName) - .replace(/%t/g, traceName); -} - - function transformOne(trace, state) { var i, j, k, attr, srcArray, groupName, newTrace, transforms, arrayLookup; var groupNameObj; @@ -224,7 +218,10 @@ function transformOne(trace, state) { if(suppliedName) { newTrace.name = suppliedName; } else { - newTrace.name = computeName(opts.nameformat, trace.name, groupName); + newTrace.name = Lib.templateString(opts.nameformat, { + trace: trace.name, + group: groupName + }); } // In order for groups to apply correctly to other transform data (e.g. diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index dde9f9a3eb0..4c037eb1fec 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1728,6 +1728,32 @@ describe('Test lib.js:', function() { }); }); }); + + describe('templateString', function() { + it('evaluates attributes', function() { + expect(Lib.templateString('foo %{bar}', {bar: 'baz'})).toEqual('foo baz'); + }); + + it('evaluates nested properties', function() { + expect(Lib.templateString('foo %{bar.baz}', {bar: {baz: 'asdf'}})).toEqual('foo asdf'); + }); + + it('evaluates array nested properties', function() { + expect(Lib.templateString('foo %{bar[0].baz}', {bar: [{baz: 'asdf'}]})).toEqual('foo asdf'); + }); + + it('subtitutes multiple matches', function() { + expect(Lib.templateString('foo %{group} %{trace}', {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;'); + }); + + it('replaces missing matches with empty string', function() { + expect(Lib.templateString('foo %{group} %{trace}', {})).toEqual('foo '); + }); + + it('replaces empty key with empty string', function() { + expect(Lib.templateString('foo %{} %{}', {})).toEqual('foo '); + }); + }); }); describe('Queue', function() { From c0089ea0fc8ea578838e4d912d4342ccb32f0c97 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 1 Aug 2017 14:41:02 -0700 Subject: [PATCH 12/19] Replace ad hoc solution with relinkPrivateKeys --- src/plots/plots.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 99eb5181296..91462a1725a 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -811,10 +811,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var expandedTrace = expandedTraces[j]; var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); - // The group key gets cleared. If set, pass it forward - if(expandedTrace._group) { - fullExpandedTrace._group = expandedTrace._group; - } + relinkPrivateKeys(fullExpandedTrace, expandedTrace); // mutate uid here using parent uid and expanded index // to promote consistency between update calls From 42d76e08aca4fc0cfda7c49cf33da4e76138d2ba Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 1 Aug 2017 15:36:28 -0700 Subject: [PATCH 13/19] Add test for editing legend --- test/jasmine/tests/legend_test.js | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 5f546fa277f..8fdfabc380b 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -9,6 +9,8 @@ var helpers = require('@src/components/legend/helpers'); var anchorUtils = require('@src/components/legend/anchor_utils'); var d3 = require('d3'); +var fail = require('../assets/fail_test'); +var delay = require('../assets/delay'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); @@ -882,4 +884,68 @@ describe('legend interaction', function() { .then(done); }); }); + + describe('editable mode interactions', function() { + var gd; + var mock = { + data: [{ + x: [1, 2, 3], + y: [5, 4, 3] + }, { + x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + y: [1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 7, 9], + transforms: [{ + type: 'groupby', + groups: [1, 2, 1, 2, 3, 4, 3, 4, 5, 6, 5, 6, 7, 8] + }] + }], + config: {editable: true} + }; + + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, Lib.extendDeep({}, mock)).then(done); + }); + + afterEach(destroyGraphDiv); + + function _setValue(index, str) { + var item = d3.selectAll('text.legendtext')[0][index || 0]; + item.dispatchEvent(new MouseEvent('click')); + return delay(20)().then(function() { + var input = d3.select('.plugin-editable.editable'); + input.text(str); + input.node().dispatchEvent(new KeyboardEvent('blur')); + }).then(delay(20)); + } + + it('sets and unsets trace group names', function(done) { + // Set the name of the first trace: + _setValue(0, 'foo').then(function() { + expect(gd.data[0].name).toEqual('foo'); + }).then(function() { + // Set the name of the third legend item: + return _setValue(3, 'bar'); + }).then(function() { + expect(gd.data[1].transforms[0].groupnames).toEqual([ + {name: 'bar', group: 3} + ]); + }).then(function() { + return _setValue(4, 'asdf'); + }).then(function() { + expect(gd.data[1].transforms[0].groupnames).toEqual([ + {name: 'bar', group: 3}, + {name: 'asdf', group: 4} + ]); + }).then(function() { + // Unset the group names: + return _setValue(3, ''); + }).then(function() { + return _setValue(4, ''); + }).then(function() { + // Verify the group names have been cleaned up: + expect(gd.data[1].transforms[0].groupnames).toEqual([]); + }).catch(fail).then(done); + }); + }); }); From efcef43b04594e6f88f412122a316a3d04a27156 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 2 Aug 2017 11:39:30 -0700 Subject: [PATCH 14/19] Add comment for relinkPrivateKeys + transforms --- src/plots/plots.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plots/plots.js b/src/plots/plots.js index 91462a1725a..e8ff7e50bc8 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -811,6 +811,8 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var expandedTrace = expandedTraces[j]; var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); + // relink private (i.e. underscore) keys expanded trace to full expanded trace so + // that transform supply-default methods can set _ keys for future use. relinkPrivateKeys(fullExpandedTrace, expandedTrace); // mutate uid here using parent uid and expanded index From daac59d725ab1cf93f02876407b33dc16a335292 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 8 Aug 2017 12:15:21 -0700 Subject: [PATCH 15/19] Enhance keyedContainer to accept nested props --- src/components/legend/draw.js | 2 +- src/lib/keyed_container.js | 30 +++++++++++++++--- src/transforms/groupby.js | 33 ++------------------ test/jasmine/tests/lib_test.js | 57 ++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 37 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 6ea40e60e6b..c74160389df 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -414,7 +414,7 @@ function drawTexts(g, gd) { var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); var index = groupbyIndices[groupbyIndices.length - 1]; - var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].groupnames', 'group', 'name'); + var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'group', 'value.name'); if(BLANK_STRING_REGEX.test(origText)) { carr.remove(legendItem.trace._group); diff --git a/src/lib/keyed_container.js b/src/lib/keyed_container.js index 8d2f58e692f..bf6a11d8460 100644 --- a/src/lib/keyed_container.js +++ b/src/lib/keyed_container.js @@ -10,6 +10,8 @@ var nestedProperty = require('./nested_property'); +var SIMPLE_PROPERTY_REGEX = /^\w*$/; + // bitmask for deciding what's updated: var NONE = 0; var NAME = 1; @@ -17,7 +19,6 @@ var VALUE = 2; var BOTH = 3; module.exports = function keyedContainer(baseObj, path, keyName, valueName) { - keyName = keyName || 'name'; valueName = valueName || 'value'; var i, arr; @@ -37,6 +38,8 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { indexLookup[arr[i][keyName]] = i; } + var isSimpleValueProp = SIMPLE_PROPERTY_REGEX.test(valueName); + var obj = { // NB: this does not actually modify the baseObj set: function(name, value) { @@ -46,12 +49,18 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { changeType = BOTH; idx = arr.length; indexLookup[name] = idx; - } else if(value !== arr[idx][valueName]) { + } else if(value !== (isSimpleValueProp ? arr[idx][valueName] : nestedProperty(arr[idx], valueName).get())) { changeType = VALUE; } var newValue = {}; newValue[keyName] = name; - newValue[valueName] = value; + + if(isSimpleValueProp) { + newValue[valueName] = value; + } else { + nestedProperty(newValue, valueName).set(value); + } + arr[idx] = newValue; changeTypes[idx] = changeTypes[idx] | changeType; @@ -60,7 +69,14 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { }, get: function(name) { var idx = indexLookup[name]; - return idx === undefined ? undefined : arr[idx][valueName]; + + if(idx === undefined) { + return undefined; + } else if(isSimpleValueProp) { + return arr[idx][valueName]; + } else { + return nestedProperty(arr[idx], valueName).get(); + } }, rename: function(name, newName) { var idx = indexLookup[name]; @@ -101,7 +117,11 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { update[astr + '.' + keyName] = arr[idx][keyName]; } if(changeTypes[idx] & VALUE) { - update[astr + '.' + valueName] = arr[idx][valueName]; + if(isSimpleValueProp) { + update[astr + '.' + valueName] = arr[idx][valueName]; + } else { + update[astr + '.' + valueName] = nestedProperty(arr[idx], valueName).get(); + } } } else { update[astr] = null; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index fa400456c91..67afbfe18a2 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -46,24 +46,6 @@ exports.attributes = { 'default "%{group} (%{trace})" would return "Monaco (GDP per capita)".' ].join(' ') }, - groupnames: { - _isLinkedToArray: 'groupname', - group: { - valType: 'string', - role: 'info', - description: [ - 'An group to which this name applies. If a `group` and `name` are specified,', - 'that name overrides the `nameformat` for that group\'s trace.' - ].join(' ') - }, - name: { - valType: 'string', - role: 'info', - description: [ - 'Trace names assigned to the grouped traces of the corresponding `group`.' - ].join(' ') - } - }, styles: { _isLinkedToArray: 'style', target: { @@ -115,17 +97,6 @@ exports.supplyDefaults = function(transformIn, traceOut, layout) { coerce('groups'); coerce('nameformat', layout._dataLength > 1 ? '%{group} (%{trace})' : '%{group}'); - var nameFormatIn = transformIn.groupnames; - var nameFormatOut = transformOut.groupnames = []; - - if(nameFormatIn) { - for(i = 0; i < nameFormatIn.length; i++) { - nameFormatOut[i] = {}; - Lib.coerce(nameFormatIn[i], nameFormatOut[i], exports.attributes.groupnames, 'group'); - Lib.coerce(nameFormatIn[i], nameFormatOut[i], exports.attributes.groupnames, 'name'); - } - } - var styleIn = transformIn.styles; var styleOut = transformOut.styles = []; @@ -195,8 +166,8 @@ function transformOne(trace, state) { styleLookup[styles[i].target] = styles[i].value; } - if(opts.groupnames) { - groupNameObj = Lib.keyedContainer(opts, 'groupnames', 'group', 'name'); + if(opts.styles) { + groupNameObj = Lib.keyedContainer(opts, 'styles', 'group', 'value.name'); } // An index to map group name --> expanded trace index diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 4c037eb1fec..2ff49dd4b60 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1727,6 +1727,63 @@ describe('Test lib.js:', function() { }); }); }); + + describe('with nested valueName', function() { + it('gets and sets values', function() { + var container = {styles: []}; + + var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar.value'); + + carr.set('name1', 'value1'); + + expect(container).toEqual({styles: [ + {foo: 'name1', bar: {value: 'value1'}} + ]}); + + expect(carr.get('name1')).toEqual('value1'); + }); + + it('renames values', function() { + var container = {styles: []}; + + var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar.value'); + + carr.set('name1', 'value1'); + carr.rename('name1', 'name2'); + + expect(container).toEqual({styles: [ + {foo: 'name2', bar: {value: 'value1'}} + ]}); + + expect(carr.get('name2')).toEqual('value1'); + expect(carr.get('name1')).toBeUndefined(); + }); + + it('constructs updates', function() { + var container = {styles: [ + {foo: 'name1', bar: {value: 'value1'}}, + {foo: 'name2', bar: {value: 'value2'}} + ]}; + + var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar.value'); + + carr.set('name3', 'value3'); + carr.remove('name2'); + carr.rename('name1', 'name2'); + + expect(container).toEqual({styles: [ + {foo: 'name2', bar: {value: 'value1'}}, + {foo: 'name3', bar: {value: 'value3'}} + ]}); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].foo': 'name2', + 'styles[1].foo': 'name3', + 'styles[1].bar.value': 'value3', + 'styles[2]': null + }); + }); + }); }); describe('templateString', function() { From 6aefb415ee1939a8e86f330034e76495a62a7a80 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 8 Aug 2017 12:47:23 -0700 Subject: [PATCH 16/19] Fix group -> target in groupby styles --- src/components/legend/draw.js | 2 +- src/transforms/groupby.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index c74160389df..4e1d37ed3ea 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -414,7 +414,7 @@ function drawTexts(g, gd) { var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); var index = groupbyIndices[groupbyIndices.length - 1]; - var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'group', 'value.name'); + var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name'); if(BLANK_STRING_REGEX.test(origText)) { carr.remove(legendItem.trace._group); diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 67afbfe18a2..d405a9e9675 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -167,7 +167,7 @@ function transformOne(trace, state) { } if(opts.styles) { - groupNameObj = Lib.keyedContainer(opts, 'styles', 'group', 'value.name'); + groupNameObj = Lib.keyedContainer(opts, 'styles', 'target', 'value.name'); } // An index to map group name --> expanded trace index From be0e693dd964a25b5068b4eac1dd34bc9d2c747f Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 11 Aug 2017 14:56:48 -0700 Subject: [PATCH 17/19] Extend keyedContainer to handle nested keys --- src/lib/keyed_container.js | 31 +++++++++++---- test/jasmine/tests/legend_test.js | 13 +++--- test/jasmine/tests/lib_test.js | 66 ++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 14 deletions(-) diff --git a/src/lib/keyed_container.js b/src/lib/keyed_container.js index bf6a11d8460..63f6bda52ec 100644 --- a/src/lib/keyed_container.js +++ b/src/lib/keyed_container.js @@ -17,6 +17,7 @@ var NONE = 0; var NAME = 1; var VALUE = 2; var BOTH = 3; +var UNSET = 4; module.exports = function keyedContainer(baseObj, path, keyName, valueName) { keyName = keyName || 'name'; @@ -43,16 +44,18 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { var obj = { // NB: this does not actually modify the baseObj set: function(name, value) { - var changeType = NONE; + var changeType = value === null ? UNSET : NONE; + var idx = indexLookup[name]; if(idx === undefined) { - changeType = BOTH; + changeType = changeType | BOTH; idx = arr.length; indexLookup[name] = idx; } else if(value !== (isSimpleValueProp ? arr[idx][valueName] : nestedProperty(arr[idx], valueName).get())) { - changeType = VALUE; + changeType = changeType | VALUE; } - var newValue = {}; + + var newValue = arr[idx] = arr[idx] || {}; newValue[keyName] = name; if(isSimpleValueProp) { @@ -61,7 +64,11 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { nestedProperty(newValue, valueName).set(value); } - arr[idx] = newValue; + // If it's not an unset, force that bit to be unset. This is all related to the fact + // that undefined and null are a bit specially implemented in nestedProperties. + if(value !== null) { + changeType = changeType & ~UNSET; + } changeTypes[idx] = changeTypes[idx] | changeType; @@ -93,7 +100,17 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { }, remove: function(name) { var idx = indexLookup[name]; + if(idx === undefined) return obj; + + var object = arr[idx]; + if(Object.keys(object).length > 2) { + // This object contains more than just the key/value, so unset + // the value without modifying the entry otherwise: + changeTypes[idx] = changeTypes[idx] | VALUE; + return obj.set(name, null); + } + for(i = idx; i < arr.length; i++) { changeTypes[i] = changeTypes[i] | BOTH; } @@ -118,9 +135,9 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { } if(changeTypes[idx] & VALUE) { if(isSimpleValueProp) { - update[astr + '.' + valueName] = arr[idx][valueName]; + update[astr + '.' + valueName] = (changeTypes[idx] & UNSET) ? null : arr[idx][valueName]; } else { - update[astr + '.' + valueName] = nestedProperty(arr[idx], valueName).get(); + update[astr + '.' + valueName] = (changeTypes[idx] & UNSET) ? null : nestedProperty(arr[idx], valueName).get(); } } } else { diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 8fdfabc380b..6f818c3c73e 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -885,6 +885,7 @@ describe('legend interaction', function() { }); }); + describe('editable mode interactions', function() { var gd; var mock = { @@ -927,15 +928,15 @@ describe('legend interaction', function() { // Set the name of the third legend item: return _setValue(3, 'bar'); }).then(function() { - expect(gd.data[1].transforms[0].groupnames).toEqual([ - {name: 'bar', group: 3} + expect(gd.data[1].transforms[0].styles).toEqual([ + {value: {name: 'bar'}, target: 3} ]); }).then(function() { return _setValue(4, 'asdf'); }).then(function() { - expect(gd.data[1].transforms[0].groupnames).toEqual([ - {name: 'bar', group: 3}, - {name: 'asdf', group: 4} + expect(gd.data[1].transforms[0].styles).toEqual([ + {value: {name: 'bar'}, target: 3}, + {value: {name: 'asdf'}, target: 4} ]); }).then(function() { // Unset the group names: @@ -944,7 +945,7 @@ describe('legend interaction', function() { return _setValue(4, ''); }).then(function() { // Verify the group names have been cleaned up: - expect(gd.data[1].transforms[0].groupnames).toEqual([]); + expect(gd.data[1].transforms[0].styles).toEqual([]); }).catch(fail).then(done); }); }); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 2ff49dd4b60..dec6ac6abd7 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1702,7 +1702,7 @@ describe('Test lib.js:', function() { }); describe('with custom named properties', function() { - it('performs standard operations', function() { + it('performs all of the operations', function() { var container = {styles: [ {foo: 'name1', bar: 'value1'}, {foo: 'name2', bar: 'value2'} @@ -1710,8 +1710,38 @@ describe('Test lib.js:', function() { var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar'); + // SET A VALUE + carr.set('name3', 'value3'); + + expect(container).toEqual({styles: [ + {foo: 'name1', bar: 'value1'}, + {foo: 'name2', bar: 'value2'}, + {foo: 'name3', bar: 'value3'} + ]}); + + expect(carr.constructUpdate()).toEqual({ + 'styles[2].foo': 'name3', + 'styles[2].bar': 'value3' + }); + + // REMOVE A VALUE + carr.remove('name2'); + + expect(container).toEqual({styles: [ + {foo: 'name1', bar: 'value1'}, + {foo: 'name3', bar: 'value3'} + ]}); + + expect(carr.constructUpdate()).toEqual({ + 'styles[1].foo': 'name3', + 'styles[1].bar': 'value3', + 'styles[2]': null + }); + + // RENAME A VALUE + carr.rename('name1', 'name2'); expect(container).toEqual({styles: [ @@ -1725,6 +1755,24 @@ describe('Test lib.js:', function() { 'styles[1].bar': 'value3', 'styles[2]': null }); + + // SET A VALUE + + carr.set('name2', 'value2'); + + expect(container).toEqual({styles: [ + {foo: 'name2', bar: 'value2'}, + {foo: 'name3', bar: 'value3'} + ]}); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].foo': 'name2', + 'styles[0].bar': 'value2', + 'styles[1].foo': 'name3', + 'styles[1].bar': 'value3', + 'styles[2]': null + }); + }); }); @@ -1783,6 +1831,22 @@ describe('Test lib.js:', function() { 'styles[2]': null }); }); + + it('unsets but does not remove items with extra top-level data', function() { + var container = {styles: [ + {foo: 'name', bar: {value: 'value'}, extra: 'data'} + ]}; + + var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar.value'); + + carr.remove('name'); + + expect(container.styles).toEqual([{foo: 'name', extra: 'data'}]); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].bar.value': null, + }); + }); }); }); From 4943e3a4771bb7b9e1698c7a17e1e34d3ecfce1c Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 15 Aug 2017 12:29:53 -0700 Subject: [PATCH 18/19] Fix keyedContainer behavior to unset entries without removing --- src/lib/keyed_container.js | 40 +++++++++++++---- test/jasmine/tests/legend_test.js | 5 ++- test/jasmine/tests/lib_test.js | 73 ++++++++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 15 deletions(-) diff --git a/src/lib/keyed_container.js b/src/lib/keyed_container.js index 63f6bda52ec..0a6b5aecc5d 100644 --- a/src/lib/keyed_container.js +++ b/src/lib/keyed_container.js @@ -12,7 +12,18 @@ var nestedProperty = require('./nested_property'); var SIMPLE_PROPERTY_REGEX = /^\w*$/; -// bitmask for deciding what's updated: +// bitmask for deciding what's updated. Sometimes the name needs to be updated, +// sometimes the value needs to be updated, and sometimes both do. This is just +// a simple way to track what's updated such that it's a simple OR operation to +// assimilate new updates. +// +// The only exception is the UNSET bit that tracks when we need to explicitly +// unset and remove the property. This concrn arises because of the special +// way in which nestedProperty handles null/undefined. When you specify `null`, +// it prunes any unused items in the tree. I ran into some issues with it getting +// null vs undefined confused, so UNSET is just a bit that forces the property +// update to send `null`, removing the property explicitly rather than setting +// it to undefined. var NONE = 0; var NAME = 1; var VALUE = 2; @@ -111,14 +122,27 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { return obj.set(name, null); } - for(i = idx; i < arr.length; i++) { - changeTypes[i] = changeTypes[i] | BOTH; - } - for(i = idx; i < arr.length; i++) { - indexLookup[arr[i][keyName]]--; + if(isSimpleValueProp) { + for(i = idx; i < arr.length; i++) { + changeTypes[i] = changeTypes[i] | BOTH; + } + for(i = idx; i < arr.length; i++) { + indexLookup[arr[i][keyName]]--; + } + arr.splice(idx, 1); + delete(indexLookup[name]); + } else { + // Perform this update *strictly* so we can check whether the result's + // been pruned. If so, it's a removal. If not, it's a value unset only. + nestedProperty(object, valueName).set(null); + + // Now check if the top level nested property has any keys left. If so, + // the object still has values so we only want to unset the key. If not, + // the entire object can be removed since there's no other data. + // var topLevelKeys = Object.keys(object[valueName.split('.')[0]] || []); + + changeTypes[idx] = changeTypes[idx] | VALUE | UNSET; } - arr.splice(idx, 1); - delete(indexLookup[name]); return obj; }, diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 6f818c3c73e..e994811d6ae 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -945,7 +945,10 @@ describe('legend interaction', function() { return _setValue(4, ''); }).then(function() { // Verify the group names have been cleaned up: - expect(gd.data[1].transforms[0].styles).toEqual([]); + expect(gd.data[1].transforms[0].styles).toEqual([ + {target: 3}, + {target: 4} + ]); }).catch(fail).then(done); }); }); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index dec6ac6abd7..e18d0ee6a28 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1817,18 +1817,19 @@ describe('Test lib.js:', function() { carr.set('name3', 'value3'); carr.remove('name2'); - carr.rename('name1', 'name2'); + carr.rename('name1', 'name4'); expect(container).toEqual({styles: [ - {foo: 'name2', bar: {value: 'value1'}}, + {foo: 'name4', bar: {value: 'value1'}}, + {foo: 'name2'}, {foo: 'name3', bar: {value: 'value3'}} ]}); expect(carr.constructUpdate()).toEqual({ - 'styles[0].foo': 'name2', - 'styles[1].foo': 'name3', - 'styles[1].bar.value': 'value3', - 'styles[2]': null + 'styles[0].foo': 'name4', + 'styles[1].bar.value': null, + 'styles[2].foo': 'name3', + 'styles[2].bar.value': 'value3', }); }); @@ -1847,6 +1848,66 @@ describe('Test lib.js:', function() { 'styles[0].bar.value': null, }); }); + + it('unsets but does not remove items with extra value data', function() { + var container = {styles: [ + {foo: 'name1', bar: {value: 'value1', extra: 'data'}}, + {foo: 'name2', bar: {value: 'value2'}}, + {foo: 'name3', bar: {value: 'value3', extra: 'data'}}, + ]}; + + var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar.value'); + + // Remove the first value: + + carr.remove('name1'); + + expect(container.styles).toEqual([ + {foo: 'name1', bar: {extra: 'data'}}, + {foo: 'name2', bar: {value: 'value2'}}, + {foo: 'name3', bar: {value: 'value3', extra: 'data'}}, + ]); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].bar.value': null + }); + + // Remove the second value: + carr.remove('name2'); + + expect(container.styles).toEqual([ + {foo: 'name1', bar: {extra: 'data'}}, + {foo: 'name2'}, + {foo: 'name3', bar: {value: 'value3', extra: 'data'}}, + ]); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].bar.value': null, + 'styles[1].bar.value': null + }); + }); + + it('does not compress nested attributes *sigh*', function() { + var container = {styles: [ + {foo: 'name1', bar: {value: 'value1'}}, + {foo: 'name2', bar: {value: 'value2', extra: 'data2'}}, + ]}; + + var carr = Lib.keyedContainer(container, 'styles', 'foo', 'bar.value'); + + // Remove the first value: + + carr.remove('name1'); + + expect(container.styles).toEqual([ + {foo: 'name1'}, + {foo: 'name2', bar: {value: 'value2', extra: 'data2'}}, + ]); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].bar.value': null + }); + }); }); }); From 4b04131544f22f729bae9276ac0ed57130b2b4b0 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 15 Aug 2017 14:00:40 -0700 Subject: [PATCH 19/19] Only blank string unsets legend items --- src/components/legend/draw.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 4e1d37ed3ea..111d5191747 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -31,7 +31,6 @@ var anchorUtils = require('./anchor_utils'); var SHOWISOLATETIP = true; var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY; -var BLANK_STRING_REGEX = /^[\s\r]*$/; module.exports = function draw(gd) { var fullLayout = gd._fullLayout; @@ -416,7 +415,7 @@ function drawTexts(g, gd) { var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name'); - if(BLANK_STRING_REGEX.test(origText)) { + if(origText === '') { carr.remove(legendItem.trace._group); } else { carr.set(legendItem.trace._group, text);