diff --git a/src/ui/public/config/defaults.js b/src/ui/public/config/defaults.js index f1e90f22aadbb..586dec9ebf492 100644 --- a/src/ui/public/config/defaults.js +++ b/src/ui/public/config/defaults.js @@ -93,6 +93,13 @@ define(function () { type: 'json', description: 'Default properties for the WMS map server support in the tile map' }, + 'visualization:colorMapping': { + type: 'json', + value: JSON.stringify({ + 'Count': '#57c17b' + }), + description: 'Maps values to specified colors within visualizations' + }, 'csv:separator': { value: ',', description: 'Separate exported values with this string', diff --git a/src/ui/public/vislib/__tests__/components/color.js b/src/ui/public/vislib/__tests__/components/color.js index b6efe110e1f30..7e38078a66092 100644 --- a/src/ui/public/vislib/__tests__/components/color.js +++ b/src/ui/public/vislib/__tests__/components/color.js @@ -1,13 +1,16 @@ var angular = require('angular'); var expect = require('expect.js'); var ngMock = require('ngMock'); +const _ = require('lodash'); +const d3 = require('d3'); describe('Vislib Color Module Test Suite', function () { var seedColors; - var MappedColors; var mappedColors; + let config; describe('Color (main)', function () { + let previousConfig; var getColors; var arr = ['good', 'better', 'best', 'never', 'let', 'it', 'rest']; var arrayOfNumbers = [1, 2, 3, 4, 5]; @@ -21,14 +24,19 @@ describe('Vislib Color Module Test Suite', function () { var color; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { + beforeEach(ngMock.inject((Private, config) => { + previousConfig = config.get('visualization:colorMapping'); + config.set('visualization:colorMapping', {}); seedColors = Private(require('ui/vislib/components/color/seed_colors')); getColors = Private(require('ui/vislib/components/color/color')); - MappedColors = Private(require('ui/vislib/components/color/mapped_colors')); - mappedColors = new MappedColors(); + mappedColors = Private(require('ui/vislib/components/color/mapped_colors')); color = getColors(arr); })); + afterEach(ngMock.inject((config) => { + config.set('visualization:colorMapping', previousConfig); + })); + it('should throw an error if input is not an array', function () { expect(function () { getColors(200); @@ -108,38 +116,66 @@ describe('Vislib Color Module Test Suite', function () { }); }); - describe('Mapped Colors', function () { + describe('Mapped Colors', () => { + let previousConfig; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - MappedColors = Private(require('ui/vislib/components/color/mapped_colors')); - mappedColors = new MappedColors(); + beforeEach(ngMock.inject((Private, config) => { + previousConfig = config.get('visualization:colorMapping'); + mappedColors = Private(require('ui/vislib/components/color/mapped_colors')); + mappedColors.mapping = {}; })); - it('should clear all the keys from the map table', function () { - mappedColors.reset(); - expect(mappedColors.count()).to.be(0); - }); + afterEach(ngMock.inject((config) => { + config.set('visualization:colorMapping', previousConfig); + })); - it('should return the color for the added value', function () { - mappedColors.reset(); - mappedColors.add('value1', '#somecolor'); - expect(mappedColors.get('value1')).to.be('#somecolor'); - }); + it('should properly map keys to unique colors', ngMock.inject((config) => { + config.set('visualization:colorMapping', {}); - it('should return the count of mapped keys', function () { - mappedColors.reset(); - mappedColors.add('value1', '#color1'); - mappedColors.add('value2', '#color2'); - expect(mappedColors.count()).to.be(2); - }); + const arr = [1, 2, 3, 4, 5]; + mappedColors.mapKeys(arr); + expect(_(mappedColors.mapping).values().uniq().size()).to.be(arr.length); + })); - it('should return all the colors in the map', function () { - mappedColors.reset(); - mappedColors.add('value1', '#colors1'); - mappedColors.add('value3', '#newColor'); - expect(mappedColors.all()).to.eql(['#colors1', '#newColor']); - }); + it('should not include colors used by the config', ngMock.inject((config) => { + const newConfig = {bar: seedColors[0]}; + config.set('visualization:colorMapping', newConfig); + + const arr = ['foo', 'baz', 'qux']; + mappedColors.mapKeys(arr); + + const colorValues = _(mappedColors.mapping).values(); + expect(colorValues.contains(seedColors[0])).to.be(false); + expect(colorValues.uniq().size()).to.be(arr.length); + })); + + it('should create a unique array of colors even when config is set', ngMock.inject((config) => { + const newConfig = {bar: seedColors[0]}; + config.set('visualization:colorMapping', newConfig); + + const arr = ['foo', 'bar', 'baz', 'qux']; + mappedColors.mapKeys(arr); + + const expectedSize = _(arr).difference(_.keys(newConfig)).size(); + expect(_(mappedColors.mapping).values().uniq().size()).to.be(expectedSize); + expect(mappedColors.get(arr[0])).to.not.be(seedColors[0]); + })); + + it('should treat different formats of colors as equal', ngMock.inject((config) => { + const color = d3.rgb(seedColors[0]); + const rgb = `rgb(${color.r}, ${color.g}, ${color.b})`; + const newConfig = {bar: rgb}; + config.set('visualization:colorMapping', newConfig); + + const arr = ['foo', 'bar', 'baz', 'qux']; + mappedColors.mapKeys(arr); + + const expectedSize = _(arr).difference(_.keys(newConfig)).size(); + expect(_(mappedColors.mapping).values().uniq().size()).to.be(expectedSize); + expect(mappedColors.get(arr[0])).to.not.be(seedColors[0]); + expect(mappedColors.get('bar')).to.be(seedColors[0]); + })); }); describe('Color Palette', function () { diff --git a/src/ui/public/vislib/components/color/color.js b/src/ui/public/vislib/components/color/color.js index 30d0131740e0f..29fe7fac327a6 100644 --- a/src/ui/public/vislib/components/color/color.js +++ b/src/ui/public/vislib/components/color/color.js @@ -1,10 +1,7 @@ define(function (require) { return function ColorUtilService(Private) { var _ = require('lodash'); - - var createColorPalette = Private(require('ui/vislib/components/color/color_palette')); - var MappedColors = Private(require('ui/vislib/components/color/mapped_colors')); - var mappedColors = new MappedColors(); + var mappedColors = Private(require('ui/vislib/components/color/mapped_colors')); /* * Accepts an array of strings or numbers that are used to create a @@ -24,15 +21,9 @@ define(function (require) { } }); - var arrayLength = arrayOfStringsOrNumbers.length; - var colors = createColorPalette(arrayLength + mappedColors.count()); - var uniqueColors = _.difference(colors, mappedColors.all()).slice(0, arrayLength + 1); - var colorObj = _.zipObject(arrayOfStringsOrNumbers, uniqueColors); + mappedColors.mapKeys(arrayOfStringsOrNumbers); return function (value) { - if (!mappedColors.get(value)) { - mappedColors.add(value, colorObj[value]); - } return mappedColors.get(value); }; }; diff --git a/src/ui/public/vislib/components/color/mapped_colors.js b/src/ui/public/vislib/components/color/mapped_colors.js index d55543ed81c2a..f992aea831c78 100644 --- a/src/ui/public/vislib/components/color/mapped_colors.js +++ b/src/ui/public/vislib/components/color/mapped_colors.js @@ -1,63 +1,50 @@ -define(function () { - return function MappedColorFactory() { - - var _ = require('lodash'); - /* - * Maintains a lookup table that associates the value (key) with a hex color (value) - * across the visualizations. - * Provides functions to interact with the lookup table - */ - - var MappedColorService = function () { - }; - - MappedColorService.colors = {}; - /** - * Allows to add value (key) and color (value) to the lookup table - * - * @method add - * @param {String} key - the value in a visualization - * @param {String} value - the color associated with the value - */ - MappedColorService.prototype.add = function (key, value) { - MappedColorService.colors[key] = value; - }; - - /** - * Provides the color (value) associated with the value (key) - * - * @method get - * @param {String} key - the value for which color is required - * @returns the color associated with the value - */ - MappedColorService.prototype.get = function (key) { - return MappedColorService.colors[key]; - }; - - /** - * Size of the mapped colors - * - * @method count - * @returns the number of values (keys) stored in the lookup table - */ - MappedColorService.prototype.count = function () { - return _.keys(MappedColorService.colors).length; - }; - - /** - * All the colors of in the lookup table - * - * @method all - * @returns all the colors used in the lookup table - */ - MappedColorService.prototype.all = function () { - return _.values(MappedColorService.colors); - }; - - MappedColorService.prototype.reset = function () { - MappedColorService.colors = {}; - }; - - return MappedColorService; - }; +define((require) => (Private, config) => { + const _ = require('lodash'); + const d3 = require('d3'); + const createColorPalette = Private(require('ui/vislib/components/color/color_palette')); + + const standardizeColor = (color) => d3.rgb(color).toString(); + function getConfigColorMapping() { + return _.mapValues(config.get('visualization:colorMapping'), standardizeColor); + } + + /* + * Maintains a lookup table that associates the value (key) with a hex color (value) + * across the visualizations. + * Provides functions to interact with the lookup table + */ + class MappedColors { + constructor() { + this.mapping = {}; + } + + get(key) { + return getConfigColorMapping()[key] || this.mapping[key]; + } + + mapKeys(keys) { + const configMapping = getConfigColorMapping(); + const configColors = _.values(configMapping); + + const keysToMap = []; + _.each(keys, (key) => { + // If this key is mapped in the config, it's unnecessary to have it mapped here + if (configMapping[key]) delete this.mapping[key]; + + // If this key is mapped to a color used by the config color mapping, we need to remap it + if (_.contains(configColors, this.mapping[key])) keysToMap.push(key); + + // If this key isn't mapped, we need to map it + if (this.get(key) == null) keysToMap.push(key); + }); + + // Generate a color palette big enough that all new keys can have unique color values + const allColors = _(this.mapping).values().union(configColors).value(); + const colorPalette = createColorPalette(allColors.length + keysToMap.length); + const newColors = _.difference(colorPalette, allColors); + _.merge(this.mapping, _.zipObject(keysToMap, newColors)); + } + } + + return new MappedColors(); });