Move color mapping logic to mapped_colors and protect against duplicate colors

This commit is contained in:
Lukas Olson 2015-09-11 16:11:51 -07:00
parent 7b14699991
commit ad07d651de
3 changed files with 109 additions and 95 deletions

View file

@ -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 () {

View file

@ -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);
};
};
};

View file

@ -1,63 +1,50 @@
define(function () {
return function MappedColorFactory() {
define((require) => (Private, config) => {
const _ = require('lodash');
const d3 = require('d3');
const createColorPalette = Private(require('ui/vislib/components/color/color_palette'));
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
*/
const standardizeColor = (color) => d3.rgb(color).toString();
function getConfigColorMapping() {
return _.mapValues(config.get('visualization:colorMapping'), standardizeColor);
}
var MappedColorService = function () {
};
/*
* 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 = {};
}
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;
};
get(key) {
return getConfigColorMapping()[key] || this.mapping[key];
}
/**
* 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];
};
mapKeys(keys) {
const configMapping = getConfigColorMapping();
const configColors = _.values(configMapping);
/**
* 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;
};
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];
/**
* 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);
};
// 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);
MappedColorService.prototype.reset = function () {
MappedColorService.colors = {};
};
// If this key isn't mapped, we need to map it
if (this.get(key) == null) keysToMap.push(key);
});
return MappedColorService;
};
// 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();
});