From a82bb588f4647c36161d8d9a5dcfc49128288605 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Fri, 17 Nov 2017 15:56:04 -0800 Subject: [PATCH] Allow users to specify label->color mapping (#3879) Users can define `label_colors` in a dashboard's JSON metadata that enforces a label to color mapping. This also makes the function that maps labels to colors case insensitive. --- docs/faq.rst | 16 ++++++++++++ .../assets/javascripts/dashboard/reducers.js | 10 ++++++++ superset/assets/javascripts/modules/colors.js | 25 ++++++++++++++++--- .../spec/javascripts/modules/colors_spec.jsx | 17 ++++++++++++- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index d825ef5ba0b28..0ca341e8da298 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -221,3 +221,19 @@ When adding columns to a table, you can have Superset detect and merge the new columns in by using the "Refresh Metadata" action in the ``Source -> Tables`` page. Simply check the box next to the tables you want the schema refreshed, and click ``Actions -> Refresh Metadata``. + +Is there a way to force the use specific colors? +------------------------------------------------ + +It is possible on a per-dashboard basis by providing a mapping of +labels to colors in the ``JSON Metadata`` attribute using the +``label_colors`` key. + +..code:: + + { + "label_colors": { + "Girls": "#FF69B4", + "Boys": "#ADD8E6" + } + } diff --git a/superset/assets/javascripts/dashboard/reducers.js b/superset/assets/javascripts/dashboard/reducers.js index 5001fd13d4911..487f56feafb05 100644 --- a/superset/assets/javascripts/dashboard/reducers.js +++ b/superset/assets/javascripts/dashboard/reducers.js @@ -6,6 +6,7 @@ import * as actions from './actions'; import { getParam } from '../modules/utils'; import { alterInArr, removeFromArr } from '../reduxUtils'; import { applyDefaultFormData } from '../explore/stores/store'; +import { getColorFromScheme } from '../modules/colors'; export function getInitialState(bootstrapData) { const { user_id, datasources, common } = bootstrapData; @@ -25,6 +26,15 @@ export function getInitialState(bootstrapData) { // } + // Priming the color palette with user's label-color mapping provided in + // the dashboard's JSON metadata + if (dashboard.metadata && dashboard.metadata.label_colors) { + const colorMap = dashboard.metadata.label_colors; + for (const label in colorMap) { + getColorFromScheme(label, null, colorMap[label]); + } + } + dashboard.posDict = {}; dashboard.layout = []; if (dashboard.position_json) { diff --git a/superset/assets/javascripts/modules/colors.js b/superset/assets/javascripts/modules/colors.js index 0c3d06a09d549..03c3bb2ed4412 100644 --- a/superset/assets/javascripts/modules/colors.js +++ b/superset/assets/javascripts/modules/colors.js @@ -103,17 +103,36 @@ export const spectrums = { ], }; +/** + * Get a color from a scheme specific palette (scheme) + * The function cycles through the palette while memoizing labels + * association to colors. If the function is called twice with the + * same string, it will return the same color. + * + * @param {string} s - The label for which we want to get a color + * @param {string} scheme - The palette name, or "scheme" + * @param {string} forcedColor - A color that the caller wants to + forcibly associate to a label. + */ export const getColorFromScheme = (function () { - // Color factory const seen = {}; - return function (s, scheme) { + 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); + let stringifyS = String(s).toLowerCase(); // next line is for superset series that should have the same color stringifyS = stringifyS.replace('---', ''); + + if (forcedColor && !forcedColors[stringifyS]) { + forcedColors[stringifyS] = forcedColor; + } + if (forcedColors[stringifyS]) { + return forcedColors[stringifyS]; + } + if (seen[selectedScheme] === undefined) { seen[selectedScheme] = {}; } diff --git a/superset/assets/spec/javascripts/modules/colors_spec.jsx b/superset/assets/spec/javascripts/modules/colors_spec.jsx index 31ccea8326670..2a24633fe7bb5 100644 --- a/superset/assets/spec/javascripts/modules/colors_spec.jsx +++ b/superset/assets/spec/javascripts/modules/colors_spec.jsx @@ -8,7 +8,7 @@ describe('colors', () => { const color1 = getColorFromScheme('CA'); expect(color1).to.equal(ALL_COLOR_SCHEMES.bnbColors[0]); }); - it('series with same scheme should have the same color', () => { + it('getColorFromScheme series with same scheme should have the same color', () => { const color1 = getColorFromScheme('CA', 'bnbColors'); const color2 = getColorFromScheme('CA', 'googleCategory20c'); const color3 = getColorFromScheme('CA', 'bnbColors'); @@ -19,7 +19,22 @@ describe('colors', () => { expect(color1).to.equal(color3); expect(color4).to.equal(ALL_COLOR_SCHEMES.bnbColors[1]); }); + 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('girls', 'bnbColors', 'pink')).to.equal('pink'); + expect(getColorFromScheme('girls', 'bnbColors')).to.equal('pink'); + expect(getColorFromScheme('girls', 'googleCategory20c')).to.equal('pink'); + }); + it('getColorFromScheme is not case sensitive', () => { + const c1 = getColorFromScheme('girls', 'bnbColors'); + const c2 = getColorFromScheme('Girls', 'bnbColors'); + const c3 = getColorFromScheme('GIRLS', 'bnbColors'); + expect(c1).to.equal(c2); + expect(c3).to.equal(c2); + }); it('hexToRGB converts properly', () => { expect(hexToRGB('#FFFFFF')).to.have.same.members([255, 255, 255, 255]); expect(hexToRGB('#000000')).to.have.same.members([0, 0, 0, 255]);