diff --git a/superset/assets/package.json b/superset/assets/package.json index b2a013fc12c17..842c45a97e659 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 a7d4d66ab6762..10e582b31ecbd 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 0000000000000..1696dd28aa33b --- /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 0000000000000..fc2e2ea99030e --- /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 0000000000000..236b1e43a0ac3 --- /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 e83b4732b115c..be8823302034c 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 67ce4982d2041..779a1692e8a23 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 e913adbf89d8a..2a4a5e26fa310 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 812882c2d8b33..3238f4f3c9ce1 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 5e422eaa20108..2a04c7354cdd3 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 0000000000000..d022bb25fbdb9 --- /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 0000000000000..eab70d218424d --- /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 0000000000000..9d21d2628e245 --- /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 0000000000000..d26a923e4d616 --- /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 0000000000000..946d14a187b72 --- /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 0000000000000..cd9412163e97d --- /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/colorSchemes/sequential.js b/superset/assets/src/modules/colorSchemes/sequential.js new file mode 100644 index 0000000000000..6970ed411cb97 --- /dev/null +++ b/superset/assets/src/modules/colorSchemes/sequential.js @@ -0,0 +1,433 @@ +export default { + 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', + ], +}; diff --git a/superset/assets/src/modules/colors.js b/superset/assets/src/modules/colors.js index 1cb9eed4e85ec..43025dc85bcd5 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 2a7cdf1a87949..672a31e549514 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 ff5ab09381262..0976ec00dfce1 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 fa7bbad869aeb..e70a1eece7823 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 875e748b5ef34..62df302020d0d 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 29ed2e2ffa18e..2509a50db6fc3 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 28fe605ebab11..7a8717311c073 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 7834cd7ca9db9..8ffd14055b5bf 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 d4d2d7e3b4cca..7458f7d336dc3 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);