Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config option to map values to colors #4894

Merged
merged 6 commits into from
Sep 16, 2015
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/ui/public/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
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(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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick - kind of inconsistent here in that you use fat arrow syntax in other new code

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
15 changes: 3 additions & 12 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) {
return function ColorUtilService(Private, config) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why the config param was added since it isn't used

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