You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/09/12 21:10:29 UTC

[incubator-superset] branch master updated: Improve categorical color management (#5815)

This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new f482a6c  Improve categorical color management (#5815)
f482a6c is described below

commit f482a6cf99fe2730193d5a09deb7a4e6f661faff
Author: Krist Wongsuphasawat <kr...@gmail.com>
AuthorDate: Wed Sep 12 14:10:26 2018 -0700

    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
---
 superset/assets/package.json                       |   1 +
 .../explore/components/ColorScheme_spec.jsx        |   4 +-
 .../modules/CategoricalColorNameSpace_spec.js      | 130 +++++
 .../modules/CategoricalColorScale_spec.js          |  96 ++++
 .../javascripts/modules/ColorSchemeManager_spec.js | 141 +++++
 .../spec/javascripts/modules/colors_spec.jsx       |  26 +-
 superset/assets/src/common.js                      |  14 +-
 .../src/dashboard/reducers/getInitialState.js      |   4 +-
 .../components/controls/AnnotationLayer.jsx        |   6 +-
 superset/assets/src/explore/controls.jsx           |   8 +-
 .../src/modules/CategoricalColorNamespace.js       |  60 +++
 .../assets/src/modules/CategoricalColorScale.js    |  64 +++
 superset/assets/src/modules/ColorSchemeManager.js  |  86 ++++
 superset/assets/src/modules/colorSchemes/airbnb.js |  25 +
 .../assets/src/modules/colorSchemes/categorical.js |  42 ++
 superset/assets/src/modules/colorSchemes/lyft.js   |  14 +
 .../{colors.js => colorSchemes/sequential.js}      | 168 +-----
 superset/assets/src/modules/colors.js              | 567 +--------------------
 superset/assets/src/visualizations/chord.jsx       |   7 +-
 .../deckgl/CategoricalDeckGLContainer.jsx          |   9 +-
 superset/assets/src/visualizations/partition.js    |   5 +-
 superset/assets/src/visualizations/rose.js         |  12 +-
 superset/assets/src/visualizations/sankey.js       |   6 +-
 superset/assets/src/visualizations/sunburst.js     |  10 +-
 superset/assets/src/visualizations/treemap.js      |   5 +-
 .../src/visualizations/wordcloud/WordCloud.js      |   6 +-
 26 files changed, 754 insertions(+), 762 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index 667b4cb..11b2b77 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -8,6 +8,7 @@
     "test": "spec"
   },
   "scripts": {
+    "tdd": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/shim.js 'spec/**/*_spec.*' --watch --recursive",
     "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/shim.js 'spec/**/*_spec.*'",
     "test:one": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/shim.js",
     "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --compilers babel-core/register --require spec/helpers/shim.js --require ignore-styles 'spec/**/*_spec.*'",
diff --git a/superset/assets/spec/javascripts/explore/components/ColorScheme_spec.jsx b/superset/assets/spec/javascripts/explore/components/ColorScheme_spec.jsx
index a7d4d66..10e582b 100644
--- a/superset/assets/spec/javascripts/explore/components/ColorScheme_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/ColorScheme_spec.jsx
@@ -7,10 +7,10 @@ import { Creatable } from 'react-select';
 
 import ColorSchemeControl from
   '../../../../src/explore/components/controls/ColorSchemeControl';
-import { ALL_COLOR_SCHEMES } from '../../../../src/modules/colors';
+import { getAllSchemes } from '../../../../src/modules/ColorSchemeManager';
 
 const defaultProps = {
-  options: Object.keys(ALL_COLOR_SCHEMES).map(s => ([s, s])),
+  options: Object.keys(getAllSchemes()).map(s => ([s, s])),
 };
 
 describe('ColorSchemeControl', () => {
diff --git a/superset/assets/spec/javascripts/modules/CategoricalColorNameSpace_spec.js b/superset/assets/spec/javascripts/modules/CategoricalColorNameSpace_spec.js
new file mode 100644
index 0000000..1696dd2
--- /dev/null
+++ b/superset/assets/spec/javascripts/modules/CategoricalColorNameSpace_spec.js
@@ -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);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/modules/CategoricalColorScale_spec.js b/superset/assets/spec/javascripts/modules/CategoricalColorScale_spec.js
new file mode 100644
index 0000000..fc2e2ea
--- /dev/null
+++ b/superset/assets/spec/javascripts/modules/CategoricalColorScale_spec.js
@@ -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'));
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/modules/ColorSchemeManager_spec.js b/superset/assets/spec/javascripts/modules/ColorSchemeManager_spec.js
new file mode 100644
index 0000000..236b1e4
--- /dev/null
+++ b/superset/assets/spec/javascripts/modules/ColorSchemeManager_spec.js
@@ -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']);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/modules/colors_spec.jsx b/superset/assets/spec/javascripts/modules/colors_spec.jsx
index e83b473..be88233 100644
--- a/superset/assets/spec/javascripts/modules/colors_spec.jsx
+++ b/superset/assets/spec/javascripts/modules/colors_spec.jsx
@@ -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');
diff --git a/superset/assets/src/common.js b/superset/assets/src/common.js
index 67ce498..779a169 100644
--- a/superset/assets/src/common.js
+++ b/superset/assets/src/common.js
@@ -1,5 +1,10 @@
 /* eslint-disable global-require */
 import $ from 'jquery';
+import airbnb from './modules/colorSchemes/airbnb';
+import categoricalSchemes from './modules/colorSchemes/categorical';
+import lyft from './modules/colorSchemes/lyft';
+import { getInstance } from './modules/ColorSchemeManager';
+
 // Everything imported in this file ends up in the common entry file
 // be mindful of double-imports
 
@@ -25,8 +30,15 @@ $(document).ready(function () {
   });
 });
 
+// Register color schemes
+getInstance()
+  .registerScheme('bnbColors', airbnb.bnbColors)
+  .registerMultipleSchemes(categoricalSchemes)
+  .registerScheme('lyftColors', lyft.lyftColors)
+  .setDefaultSchemeName('bnbColors');
+
 export function appSetup() {
-  // A set of hacks to allow apps to run within a FAB template
+    // A set of hacks to allow apps to run within a FAB template
   // this allows for the server side generated menus to function
   window.$ = $;
   window.jQuery = $;
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index e913adb..2a4a5e2 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -5,7 +5,6 @@ import { chart } from '../../chart/chartReducer';
 import { initSliceEntities } from './sliceEntities';
 import { getParam } from '../../modules/utils';
 import { applyDefaultFormData } from '../../explore/store';
-import { getColorFromScheme } from '../../modules/colors';
 import findFirstParentContainerId from '../util/findFirstParentContainer';
 import getEmptyLayout from '../util/getEmptyLayout';
 import newComponentFactory from '../util/newComponentFactory';
@@ -19,6 +18,7 @@ import {
   CHART_TYPE,
   ROW_TYPE,
 } from '../util/componentTypes';
+import { getScale } from '../../modules/CategoricalColorNamespace';
 
 export default function(bootstrapData) {
   const { user_id, datasources, common, editMode } = bootstrapData;
@@ -41,7 +41,7 @@ export default function(bootstrapData) {
   if (dashboard.metadata && dashboard.metadata.label_colors) {
     const colorMap = dashboard.metadata.label_colors;
     Object.keys(colorMap).forEach(label => {
-      getColorFromScheme(label, null, colorMap[label]);
+      getScale().setColor(label, colorMap[label]);
     });
   }
 
diff --git a/superset/assets/src/explore/components/controls/AnnotationLayer.jsx b/superset/assets/src/explore/components/controls/AnnotationLayer.jsx
index 812882c..3238f4f 100644
--- a/superset/assets/src/explore/components/controls/AnnotationLayer.jsx
+++ b/superset/assets/src/explore/components/controls/AnnotationLayer.jsx
@@ -20,13 +20,13 @@ import AnnotationTypes, {
   requiresQuery,
 } from '../../../modules/AnnotationTypes';
 
-import { ALL_COLOR_SCHEMES } from '../../../modules/colors';
 import PopoverSection from '../../../components/PopoverSection';
 import ControlHeader from '../ControlHeader';
 import { nonEmpty } from '../../validators';
 import vizTypes from '../../visTypes';
 
 import { t } from '../../../locales';
+import { getScheme } from '../../../modules/ColorSchemeManager';
 
 const AUTOMATIC_COLOR = '';
 
@@ -276,7 +276,7 @@ export default class AnnotationLayer extends React.PureComponent {
         description = t('Select the Annotation Layer you would like to use.');
       } else {
         label = t('Chart');
-        description = `Use a pre defined Superset Chart as a source for annotations and overlays. 
+        description = `Use a pre defined Superset Chart as a source for annotations and overlays.
         'your chart must be one of these visualization types:
         '[${getSupportedSourceTypes(annotationType)
             .map(x => vizTypes[x].label).join(', ')}]'`;
@@ -478,7 +478,7 @@ export default class AnnotationLayer extends React.PureComponent {
 
   renderDisplayConfiguration() {
     const { color, opacity, style, width, showMarkers, hideLine, annotationType } = this.state;
-    const colorScheme = [...ALL_COLOR_SCHEMES[this.props.colorScheme]];
+    const colorScheme = [...getScheme(this.props.colorScheme)];
     if (color && color !== AUTOMATIC_COLOR &&
       !colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) {
       colorScheme.push(color);
diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx
index 5e422ea..2a04c73 100644
--- a/superset/assets/src/explore/controls.jsx
+++ b/superset/assets/src/explore/controls.jsx
@@ -45,11 +45,15 @@ import {
   mainMetric,
 } from '../modules/utils';
 import * as v from './validators';
-import { colorPrimary, ALL_COLOR_SCHEMES, spectrums } from '../modules/colors';
+import { colorPrimary } from '../modules/colors';
 import { defaultViewport } from '../modules/geo';
 import ColumnOption from '../components/ColumnOption';
 import OptionDescription from '../components/OptionDescription';
 import { t } from '../locales';
+import { getAllSchemes } from '../modules/ColorSchemeManager';
+import sequentialSchemes from '../modules/colorSchemes/sequential';
+
+const ALL_COLOR_SCHEMES = getAllSchemes();
 
 const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format';
 
@@ -371,7 +375,7 @@ export const controls = {
     clearable: false,
     description: '',
     renderTrigger: true,
-    schemes: spectrums,
+    schemes: sequentialSchemes,
     isLinear: true,
   },
 
diff --git a/superset/assets/src/modules/CategoricalColorNamespace.js b/superset/assets/src/modules/CategoricalColorNamespace.js
new file mode 100644
index 0000000..d022bb2
--- /dev/null
+++ b/superset/assets/src/modules/CategoricalColorNamespace.js
@@ -0,0 +1,60 @@
+import CategoricalColorScale from './CategoricalColorScale';
+import { getScheme, getDefaultSchemeName } from './ColorSchemeManager';
+
+class CategoricalColorNamespace {
+  constructor(name) {
+    this.name = name;
+    this.scales = {};
+    this.forcedItems = {};
+  }
+
+  getScale(schemeName) {
+    const name = schemeName || getDefaultSchemeName();
+    const scale = this.scales[name];
+    if (scale) {
+      return scale;
+    }
+    const newScale = new CategoricalColorScale(
+      getScheme(name),
+      this.forcedItems,
+    );
+    this.scales[name] = newScale;
+    return newScale;
+  }
+
+  /**
+   * Enforce specific color for given value
+   * This will apply across all color scales
+   * in this namespace.
+   * @param {*} value value
+   * @param {*} forcedColor color
+   */
+  setColor(value, forcedColor) {
+    this.forcedItems[value] = forcedColor;
+    return this;
+  }
+}
+
+const namespaces = {};
+export const DEFAULT_NAMESPACE = 'GLOBAL';
+
+export function getNamespace(name = DEFAULT_NAMESPACE) {
+  const instance = namespaces[name];
+  if (instance) {
+    return instance;
+  }
+  const newInstance = new CategoricalColorNamespace(name);
+  namespaces[name] = newInstance;
+  return newInstance;
+}
+
+export function getColor(value, scheme, namespace) {
+  return getNamespace(namespace)
+    .getScale(scheme)
+    .getColor(value);
+}
+
+export function getScale(scheme, namespace) {
+  return getNamespace(namespace)
+    .getScale(scheme);
+}
diff --git a/superset/assets/src/modules/CategoricalColorScale.js b/superset/assets/src/modules/CategoricalColorScale.js
new file mode 100644
index 0000000..eab70d2
--- /dev/null
+++ b/superset/assets/src/modules/CategoricalColorScale.js
@@ -0,0 +1,64 @@
+import { TIME_SHIFT_PATTERN } from '../utils/common';
+
+export function cleanValue(value) {
+  // for superset series that should have the same color
+  return String(value).trim()
+    .toLowerCase()
+    .split(', ')
+    .filter(k => !TIME_SHIFT_PATTERN.test(k))
+    .join(', ');
+}
+
+export default class CategoricalColorScale {
+  /**
+   * Constructor
+   * @param {*} colors an array of colors
+   * @param {*} parentForcedColors optional parameter that comes from parent
+   * (usually CategoricalColorNamespace) and supersede this.forcedColors
+   */
+  constructor(colors, parentForcedColors) {
+    this.colors = colors;
+    this.parentForcedColors = parentForcedColors;
+    this.forcedColors = {};
+    this.seen = {};
+    this.fn = value => this.getColor(value);
+  }
+
+  getColor(value) {
+    const cleanedValue = cleanValue(value);
+
+    const parentColor = this.parentForcedColors && this.parentForcedColors[cleanedValue];
+    if (parentColor) {
+      return parentColor;
+    }
+
+    const forcedColor = this.forcedColors[cleanedValue];
+    if (forcedColor) {
+      return forcedColor;
+    }
+
+    const seenColor = this.seen[cleanedValue];
+    const length = this.colors.length;
+    if (seenColor !== undefined) {
+      return this.colors[seenColor % length];
+    }
+
+    const index = Object.keys(this.seen).length;
+    this.seen[cleanedValue] = index;
+    return this.colors[index % length];
+  }
+
+  /**
+   * Enforce specific color for given value
+   * @param {*} value value
+   * @param {*} forcedColor forcedColor
+   */
+  setColor(value, forcedColor) {
+    this.forcedColors[value] = forcedColor;
+    return this;
+  }
+
+  toFunction() {
+    return this.fn;
+  }
+}
diff --git a/superset/assets/src/modules/ColorSchemeManager.js b/superset/assets/src/modules/ColorSchemeManager.js
new file mode 100644
index 0000000..9d21d26
--- /dev/null
+++ b/superset/assets/src/modules/ColorSchemeManager.js
@@ -0,0 +1,86 @@
+class ColorSchemeManager {
+  constructor() {
+    this.schemes = {};
+    this.defaultSchemeName = undefined;
+  }
+
+  clearScheme() {
+    this.schemes = {};
+    return this;
+  }
+
+  getScheme(schemeName) {
+    return this.schemes[schemeName || this.defaultSchemeName];
+  }
+
+  getAllSchemes() {
+    return this.schemes;
+  }
+
+  getDefaultSchemeName() {
+    return this.defaultSchemeName;
+  }
+
+  setDefaultSchemeName(schemeName) {
+    this.defaultSchemeName = schemeName;
+    return this;
+  }
+
+  registerScheme(schemeName, colors) {
+    this.schemes[schemeName] = colors;
+    // If there is no default, set as default
+    if (!this.defaultSchemeName) {
+      this.defaultSchemeName = schemeName;
+    }
+    return this;
+  }
+
+  registerMultipleSchemes(multipleSchemes) {
+    Object.assign(this.schemes, multipleSchemes);
+    // If there is no default, set the first scheme as default
+    const keys = Object.keys(multipleSchemes);
+    if (!this.defaultSchemeName && keys.length > 0) {
+      this.defaultSchemeName = keys[0];
+    }
+    return this;
+  }
+}
+
+let singleton;
+
+export function getInstance() {
+  if (!singleton) {
+    singleton = new ColorSchemeManager();
+  }
+  return singleton;
+}
+
+const staticFunctions = Object.getOwnPropertyNames(ColorSchemeManager.prototype)
+  .filter(fn => fn !== 'constructor')
+  .reduce((all, fn) => {
+    const functions = all;
+    functions[fn] = function (...args) {
+      return getInstance()[fn](...args);
+    };
+    return functions;
+  }, { getInstance });
+
+const {
+  clearScheme,
+  getScheme,
+  getAllSchemes,
+  getDefaultSchemeName,
+  setDefaultSchemeName,
+  registerScheme,
+  registerMultipleSchemes,
+} = staticFunctions;
+
+export {
+  clearScheme,
+  getScheme,
+  getAllSchemes,
+  getDefaultSchemeName,
+  setDefaultSchemeName,
+  registerScheme,
+  registerMultipleSchemes,
+};
diff --git a/superset/assets/src/modules/colorSchemes/airbnb.js b/superset/assets/src/modules/colorSchemes/airbnb.js
new file mode 100644
index 0000000..d26a923
--- /dev/null
+++ b/superset/assets/src/modules/colorSchemes/airbnb.js
@@ -0,0 +1,25 @@
+export default {
+  bnbColors: [
+    '#ff5a5f', // rausch
+    '#7b0051', // hackb
+    '#007A87', // kazan
+    '#00d1c1', // babu
+    '#8ce071', // lima
+    '#ffb400', // beach
+    '#b4a76c', // barol
+    '#ff8083',
+    '#cc0086',
+    '#00a1b3',
+    '#00ffeb',
+    '#bbedab',
+    '#ffd266',
+    '#cbc29a',
+    '#ff3339',
+    '#ff1ab1',
+    '#005c66',
+    '#00b3a5',
+    '#55d12e',
+    '#b37e00',
+    '#988b4e',
+  ],
+};
diff --git a/superset/assets/src/modules/colorSchemes/categorical.js b/superset/assets/src/modules/colorSchemes/categorical.js
new file mode 100644
index 0000000..946d14a
--- /dev/null
+++ b/superset/assets/src/modules/colorSchemes/categorical.js
@@ -0,0 +1,42 @@
+import d3 from 'd3';
+
+export default {
+  d3Category10: d3.scale.category10().range(),
+  d3Category20: d3.scale.category20().range(),
+  d3Category20b: d3.scale.category20b().range(),
+  d3Category20c: d3.scale.category20c().range(),
+  googleCategory10c: [
+    '#3366cc',
+    '#dc3912',
+    '#ff9900',
+    '#109618',
+    '#990099',
+    '#0099c6',
+    '#dd4477',
+    '#66aa00',
+    '#b82e2e',
+    '#316395',
+  ],
+  googleCategory20c: [
+    '#3366cc',
+    '#dc3912',
+    '#ff9900',
+    '#109618',
+    '#990099',
+    '#0099c6',
+    '#dd4477',
+    '#66aa00',
+    '#b82e2e',
+    '#316395',
+    '#994499',
+    '#22aa99',
+    '#aaaa11',
+    '#6633cc',
+    '#e67300',
+    '#8b0707',
+    '#651067',
+    '#329262',
+    '#5574a6',
+    '#3b3eac',
+  ],
+};
diff --git a/superset/assets/src/modules/colorSchemes/lyft.js b/superset/assets/src/modules/colorSchemes/lyft.js
new file mode 100644
index 0000000..cd94121
--- /dev/null
+++ b/superset/assets/src/modules/colorSchemes/lyft.js
@@ -0,0 +1,14 @@
+export default {
+  lyftColors: [
+    '#EA0B8C',
+    '#6C838E',
+    '#29ABE2',
+    '#33D9C1',
+    '#9DACB9',
+    '#7560AA',
+    '#2D5584',
+    '#831C4A',
+    '#333D47',
+    '#AC2077',
+  ],
+};
diff --git a/superset/assets/src/modules/colors.js b/superset/assets/src/modules/colorSchemes/sequential.js
similarity index 60%
copy from superset/assets/src/modules/colors.js
copy to superset/assets/src/modules/colorSchemes/sequential.js
index 1cb9eed..6970ed4 100644
--- a/superset/assets/src/modules/colors.js
+++ b/superset/assets/src/modules/colorSchemes/sequential.js
@@ -1,97 +1,4 @@
-import d3 from 'd3';
-import { TIME_SHIFT_PATTERN } from '../utils/common';
-
-export const brandColor = '#00A699';
-export const colorPrimary = { r: 0, g: 122, b: 135, a: 1 };
-
-// Color related utility functions go in this object
-export const bnbColors = [
-  '#ff5a5f', // rausch
-  '#7b0051', // hackb
-  '#007A87', // kazan
-  '#00d1c1', // babu
-  '#8ce071', // lima
-  '#ffb400', // beach
-  '#b4a76c', // barol
-  '#ff8083',
-  '#cc0086',
-  '#00a1b3',
-  '#00ffeb',
-  '#bbedab',
-  '#ffd266',
-  '#cbc29a',
-  '#ff3339',
-  '#ff1ab1',
-  '#005c66',
-  '#00b3a5',
-  '#55d12e',
-  '#b37e00',
-  '#988b4e',
-];
-
-export const lyftColors = [
-  '#EA0B8C',
-  '#6C838E',
-  '#29ABE2',
-  '#33D9C1',
-  '#9DACB9',
-  '#7560AA',
-  '#2D5584',
-  '#831C4A',
-  '#333D47',
-  '#AC2077',
-];
-
-const d3Category10 = d3.scale.category10().range();
-const d3Category20 = d3.scale.category20().range();
-const d3Category20b = d3.scale.category20b().range();
-const d3Category20c = d3.scale.category20c().range();
-const googleCategory10c = [
-  '#3366cc',
-  '#dc3912',
-  '#ff9900',
-  '#109618',
-  '#990099',
-  '#0099c6',
-  '#dd4477',
-  '#66aa00',
-  '#b82e2e',
-  '#316395',
-];
-const googleCategory20c = [
-  '#3366cc',
-  '#dc3912',
-  '#ff9900',
-  '#109618',
-  '#990099',
-  '#0099c6',
-  '#dd4477',
-  '#66aa00',
-  '#b82e2e',
-  '#316395',
-  '#994499',
-  '#22aa99',
-  '#aaaa11',
-  '#6633cc',
-  '#e67300',
-  '#8b0707',
-  '#651067',
-  '#329262',
-  '#5574a6',
-  '#3b3eac',
-];
-export const ALL_COLOR_SCHEMES = {
-  bnbColors,
-  d3Category10,
-  d3Category20,
-  d3Category20b,
-  d3Category20c,
-  googleCategory10c,
-  googleCategory20c,
-  lyftColors,
-};
-
-export const spectrums = {
+export default {
   blue_white_yellow: [
     '#00d1c1',
     'white',
@@ -524,76 +431,3 @@ export const spectrums = {
     '#800026',
   ],
 };
-
-export function hexToRGB(hex, alpha = 255) {
-  if (!hex) {
-    return [0, 0, 0, alpha];
-  }
-  const r = parseInt(hex.slice(1, 3), 16);
-  const g = parseInt(hex.slice(3, 5), 16);
-  const b = parseInt(hex.slice(5, 7), 16);
-  return [r, g, b, alpha];
-}
-
-/**
- * Get a color from a scheme specific palette (scheme)
- * The function cycles through the palette while memoizing labels
- * association to colors. If the function is called twice with the
- * same string, it will return the same color.
- *
- * @param {string} s - The label for which we want to get a color
- * @param {string} scheme - The palette name, or "scheme"
- * @param {string} forcedColor - A color that the caller wants to
- forcibly associate to a label.
- */
-export const getColorFromScheme = (function () {
-  const seen = {};
-  const forcedColors = {};
-  return function (s, scheme, forcedColor) {
-    if (!s) {
-      return;
-    }
-    const selectedScheme = scheme ? ALL_COLOR_SCHEMES[scheme] : ALL_COLOR_SCHEMES.bnbColors;
-    let stringifyS = String(s).toLowerCase();
-    // next line is for superset series that should have the same color
-    stringifyS = stringifyS.split(', ').filter(k => !TIME_SHIFT_PATTERN.test(k)).join(', ');
-
-    if (forcedColor && !forcedColors[stringifyS]) {
-      forcedColors[stringifyS] = forcedColor;
-    }
-    if (forcedColors[stringifyS]) {
-      return forcedColors[stringifyS];
-    }
-
-    if (seen[selectedScheme] === undefined) {
-      seen[selectedScheme] = {};
-    }
-    if (seen[selectedScheme][stringifyS] === undefined) {
-      seen[selectedScheme][stringifyS] = Object.keys(seen[selectedScheme]).length;
-    }
-    /* eslint consistent-return: 0 */
-    return selectedScheme[seen[selectedScheme][stringifyS] % selectedScheme.length];
-  };
-}());
-
-export const colorScalerFactory = function (colors, data, accessor, extents, outputRGBA = false) {
-  // Returns a linear scaler our of an array of color
-  if (!Array.isArray(colors)) {
-    /* eslint no-param-reassign: 0 */
-    colors = spectrums[colors];
-  }
-  let ext = [0, 1];
-  if (extents) {
-    ext = extents;
-  }
-  if (data) {
-    ext = d3.extent(data, accessor);
-  }
-  const chunkSize = (ext[1] - ext[0]) / (colors.length - 1);
-  const points = colors.map((col, i) => ext[0] + (i * chunkSize));
-  const scaler = d3.scale.linear().domain(points).range(colors).clamp(true);
-  if (outputRGBA) {
-    return v => hexToRGB(scaler(v));
-  }
-  return scaler;
-};
diff --git a/superset/assets/src/modules/colors.js b/superset/assets/src/modules/colors.js
index 1cb9eed..43025dc 100644
--- a/superset/assets/src/modules/colors.js
+++ b/superset/assets/src/modules/colors.js
@@ -1,529 +1,13 @@
 import d3 from 'd3';
-import { TIME_SHIFT_PATTERN } from '../utils/common';
+import { getScale } from './CategoricalColorNamespace';
+import sequentialSchemes from './colorSchemes/sequential';
+import airbnb from './colorSchemes/airbnb';
+import lyft from './colorSchemes/lyft';
 
 export const brandColor = '#00A699';
 export const colorPrimary = { r: 0, g: 122, b: 135, a: 1 };
-
-// Color related utility functions go in this object
-export const bnbColors = [
-  '#ff5a5f', // rausch
-  '#7b0051', // hackb
-  '#007A87', // kazan
-  '#00d1c1', // babu
-  '#8ce071', // lima
-  '#ffb400', // beach
-  '#b4a76c', // barol
-  '#ff8083',
-  '#cc0086',
-  '#00a1b3',
-  '#00ffeb',
-  '#bbedab',
-  '#ffd266',
-  '#cbc29a',
-  '#ff3339',
-  '#ff1ab1',
-  '#005c66',
-  '#00b3a5',
-  '#55d12e',
-  '#b37e00',
-  '#988b4e',
-];
-
-export const lyftColors = [
-  '#EA0B8C',
-  '#6C838E',
-  '#29ABE2',
-  '#33D9C1',
-  '#9DACB9',
-  '#7560AA',
-  '#2D5584',
-  '#831C4A',
-  '#333D47',
-  '#AC2077',
-];
-
-const d3Category10 = d3.scale.category10().range();
-const d3Category20 = d3.scale.category20().range();
-const d3Category20b = d3.scale.category20b().range();
-const d3Category20c = d3.scale.category20c().range();
-const googleCategory10c = [
-  '#3366cc',
-  '#dc3912',
-  '#ff9900',
-  '#109618',
-  '#990099',
-  '#0099c6',
-  '#dd4477',
-  '#66aa00',
-  '#b82e2e',
-  '#316395',
-];
-const googleCategory20c = [
-  '#3366cc',
-  '#dc3912',
-  '#ff9900',
-  '#109618',
-  '#990099',
-  '#0099c6',
-  '#dd4477',
-  '#66aa00',
-  '#b82e2e',
-  '#316395',
-  '#994499',
-  '#22aa99',
-  '#aaaa11',
-  '#6633cc',
-  '#e67300',
-  '#8b0707',
-  '#651067',
-  '#329262',
-  '#5574a6',
-  '#3b3eac',
-];
-export const ALL_COLOR_SCHEMES = {
-  bnbColors,
-  d3Category10,
-  d3Category20,
-  d3Category20b,
-  d3Category20c,
-  googleCategory10c,
-  googleCategory20c,
-  lyftColors,
-};
-
-export const spectrums = {
-  blue_white_yellow: [
-    '#00d1c1',
-    'white',
-    '#ffb400',
-  ],
-  fire: [
-    'white',
-    'yellow',
-    'red',
-    'black',
-  ],
-  white_black: [
-    'white',
-    'black',
-  ],
-  black_white: [
-    'black',
-    'white',
-  ],
-  dark_blue: [
-    '#EBF5F8',
-    '#6BB1CC',
-    '#357E9B',
-    '#1B4150',
-    '#092935',
-  ],
-  pink_grey: [
-    '#E70B81',
-    '#FAFAFA',
-    '#666666',
-  ],
-  greens: [
-    '#ffffcc',
-    '#78c679',
-    '#006837',
-  ],
-  purples: [
-    '#f2f0f7',
-    '#9e9ac8',
-    '#54278f',
-  ],
-  oranges: [
-    '#fef0d9',
-    '#fc8d59',
-    '#b30000',
-  ],
-  red_yellow_blue: [
-    '#d7191c',
-    '#fdae61',
-    '#ffffbf',
-    '#abd9e9',
-    '#2c7bb6',
-  ],
-  brown_white_green: [
-    '#a6611a',
-    '#dfc27d',
-    '#f5f5f5',
-    '#80cdc1',
-    '#018571',
-  ],
-  purple_white_green: [
-    '#7b3294',
-    '#c2a5cf',
-    '#f7f7f7',
-    '#a6dba0',
-    '#008837',
-  ],
-  schemeBrBG: [
-    '#543005',
-    '#8c510a',
-    '#bf812d',
-    '#dfc27d',
-    '#f6e8c3',
-    '#c7eae5',
-    '#80cdc1',
-    '#35978f',
-    '#01665e',
-    '#003c30',
-  ],
-  schemePRGn: [
-    '#40004b',
-    '#762a83',
-    '#9970ab',
-    '#c2a5cf',
-    '#e7d4e8',
-    '#d9f0d3',
-    '#a6dba0',
-    '#5aae61',
-    '#1b7837',
-    '#00441b',
-  ],
-  schemePiYG: [
-    '#8e0152',
-    '#c51b7d',
-    '#de77ae',
-    '#f1b6da',
-    '#fde0ef',
-    '#e6f5d0',
-    '#b8e186',
-    '#7fbc41',
-    '#4d9221',
-    '#276419',
-  ],
-  schemePuOr: [
-    '#2d004b',
-    '#542788',
-    '#8073ac',
-    '#b2abd2',
-    '#d8daeb',
-    '#fee0b6',
-    '#fdb863',
-    '#e08214',
-    '#b35806',
-    '#7f3b08',
-  ],
-  schemeRdBu: [
-    '#67001f',
-    '#b2182b',
-    '#d6604d',
-    '#f4a582',
-    '#fddbc7',
-    '#d1e5f0',
-    '#92c5de',
-    '#4393c3',
-    '#2166ac',
-    '#053061',
-  ],
-  schemeRdGy: [
-    '#67001f',
-    '#b2182b',
-    '#d6604d',
-    '#f4a582',
-    '#fddbc7',
-    '#e0e0e0',
-    '#bababa',
-    '#878787',
-    '#4d4d4d',
-    '#1a1a1a',
-  ],
-  schemeRdYlBu: [
-    '#a50026',
-    '#d73027',
-    '#f46d43',
-    '#fdae61',
-    '#fee090',
-    '#e0f3f8',
-    '#abd9e9',
-    '#74add1',
-    '#4575b4',
-    '#313695',
-  ],
-  schemeRdYlGn: [
-    '#a50026',
-    '#d73027',
-    '#f46d43',
-    '#fdae61',
-    '#fee08b',
-    '#d9ef8b',
-    '#a6d96a',
-    '#66bd63',
-    '#1a9850',
-    '#006837',
-  ],
-  schemeSpectral: [
-    '#9e0142',
-    '#d53e4f',
-    '#f46d43',
-    '#fdae61',
-    '#fee08b',
-    '#e6f598',
-    '#abdda4',
-    '#66c2a5',
-    '#3288bd',
-    '#5e4fa2',
-  ],
-  schemeBlues: [
-    '#b5d4e9',
-    '#93c3df',
-    '#6daed5',
-    '#4b97c9',
-    '#2f7ebc',
-    '#1864aa',
-    '#0a4a90',
-    '#08306b',
-  ],
-  schemeGreens: [
-    '#b7e2b1',
-    '#97d494',
-    '#73c378',
-    '#4daf62',
-    '#2f984f',
-    '#157f3b',
-    '#036429',
-    '#00441b',
-  ],
-  schemeGrays: [
-    '#cecece',
-    '#b4b4b4',
-    '#979797',
-    '#7a7a7a',
-    '#5f5f5f',
-    '#404040',
-    '#1e1e1e',
-    '#000000',
-  ],
-  schemeOranges: [
-    '#fdc28c',
-    '#fda762',
-    '#fb8d3d',
-    '#f2701d',
-    '#e25609',
-    '#c44103',
-    '#9f3303',
-    '#7f2704',
-  ],
-  schemePurples: [
-    '#cecee5',
-    '#b6b5d8',
-    '#9e9bc9',
-    '#8782bc',
-    '#7363ac',
-    '#61409b',
-    '#501f8c',
-    '#3f007d',
-  ],
-  schemeReds: [
-    '#fcaa8e',
-    '#fc8a6b',
-    '#f9694c',
-    '#ef4533',
-    '#d92723',
-    '#bb151a',
-    '#970b13',
-    '#67000d',
-  ],
-  schemeViridis: [
-    '#482475',
-    '#414487',
-    '#355f8d',
-    '#2a788e',
-    '#21918c',
-    '#22a884',
-    '#44bf70',
-    '#7ad151',
-    '#bddf26',
-    '#fde725',
-  ],
-  schemeInferno: [
-    '#160b39',
-    '#420a68',
-    '#6a176e',
-    '#932667',
-    '#bc3754',
-    '#dd513a',
-    '#f37819',
-    '#fca50a',
-    '#f6d746',
-    '#fcffa4',
-  ],
-  schemeMagma: [
-    '#140e36',
-    '#3b0f70',
-    '#641a80',
-    '#8c2981',
-    '#b73779',
-    '#de4968',
-    '#f7705c',
-    '#fe9f6d',
-    '#fecf92',
-    '#fcfdbf',
-  ],
-  schemeWarm: [
-    '#963db3',
-    '#bf3caf',
-    '#e4419d',
-    '#fe4b83',
-    '#ff5e63',
-    '#ff7847',
-    '#fb9633',
-    '#e2b72f',
-    '#c6d63c',
-    '#aff05b',
-  ],
-  schemeCool: [
-    '#6054c8',
-    '#4c6edb',
-    '#368ce1',
-    '#23abd8',
-    '#1ac7c2',
-    '#1ddfa3',
-    '#30ef82',
-    '#52f667',
-    '#7ff658',
-    '#aff05b',
-  ],
-  schemeCubehelixDefault: [
-    '#1a1530',
-    '#163d4e',
-    '#1f6642',
-    '#54792f',
-    '#a07949',
-    '#d07e93',
-    '#cf9cda',
-    '#c1caf3',
-    '#d2eeef',
-    '#ffffff',
-  ],
-  schemeBuGn: [
-    '#b7e4da',
-    '#8fd3c1',
-    '#68c2a3',
-    '#49b17f',
-    '#2f9959',
-    '#157f3c',
-    '#036429',
-    '#00441b',
-  ],
-  schemeBuPu: [
-    '#b2cae1',
-    '#9cb3d5',
-    '#8f95c6',
-    '#8c74b5',
-    '#8952a5',
-    '#852d8f',
-    '#730f71',
-    '#4d004b',
-  ],
-  schemeGnBu: [
-    '#bde5bf',
-    '#9ed9bb',
-    '#7bcbc4',
-    '#58b7cd',
-    '#399cc6',
-    '#1d7eb7',
-    '#0b60a1',
-    '#084081',
-  ],
-  schemeOrRd: [
-    '#fdca94',
-    '#fdb07a',
-    '#fa8e5d',
-    '#f16c49',
-    '#e04530',
-    '#c81d13',
-    '#a70403',
-    '#7f0000',
-  ],
-  schemePuBuGn: [
-    '#bec9e2',
-    '#98b9d9',
-    '#69a8cf',
-    '#4096c0',
-    '#19879f',
-    '#037877',
-    '#016353',
-    '#014636',
-  ],
-  schemePuBu: [
-    '#bfc9e2',
-    '#9bb9d9',
-    '#72a8cf',
-    '#4394c3',
-    '#1a7db6',
-    '#0667a1',
-    '#045281',
-    '#023858',
-  ],
-  schemePuRd: [
-    '#d0aad2',
-    '#d08ac2',
-    '#dd63ae',
-    '#e33890',
-    '#d71c6c',
-    '#b70b4f',
-    '#8f023a',
-    '#67001f',
-  ],
-  schemeRdPu: [
-    '#fbb5bc',
-    '#f993b0',
-    '#f369a3',
-    '#e03e98',
-    '#c01788',
-    '#99037c',
-    '#700174',
-    '#49006a',
-  ],
-  schemeYlGnBu: [
-    '#d5eeb3',
-    '#a9ddb7',
-    '#73c9bd',
-    '#45b4c2',
-    '#2897bf',
-    '#2073b2',
-    '#234ea0',
-    '#1c3185',
-    '#081d58',
-  ],
-  schemeYlGn: [
-    '#e4f4ac',
-    '#c7e89b',
-    '#a2d88a',
-    '#78c578',
-    '#4eaf63',
-    '#2f944e',
-    '#15793f',
-    '#036034',
-    '#004529',
-  ],
-  schemeYlOrBr: [
-    '#feeaa1',
-    '#fed676',
-    '#feba4a',
-    '#fb992c',
-    '#ee7918',
-    '#d85b0a',
-    '#b74304',
-    '#8f3204',
-    '#662506',
-  ],
-  schemeYlOrRd: [
-    '#fee087',
-    '#fec965',
-    '#feab4b',
-    '#fd893c',
-    '#fa5c2e',
-    '#ec3023',
-    '#d31121',
-    '#af0225',
-    '#800026',
-  ],
-};
+export const bnbColors = airbnb.bnbColors;
+export const lyftColors = lyft.lyftColors;
 
 export function hexToRGB(hex, alpha = 255) {
   if (!hex) {
@@ -546,41 +30,20 @@ export function hexToRGB(hex, alpha = 255) {
  * @param {string} forcedColor - A color that the caller wants to
  forcibly associate to a label.
  */
-export const getColorFromScheme = (function () {
-  const seen = {};
-  const forcedColors = {};
-  return function (s, scheme, forcedColor) {
-    if (!s) {
-      return;
-    }
-    const selectedScheme = scheme ? ALL_COLOR_SCHEMES[scheme] : ALL_COLOR_SCHEMES.bnbColors;
-    let stringifyS = String(s).toLowerCase();
-    // next line is for superset series that should have the same color
-    stringifyS = stringifyS.split(', ').filter(k => !TIME_SHIFT_PATTERN.test(k)).join(', ');
-
-    if (forcedColor && !forcedColors[stringifyS]) {
-      forcedColors[stringifyS] = forcedColor;
-    }
-    if (forcedColors[stringifyS]) {
-      return forcedColors[stringifyS];
-    }
-
-    if (seen[selectedScheme] === undefined) {
-      seen[selectedScheme] = {};
-    }
-    if (seen[selectedScheme][stringifyS] === undefined) {
-      seen[selectedScheme][stringifyS] = Object.keys(seen[selectedScheme]).length;
-    }
-    /* eslint consistent-return: 0 */
-    return selectedScheme[seen[selectedScheme][stringifyS] % selectedScheme.length];
-  };
-}());
+export function getColorFromScheme(value, schemeName, forcedColor) {
+  const scale = getScale(schemeName);
+  if (forcedColor) {
+    scale.setColor(value, forcedColor);
+    return forcedColor;
+  }
+  return scale.getColor(value);
+}
 
 export const colorScalerFactory = function (colors, data, accessor, extents, outputRGBA = false) {
   // Returns a linear scaler our of an array of color
   if (!Array.isArray(colors)) {
     /* eslint no-param-reassign: 0 */
-    colors = spectrums[colors];
+    colors = sequentialSchemes[colors];
   }
   let ext = [0, 1];
   if (extents) {
diff --git a/superset/assets/src/visualizations/chord.jsx b/superset/assets/src/visualizations/chord.jsx
index 2a7cdf1..672a31e 100644
--- a/superset/assets/src/visualizations/chord.jsx
+++ b/superset/assets/src/visualizations/chord.jsx
@@ -1,7 +1,7 @@
 /* eslint-disable no-param-reassign */
 import d3 from 'd3';
 import PropTypes from 'prop-types';
-import { getColorFromScheme } from '../modules/colors';
+import { getScale } from '../modules/CategoricalColorNamespace';
 import './chord.css';
 
 const propTypes = {
@@ -31,6 +31,7 @@ function chordVis(element, props) {
   const div = d3.select(element);
   const { nodes, matrix } = data;
   const f = d3.format(numberFormat);
+  const colorFn = getScale(colorScheme).toFunction();
 
   const outerRadius = Math.min(width, height) / 2 - 10;
   const innerRadius = outerRadius - 24;
@@ -78,7 +79,7 @@ function chordVis(element, props) {
   const groupPath = group.append('path')
     .attr('id', (d, i) => 'group' + i)
     .attr('d', arc)
-    .style('fill', (d, i) => getColorFromScheme(nodes[i], colorScheme));
+    .style('fill', (d, i) => colorFn(nodes[i]));
 
   // Add a text label.
   const groupText = group.append('text')
@@ -102,7 +103,7 @@ function chordVis(element, props) {
     .on('mouseover', (d) => {
       chord.classed('fade', p => p !== d);
     })
-    .style('fill', d => getColorFromScheme(nodes[d.source.index], colorScheme))
+    .style('fill', d => colorFn(nodes[d.source.index]))
     .attr('d', path);
 
   // Add an elaborate mouseover title for each chord.
diff --git a/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx b/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx
index ff5ab09..0976ec0 100644
--- a/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx
+++ b/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx
@@ -6,19 +6,21 @@ import PropTypes from 'prop-types';
 import AnimatableDeckGLContainer from './AnimatableDeckGLContainer';
 import Legend from '../Legend';
 
-import { getColorFromScheme, hexToRGB } from '../../modules/colors';
+import { getScale } from '../../modules/CategoricalColorNamespace';
+import { hexToRGB } from '../../modules/colors';
 import { getPlaySliderParams } from '../../modules/time';
 import sandboxedEval from '../../modules/sandbox';
 
 function getCategories(fd, data) {
   const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
   const fixedColor = [c.r, c.g, c.b, 255 * c.a];
+  const colorFn = getScale(fd.color_scheme).toFunction();
   const categories = {};
   data.forEach((d) => {
     if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
       let color;
       if (fd.dimension) {
-        color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
+        color = hexToRGB(colorFn(d.cat_color), c.a * 255);
       } else {
         color = fixedColor;
       }
@@ -98,10 +100,11 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
   }
   addColor(data, fd) {
     const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
+    const colorFn = getScale(fd.color_scheme).toFunction();
     return data.map((d) => {
       let color;
       if (fd.dimension) {
-        color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
+        color = hexToRGB(colorFn(d.cat_color), c.a * 255);
         return { ...d, color };
       }
       return d;
diff --git a/superset/assets/src/visualizations/partition.js b/superset/assets/src/visualizations/partition.js
index fa7bbad..e70a1ee 100644
--- a/superset/assets/src/visualizations/partition.js
+++ b/superset/assets/src/visualizations/partition.js
@@ -2,8 +2,8 @@
 import d3 from 'd3';
 import PropTypes from 'prop-types';
 import { hierarchy } from 'd3-hierarchy';
+import { getScale } from '../modules/CategoricalColorNamespace';
 import { d3TimeFormatPreset } from '../modules/utils';
-import { getColorFromScheme } from '../modules/colors';
 import './partition.css';
 
 // Compute dx, dy, x, y for each node and
@@ -97,6 +97,7 @@ function Icicle(element, props) {
   const hasTime = ['adv_anal', 'time_series'].indexOf(chartType) >= 0;
   const format = d3.format(numberFormat);
   const timeFormat = d3TimeFormatPreset(dateTimeFormat);
+  const colorFn = getScale(colorScheme).toFunction();
 
   div.selectAll('*').remove();
   const tooltip = div
@@ -363,7 +364,7 @@ function Icicle(element, props) {
     // Apply color scheme
     g.selectAll('rect')
       .style('fill', (d) => {
-        d.color = getColorFromScheme(d.name, colorScheme);
+        d.color = colorFn(d.name);
         return d.color;
       });
   }
diff --git a/superset/assets/src/visualizations/rose.js b/superset/assets/src/visualizations/rose.js
index 875e748..62df302 100644
--- a/superset/assets/src/visualizations/rose.js
+++ b/superset/assets/src/visualizations/rose.js
@@ -2,8 +2,8 @@
 import d3 from 'd3';
 import PropTypes from 'prop-types';
 import nv from 'nvd3';
+import { getScale } from '../modules/CategoricalColorNamespace';
 import { d3TimeFormatPreset } from '../modules/utils';
-import { getColorFromScheme } from '../modules/colors';
 import './rose.css';
 
 const propTypes = {
@@ -62,6 +62,7 @@ function Rose(element, props) {
   const numGroups = datum[times[0]].length;
   const format = d3.format(numberFormat);
   const timeFormat = d3TimeFormatPreset(dateTimeFormat);
+  const colorFn = getScale(colorScheme).toFunction();
 
   d3.select('.nvtooltip').remove();
   div.selectAll('*').remove();
@@ -70,7 +71,6 @@ function Rose(element, props) {
   const legend = nv.models.legend();
   const tooltip = nv.models.tooltip();
   const state = { disabled: datum[times[0]].map(() => false) };
-  const color = name => getColorFromScheme(name, colorScheme);
 
   const svg = div
     .append('svg')
@@ -101,9 +101,9 @@ function Rose(element, props) {
         .map(v => ({
           key: v.name,
           value: v.value,
-          color: color(v.name),
+          color: colorFn(v.name),
           highlight: v.id === d.arcId,
-        })) : [{ key: d.name, value: d.val, color: color(d.name) }];
+        })) : [{ key: d.name, value: d.val, color: colorFn(d.name) }];
     return {
       key: 'Date',
       value: d.time,
@@ -113,7 +113,7 @@ function Rose(element, props) {
 
   legend
     .width(width)
-    .color(d => getColorFromScheme(d.key, colorScheme));
+    .color(d => colorFn(d.key));
   legendWrap
     .datum(legendData(datum))
     .call(legend);
@@ -331,7 +331,7 @@ function Rose(element, props) {
   const arcs = ae
     .append('path')
     .attr('class', 'arc')
-    .attr('fill', d => color(d.name))
+    .attr('fill', d => colorFn(d.name))
     .attr('d', arc);
 
   function mousemove() {
diff --git a/superset/assets/src/visualizations/sankey.js b/superset/assets/src/visualizations/sankey.js
index 29ed2e2..2509a50 100644
--- a/superset/assets/src/visualizations/sankey.js
+++ b/superset/assets/src/visualizations/sankey.js
@@ -2,7 +2,7 @@
 import d3 from 'd3';
 import PropTypes from 'prop-types';
 import { sankey as d3Sankey } from 'd3-sankey';
-import { getColorFromScheme } from '../modules/colors';
+import { getScale } from '../modules/CategoricalColorNamespace';
 import './sankey.css';
 
 const propTypes = {
@@ -49,6 +49,8 @@ function Sankey(element, props) {
     .attr('class', 'sankey-tooltip')
     .style('opacity', 0);
 
+  const colorFn = getScale(colorScheme).toFunction();
+
   const sankey = d3Sankey()
     .nodeWidth(15)
     .nodePadding(10)
@@ -153,7 +155,7 @@ function Sankey(element, props) {
     .attr('width', sankey.nodeWidth())
     .style('fill', function (d) {
       const name = d.name || 'N/A';
-      d.color = getColorFromScheme(name.replace(/ .*/, ''), colorScheme);
+      d.color = colorFn(name.replace(/ .*/, ''));
       return d.color;
     })
     .style('stroke', d => d3.rgb(d.color).darker(2))
diff --git a/superset/assets/src/visualizations/sunburst.js b/superset/assets/src/visualizations/sunburst.js
index 28fe605..7a87173 100644
--- a/superset/assets/src/visualizations/sunburst.js
+++ b/superset/assets/src/visualizations/sunburst.js
@@ -1,7 +1,7 @@
 /* eslint-disable no-param-reassign */
 import d3 from 'd3';
 import PropTypes from 'prop-types';
-import { getColorFromScheme } from '../modules/colors';
+import { getScale } from '../modules/CategoricalColorNamespace';
 import { wrapSvgText } from '../modules/utils';
 import './sunburst.css';
 
@@ -68,6 +68,8 @@ function Sunburst(element, props) {
   let arcs;
   let gMiddleText; // dom handles
 
+  const colorFn = getScale(colorScheme).toFunction();
+
   // Helper + path gen functions
   const partition = d3.layout.partition()
     .size([2 * Math.PI, radius * radius])
@@ -132,7 +134,7 @@ function Sunburst(element, props) {
         .attr('points', breadcrumbPoints)
         .style('fill', function (d) {
           return colorByCategory ?
-            getColorFromScheme(d.name, colorScheme) :
+            colorFn(d.name) :
             colorScale(d.m2 / d.m1);
         });
 
@@ -143,7 +145,7 @@ function Sunburst(element, props) {
         .style('fill', function (d) {
           // Make text white or black based on the lightness of the background
           const col = d3.hsl(colorByCategory ?
-            getColorFromScheme(d.name, colorScheme) :
+            colorFn(d.name) :
             colorScale(d.m2 / d.m1));
           return col.l < 0.5 ? 'white' : 'black';
         })
@@ -377,7 +379,7 @@ function Sunburst(element, props) {
         .attr('d', arc)
         .attr('fill-rule', 'evenodd')
         .style('fill', d => colorByCategory
-          ? getColorFromScheme(d.name, colorScheme)
+          ? colorFn(d.name)
           : colorScale(d.m2 / d.m1))
         .style('opacity', 1)
         .on('mouseenter', mouseenter);
diff --git a/superset/assets/src/visualizations/treemap.js b/superset/assets/src/visualizations/treemap.js
index 7834cd7..8ffd140 100644
--- a/superset/assets/src/visualizations/treemap.js
+++ b/superset/assets/src/visualizations/treemap.js
@@ -1,7 +1,7 @@
 /* eslint-disable no-shadow, no-param-reassign */
 import d3 from 'd3';
 import PropTypes from 'prop-types';
-import { getColorFromScheme } from '../modules/colors';
+import { getScale } from '../modules/CategoricalColorNamespace';
 import './treemap.css';
 
 // Declare PropTypes for recursive data structures
@@ -63,6 +63,7 @@ function treemap(element, props) {
   } = props;
   const div = d3.select(element);
   const formatNumber = d3.format(numberFormat);
+  const colorFn = getScale(colorScheme).toFunction();
 
   function draw(data, eltWidth, eltHeight) {
     const navBarHeight = 36;
@@ -282,7 +283,7 @@ function treemap(element, props) {
         .text(d => formatNumber(d.value));
       t.call(text);
       g.selectAll('rect')
-        .style('fill', d => getColorFromScheme(d.name, colorScheme));
+        .style('fill', d => colorFn(d.name));
 
       return g;
     };
diff --git a/superset/assets/src/visualizations/wordcloud/WordCloud.js b/superset/assets/src/visualizations/wordcloud/WordCloud.js
index d4d2d7e..7458f7d 100644
--- a/superset/assets/src/visualizations/wordcloud/WordCloud.js
+++ b/superset/assets/src/visualizations/wordcloud/WordCloud.js
@@ -1,7 +1,7 @@
 import d3 from 'd3';
 import PropTypes from 'prop-types';
 import cloudLayout from 'd3-cloud';
-import { getColorFromScheme } from '../../modules/colors';
+import { getScale } from '../../modules/CategoricalColorNamespace';
 
 const ROTATION = {
   square: () => Math.floor((Math.random() * 2)) * 90,
@@ -50,6 +50,8 @@ function wordCloud(element, props) {
     .fontWeight('bold')
     .fontSize(d => scale(d.size));
 
+  const colorFn = getScale(colorScheme).toFunction();
+
   function draw(words) {
     chart.selectAll('*').remove();
 
@@ -67,7 +69,7 @@ function wordCloud(element, props) {
         .style('font-size', d => `${d.size}px`)
         .style('font-weight', 'bold')
         .style('font-family', 'Helvetica')
-        .style('fill', d => getColorFromScheme(d.text, colorScheme))
+        .style('fill', d => colorFn(d.text))
         .attr('text-anchor', 'middle')
         .attr('transform', d => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
         .text(d => d.text);