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