mirror of
https://github.com/apache/superset.git
synced 2026-04-17 07:05:04 +00:00
Improve categorical color management (#5815)
* Create new classes for handling categorical colors * verify to pass existing unit tests * separate logic for forcing color and getting color * replace getColorFromScheme with CategoricalColorManager * organize static functions * migrate to new function * Remove ALL_COLOR_SCHEMES * move sequential colors to another file * extract categorical colors to separate file * move airbnb and lyft colors to separate files * fix missing toFunction() * Rewrite to support local and global force items, plus namespacing. * fix references * revert nvd3 * update namespace api * Update the visualizations * update usage with static functions * update unit test * add unit test * rename default namespace * add unit test for color namespace * add unit test for namespace * start unit test for colorschememanager * add unit tests for color scheme manager * check returns for chaining * complete unit test for the new classes * fix color tests * update unit tests * update unit tests * move color scheme registration to common * update unit test * rename sharedForcedColors to parentForcedColors * remove import
This commit is contained in:
committed by
Chris Williams
parent
bec0b4cc37
commit
f482a6cf99
@@ -0,0 +1,130 @@
|
||||
import { it, describe, before } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import CategoricalColorNamespace, {
|
||||
getNamespace,
|
||||
getScale,
|
||||
getColor,
|
||||
DEFAULT_NAMESPACE,
|
||||
} from '../../../src/modules/CategoricalColorNamespace';
|
||||
import { registerScheme } from '../../../src/modules/ColorSchemeManager';
|
||||
|
||||
describe('CategoricalColorNamespace', () => {
|
||||
before(() => {
|
||||
registerScheme('testColors', ['red', 'green', 'blue']);
|
||||
registerScheme('testColors2', ['red', 'green', 'blue']);
|
||||
});
|
||||
it('The class constructor cannot be accessed directly', () => {
|
||||
expect(CategoricalColorNamespace).to.not.be.a('Function');
|
||||
});
|
||||
describe('static getNamespace()', () => {
|
||||
it('returns default namespace if name is not specified', () => {
|
||||
const namespace = getNamespace();
|
||||
expect(namespace !== undefined).to.equal(true);
|
||||
expect(namespace.name).to.equal(DEFAULT_NAMESPACE);
|
||||
});
|
||||
it('returns namespace with specified name', () => {
|
||||
const namespace = getNamespace('myNamespace');
|
||||
expect(namespace !== undefined).to.equal(true);
|
||||
expect(namespace.name).to.equal('myNamespace');
|
||||
});
|
||||
it('returns existing instance if the name already exists', () => {
|
||||
const ns1 = getNamespace('myNamespace');
|
||||
const ns2 = getNamespace('myNamespace');
|
||||
expect(ns1).to.equal(ns2);
|
||||
const ns3 = getNamespace();
|
||||
const ns4 = getNamespace();
|
||||
expect(ns3).to.equal(ns4);
|
||||
});
|
||||
});
|
||||
describe('.getScale()', () => {
|
||||
it('returns a CategoricalColorScale from given scheme name', () => {
|
||||
const namespace = getNamespace('test-get-scale1');
|
||||
const scale = namespace.getScale('testColors');
|
||||
expect(scale).to.not.equal(undefined);
|
||||
expect(scale.getColor('dog')).to.not.equal(undefined);
|
||||
});
|
||||
it('returns same scale if the scale with that name already exists in this namespace', () => {
|
||||
const namespace = getNamespace('test-get-scale2');
|
||||
const scale1 = namespace.getScale('testColors');
|
||||
const scale2 = namespace.getScale('testColors2');
|
||||
const scale3 = namespace.getScale('testColors2');
|
||||
const scale4 = namespace.getScale('testColors');
|
||||
expect(scale1).to.equal(scale4);
|
||||
expect(scale2).to.equal(scale3);
|
||||
});
|
||||
});
|
||||
describe('.setColor()', () => {
|
||||
it('overwrites color for all CategoricalColorScales in this namespace', () => {
|
||||
const namespace = getNamespace('test-set-scale1');
|
||||
namespace.setColor('dog', 'black');
|
||||
const scale = namespace.getScale('testColors');
|
||||
expect(scale.getColor('dog')).to.equal('black');
|
||||
expect(scale.getColor('boy')).to.not.equal('black');
|
||||
});
|
||||
it('can override forcedColors in each scale', () => {
|
||||
const namespace = getNamespace('test-set-scale2');
|
||||
namespace.setColor('dog', 'black');
|
||||
const scale = namespace.getScale('testColors');
|
||||
scale.setColor('dog', 'pink');
|
||||
expect(scale.getColor('dog')).to.equal('black');
|
||||
expect(scale.getColor('boy')).to.not.equal('black');
|
||||
});
|
||||
it('does not affect scales in other namespaces', () => {
|
||||
const ns1 = getNamespace('test-set-scale3.1');
|
||||
ns1.setColor('dog', 'black');
|
||||
const scale1 = ns1.getScale('testColors');
|
||||
const ns2 = getNamespace('test-set-scale3.2');
|
||||
const scale2 = ns2.getScale('testColors');
|
||||
expect(scale1.getColor('dog')).to.equal('black');
|
||||
expect(scale2.getColor('dog')).to.not.equal('black');
|
||||
});
|
||||
it('returns the namespace instance', () => {
|
||||
const ns1 = getNamespace('test-set-scale3.1');
|
||||
const ns2 = ns1.setColor('dog', 'black');
|
||||
expect(ns1).to.equal(ns2);
|
||||
});
|
||||
});
|
||||
describe('static getScale()', () => {
|
||||
it('getScale() returns a CategoricalColorScale with default scheme in default namespace', () => {
|
||||
const scale = getScale();
|
||||
expect(scale).to.not.equal(undefined);
|
||||
const scale2 = getNamespace().getScale();
|
||||
expect(scale).to.equal(scale2);
|
||||
});
|
||||
it('getScale(scheme) returns a CategoricalColorScale with specified scheme in default namespace', () => {
|
||||
const scale = getScale('testColors');
|
||||
expect(scale).to.not.equal(undefined);
|
||||
const scale2 = getNamespace().getScale('testColors');
|
||||
expect(scale).to.equal(scale2);
|
||||
});
|
||||
it('getScale(scheme, namespace) returns a CategoricalColorScale with specified scheme in specified namespace', () => {
|
||||
const scale = getScale('testColors', 'test-getScale');
|
||||
expect(scale).to.not.equal(undefined);
|
||||
const scale2 = getNamespace('test-getScale').getScale('testColors');
|
||||
expect(scale).to.equal(scale2);
|
||||
});
|
||||
});
|
||||
describe('static getColor()', () => {
|
||||
it('getColor(value) returns a color from default scheme in default namespace', () => {
|
||||
const value = 'dog';
|
||||
const color = getColor(value);
|
||||
const color2 = getNamespace().getScale().getColor(value);
|
||||
expect(color).to.equal(color2);
|
||||
});
|
||||
it('getColor(value, scheme) returns a color from specified scheme in default namespace', () => {
|
||||
const value = 'dog';
|
||||
const scheme = 'testColors';
|
||||
const color = getColor(value, scheme);
|
||||
const color2 = getNamespace().getScale(scheme).getColor(value);
|
||||
expect(color).to.equal(color2);
|
||||
});
|
||||
it('getColor(value, scheme, namespace) returns a color from specified scheme in specified namespace', () => {
|
||||
const value = 'dog';
|
||||
const scheme = 'testColors';
|
||||
const namespace = 'test-getColor';
|
||||
const color = getColor(value, scheme, namespace);
|
||||
const color2 = getNamespace(namespace).getScale(scheme).getColor(value);
|
||||
expect(color).to.equal(color2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import CategoricalColorScale from '../../../src/modules/CategoricalColorScale';
|
||||
|
||||
describe('CategoricalColorScale', () => {
|
||||
it('exists', () => {
|
||||
expect(CategoricalColorScale !== undefined).to.equal(true);
|
||||
});
|
||||
|
||||
describe('new CategoricalColorScale(colors, parentForcedColors)', () => {
|
||||
it('can create new scale when parentForcedColors is not given', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
expect(scale).to.be.instanceOf(CategoricalColorScale);
|
||||
});
|
||||
it('can create new scale when parentForcedColors is given', () => {
|
||||
const parentForcedColors = {};
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green'], parentForcedColors);
|
||||
expect(scale).to.be.instanceOf(CategoricalColorScale);
|
||||
expect(scale.parentForcedColors).to.equal(parentForcedColors);
|
||||
});
|
||||
});
|
||||
describe('.getColor(value)', () => {
|
||||
it('returns same color for same value', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
const c1 = scale.getColor('pig');
|
||||
const c2 = scale.getColor('horse');
|
||||
const c3 = scale.getColor('pig');
|
||||
scale.getColor('cow');
|
||||
const c5 = scale.getColor('horse');
|
||||
|
||||
expect(c1).to.equal(c3);
|
||||
expect(c2).to.equal(c5);
|
||||
});
|
||||
it('returns different color for consecutive items', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
const c1 = scale.getColor('pig');
|
||||
const c2 = scale.getColor('horse');
|
||||
const c3 = scale.getColor('cat');
|
||||
|
||||
expect(c1).to.not.equal(c2);
|
||||
expect(c2).to.not.equal(c3);
|
||||
expect(c3).to.not.equal(c1);
|
||||
});
|
||||
it('recycles colors when number of items exceed available colors', () => {
|
||||
const colorSet = {};
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
const colors = [
|
||||
scale.getColor('pig'),
|
||||
scale.getColor('horse'),
|
||||
scale.getColor('cat'),
|
||||
scale.getColor('cow'),
|
||||
scale.getColor('donkey'),
|
||||
scale.getColor('goat'),
|
||||
];
|
||||
colors.forEach((color) => {
|
||||
if (colorSet[color]) {
|
||||
colorSet[color]++;
|
||||
} else {
|
||||
colorSet[color] = 1;
|
||||
}
|
||||
});
|
||||
expect(Object.keys(colorSet).length).to.equal(3);
|
||||
['blue', 'red', 'green'].forEach((color) => {
|
||||
expect(colorSet[color]).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('.setColor(value, forcedColor)', () => {
|
||||
it('overrides default color', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale.setColor('pig', 'pink');
|
||||
expect(scale.getColor('pig')).to.equal('pink');
|
||||
});
|
||||
it('does not override parentForcedColors', () => {
|
||||
const scale1 = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
scale1.setColor('pig', 'black');
|
||||
const scale2 = new CategoricalColorScale(['blue', 'red', 'green'], scale1.forcedColors);
|
||||
scale2.setColor('pig', 'pink');
|
||||
expect(scale1.getColor('pig')).to.equal('black');
|
||||
expect(scale2.getColor('pig')).to.equal('black');
|
||||
});
|
||||
it('returns the scale', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
const output = scale.setColor('pig', 'pink');
|
||||
expect(scale).to.equal(output);
|
||||
});
|
||||
});
|
||||
describe('.toFunction()', () => {
|
||||
it('returns a function that wraps getColor', () => {
|
||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
const colorFn = scale.toFunction();
|
||||
expect(scale.getColor('pig')).to.equal(colorFn('pig'));
|
||||
expect(scale.getColor('cat')).to.equal(colorFn('cat'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { it, describe, beforeEach } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import ColorSchemeManager, {
|
||||
getInstance,
|
||||
getScheme,
|
||||
getAllSchemes,
|
||||
getDefaultSchemeName,
|
||||
setDefaultSchemeName,
|
||||
registerScheme,
|
||||
registerMultipleSchemes,
|
||||
} from '../../../src/modules/ColorSchemeManager';
|
||||
|
||||
describe('ColorSchemeManager', () => {
|
||||
beforeEach(() => {
|
||||
const m = getInstance();
|
||||
m.clearScheme();
|
||||
m.registerScheme('test', ['red', 'green', 'blue']);
|
||||
m.registerScheme('test2', ['orange', 'yellow', 'pink']);
|
||||
m.setDefaultSchemeName('test');
|
||||
});
|
||||
it('The class constructor cannot be accessed directly', () => {
|
||||
expect(ColorSchemeManager).to.not.be.a('Function');
|
||||
});
|
||||
describe('static getInstance()', () => {
|
||||
it('returns a singleton instance', () => {
|
||||
const m1 = getInstance();
|
||||
const m2 = getInstance();
|
||||
expect(m1).to.not.equal(undefined);
|
||||
expect(m1).to.equal(m2);
|
||||
});
|
||||
});
|
||||
describe('.getScheme()', () => {
|
||||
it('.getScheme() returns default color scheme', () => {
|
||||
const scheme = getInstance().getScheme();
|
||||
expect(scheme).to.deep.equal(['red', 'green', 'blue']);
|
||||
});
|
||||
it('.getScheme(name) returns color scheme with specified name', () => {
|
||||
const scheme = getInstance().getScheme('test2');
|
||||
expect(scheme).to.deep.equal(['orange', 'yellow', 'pink']);
|
||||
});
|
||||
});
|
||||
describe('.getAllSchemes()', () => {
|
||||
it('returns all registered schemes', () => {
|
||||
const schemes = getInstance().getAllSchemes();
|
||||
expect(schemes).to.deep.equal({
|
||||
test: ['red', 'green', 'blue'],
|
||||
test2: ['orange', 'yellow', 'pink'],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('.getDefaultSchemeName()', () => {
|
||||
it('returns default scheme name', () => {
|
||||
const name = getInstance().getDefaultSchemeName();
|
||||
expect(name).to.equal('test');
|
||||
});
|
||||
});
|
||||
describe('.setDefaultSchemeName()', () => {
|
||||
it('set default scheme name', () => {
|
||||
getInstance().setDefaultSchemeName('test2');
|
||||
const name = getInstance().getDefaultSchemeName();
|
||||
expect(name).to.equal('test2');
|
||||
getInstance().setDefaultSchemeName('test');
|
||||
});
|
||||
it('returns the ColorSchemeManager instance', () => {
|
||||
const instance = getInstance().setDefaultSchemeName('test');
|
||||
expect(instance).to.equal(getInstance());
|
||||
});
|
||||
});
|
||||
describe('.registerScheme(name, colors)', () => {
|
||||
it('sets schemename and color', () => {
|
||||
getInstance().registerScheme('test3', ['cyan', 'magenta']);
|
||||
const scheme = getInstance().getScheme('test3');
|
||||
expect(scheme).to.deep.equal(['cyan', 'magenta']);
|
||||
});
|
||||
it('returns the ColorSchemeManager instance', () => {
|
||||
const instance = getInstance().registerScheme('test3', ['cyan', 'magenta']);
|
||||
expect(instance).to.equal(getInstance());
|
||||
});
|
||||
});
|
||||
describe('.registerMultipleSchemes(object)', () => {
|
||||
it('sets multiple schemes at once', () => {
|
||||
getInstance().registerMultipleSchemes({
|
||||
test4: ['cyan', 'magenta'],
|
||||
test5: ['brown', 'purple'],
|
||||
});
|
||||
const scheme1 = getInstance().getScheme('test4');
|
||||
expect(scheme1).to.deep.equal(['cyan', 'magenta']);
|
||||
const scheme2 = getInstance().getScheme('test5');
|
||||
expect(scheme2).to.deep.equal(['brown', 'purple']);
|
||||
});
|
||||
it('returns the ColorSchemeManager instance', () => {
|
||||
const instance = getInstance().registerMultipleSchemes({
|
||||
test4: ['cyan', 'magenta'],
|
||||
test5: ['brown', 'purple'],
|
||||
});
|
||||
expect(instance).to.equal(getInstance());
|
||||
});
|
||||
});
|
||||
describe('static getScheme()', () => {
|
||||
it('is equivalent to getInstance().getScheme()', () => {
|
||||
expect(getInstance().getScheme()).to.equal(getScheme());
|
||||
});
|
||||
});
|
||||
describe('static getAllSchemes()', () => {
|
||||
it('is equivalent to getInstance().getAllSchemes()', () => {
|
||||
expect(getInstance().getAllSchemes()).to.equal(getAllSchemes());
|
||||
});
|
||||
});
|
||||
describe('static getDefaultSchemeName()', () => {
|
||||
it('is equivalent to getInstance().getDefaultSchemeName()', () => {
|
||||
expect(getInstance().getDefaultSchemeName()).to.equal(getDefaultSchemeName());
|
||||
});
|
||||
});
|
||||
describe('static setDefaultSchemeName()', () => {
|
||||
it('is equivalent to getInstance().setDefaultSchemeName()', () => {
|
||||
setDefaultSchemeName('test2');
|
||||
const name = getInstance().getDefaultSchemeName();
|
||||
expect(name).to.equal('test2');
|
||||
setDefaultSchemeName('test');
|
||||
});
|
||||
});
|
||||
describe('static registerScheme()', () => {
|
||||
it('is equivalent to getInstance().registerScheme()', () => {
|
||||
registerScheme('test3', ['cyan', 'magenta']);
|
||||
const scheme = getInstance().getScheme('test3');
|
||||
expect(scheme).to.deep.equal(['cyan', 'magenta']);
|
||||
});
|
||||
});
|
||||
describe('static registerMultipleSchemes()', () => {
|
||||
it('is equivalent to getInstance().registerMultipleSchemes()', () => {
|
||||
registerMultipleSchemes({
|
||||
test4: ['cyan', 'magenta'],
|
||||
test5: ['brown', 'purple'],
|
||||
});
|
||||
const scheme1 = getInstance().getScheme('test4');
|
||||
expect(scheme1).to.deep.equal(['cyan', 'magenta']);
|
||||
const scheme2 = getInstance().getScheme('test5');
|
||||
expect(scheme2).to.deep.equal(['brown', 'purple']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,21 @@
|
||||
import { it, describe } from 'mocha';
|
||||
import { it, describe, before } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { ALL_COLOR_SCHEMES, getColorFromScheme, hexToRGB } from '../../../src/modules/colors';
|
||||
import { getColorFromScheme, hexToRGB } from '../../../src/modules/colors';
|
||||
import { getInstance } from '../../../src/modules/ColorSchemeManager';
|
||||
import airbnb from '../../../src/modules/colorSchemes/airbnb';
|
||||
import categoricalSchemes from '../../../src/modules/colorSchemes/categorical';
|
||||
|
||||
describe('colors', () => {
|
||||
before(() => {
|
||||
// Register color schemes
|
||||
getInstance()
|
||||
.registerScheme('bnbColors', airbnb.bnbColors)
|
||||
.registerMultipleSchemes(categoricalSchemes)
|
||||
.setDefaultSchemeName('bnbColors');
|
||||
});
|
||||
it('default to bnbColors', () => {
|
||||
const color1 = getColorFromScheme('CA');
|
||||
expect(color1).to.equal(ALL_COLOR_SCHEMES.bnbColors[0]);
|
||||
expect(airbnb.bnbColors).to.include(color1);
|
||||
});
|
||||
it('getColorFromScheme series with same scheme should have the same color', () => {
|
||||
const color1 = getColorFromScheme('CA', 'bnbColors');
|
||||
@@ -14,19 +23,18 @@ describe('colors', () => {
|
||||
const color3 = getColorFromScheme('CA', 'bnbColors');
|
||||
const color4 = getColorFromScheme('NY', 'bnbColors');
|
||||
|
||||
expect(color1).to.equal(ALL_COLOR_SCHEMES.bnbColors[0]);
|
||||
expect(color2).to.equal(ALL_COLOR_SCHEMES.googleCategory20c[0]);
|
||||
expect(color1).to.equal(color3);
|
||||
expect(color4).to.equal(ALL_COLOR_SCHEMES.bnbColors[1]);
|
||||
expect(color1).to.not.equal(color2);
|
||||
expect(color1).to.not.equal(color4);
|
||||
});
|
||||
it('getColorFromScheme forcing colors persists through calls', () => {
|
||||
expect(getColorFromScheme('boys', 'bnbColors', 'blue')).to.equal('blue');
|
||||
expect(getColorFromScheme('boys', 'bnbColors')).to.equal('blue');
|
||||
expect(getColorFromScheme('boys', 'googleCategory20c')).to.equal('blue');
|
||||
expect(getColorFromScheme('boys', 'googleCategory20c')).to.not.equal('blue');
|
||||
|
||||
expect(getColorFromScheme('girls', 'bnbColors', 'pink')).to.equal('pink');
|
||||
expect(getColorFromScheme('girls', 'bnbColors')).to.equal('pink');
|
||||
expect(getColorFromScheme('girls', 'googleCategory20c')).to.equal('pink');
|
||||
expect(getColorFromScheme('girls', 'googleCategory20c')).to.not.equal('pink');
|
||||
});
|
||||
it('getColorFromScheme is not case sensitive', () => {
|
||||
const c1 = getColorFromScheme('girls', 'bnbColors');
|
||||
|
||||
Reference in New Issue
Block a user