Skip to content

Commit

Permalink
Merge pull request #4894 from lukasolson/color-mapping-config
Browse files Browse the repository at this point in the history
Add config option to map values to colors
  • Loading branch information
rashidkpc committed Sep 16, 2015
2 parents a03b36d + 46096e8 commit aa1511b
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 102 deletions.
7 changes: 7 additions & 0 deletions src/ui/public/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,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',
Expand Down
94 changes: 65 additions & 29 deletions src/ui/public/vislib/__tests__/components/color.js
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -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);
Expand Down Expand Up @@ -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 () {
Expand Down
13 changes: 2 additions & 11 deletions src/ui/public/vislib/components/color/color.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
};
};
Expand Down
111 changes: 49 additions & 62 deletions src/ui/public/vislib/components/color/mapped_colors.js
Original file line number Diff line number Diff line change
@@ -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();
});

0 comments on commit aa1511b

Please sign in to comment.