You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by GitBox <gi...@apache.org> on 2018/09/12 21:10:27 UTC
[GitHub] williaster closed pull request #5815: Improve categorical color
management
williaster closed pull request #5815: Improve categorical color management
URL: https://github.com/apache/incubator-superset/pull/5815
This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:
As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 667b4cb321..11b2b77f4b 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 a7d4d66ab6..10e582b31e 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 0000000000..1696dd28aa
--- /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 0000000000..fc2e2ea990
--- /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 0000000000..236b1e43a0
--- /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 e83b4732b1..be88233020 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 67ce4982d2..779a1692e8 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 e913adbf89..2a4a5e26fa 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 812882c2d8..3238f4f3c9 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 5e422eaa20..2a04c7354c 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 0000000000..d022bb25fb
--- /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 0000000000..eab70d2184
--- /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 0000000000..9d21d2628e
--- /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 0000000000..d26a923e4d
--- /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 0000000000..946d14a187
--- /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 0000000000..cd9412163e
--- /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 0000000000..6970ed411c
--- /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 1cb9eed4e8..43025dc85b 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 2a7cdf1a87..672a31e549 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 ff5ab09381..0976ec00df 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 fa7bbad869..e70a1eece7 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 875e748b5e..62df302020 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 29ed2e2ffa..2509a50db6 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 28fe605eba..7a8717311c 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 7834cd7ca9..8ffd14055b 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 d4d2d7e3b4..7458f7d336 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);
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
With regards,
Apache Git Services
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org