diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 807f0c97b41..111d5191747 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -392,24 +392,41 @@ 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 transforms, direction; + var fullInput = legendItem.trace._fullInput || {}; + 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(Registry.hasTransform(fullInput, 'groupby')) { + var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); + var index = groupbyIndices[groupbyIndices.length - 1]; + + var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name'); - astr = direction + '.name'; + if(origText === '') { + carr.remove(legendItem.trace._group); + } else { + carr.set(legendItem.trace._group, text); + } + + update = carr.constructUpdate(); + } else { + update.name = text; } - else astr = 'name'; - Plotly.restyle(gd, astr, text, traceIndex); + return Plotly.restyle(gd, update, traceIndex); }); } else text.call(textLayout); diff --git a/src/lib/index.js b/src/lib/index.js index 4e22ac3a4f7..b3335928f24 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'); @@ -727,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/lib/keyed_container.js b/src/lib/keyed_container.js new file mode 100644 index 00000000000..0a6b5aecc5d --- /dev/null +++ b/src/lib/keyed_container.js @@ -0,0 +1,177 @@ +/** +* 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'); + +var SIMPLE_PROPERTY_REGEX = /^\w*$/; + +// 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; +var BOTH = 3; +var UNSET = 4; + +module.exports = function keyedContainer(baseObj, path, keyName, valueName) { + keyName = keyName || 'name'; + valueName = valueName || 'value'; + 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][keyName]] = i; + } + + var isSimpleValueProp = SIMPLE_PROPERTY_REGEX.test(valueName); + + var obj = { + // NB: this does not actually modify the baseObj + set: function(name, value) { + var changeType = value === null ? UNSET : NONE; + + var idx = indexLookup[name]; + if(idx === undefined) { + changeType = changeType | BOTH; + idx = arr.length; + indexLookup[name] = idx; + } else if(value !== (isSimpleValueProp ? arr[idx][valueName] : nestedProperty(arr[idx], valueName).get())) { + changeType = changeType | VALUE; + } + + var newValue = arr[idx] = arr[idx] || {}; + newValue[keyName] = name; + + if(isSimpleValueProp) { + newValue[valueName] = value; + } else { + nestedProperty(newValue, valueName).set(value); + } + + // 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; + + return obj; + }, + get: function(name) { + var idx = indexLookup[name]; + + 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]; + + if(idx === undefined) return obj; + changeTypes[idx] = changeTypes[idx] | NAME; + + indexLookup[newName] = idx; + delete indexLookup[name]; + + arr[idx][keyName] = newName; + + return obj; + }, + 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); + } + + 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; + } + + 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 + '.' + keyName] = arr[idx][keyName]; + } + if(changeTypes[idx] & VALUE) { + if(isSimpleValueProp) { + update[astr + '.' + valueName] = (changeTypes[idx] & UNSET) ? null : arr[idx][valueName]; + } else { + update[astr + '.' + valueName] = (changeTypes[idx] & UNSET) ? null : nestedProperty(arr[idx], valueName).get(); + } + } + } else { + update[astr] = null; + } + } + + return update; + } + }; + + return obj; +}; diff --git a/src/plots/plots.js b/src/plots/plots.js index 17b22b4f230..e8ff7e50bc8 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -811,6 +811,10 @@ 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 // to promote consistency between update calls expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j; 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/src/transforms/groupby.js b/src/transforms/groupby.js index 92e00d17fb3..d405a9e9675 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -35,6 +35,17 @@ exports.attributes = { 'with `x` [1, 3] and one trace with `x` [2, 4].' ].join(' ') }, + nameformat: { + valType: 'string', + description: [ + 'Pattern by which grouped traces are named. If only one trace is present,', + '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 "%{group} (%{trace})" would return "Monaco (GDP per capita)".' + ].join(' ') + }, styles: { _isLinkedToArray: 'style', target: { @@ -71,7 +82,8 @@ exports.attributes = { * @return {object} transformOut * copy of transformIn that contains attribute defaults */ -exports.supplyDefaults = function(transformIn) { +exports.supplyDefaults = function(transformIn, traceOut, layout) { + var i; var transformOut = {}; function coerce(attr, dflt) { @@ -83,12 +95,13 @@ exports.supplyDefaults = function(transformIn) { if(!enabled) return transformOut; coerce('groups'); + coerce('nameformat', layout._dataLength > 1 ? '%{group} (%{trace})' : '%{group}'); 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'); @@ -130,9 +143,9 @@ exports.transform = function(data, state) { return newData; }; - 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 +166,10 @@ function transformOne(trace, state) { styleLookup[styles[i].target] = styles[i].value; } + if(opts.styles) { + groupNameObj = Lib.keyedContainer(opts, 'styles', 'target', 'value.name'); + } + // An index to map group name --> expanded trace index var indexLookup = {}; @@ -162,7 +179,21 @@ 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 = Lib.templateString(opts.nameformat, { + trace: trace.name, + group: 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/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 5f546fa277f..e994811d6ae 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,72 @@ 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].styles).toEqual([ + {value: {name: 'bar'}, target: 3} + ]); + }).then(function() { + return _setValue(4, 'asdf'); + }).then(function() { + expect(gd.data[1].transforms[0].styles).toEqual([ + {value: {name: 'bar'}, target: 3}, + {value: {name: 'asdf'}, target: 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].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 69573e1aa85..e18d0ee6a28 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1605,6 +1605,337 @@ 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('constructing 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('with custom named properties', function() { + it('performs all of the operations', function() { + var container = {styles: [ + {foo: 'name1', bar: 'value1'}, + {foo: 'name2', bar: 'value2'} + ]}; + + 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: [ + {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 + }); + + // 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 + }); + + }); + }); + + 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', 'name4'); + + expect(container).toEqual({styles: [ + {foo: 'name4', bar: {value: 'value1'}}, + {foo: 'name2'}, + {foo: 'name3', bar: {value: 'value3'}} + ]}); + + expect(carr.constructUpdate()).toEqual({ + 'styles[0].foo': 'name4', + 'styles[1].bar.value': null, + 'styles[2].foo': 'name3', + 'styles[2].bar.value': 'value3', + }); + }); + + 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, + }); + }); + + 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 + }); + }); + }); + }); + + 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() { 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); + }); + }); });