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.
(cherry picked from commit a82bb588f4647c36161d8d9a5dcfc49128288605)
diff --git a/docs/faq.rst b/docs/faq.rst
index d825ef5..0ca341e 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -221,3 +221,19 @@
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 8d7b7f4..57475a5 100644
--- a/superset/assets/javascripts/dashboard/reducers.js
+++ b/superset/assets/javascripts/dashboard/reducers.js
@@ -6,6 +6,7 @@
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 @@
//
}
+ // 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 0c3d06a..03c3bb2 100644
--- a/superset/assets/javascripts/modules/colors.js
+++ b/superset/assets/javascripts/modules/colors.js
@@ -103,17 +103,36 @@
],
};
+/**
+ * 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 31ccea8..2a24633 100644
--- a/superset/assets/spec/javascripts/modules/colors_spec.jsx
+++ b/superset/assets/spec/javascripts/modules/colors_spec.jsx
@@ -8,7 +8,7 @@
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 @@
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]);