From 6ba8626c5b564088a1d7ff27f06ab595d1186b9a Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 8 Sep 2015 14:22:01 -0700 Subject: [PATCH 1/4] Add config option to map values to colors --- src/ui/public/config/defaults.js | 7 +++++++ src/ui/public/vislib/components/color/color.js | 11 +++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) 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/components/color/color.js b/src/ui/public/vislib/components/color/color.js index 30d0131740e0f..3ab40f06b95e0 100644 --- a/src/ui/public/vislib/components/color/color.js +++ b/src/ui/public/vislib/components/color/color.js @@ -1,10 +1,11 @@ define(function (require) { - return function ColorUtilService(Private) { + return function ColorUtilService(Private, config) { 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 colorMapping = config.get('visualization:colorMapping'); /* * Accepts an array of strings or numbers that are used to create a @@ -30,10 +31,12 @@ define(function (require) { var colorObj = _.zipObject(arrayOfStringsOrNumbers, uniqueColors); return function (value) { - if (!mappedColors.get(value)) { - mappedColors.add(value, colorObj[value]); + var mappedColor = mappedColors.get(value); + if (!mappedColor) { + mappedColor = colorMapping[value] || colorObj[value]; + mappedColors.add(value, mappedColor); } - return mappedColors.get(value); + return mappedColor; }; }; }; From 1609cba8c8efb13a118d72697d1c068d8d5c7e7e Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 8 Sep 2015 16:30:50 -0700 Subject: [PATCH 2/4] Better variable name and load color from config dynamically --- src/ui/public/vislib/components/color/color.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ui/public/vislib/components/color/color.js b/src/ui/public/vislib/components/color/color.js index 3ab40f06b95e0..5b16c19307c5d 100644 --- a/src/ui/public/vislib/components/color/color.js +++ b/src/ui/public/vislib/components/color/color.js @@ -5,7 +5,6 @@ define(function (require) { 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 colorMapping = config.get('visualization:colorMapping'); /* * Accepts an array of strings or numbers that are used to create a @@ -25,17 +24,15 @@ define(function (require) { } }); + var configColorMapping = config.get('visualization:colorMapping'); 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); return function (value) { - var mappedColor = mappedColors.get(value); - if (!mappedColor) { - mappedColor = colorMapping[value] || colorObj[value]; - mappedColors.add(value, mappedColor); - } + var mappedColor = configColorMapping[value] || mappedColors.get(value); + if (!mappedColor) mappedColors.add(value, mappedColor = colorObj[value]); return mappedColor; }; }; From ad07d651def389a7a1b1e55e644562291d14c7ae Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 11 Sep 2015 16:11:51 -0700 Subject: [PATCH 3/4] Move color mapping logic to mapped_colors and protect against duplicate colors --- .../vislib/__tests__/components/color.js | 94 ++++++++++----- .../public/vislib/components/color/color.js | 15 +-- .../vislib/components/color/mapped_colors.js | 111 ++++++++---------- 3 files changed, 117 insertions(+), 103 deletions(-) diff --git a/src/ui/public/vislib/__tests__/components/color.js b/src/ui/public/vislib/__tests__/components/color.js index b6efe110e1f30..a882d4346b233 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(function (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(function (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 5b16c19307c5d..522eaf7020840 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, config) { 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,16 +21,10 @@ define(function (require) { } }); - var configColorMapping = config.get('visualization:colorMapping'); - 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) { - var mappedColor = configColorMapping[value] || mappedColors.get(value); - if (!mappedColor) mappedColors.add(value, mappedColor = colorObj[value]); - return mappedColor; + 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(); }); From 46096e8bf86b81ad79b579761892847ead75c06d Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 14 Sep 2015 21:29:51 -0700 Subject: [PATCH 4/4] Remove unused config argument and change to fat arrow functions --- src/ui/public/vislib/__tests__/components/color.js | 4 ++-- src/ui/public/vislib/components/color/color.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/public/vislib/__tests__/components/color.js b/src/ui/public/vislib/__tests__/components/color.js index a882d4346b233..7e38078a66092 100644 --- a/src/ui/public/vislib/__tests__/components/color.js +++ b/src/ui/public/vislib/__tests__/components/color.js @@ -24,7 +24,7 @@ describe('Vislib Color Module Test Suite', function () { var color; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private, config) { + beforeEach(ngMock.inject((Private, config) => { previousConfig = config.get('visualization:colorMapping'); config.set('visualization:colorMapping', {}); seedColors = Private(require('ui/vislib/components/color/seed_colors')); @@ -33,7 +33,7 @@ describe('Vislib Color Module Test Suite', function () { color = getColors(arr); })); - afterEach(ngMock.inject(function (config) { + afterEach(ngMock.inject((config) => { config.set('visualization:colorMapping', previousConfig); })); diff --git a/src/ui/public/vislib/components/color/color.js b/src/ui/public/vislib/components/color/color.js index 522eaf7020840..29fe7fac327a6 100644 --- a/src/ui/public/vislib/components/color/color.js +++ b/src/ui/public/vislib/components/color/color.js @@ -1,5 +1,5 @@ define(function (require) { - return function ColorUtilService(Private, config) { + return function ColorUtilService(Private) { var _ = require('lodash'); var mappedColors = Private(require('ui/vislib/components/color/mapped_colors'));