fix(Dashboard): Color inconsistency on refreshes and conflicts (#27439)

This commit is contained in:
Geido 2024-06-20 15:30:11 +02:00 committed by GitHub
parent 1770f8b783
commit 313ee596f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1050 additions and 742 deletions

View File

@ -89,6 +89,7 @@ These features flags currently default to True and **will be removed in a future
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
- AVOID_COLORS_COLLISION
- DASHBOARD_CROSS_FILTERS
- ENABLE_JAVASCRIPT_CONTROLS
- KV_STORE

View File

@ -124,7 +124,7 @@ function selectColorScheme(color: string) {
)
.first()
.click();
cy.getBySel(color).click();
cy.getBySel(color).click({ force: true });
}
function applyChanges() {
@ -169,6 +169,7 @@ function writeMetadata(metadata: string) {
function openExplore(chartName: string) {
interceptExploreJson();
interceptGet();
cy.get(
`[data-test-chart-name='${chartName}'] [aria-label='More Options']`,
@ -210,7 +211,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(69, 78, 124)');
.should('have.css', 'fill', 'rgb(31, 168, 201)');
});
it('should apply same color to same labels with color scheme set', () => {
@ -231,7 +232,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(0, 234, 162)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
// open 2nd main tab
openTab(0, 1);
@ -240,7 +241,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(0, 234, 162)');
.should('have.css', 'fill', 'rgb(50, 0, 167)');
});
it('should apply same color to same labels with no color scheme set', () => {
@ -261,7 +262,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(69, 78, 124)');
.should('have.css', 'fill', 'rgb(31, 168, 201)');
// open 2nd main tab
openTab(0, 1);
@ -270,7 +271,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(69, 78, 124)');
.should('have.css', 'fill', 'rgb(31, 168, 201)');
});
it('custom label colors should take the precedence in nested tabs', () => {
@ -384,17 +385,17 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(69, 78, 124)');
.should('have.css', 'fill', 'rgb(31, 168, 201)');
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.eq(1)
.should('have.css', 'fill', 'rgb(224, 67, 85)');
.should('have.css', 'fill', 'rgb(69, 78, 124)');
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.eq(2)
.should('have.css', 'fill', 'rgb(163, 143, 121)');
.should('have.css', 'fill', 'rgb(90, 193, 137)');
openProperties();
cy.get('[aria-label="Select color scheme"]').should('have.value', '');
@ -423,17 +424,17 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(69, 78, 124)');
.should('have.css', 'fill', 'rgb(31, 168, 201)');
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.eq(1)
.should('have.css', 'fill', 'rgb(224, 67, 85)');
.should('have.css', 'fill', 'rgb(69, 78, 124)');
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.eq(2)
.should('have.css', 'fill', 'rgb(163, 143, 121)');
.should('have.css', 'fill', 'rgb(90, 193, 137)');
});
it('should show the same colors in Explore', () => {
@ -459,12 +460,6 @@ describe('Dashboard edit', () => {
)
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get(
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.eq(1)
.should('have.css', 'fill', 'rgb(172, 32, 119)');
openExplore('Top 10 California Names Timeseries');
@ -472,10 +467,6 @@ describe('Dashboard edit', () => {
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(255, 0, 0)');
// label Christopher
cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
.eq(1)
.should('have.css', 'fill', 'rgb(172, 32, 119)');
});
it.skip('should change color scheme multiple times', () => {
@ -496,7 +487,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(51, 61, 71)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
// open 2nd main tab
openTab(0, 1);
@ -505,7 +496,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(51, 61, 71)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
editDashboard();
openProperties();
@ -516,7 +507,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
.should('have.css', 'fill', 'rgb(244, 176, 42)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
// open main tab and nested tab
openTab(0, 0);
@ -527,7 +518,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(244, 176, 42)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
});
it.skip('should apply the color scheme across main tabs', () => {
@ -542,7 +533,7 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(51, 61, 71)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
});
it.skip('should apply the color scheme across main tabs for rendered charts', () => {
@ -558,7 +549,7 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(156, 52, 152)');
.should('have.css', 'fill', 'rgb(41, 105, 107)');
// change scheme now that charts are rendered across the main tabs
editDashboard();
@ -588,7 +579,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
.should('have.css', 'fill', 'rgb(51, 61, 71)');
.should('have.css', 'fill', 'rgb(234, 11, 140)');
// open another nested tab
openTab(2, 1);

View File

@ -97,8 +97,5 @@ describe('Visualization > Compare', () => {
cy.get(
'.Control[data-test="color_scheme"] .ant-select-selection-item [data-test="supersetColors"]',
).should('exist');
cy.get('.compare .nv-legend .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(31, 168, 201)');
});
});

View File

@ -87,8 +87,5 @@ describe('Visualization > Distribution bar chart', () => {
cy.get(
'.Control[data-test="color_scheme"] .ant-select-selection-item [data-test="bnbColors"]',
).should('exist');
cy.get('.dist_bar .nv-legend .nv-legend-symbol')
.first()
.should('have.css', 'fill', 'rgb(41, 105, 107)');
});
});

View File

@ -17,19 +17,18 @@
* under the License.
*/
/* eslint-disable no-dupe-class-members */
import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
import { ExtensibleFunction } from '../models';
import { ColorsInitLookup, ColorsLookup } from './types';
import stringifyAndTrim from './stringifyAndTrim';
import getSharedLabelColor from './SharedLabelColorSingleton';
import getLabelsColorMap from './LabelsColorMapSingleton';
import { getAnalogousColors } from './utils';
import { FeatureFlag, isFeatureEnabled } from '../utils';
// Use type augmentation to correct the fact that
// an instance of CategoricalScale is also a function
interface CategoricalColorScale {
(x: { toString(): string }, y?: number): string;
(x: { toString(): string }, y?: number, w?: string): string;
}
class CategoricalColorScale extends ExtensibleFunction {
@ -39,101 +38,183 @@ class CategoricalColorScale extends ExtensibleFunction {
scale: ScaleOrdinal<{ toString(): string }, string>;
parentForcedColors: ColorsLookup;
forcedColors: ColorsLookup;
labelsColorMapInstance: ReturnType<typeof getLabelsColorMap>;
chartLabelsColorMap: Map<string, string>;
multiple: number;
/**
* Constructor
* @param {*} colors an array of colors
* @param {*} parentForcedColors optional parameter that comes from parent
* (usually CategoricalColorNamespace) and supersede this.forcedColors
* @param {*} forcedColors optional parameter that comes from parent
* (usually CategoricalColorNamespace)
*/
constructor(colors: string[], parentForcedColors: ColorsInitLookup = {}) {
super((value: string, sliceId?: number) => this.getColor(value, sliceId));
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
super((value: string, sliceId?: number, colorScheme?: string) =>
this.getColor(value, sliceId, colorScheme),
);
// holds original color scheme colors
this.originColors = colors;
// holds the extended color range (includes analagous colors)
this.colors = colors;
// holds the values of this specific slice (label+color)
this.chartLabelsColorMap = new Map();
// shared color map instance (when context is shared, i.e. dashboard)
this.labelsColorMapInstance = getLabelsColorMap();
// holds the multiple value for analogous colors range
this.multiple = 0;
this.scale = scaleOrdinal<{ toString(): string }, string>();
this.scale.range(colors);
// reserve fixed colors in parent map based on their index in the scale
Object.entries(parentForcedColors).forEach(([key, value]) => {
Object.entries(forcedColors).forEach(([key, value]) => {
if (typeof value === 'number') {
// eslint-disable-next-line no-param-reassign
parentForcedColors[key] = colors[value % colors.length];
forcedColors[key] = colors[value % colors.length];
}
});
// all indexes have been replaced by a fixed color
this.parentForcedColors = parentForcedColors as ColorsLookup;
this.forcedColors = {};
this.multiple = 0;
// forced colors from parent (usually CategoricalColorNamespace)
// currently used in dashboards to set custom label colors
this.forcedColors = forcedColors as ColorsLookup;
}
removeSharedLabelColorFromRange(
sharedColorMap: Map<string, string>,
cleanedValue: string,
) {
// make sure we don't overwrite the origin colors
const updatedRange = new Set(this.originColors);
// remove the color option from shared color
sharedColorMap.forEach((value: string, key: string) => {
if (key !== cleanedValue) {
updatedRange.delete(value);
}
});
// remove the color option from forced colors
Object.entries(this.parentForcedColors).forEach(([key, value]) => {
if (key !== cleanedValue) {
updatedRange.delete(value);
}
});
this.range(updatedRange.size > 0 ? [...updatedRange] : this.originColors);
/**
* Increment the color range with analogous colors
*/
incrementColorRange() {
const multiple = Math.floor(
this.domain().length / this.originColors.length,
);
// the domain has grown larger than the original range
// increments the range with analogous colors
if (multiple > this.multiple) {
this.multiple = multiple;
const newRange = getAnalogousColors(this.originColors, multiple);
const extendedColors = this.originColors.concat(newRange);
this.range(extendedColors);
this.colors = extendedColors;
}
}
getColor(value?: string, sliceId?: number) {
/**
* Get the color for a given value
*
* @param value the value of a label to get the color for
* @param sliceId the ID of the current chart
* @param colorScheme the original color scheme of the chart
* @returns the color or the next available color
*/
getColor(value?: string, sliceId?: number, colorScheme?: string): string {
const cleanedValue = stringifyAndTrim(value);
const sharedLabelColor = getSharedLabelColor();
const sharedColorMap = sharedLabelColor.getColorMap();
const sharedColor = sharedColorMap.get(cleanedValue);
// priority: forced color (i.e. custom label colors) > shared color > scale color
const forcedColor = this.forcedColors?.[cleanedValue];
const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
let color = forcedColor || this.scale(cleanedValue);
// priority: parentForcedColors > forcedColors > labelColors
let color =
this.parentForcedColors?.[cleanedValue] ||
this.forcedColors?.[cleanedValue] ||
sharedColor;
// a forced color will always be used independently of the usage count
if (!forcedColor && !isExistingLabel) {
if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
this.incrementColorRange();
}
if (
// feature flag to be deprecated (will become standard behaviour)
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
this.isColorUsed(color)
) {
// fallback to least used color
color = this.getNextAvailableColor(color);
}
}
if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
const multiple = Math.floor(
this.domain().length / this.originColors.length,
// keep track of values in this slice
this.chartLabelsColorMap.set(cleanedValue, color);
// store the value+color in the LabelsColorMapSingleton
if (sliceId) {
this.labelsColorMapInstance.addSlice(
cleanedValue,
color,
sliceId,
colorScheme,
);
if (multiple > this.multiple) {
this.multiple = multiple;
const newRange = getAnalogousColors(this.originColors, multiple);
this.range(this.originColors.concat(newRange));
}
}
const newColor = this.scale(cleanedValue);
if (!color) {
color = newColor;
if (isFeatureEnabled(FeatureFlag.AvoidColorsCollision)) {
this.removeSharedLabelColorFromRange(sharedColorMap, cleanedValue);
color = this.scale(cleanedValue);
}
}
sharedLabelColor.addSlice(cleanedValue, color, sliceId);
return color;
}
/**
* Enforce specific color for given value
* Verify if a color is used in this slice
*
* @param color
* @returns true if the color is used in this slice
*/
isColorUsed(color: string): boolean {
return this.getColorUsageCount(color) > 0;
}
/**
* Get the count of the color usage in this slice
*
* @param sliceId the ID of the current slice
* @param color the color to check
* @returns the count of the color usage in this slice
*/
getColorUsageCount(currentColor: string): number {
let count = 0;
this.chartLabelsColorMap.forEach(color => {
if (color === currentColor) {
count += 1;
}
});
return count;
}
/**
* Lower chances of color collision by returning the least used color
* Checks across colors of current slice within LabelsColorMapSingleton
*
* @param currentColor the current color
* @returns the least used color that is not the excluded color
*/
getNextAvailableColor(currentColor: string) {
const colorUsageArray = this.colors.map(color => ({
color,
count: this.getColorUsageCount(color),
}));
const currentColorCount = this.getColorUsageCount(currentColor);
const otherColors = colorUsageArray.filter(
colorEntry => colorEntry.color !== currentColor,
);
// all other colors are used as much or more than currentColor
const hasNoneAvailable = otherColors.every(
colorEntry => colorEntry.count >= currentColorCount,
);
// fallback to currentColor color
if (!otherColors.length || hasNoneAvailable) {
return currentColor;
}
// Finding the least used color
const leastUsedColor = otherColors.reduce((min, entry) =>
entry.count < min.count ? entry : min,
).color;
return leastUsedColor;
}
/**
* Enforce specific color for a given value at the scale level
* Overrides any existing color and forced color for the given value
*
* @param {*} value value
* @param {*} forcedColor forcedColor
* @returns {CategoricalColorScale}
*/
setColor(value: string, forcedColor: string) {
this.forcedColors[stringifyAndTrim(value)] = forcedColor;
@ -142,6 +223,7 @@ class CategoricalColorScale extends ExtensibleFunction {
/**
* Get a mapping of data values to colors
*
* @returns an object where the key is the data value and the value is the hex color code
*/
getColorMap() {
@ -153,22 +235,23 @@ class CategoricalColorScale extends ExtensibleFunction {
return {
...colorMap,
...this.forcedColors,
...this.parentForcedColors,
};
}
/**
* Returns an exact copy of this scale. Changes to this scale will not affect the returned scale, and vice versa.
* Return an exact copy of this scale.
* Changes to this scale will not affect the returned scale and vice versa.
*
* @returns {CategoricalColorScale} A copy of this scale.
*/
copy() {
const copy = new CategoricalColorScale(
this.scale.range(),
this.parentForcedColors,
this.forcedColors,
);
copy.forcedColors = { ...this.forcedColors };
copy.domain(this.domain());
copy.unknown(this.unknown());
return copy;
}

View File

@ -17,36 +17,37 @@
* under the License.
*/
import { CategoricalColorNamespace } from '.';
import { makeSingleton } from '../utils';
export enum SharedLabelColorSource {
export enum LabelsColorMapSource {
Dashboard,
Explore,
}
export class SharedLabelColor {
sliceLabelMap: Map<number, string[]>;
export class LabelsColorMap {
chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
colorMap: Map<string, string>;
source: SharedLabelColorSource;
source: LabelsColorMapSource;
constructor() {
// { sliceId1: [label1, label2, ...], sliceId2: [label1, label2, ...] }
this.sliceLabelMap = new Map();
// holds labels and original color schemes for each chart in context
this.chartsLabelsMap = new Map();
this.colorMap = new Map();
this.source = SharedLabelColorSource.Dashboard;
this.source = LabelsColorMapSource.Dashboard;
}
updateColorMap(colorNamespace?: string, colorScheme?: string) {
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNamespace);
updateColorMap(categoricalNamespace: any, colorScheme?: string) {
const newColorMap = new Map();
this.colorMap.clear();
this.sliceLabelMap.forEach(labels => {
const colorScale = categoricalNamespace.getScale(colorScheme);
this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
const { labels, scheme: originalChartColorScheme } = chartConfig;
const currentColorScheme = colorScheme || originalChartColorScheme;
const colorScale = categoricalNamespace.getScale(currentColorScheme);
labels.forEach(label => {
const newColor = colorScale(label);
const newColor = colorScale.getColor(label, sliceId);
newColorMap.set(label, newColor);
});
});
@ -57,25 +58,37 @@ export class SharedLabelColor {
return this.colorMap;
}
addSlice(label: string, color: string, sliceId?: number) {
if (
this.source !== SharedLabelColorSource.Dashboard ||
sliceId === undefined
)
return;
const labels = this.sliceLabelMap.get(sliceId) || [];
addSlice(
label: string,
color: string,
sliceId: number,
colorScheme?: string,
) {
if (this.source !== LabelsColorMapSource.Dashboard) return;
const chartConfig = this.chartsLabelsMap.get(sliceId) || {
labels: [],
scheme: '',
};
const { labels } = chartConfig;
if (!labels.includes(label)) {
labels.push(label);
this.sliceLabelMap.set(sliceId, labels);
this.chartsLabelsMap.set(sliceId, {
labels,
scheme: colorScheme,
});
}
this.colorMap.set(label, color);
}
removeSlice(sliceId: number) {
if (this.source !== SharedLabelColorSource.Dashboard) return;
this.sliceLabelMap.delete(sliceId);
if (this.source !== LabelsColorMapSource.Dashboard) return;
this.chartsLabelsMap.delete(sliceId);
const newColorMap = new Map();
this.sliceLabelMap.forEach(labels => {
this.chartsLabelsMap.forEach(chartConfig => {
const { labels } = chartConfig;
labels.forEach(label => {
newColorMap.set(label, this.colorMap.get(label));
});
@ -83,19 +96,12 @@ export class SharedLabelColor {
this.colorMap = newColorMap;
}
reset() {
const copyColorMap = new Map(this.colorMap);
copyColorMap.forEach((_, label) => {
this.colorMap.set(label, '');
});
}
clear() {
this.sliceLabelMap.clear();
this.chartsLabelsMap.clear();
this.colorMap.clear();
}
}
const getInstance = makeSingleton(SharedLabelColor);
const getInstance = makeSingleton(LabelsColorMap);
export default getInstance;

View File

@ -34,9 +34,9 @@ export * from './colorSchemes';
export * from './utils';
export * from './types';
export {
default as getSharedLabelColor,
SharedLabelColor,
SharedLabelColorSource,
} from './SharedLabelColorSingleton';
default as getLabelsColorMap,
LabelsColorMap,
LabelsColorMapSource,
} from './LabelsColorMapSingleton';
export const BRAND_COLOR = '#00A699';

View File

@ -55,11 +55,12 @@ export function getContrastingColor(color: string, thresholds = 186) {
export function getAnalogousColors(colors: string[], results: number) {
const generatedColors: string[] = [];
// This is to solve the problem that the first three values generated by tinycolor.analogous
// may have the same or very close colors.
const ext = 3;
const analogousColors = colors.map(color => {
// returns an array of tinycolor instances
const result = tinycolor(color).analogous(results + ext);
// remove the first three colors to avoid the same or very close colors
return result.slice(ext);
});

View File

@ -17,9 +17,12 @@
* under the License.
*/
import { EffectCallback, useEffect, useRef } from 'react';
import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
export const useComponentDidUpdate = (effect: EffectCallback) => {
export const useComponentDidUpdate = (
effect: EffectCallback,
deps?: DependencyList,
) => {
const isMountedRef = useRef(false);
useEffect(() => {
if (isMountedRef.current) {
@ -27,5 +30,6 @@ export const useComponentDidUpdate = (effect: EffectCallback) => {
} else {
isMountedRef.current = true;
}
}, [effect]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...(deps || [effect])]);
};

View File

@ -98,7 +98,7 @@ describe('CategoricalColorNamespace', () => {
namespace.setColor('dog', 'black');
const scale = namespace.getScale('testColors');
scale.setColor('dog', 'pink');
expect(scale.getColor('dog')).toBe('black');
expect(scale.getColor('dog')).toBe('pink');
expect(scale.getColor('boy')).not.toBe('black');
});
it('does not affect scales in other namespaces', () => {

View File

@ -18,50 +18,76 @@
*/
import { ScaleOrdinal } from 'd3-scale';
import {
CategoricalColorScale,
FeatureFlag,
getSharedLabelColor,
} from '@superset-ui/core';
import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
describe('CategoricalColorScale', () => {
beforeEach(() => {
window.featureFlags = {};
});
it('exists', () => {
expect(CategoricalColorScale !== undefined).toBe(true);
});
describe('new CategoricalColorScale(colors, parentForcedColors)', () => {
it('can create new scale when parentForcedColors is not given', () => {
describe('new CategoricalColorScale(colors, forcedColors)', () => {
it('can create new scale when forcedColors is not given', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
expect(scale).toBeInstanceOf(CategoricalColorScale);
});
it('can create new scale when parentForcedColors is given', () => {
const parentForcedColors = {};
it('can create new scale when forcedColors is given', () => {
const forcedColors = {};
const scale = new CategoricalColorScale(
['blue', 'red', 'green'],
parentForcedColors,
forcedColors,
);
expect(scale).toBeInstanceOf(CategoricalColorScale);
expect(scale.parentForcedColors).toBe(parentForcedColors);
expect(scale.forcedColors).toBe(forcedColors);
});
it('can refer to colors based on their index', () => {
const parentForcedColors = { pig: 1, horse: 5 };
const forcedColors = { pig: 1, horse: 5 };
const scale = new CategoricalColorScale(
['blue', 'red', 'green'],
parentForcedColors,
forcedColors,
);
expect(scale.getColor('pig')).toEqual('red');
expect(parentForcedColors.pig).toEqual('red');
expect(forcedColors.pig).toEqual('red');
// can loop around the scale
expect(scale.getColor('horse')).toEqual('green');
expect(parentForcedColors.horse).toEqual('green');
expect(forcedColors.horse).toEqual('green');
});
});
describe('.getColor(value)', () => {
describe('.getColor(value, sliceId)', () => {
let scale: CategoricalColorScale;
let addSliceSpy: jest.SpyInstance<
void,
[label: string, color: string, sliceId: number, colorScheme?: string]
>;
let getNextAvailableColorSpy: jest.SpyInstance<
string,
[currentColor: string]
>;
beforeEach(() => {
scale = new CategoricalColorScale(['blue', 'red', 'green']);
// Spy on the addSlice method of labelsColorMapInstance
addSliceSpy = jest.spyOn(scale.labelsColorMapInstance, 'addSlice');
getNextAvailableColorSpy = jest
.spyOn(scale, 'getNextAvailableColor')
.mockImplementation(color => color);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('returns same color for same value', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
const scale = new CategoricalColorScale(['blue', 'red', 'green'], {
pig: 'red',
horse: 'green',
});
const c1 = scale.getColor('pig');
const c2 = scale.getColor('horse');
const c3 = scale.getColor('pig');
@ -82,9 +108,6 @@ describe('CategoricalColorScale', () => {
expect(c3).not.toBe(c1);
});
it('recycles colors when number of items exceed available colors', () => {
window.featureFlags = {
[FeatureFlag.UseAnalagousColors]: false,
};
const colorSet: { [key: string]: number } = {};
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
const colors = [
@ -118,43 +141,70 @@ describe('CategoricalColorScale', () => {
scale.getColor('cow');
scale.getColor('donkey');
scale.getColor('goat');
expect(scale.range()).toHaveLength(6);
expect(scale.range()).toHaveLength(9);
});
it('adds the color and value to chartLabelsColorMap and calls addSlice', () => {
const value = 'testValue';
const sliceId = 123;
const colorScheme = 'preset';
it('should remove shared color from range if avoid colors collision enabled', () => {
expect(scale.chartLabelsColorMap.has(value)).toBe(false);
scale.getColor(value, sliceId, colorScheme);
expect(scale.chartLabelsColorMap.has(value)).toBe(true);
expect(scale.chartLabelsColorMap.get(value)).toBeDefined();
expect(addSliceSpy).toHaveBeenCalledWith(
value,
expect.any(String),
sliceId,
colorScheme,
);
const expectedColor = scale.chartLabelsColorMap.get(value);
const returnedColor = scale.getColor(value, sliceId);
expect(returnedColor).toBe(expectedColor);
});
it('conditionally calls getNextAvailableColor', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
const color1 = scale.getColor('a', 1);
expect(scale.range()).toHaveLength(3);
const color2 = scale.getColor('a', 2);
expect(color1).toBe(color2);
scale.getColor('b', 2);
expect(scale.range()).toHaveLength(2);
scale.getColor('c', 2);
expect(scale.range()).toHaveLength(1);
scale.getColor('testValue1');
scale.getColor('testValue2');
scale.getColor('testValue1');
scale.getColor('testValue3');
scale.getColor('testValue4');
expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
getNextAvailableColorSpy.mockClear();
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: false,
};
scale.getColor('testValue3');
expect(getNextAvailableColorSpy).not.toHaveBeenCalled();
});
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: false,
};
});
describe('.setColor(value, forcedColor)', () => {
it('overrides default color', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.setColor('pig', 'pink');
expect(scale.getColor('pig')).toBe('pink');
});
it('does not override parentForcedColors', () => {
it('does override forcedColors', () => {
const scale1 = new CategoricalColorScale(['blue', 'red', 'green']);
scale1.setColor('pig', 'black');
const scale2 = new CategoricalColorScale(
['blue', 'red', 'green'],
scale1.forcedColors,
);
const scale2 = new CategoricalColorScale(['blue', 'red', 'green']);
scale2.setColor('pig', 'pink');
expect(scale2.getColor('pig')).toBe('pink');
expect(scale1.getColor('pig')).toBe('black');
expect(scale2.getColor('pig')).toBe('black');
});
it('returns the scale', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
@ -163,7 +213,7 @@ describe('CategoricalColorScale', () => {
});
});
describe('.getColorMap()', () => {
it('returns correct mapping and parentForcedColors and forcedColors are specified', () => {
it('returns correct mapping using least used color', () => {
const scale1 = new CategoricalColorScale(['blue', 'red', 'green']);
scale1.setColor('cow', 'black');
const scale2 = new CategoricalColorScale(
@ -177,7 +227,7 @@ describe('CategoricalColorScale', () => {
expect(scale2.getColorMap()).toEqual({
cow: 'black',
pig: 'pink',
horse: 'green',
horse: 'blue', // least used color
});
});
});
@ -230,10 +280,114 @@ describe('CategoricalColorScale', () => {
});
describe('a CategoricalColorScale instance is also a color function itself', () => {
it('scale(value) returns color similar to calling scale.getColor(value)', () => {
it('scale(value) returns same color for same value', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
expect(scale.getColor('pig')).toBe(scale('pig'));
expect(scale.getColor('cat')).toBe(scale('cat'));
expect(scale.getColor('pig')).toBe('blue');
expect(scale('pig')).toBe('blue');
expect(scale.getColor('cat')).toBe('red');
expect(scale('cat')).toBe('red');
});
});
describe('.getNextAvailableColor(currentColor)', () => {
it('returns the current color if it is the least used or equally used among colors', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat');
scale.getColor('dog');
// Since 'green' hasn't been used, it's considered the least used.
expect(scale.getNextAvailableColor('blue')).toBe('green');
});
it('handles cases where all colors are equally used and returns the current color', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.getColor('fish'); // green
// All colors used once, so the function should return the current color
expect(scale.getNextAvailableColor('red')).toBe('red');
});
it('returns the least used color accurately even when some colors are used more frequently', () => {
const scale = new CategoricalColorScale([
'blue',
'red',
'green',
'yellow',
]);
scale.getColor('cat'); // blue
scale.getColor('dog'); // red
scale.getColor('frog'); // green
scale.getColor('fish'); // yellow
scale.getColor('goat'); // blue
scale.getColor('horse'); // red
scale.getColor('pony'); // green
// Yellow is the least used color, so it should be returned.
expect(scale.getNextAvailableColor('blue')).toBe('yellow');
});
});
describe('.isColorUsed(color)', () => {
it('returns true if the color is already used, false otherwise', () => {
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
// Initially, no color is used
expect(scale.isColorUsed('blue')).toBe(false);
expect(scale.isColorUsed('red')).toBe(false);
expect(scale.isColorUsed('green')).toBe(false);
scale.getColor('item1');
// Now, 'blue' is used, but 'red' and 'green' are not
expect(scale.isColorUsed('blue')).toBe(true);
expect(scale.isColorUsed('red')).toBe(false);
expect(scale.isColorUsed('green')).toBe(false);
// Simulate using the 'red' color
scale.getColor('item2'); // Assigns 'red' to 'item2'
// Now, 'blue' and 'red' are used
expect(scale.isColorUsed('blue')).toBe(true);
expect(scale.isColorUsed('red')).toBe(true);
expect(scale.isColorUsed('green')).toBe(false);
});
});
describe('.getColorUsageCount(color)', () => {
it('accurately counts the occurrences of a specific color', () => {
const scale = new CategoricalColorScale([
'blue',
'red',
'green',
'yellow',
]);
// No colors are used initially
expect(scale.getColorUsageCount('blue')).toBe(0);
expect(scale.getColorUsageCount('red')).toBe(0);
expect(scale.getColorUsageCount('green')).toBe(0);
expect(scale.getColorUsageCount('yellow')).toBe(0);
// Simulate using colors
scale.getColor('item1');
scale.getColor('item2');
scale.getColor('item1');
// Check the counts after using the colors
expect(scale.getColorUsageCount('blue')).toBe(1);
expect(scale.getColorUsageCount('red')).toBe(1);
expect(scale.getColorUsageCount('green')).toBe(0);
expect(scale.getColorUsageCount('yellow')).toBe(0);
// Simulate using colors more
scale.getColor('item3');
scale.getColor('item4');
scale.getColor('item3');
// Final counts
expect(scale.getColorUsageCount('blue')).toBe(1);
expect(scale.getColorUsageCount('red')).toBe(1);
expect(scale.getColorUsageCount('green')).toBe(1);
expect(scale.getColorUsageCount('yellow')).toBe(1);
});
});
@ -244,50 +398,4 @@ describe('CategoricalColorScale', () => {
expect(scale('pig')).toBe('blue');
});
});
describe('.removeSharedLabelColorFromRange(colorMap, cleanedValue)', () => {
it('should remove shared color from range', () => {
const scale = new CategoricalColorScale(['blue', 'green', 'red']);
expect(scale.range()).toEqual(['blue', 'green', 'red']);
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.clear();
const colorMap = sharedLabelColor.getColorMap();
sharedLabelColor.addSlice('cow', 'blue', 1);
scale.removeSharedLabelColorFromRange(colorMap, 'pig');
expect(scale.range()).toEqual(['green', 'red']);
scale.removeSharedLabelColorFromRange(colorMap, 'cow');
expect(scale.range()).toEqual(['blue', 'green', 'red']);
sharedLabelColor.clear();
});
it('recycles colors when all colors are in sharedLabelColor', () => {
const scale = new CategoricalColorScale(['blue', 'green', 'red']);
expect(scale.range()).toEqual(['blue', 'green', 'red']);
const sharedLabelColor = getSharedLabelColor();
const colorMap = sharedLabelColor.getColorMap();
sharedLabelColor.addSlice('cow', 'blue', 1);
sharedLabelColor.addSlice('pig', 'red', 1);
sharedLabelColor.addSlice('horse', 'green', 1);
scale.removeSharedLabelColorFromRange(colorMap, 'goat');
expect(scale.range()).toEqual(['blue', 'green', 'red']);
sharedLabelColor.clear();
});
it('should remove parentForcedColors from range', () => {
const parentForcedColors = { house: 'blue', cow: 'red' };
const scale = new CategoricalColorScale(
['blue', 'red', 'green'],
parentForcedColors,
);
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.clear();
const colorMap = sharedLabelColor.getColorMap();
scale.removeSharedLabelColorFromRange(colorMap, 'pig');
expect(scale.range()).toEqual(['green']);
scale.removeSharedLabelColorFromRange(colorMap, 'cow');
expect(scale.range()).toEqual(['red', 'green']);
sharedLabelColor.clear();
});
});
});

View File

@ -0,0 +1,234 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
CategoricalColorNamespace,
CategoricalScheme,
FeatureFlag,
getCategoricalSchemeRegistry,
getLabelsColorMap,
LabelsColorMapSource,
LabelsColorMap,
} from '@superset-ui/core';
const actual = jest.requireActual('../../src/color/utils');
const getAnalogousColorsSpy = jest
.spyOn(actual, 'getAnalogousColors')
.mockImplementation(() => ['red', 'green', 'blue']);
describe('LabelsColorMap', () => {
beforeAll(() => {
getCategoricalSchemeRegistry()
.registerValue(
'testColors',
new CategoricalScheme({
id: 'testColors',
colors: ['red', 'green', 'blue'],
}),
)
.registerValue(
'testColors2',
new CategoricalScheme({
id: 'testColors2',
colors: ['yellow', 'green', 'blue'],
}),
);
});
beforeEach(() => {
getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
getLabelsColorMap().clear();
});
it('has default value out-of-the-box', () => {
expect(getLabelsColorMap()).toBeInstanceOf(LabelsColorMap);
});
describe('.addSlice(value, color, sliceId)', () => {
it('should add to sliceLabelColorMap when first adding label', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('a', 'red', 1, 'preset');
expect(labelsColorMap.chartsLabelsMap.has(1)).toEqual(true);
const chartConfig = labelsColorMap.chartsLabelsMap.get(1);
expect(chartConfig?.labels?.includes('a')).toEqual(true);
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ a: 'red' });
});
it('should add to sliceLabelColorMap when slice exist', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 1);
const chartConfig = labelsColorMap.chartsLabelsMap.get(1);
expect(chartConfig?.labels?.includes('b')).toEqual(true);
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
});
it('should use last color if adding label repeatedly', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('b', 'blue', 1);
labelsColorMap.addSlice('b', 'green', 1);
const chartConfig = labelsColorMap.chartsLabelsMap.get(1);
expect(chartConfig?.labels?.includes('b')).toEqual(true);
expect(chartConfig?.labels?.length).toEqual(1);
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
});
it('should do nothing when source is not dashboard', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.source = LabelsColorMapSource.Explore;
labelsColorMap.addSlice('a', 'red', 1);
expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
});
});
describe('.remove(sliceId)', () => {
it('should remove sliceId', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.removeSlice(1);
expect(labelsColorMap.chartsLabelsMap.has(1)).toEqual(false);
});
it('should update colorMap', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 2);
labelsColorMap.removeSlice(1);
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ b: 'blue' });
});
it('should do nothing when source is not dashboard', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.source = LabelsColorMapSource.Explore;
labelsColorMap.removeSlice(1);
expect(labelsColorMap.chartsLabelsMap.has(1)).toEqual(true);
});
});
describe('.updateColorMap(namespace, scheme)', () => {
let categoricalNamespace: any;
let mockedNamespace: any;
let labelsColorMap: any;
beforeEach(() => {
labelsColorMap = getLabelsColorMap();
categoricalNamespace = CategoricalColorNamespace.getNamespace(undefined);
mockedNamespace = {
getScale: jest.fn().mockReturnValue({
getColor: jest.fn(() => 'mockColor'),
}),
};
});
it('should use provided color scheme', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.updateColorMap(mockedNamespace, 'testColors2');
expect(mockedNamespace.getScale).toHaveBeenCalledWith('testColors2');
});
it('should fallback to original chart color scheme if no color scheme is provided', () => {
labelsColorMap.addSlice('a', 'red', 1, 'originalScheme');
labelsColorMap.updateColorMap(mockedNamespace);
expect(mockedNamespace.getScale).toHaveBeenCalledWith('originalScheme');
});
it('should fallback to undefined if no color scheme is provided', () => {
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 2);
labelsColorMap.updateColorMap(mockedNamespace);
expect(mockedNamespace.getScale).toHaveBeenCalledWith(undefined);
});
it('should update color map', () => {
// override color with forcedItems
categoricalNamespace.setColor('b', 'green');
// testColors2: 'yellow', 'green', 'blue'
// first-time label, gets color, yellow
labelsColorMap.addSlice('a', 'red', 1);
// overridden, gets green
labelsColorMap.addSlice('b', 'pink', 1);
// overridden, gets green
labelsColorMap.addSlice('b', 'green', 2);
// first-time slice label, gets color, yellow
labelsColorMap.addSlice('c', 'blue', 2);
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors2');
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({
a: 'yellow',
b: 'green',
c: 'yellow',
});
});
it('should use recycle colors', () => {
window.featureFlags = {
[FeatureFlag.UseAnalagousColors]: false,
};
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 2);
labelsColorMap.addSlice('c', 'green', 3);
labelsColorMap.addSlice('d', 'red', 4);
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors');
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).not.toEqual({});
expect(getAnalogousColorsSpy).not.toBeCalled();
});
it('should use analagous colors', () => {
window.featureFlags = {
[FeatureFlag.UseAnalagousColors]: true,
};
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 1);
labelsColorMap.addSlice('c', 'green', 1);
labelsColorMap.addSlice('d', 'red', 1);
labelsColorMap.updateColorMap(categoricalNamespace, 'testColors');
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).not.toEqual({});
expect(getAnalogousColorsSpy).toBeCalled();
});
});
describe('.getColorMap()', () => {
it('should get color map', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 2);
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
});
});
describe('.reset()', () => {
it('should reset color map', () => {
const labelsColorMap = getLabelsColorMap();
labelsColorMap.addSlice('a', 'red', 1);
labelsColorMap.addSlice('b', 'blue', 2);
labelsColorMap.clear();
const colorMap = labelsColorMap.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({});
});
});
});

View File

@ -1,201 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
CategoricalScheme,
FeatureFlag,
getCategoricalSchemeRegistry,
getSharedLabelColor,
SharedLabelColor,
SharedLabelColorSource,
} from '@superset-ui/core';
const actual = jest.requireActual('../../src/color/utils');
const getAnalogousColorsSpy = jest
.spyOn(actual, 'getAnalogousColors')
.mockImplementation(() => ['red', 'green', 'blue']);
describe('SharedLabelColor', () => {
beforeAll(() => {
getCategoricalSchemeRegistry()
.registerValue(
'testColors',
new CategoricalScheme({
id: 'testColors',
colors: ['red', 'green', 'blue'],
}),
)
.registerValue(
'testColors2',
new CategoricalScheme({
id: 'testColors2',
colors: ['yellow', 'green', 'blue'],
}),
);
});
beforeEach(() => {
getSharedLabelColor().source = SharedLabelColorSource.Dashboard;
getSharedLabelColor().clear();
});
it('has default value out-of-the-box', () => {
expect(getSharedLabelColor()).toBeInstanceOf(SharedLabelColor);
});
describe('.addSlice(value, color, sliceId)', () => {
it('should add to sliceLabelColorMap when first adding label', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
expect(sharedLabelColor.sliceLabelMap.has(1)).toEqual(true);
const labels = sharedLabelColor.sliceLabelMap.get(1);
expect(labels?.includes('a')).toEqual(true);
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ a: 'red' });
});
it('should add to sliceLabelColorMap when slice exist', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.addSlice('b', 'blue', 1);
const labels = sharedLabelColor.sliceLabelMap.get(1);
expect(labels?.includes('b')).toEqual(true);
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
});
it('should use last color if adding label repeatedly', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('b', 'blue', 1);
sharedLabelColor.addSlice('b', 'green', 1);
const labels = sharedLabelColor.sliceLabelMap.get(1);
expect(labels?.includes('b')).toEqual(true);
expect(labels?.length).toEqual(1);
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
});
it('should do nothing when source is not dashboard', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.source = SharedLabelColorSource.Explore;
sharedLabelColor.addSlice('a', 'red');
expect(Object.fromEntries(sharedLabelColor.sliceLabelMap)).toEqual({});
});
it('should do nothing when sliceId is undefined', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red');
expect(Object.fromEntries(sharedLabelColor.sliceLabelMap)).toEqual({});
});
});
describe('.remove(sliceId)', () => {
it('should remove sliceId', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.removeSlice(1);
expect(sharedLabelColor.sliceLabelMap.has(1)).toEqual(false);
});
it('should update colorMap', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.addSlice('b', 'blue', 2);
sharedLabelColor.removeSlice(1);
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ b: 'blue' });
});
it('should do nothing when source is not dashboard', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.source = SharedLabelColorSource.Explore;
sharedLabelColor.removeSlice(1);
expect(sharedLabelColor.sliceLabelMap.has(1)).toEqual(true);
});
});
describe('.updateColorMap(namespace, scheme)', () => {
it('should update color map', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.addSlice('b', 'pink', 1);
sharedLabelColor.addSlice('b', 'green', 2);
sharedLabelColor.addSlice('c', 'blue', 2);
sharedLabelColor.updateColorMap('', 'testColors2');
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({
a: 'yellow',
b: 'yellow',
c: 'green',
});
});
it('should use recycle colors', () => {
window.featureFlags = {
[FeatureFlag.UseAnalagousColors]: false,
};
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.addSlice('b', 'blue', 2);
sharedLabelColor.addSlice('c', 'green', 3);
sharedLabelColor.addSlice('d', 'red', 4);
sharedLabelColor.updateColorMap('', 'testColors');
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).not.toEqual({});
expect(getAnalogousColorsSpy).not.toBeCalled();
});
it('should use analagous colors', () => {
window.featureFlags = {
[FeatureFlag.UseAnalagousColors]: true,
};
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.addSlice('b', 'blue', 1);
sharedLabelColor.addSlice('c', 'green', 1);
sharedLabelColor.addSlice('d', 'red', 1);
sharedLabelColor.updateColorMap('', 'testColors');
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).not.toEqual({});
expect(getAnalogousColorsSpy).toBeCalled();
});
});
describe('.getColorMap()', () => {
it('should get color map', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.addSlice('b', 'blue', 2);
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
});
});
describe('.reset()', () => {
it('should reset color map', () => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.addSlice('a', 'red', 1);
sharedLabelColor.addSlice('b', 'blue', 2);
sharedLabelColor.reset();
const colorMap = sharedLabelColor.getColorMap();
expect(Object.fromEntries(colorMap)).toEqual({ a: '', b: '' });
});
});
});

View File

@ -93,7 +93,7 @@ function Chord(element, props) {
.append('path')
.attr('id', (d, i) => `group${i}`)
.attr('d', arc)
.style('fill', (d, i) => colorFn(nodes[i], sliceId));
.style('fill', (d, i) => colorFn(nodes[i], sliceId, colorScheme));
// Add a text label.
const groupText = group.append('text').attr('x', 6).attr('dy', 15);
@ -121,7 +121,7 @@ function Chord(element, props) {
.on('mouseover', d => {
chord.classed('fade', p => p !== d);
})
.style('fill', d => colorFn(nodes[d.source.index], sliceId))
.style('fill', d => colorFn(nodes[d.source.index], sliceId, colorScheme))
.attr('d', path);
// Add an elaborate mouseover title for each chord.

View File

@ -78,7 +78,7 @@ class CustomHistogram extends PureComponent {
const keys = data.map(d => d.key);
const colorScale = scaleOrdinal({
domain: keys,
range: keys.map(x => colorFn(x, sliceId)),
range: keys.map(x => colorFn(x, sliceId, colorScheme)),
});
return (

View File

@ -384,7 +384,7 @@ function Icicle(element, props) {
// Apply color scheme
g.selectAll('rect').style('fill', d => {
d.color = colorFn(d.name, sliceId);
d.color = colorFn(d.name, sliceId, colorScheme);
return d.color;
});

View File

@ -120,10 +120,16 @@ function Rose(element, props) {
.map(v => ({
key: v.name,
value: v.value,
color: colorFn(v.name, sliceId),
color: colorFn(v.name, sliceId, colorScheme),
highlight: v.id === d.arcId,
}))
: [{ key: d.name, value: d.val, color: colorFn(d.name, sliceId) }];
: [
{
key: d.name,
value: d.val,
color: colorFn(d.name, sliceId, colorScheme),
},
];
return {
key: 'Date',
@ -132,7 +138,7 @@ function Rose(element, props) {
};
}
legend.width(width).color(d => colorFn(d.key, sliceId));
legend.width(width).color(d => colorFn(d.key, sliceId, colorScheme));
legendWrap.datum(legendData(datum)).call(legend);
tooltip.headerFormatter(timeFormat).valueFormatter(format);
@ -379,7 +385,7 @@ function Rose(element, props) {
const arcs = ae
.append('path')
.attr('class', 'arc')
.attr('fill', d => colorFn(d.name, sliceId))
.attr('fill', d => colorFn(d.name, sliceId, colorScheme))
.attr('d', arc);
function mousemove() {

View File

@ -219,7 +219,7 @@ function Sankey(element, props) {
.attr('width', sankey.nodeWidth())
.style('fill', d => {
const name = d.name || 'N/A';
d.color = colorFn(name, sliceId);
d.color = colorFn(name, sliceId, colorScheme);
return d.color;
})

View File

@ -58,7 +58,10 @@ function getCategories(fd: QueryFormData, data: JsonObject[]) {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;
if (fd.dimension) {
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
color = hexToRGB(
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
c.a * 255,
);
} else {
color = fixedColor;
}
@ -134,7 +137,10 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
return data.map(d => {
let color;
if (fd.dimension) {
color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
color = hexToRGB(
colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
c.a * 255,
);
return { ...d, color };
}

View File

@ -658,7 +658,9 @@ function nvd3Vis(element, props) {
} else if (vizType !== 'bullet') {
const colorFn = getScale(colorScheme);
chart.color(
d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
d =>
d.color ||
colorFn(cleanColorInput(d[colorKey]), sliceId, colorScheme),
);
}

View File

@ -108,9 +108,9 @@ export default function transformProps(
datum[`${metric}__outliers`],
],
itemStyle: {
color: colorFn(groupbyLabel, sliceId),
color: colorFn(groupbyLabel, sliceId, colorScheme),
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
borderColor: colorFn(groupbyLabel, sliceId),
borderColor: colorFn(groupbyLabel, sliceId, colorScheme),
},
};
});
@ -149,7 +149,7 @@ export default function transformProps(
},
},
itemStyle: {
color: colorFn(groupbyLabel, sliceId),
color: colorFn(groupbyLabel, sliceId, colorScheme),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@ -174,7 +174,7 @@ export default function transformProps(
value,
name,
itemStyle: {
color: colorFn(name, sliceId),
color: colorFn(name, sliceId, colorScheme),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@ -173,7 +173,7 @@ export default function transformProps(
value: data_point[metricLabel] as number,
name,
itemStyle: {
color: colorFn(index, sliceId),
color: colorFn(index, sliceId, colorScheme),
},
title: {
offsetCenter: [
@ -201,7 +201,7 @@ export default function transformProps(
item = {
...item,
itemStyle: {
color: colorFn(index, sliceId),
color: colorFn(index, sliceId, colorScheme),
opacity: OpacityEnum.SemiTransparent,
},
detail: {

View File

@ -277,7 +277,7 @@ export default function transformProps(
type: 'graph',
categories: categoryList.map(c => ({
name: c,
itemStyle: { color: colorFn(c, sliceId) },
itemStyle: { color: colorFn(c, sliceId, colorScheme) },
})),
layout,
force: {

View File

@ -222,7 +222,7 @@ export default function transformProps(
value,
name,
itemStyle: {
color: colorFn(name, sliceId),
color: colorFn(name, sliceId, colorScheme),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,

View File

@ -165,7 +165,7 @@ export default function transformProps(
value: metricLabels.map(metricLabel => datum[metricLabel]),
name: joinedName,
itemStyle: {
color: colorFn(joinedName, sliceId),
color: colorFn(joinedName, sliceId, colorScheme),
opacity: isFiltered
? OpacityEnum.Transparent
: OpacityEnum.NonTransparent,

View File

@ -183,7 +183,7 @@ export default function transformProps(
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(name, sliceId),
color: colorFn(name, sliceId, colorScheme),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},
@ -216,7 +216,7 @@ export default function transformProps(
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(`${metricLabel}`, sliceId),
color: colorFn(`${metricLabel}`, sliceId, colorScheme),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},

View File

@ -66,6 +66,7 @@ export interface WordCloudProps extends WordCloudVisualProps {
height: number;
width: number;
sliceId: number;
colorScheme: string;
}
export interface WordCloudState {
@ -221,7 +222,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
render() {
const { scaleFactor } = this.state;
const { width, height, encoding, sliceId } = this.props;
const { width, height, encoding, sliceId, colorScheme } = this.props;
const { words } = this.state;
// @ts-ignore
@ -249,7 +250,11 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
fontSize={`${w.size}px`}
fontWeight={w.weight}
fontFamily={w.font}
fill={colorFn(getValueFromDatum(w) as string, sliceId)}
fill={colorFn(
getValueFromDatum(w) as string,
sliceId,
colorScheme,
)}
textAnchor="middle"
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
>

View File

@ -80,5 +80,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps {
rotation,
width,
sliceId,
colorScheme,
};
}

View File

@ -23,7 +23,8 @@ import { WordCloudFormData } from '../types';
export default function transformProps(chartProps: ChartProps): WordCloudProps {
const { width, height, formData, queriesData } = chartProps;
const { encoding, rotation, sliceId } = formData as WordCloudFormData;
const { encoding, rotation, sliceId, colorScheme } =
formData as WordCloudFormData;
return {
data: queriesData[0].data,
@ -32,5 +33,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps {
rotation,
width,
sliceId,
colorScheme,
};
}

View File

@ -68,6 +68,7 @@ describe('WordCloud transformProps', () => {
},
},
rotation: 'square',
colorScheme: 'bnbColors',
data: [{ name: 'Hulk', sum__num: 1 }],
});
});

View File

@ -54,8 +54,8 @@ const propTypes = {
// formData contains chart's own filter parameter
// and merged with extra filter that current dashboard applying
formData: PropTypes.object.isRequired,
labelColors: PropTypes.object,
sharedLabelColors: PropTypes.object,
labelsColor: PropTypes.object,
labelsColorMap: PropTypes.object,
width: PropTypes.number,
height: PropTypes.number,
setControlValue: PropTypes.func,

View File

@ -41,8 +41,8 @@ const propTypes = {
initialValues: PropTypes.object,
formData: PropTypes.object.isRequired,
latestQueryFormData: PropTypes.object,
labelColors: PropTypes.object,
sharedLabelColors: PropTypes.object,
labelsColor: PropTypes.object,
labelsColorMap: PropTypes.object,
height: PropTypes.number,
width: PropTypes.number,
setControlValue: PropTypes.func,
@ -153,8 +153,8 @@ class ChartRenderer extends Component {
nextProps.height !== this.props.height ||
nextProps.width !== this.props.width ||
nextProps.triggerRender ||
nextProps.labelColors !== this.props.labelColors ||
nextProps.sharedLabelColors !== this.props.sharedLabelColors ||
nextProps.labelsColor !== this.props.labelsColor ||
nextProps.labelsColorMap !== this.props.labelsColorMap ||
nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
nextProps.formData.stack !== this.props.formData.stack ||
nextProps.cacheBusterProp !== this.props.cacheBusterProp ||

View File

@ -17,13 +17,7 @@
* under the License.
*/
import { Dispatch } from 'redux';
import {
makeApi,
CategoricalColorNamespace,
t,
getErrorText,
} from '@superset-ui/core';
import { isString } from 'lodash';
import { makeApi, t, getErrorText } from '@superset-ui/core';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import {
ChartConfiguration,
@ -36,39 +30,8 @@ import { onSave } from './dashboardState';
export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED';
export function updateColorSchema(
metadata: Record<string, any>,
labelColors: Record<string, string>,
) {
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
metadata?.color_namespace,
);
const colorMap = isString(labelColors)
? JSON.parse(labelColors)
: labelColors;
Object.keys(colorMap).forEach(label => {
categoricalNamespace.setColor(label, colorMap[label]);
});
}
// updates partially changed dashboard info
export function dashboardInfoChanged(newInfo: { metadata: any }) {
const { metadata } = newInfo;
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
metadata?.color_namespace,
);
categoricalNamespace.resetColors();
if (metadata?.shared_label_colors) {
updateColorSchema(metadata, metadata?.shared_label_colors);
}
if (metadata?.label_colors) {
updateColorSchema(metadata, metadata?.label_colors);
}
return { type: DASHBOARD_INFO_UPDATED, newInfo };
}
export const SAVE_CHART_CONFIG_BEGIN = 'SAVE_CHART_CONFIG_BEGIN';

View File

@ -23,10 +23,11 @@ import {
ensureIsArray,
isFeatureEnabled,
FeatureFlag,
getSharedLabelColor,
getLabelsColorMap,
SupersetClient,
t,
getClientErrorObject,
getCategoricalSchemeRegistry,
} from '@superset-ui/core';
import {
addChart,
@ -64,6 +65,13 @@ import { fetchDatasourceMetadata } from './datasources';
import { updateDirectPathToFilter } from './dashboardFilters';
import { SET_FILTER_CONFIG_COMPLETE } from './nativeFilters';
import getOverwriteItems from '../util/getOverwriteItems';
import {
applyColors,
isLabelsColorMapSynced,
getLabelsColorMapEntries,
getColorSchemeDomain,
getColorNamespace,
} from '../../utils/colorScheme';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
export function setUnsavedChanges(hasUnsavedChanges) {
@ -261,7 +269,7 @@ export function saveDashboardRequest(data, id, saveType) {
slug: slug || null,
metadata: {
...data.metadata,
color_namespace: data.metadata?.color_namespace || undefined,
color_namespace: getColorNamespace(data.metadata?.color_namespace),
color_scheme: data.metadata?.color_scheme || '',
color_scheme_domain: data.metadata?.color_scheme_domain || [],
expanded_slices: data.metadata?.expanded_slices || {},
@ -584,7 +592,7 @@ export function removeSliceFromDashboard(id) {
return dispatch => {
dispatch(removeSlice(id));
dispatch(removeChart(id));
getSharedLabelColor().removeSlice(id);
getLabelsColorMap().removeSlice(id);
};
}
@ -657,3 +665,69 @@ export function setDatasetsStatus(status) {
status,
};
}
const updateDashboardMetadata = async (id, metadata, dispatch) => {
await SupersetClient.put({
endpoint: `/api/v1/dashboard/${id}`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ json_metadata: JSON.stringify(metadata) }),
});
dispatch(dashboardInfoChanged({ metadata }));
};
export const updateDashboardLabelsColor = () => async (dispatch, getState) => {
const {
dashboardInfo: { id, metadata },
} = getState();
const categoricalSchemes = getCategoricalSchemeRegistry();
const colorScheme = metadata?.color_scheme;
const colorSchemeRegistry = categoricalSchemes.get(
metadata?.color_scheme,
true,
);
const defaultScheme = categoricalSchemes.defaultKey;
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
const colorSchemeDomain = metadata?.color_scheme_domain || [];
try {
const updatedMetadata = { ...metadata };
let updatedScheme = metadata?.color_scheme;
// Color scheme does not exist anymore, fallback to default
if (colorScheme && !colorSchemeRegistry) {
updatedScheme = fallbackScheme;
updatedMetadata.color_scheme = updatedScheme;
updatedMetadata.color_scheme_domain = getColorSchemeDomain(colorScheme);
dispatch(setColorScheme(updatedScheme));
// must re-apply colors from fresh labels color map
applyColors(updatedMetadata, true);
}
// stored labels color map and applied might differ
const isMapSynced = isLabelsColorMapSynced(metadata);
if (!isMapSynced) {
// re-apply a fresh labels color map
applyColors(updatedMetadata, true);
// pull and store the just applied labels color map
updatedMetadata.shared_label_colors = getLabelsColorMapEntries();
}
// the stored color domain registry and fresh might differ at this point
const freshColorSchemeDomain = getColorSchemeDomain(colorScheme);
const isRegistrySynced =
colorSchemeDomain.toString() !== freshColorSchemeDomain.toString();
if (colorScheme && !isRegistrySynced) {
updatedMetadata.color_scheme_domain = freshColorSchemeDomain;
}
if (
(colorScheme && (!colorSchemeRegistry || !isRegistrySynced)) ||
!isMapSynced
) {
await updateDashboardMetadata(id, updatedMetadata, dispatch);
}
} catch (error) {
console.error('Failed to update dashboard color settings:', error);
}
};

View File

@ -51,7 +51,6 @@ import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import extractUrlParams from '../util/extractUrlParams';
import { updateColorSchema } from './dashboardInfo';
import updateComponentParentsList from '../util/updateComponentParentsList';
import { FilterBarOrientation } from '../types';
@ -71,16 +70,6 @@ export const hydrateDashboard =
chart.slice_id = chart.form_data.slice_id;
});
if (metadata?.shared_label_colors) {
updateColorSchema(metadata, metadata?.shared_label_colors);
}
// Priming the color palette with user's label-color mapping provided in
// the dashboard's JSON metadata
if (metadata?.label_colors) {
updateColorSchema(metadata, metadata?.label_colors);
}
// new dash: position_json could be {} or null
const layout =
positionData && Object.keys(positionData).length > 0

View File

@ -27,11 +27,11 @@ const propTypes = {
onChange: PropTypes.func,
labelMargin: PropTypes.number,
colorScheme: PropTypes.string,
hasCustomLabelColors: PropTypes.bool,
hasCustomLabelsColor: PropTypes.bool,
};
const defaultProps = {
hasCustomLabelColors: false,
hasCustomLabelsColor: false,
colorScheme: undefined,
onChange: () => {},
};
@ -50,7 +50,7 @@ class ColorSchemeControlWrapper extends PureComponent {
}
render() {
const { colorScheme, labelMargin = 0, hasCustomLabelColors } = this.props;
const { colorScheme, labelMargin = 0, hasCustomLabelsColor } = this.props;
return (
<ColorSchemeControl
description={t(
@ -64,7 +64,7 @@ class ColorSchemeControlWrapper extends PureComponent {
clearable
schemes={this.schemes}
hovered={this.state.hovered}
hasCustomLabelColors={hasCustomLabelColors}
hasCustomLabelsColor={hasCustomLabelsColor}
/>
);
}

View File

@ -18,14 +18,13 @@
*/
// ParentSize uses resize observer so the dashboard will update size
// when its container size changes, due to e.g., builder side panel opening
import { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import { FC, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Filter,
Filters,
getCategoricalSchemeRegistry,
SupersetClient,
useComponentDidUpdate,
LabelsColorMapSource,
getLabelsColorMap,
} from '@superset-ui/core';
import { ParentSize } from '@visx/responsive';
import { pick } from 'lodash';
@ -44,9 +43,12 @@ import {
import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
import { setInScopeStatusOfFilters } from 'src/dashboard/actions/nativeFilters';
import { dashboardInfoChanged } from 'src/dashboard/actions/dashboardInfo';
import { setColorScheme } from 'src/dashboard/actions/dashboardState';
import jsonStringify from 'json-stringify-pretty-compact';
import { updateDashboardLabelsColor } from 'src/dashboard/actions/dashboardState';
import {
applyColors,
getColorNamespace,
resetColors,
} from 'src/utils/colorScheme';
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
import { getRootLevelTabsComponent } from './utils';
@ -131,85 +133,6 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
dispatch(setInScopeStatusOfFilters(scopes));
}, [nativeFilterScopes, dashboardLayout, dispatch]);
const verifyUpdateColorScheme = useCallback(() => {
const currentMetadata = dashboardInfo.metadata;
if (currentMetadata?.color_scheme) {
const metadata = { ...currentMetadata };
const colorScheme = metadata?.color_scheme;
const colorSchemeDomain = metadata?.color_scheme_domain || [];
const categoricalSchemes = getCategoricalSchemeRegistry();
const registryColorScheme =
categoricalSchemes.get(colorScheme, true) || undefined;
const registryColorSchemeDomain = registryColorScheme?.colors || [];
const defaultColorScheme = categoricalSchemes.defaultKey;
const colorSchemeExists = !!registryColorScheme;
const updateDashboardData = () => {
SupersetClient.put({
endpoint: `/api/v1/dashboard/${dashboardInfo.id}`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
json_metadata: jsonStringify(metadata),
}),
}).catch(e => console.log(e));
};
const updateColorScheme = (scheme: string) => {
dispatch(setColorScheme(scheme));
};
const updateDashboard = () => {
dispatch(
dashboardInfoChanged({
metadata,
}),
);
updateDashboardData();
};
// selected color scheme does not exist anymore
// must fallback to the available default one
if (!colorSchemeExists) {
const updatedScheme =
defaultColorScheme?.toString() || 'supersetColors';
metadata.color_scheme = updatedScheme;
metadata.color_scheme_domain =
categoricalSchemes.get(defaultColorScheme)?.colors || [];
// reset shared_label_colors
// TODO: Requires regenerating the shared_label_colors after
// fixing a bug which affects their generation on dashboards with tabs
metadata.shared_label_colors = {};
updateColorScheme(updatedScheme);
updateDashboard();
} else {
// if this dashboard does not have a color_scheme_domain saved
// must create one and store it for the first time
if (colorSchemeExists && !colorSchemeDomain.length) {
metadata.color_scheme_domain = registryColorSchemeDomain;
updateDashboard();
}
// if the color_scheme_domain is not the same as the registry domain
// must update the existing color_scheme_domain
if (
colorSchemeExists &&
colorSchemeDomain.length &&
registryColorSchemeDomain.toString() !== colorSchemeDomain.toString()
) {
metadata.color_scheme_domain = registryColorSchemeDomain;
// reset shared_label_colors
// TODO: Requires regenerating the shared_label_colors after
// fixing a bug which affects their generation on dashboards with tabs
metadata.shared_label_colors = {};
updateColorScheme(colorScheme);
updateDashboard();
}
}
}
}, [chartIds]);
useComponentDidUpdate(verifyUpdateColorScheme);
const childIds: string[] = topLevelTabs
? topLevelTabs.children
: [DASHBOARD_GRID_ID];
@ -217,6 +140,29 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const activeKey = min === 0 ? DASHBOARD_GRID_ID : min.toString();
const TOP_OF_PAGE_RANGE = 220;
useEffect(() => {
// verify freshness of color map on tab change
// and when loading for first time
setTimeout(() => {
dispatch(updateDashboardLabelsColor());
}, 500);
}, [directPathToChild, dispatch]);
useEffect(() => {
const labelsColorMap = getLabelsColorMap();
const colorNamespace = getColorNamespace(
dashboardInfo?.metadata?.color_namespace,
);
labelsColorMap.source = LabelsColorMapSource.Dashboard;
// apply labels color as dictated by stored metadata
applyColors(dashboardInfo.metadata);
return () => {
resetColors(getColorNamespace(colorNamespace));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardInfo.id, dispatch]);
return (
<div className="grid-container" data-test="grid-container">
<ParentSize>

View File

@ -26,7 +26,6 @@ import {
isFeatureEnabled,
FeatureFlag,
t,
getSharedLabelColor,
getExtensionsRegistry,
} from '@superset-ui/core';
import { Global } from '@emotion/react';
@ -374,13 +373,10 @@ class Header extends PureComponent {
? currentRefreshFrequency
: dashboardInfo.metadata?.refresh_frequency;
const currentColorScheme =
dashboardInfo?.metadata?.color_scheme || colorScheme;
const currentColorNamespace =
dashboardInfo?.metadata?.color_namespace || colorNamespace;
const currentSharedLabelColors = Object.fromEntries(
getSharedLabelColor().getColorMap(),
);
const currentColorScheme =
dashboardInfo?.metadata?.color_scheme || colorScheme;
const data = {
certified_by: dashboardInfo.certified_by,
@ -397,7 +393,6 @@ class Header extends PureComponent {
color_scheme: currentColorScheme,
positions,
refresh_frequency: refreshFrequency,
shared_label_colors: currentSharedLabelColors,
},
};

View File

@ -25,12 +25,10 @@ import Button from 'src/components/Button';
import { AntdForm, AsyncSelect, Col, Row } from 'src/components';
import rison from 'rison';
import {
CategoricalColorNamespace,
ensureIsArray,
isFeatureEnabled,
FeatureFlag,
getCategoricalSchemeRegistry,
getSharedLabelColor,
styled,
SupersetClient,
t,
@ -46,6 +44,7 @@ import withToasts from 'src/components/MessageToasts/withToasts';
import TagType from 'src/types/TagType';
import { fetchTags, OBJECT_TYPES } from 'src/features/tags/tags';
import { loadTags } from 'src/components/Tags/utils';
import { applyColors, getColorNamespace } from 'src/utils/colorScheme';
const StyledFormItem = styled(FormItem)`
margin-bottom: 0;
@ -307,7 +306,6 @@ const PropertiesModal = ({
const { title, slug, certifiedBy, certificationDetails } =
form.getFieldsValue();
let currentColorScheme = colorScheme;
let colorNamespace = '';
let currentJsonMetadata = jsonMetadata;
// validate currentJsonMetadata
@ -325,11 +323,13 @@ const PropertiesModal = ({
return;
}
const copyMetadata = { ...metadata };
const colorNamespace = getColorNamespace(metadata?.color_namespace);
// color scheme in json metadata has precedence over selection
currentColorScheme = metadata?.color_scheme || colorScheme;
colorNamespace = metadata?.color_namespace;
// filter shared_label_color from user input
// remove information from user facing input
if (metadata?.shared_label_colors) {
delete metadata.shared_label_colors;
}
@ -337,22 +337,8 @@ const PropertiesModal = ({
delete metadata.color_scheme_domain;
}
const sharedLabelColor = getSharedLabelColor();
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNamespace);
categoricalNamespace.resetColors();
if (currentColorScheme) {
sharedLabelColor.updateColorMap(colorNamespace, currentColorScheme);
metadata.shared_label_colors = Object.fromEntries(
sharedLabelColor.getColorMap(),
);
metadata.color_scheme_domain =
categoricalSchemeRegistry.get(colorScheme)?.colors || [];
} else {
sharedLabelColor.reset();
metadata.shared_label_colors = {};
metadata.color_scheme_domain = [];
}
// only apply colors, the user has not saved yet
applyColors(copyMetadata, true);
currentJsonMetadata = jsonStringify(metadata);
@ -410,7 +396,7 @@ const PropertiesModal = ({
const getRowsWithoutRoles = () => {
const jsonMetadataObj = getJsonMetadata();
const hasCustomLabelColors = !!Object.keys(
const hasCustomLabelsColor = !!Object.keys(
jsonMetadataObj?.label_colors || {},
).length;
@ -440,7 +426,7 @@ const PropertiesModal = ({
<Col xs={24} md={12}>
<h3 style={{ marginTop: '1em' }}>{t('Colors')}</h3>
<ColorSchemeControlWrapper
hasCustomLabelColors={hasCustomLabelColors}
hasCustomLabelsColor={hasCustomLabelsColor}
onChange={onColorSchemeChange}
colorScheme={colorScheme}
labelMargin={4}
@ -452,7 +438,7 @@ const PropertiesModal = ({
const getRowsWithRoles = () => {
const jsonMetadataObj = getJsonMetadata();
const hasCustomLabelColors = !!Object.keys(
const hasCustomLabelsColor = !!Object.keys(
jsonMetadataObj?.label_colors || {},
).length;
@ -509,7 +495,7 @@ const PropertiesModal = ({
<Row>
<Col xs={24} md={12}>
<ColorSchemeControlWrapper
hasCustomLabelColors={hasCustomLabelColors}
hasCustomLabelsColor={hasCustomLabelsColor}
onChange={onColorSchemeChange}
colorScheme={colorScheme}
labelMargin={4}

View File

@ -65,8 +65,8 @@ const SyncDashboardState: FC<Props> = ({ dashboardPageId }) => {
DashboardContextForExplore
>(
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
labelColors: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
sharedLabelColors:
labelsColor: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
labelsColorMap:
dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
colorScheme: dashboardState?.colorScheme,
chartConfiguration:

View File

@ -54,8 +54,8 @@ const propTypes = {
// from redux
chart: chartPropShape.isRequired,
formData: PropTypes.object.isRequired,
labelColors: PropTypes.object,
sharedLabelColors: PropTypes.object,
labelsColor: PropTypes.object,
labelsColorMap: PropTypes.object,
datasource: PropTypes.object,
slice: slicePropShape.isRequired,
sliceName: PropTypes.string.isRequired,
@ -382,8 +382,8 @@ class Chart extends Component {
editMode,
filters,
formData,
labelColors,
sharedLabelColors,
labelsColor,
labelsColorMap,
updateSliceName,
sliceName,
toggleExpandSlice,
@ -509,8 +509,8 @@ class Chart extends Component {
dashboardId={dashboardId}
initialValues={initialValues}
formData={formData}
labelColors={labelColors}
sharedLabelColors={sharedLabelColors}
labelsColor={labelsColor}
labelsColorMap={labelsColorMap}
ownState={ownState}
filterState={filterState}
queriesResponse={chart.queriesResponse}

View File

@ -429,4 +429,5 @@ function mapStateToProps(state) {
directPathToChild: state.dashboardState.directPathToChild,
};
}
export default connect(mapStateToProps)(Tabs);

View File

@ -60,8 +60,8 @@ function mapStateToProps(
(chart && chart.form_data && datasources[chart.form_data.datasource]) ||
PLACEHOLDER_DATASOURCE;
const { colorScheme, colorNamespace, datasetsStatus } = dashboardState;
const labelColors = dashboardInfo?.metadata?.label_colors || {};
const sharedLabelColors = dashboardInfo?.metadata?.shared_label_colors || {};
const labelsColor = dashboardInfo?.metadata?.label_colors || {};
const labelsColorMap = dashboardInfo?.metadata?.shared_label_colors || {};
// note: this method caches filters if possible to prevent render cascades
const formData = getFormDataWithExtraFilters({
chart,
@ -75,8 +75,8 @@ function mapStateToProps(
allSliceIds: dashboardState.sliceIds,
dataMask,
extraControls,
labelColors,
sharedLabelColors,
labelsColor,
labelsColorMap,
});
formData.dashboardId = dashboardInfo.id;
@ -84,8 +84,8 @@ function mapStateToProps(
return {
chart,
datasource,
labelColors,
sharedLabelColors,
labelsColor,
labelsColorMap,
slice: sliceEntities.slices[id],
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
filters: getActiveFilters() || EMPTY_OBJECT,

View File

@ -78,6 +78,7 @@ function mapStateToProps(
editMode: dashboardState.editMode,
filters: getActiveFilters(),
dashboardId: dashboardInfo.id,
dashboardInfo,
fullSizeChartId: dashboardState.fullSizeChartId,
};

View File

@ -19,13 +19,7 @@
import { createContext, lazy, FC, useEffect, useMemo, useRef } from 'react';
import { Global } from '@emotion/react';
import { useHistory } from 'react-router-dom';
import {
CategoricalColorNamespace,
getSharedLabelColor,
SharedLabelColorSource,
t,
useTheme,
} from '@superset-ui/core';
import { t, useTheme } from '@superset-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import Loading from 'src/components/Loading';
@ -101,7 +95,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
const error = dashboardApiError || chartsApiError;
const readyToRender = Boolean(dashboard && charts);
const { dashboard_title, css, metadata, id = 0 } = dashboard || {};
const { dashboard_title, css, id = 0 } = dashboard || {};
useEffect(() => {
// mark tab id as redundant when user closes browser tab - a new id will be
@ -187,19 +181,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
return () => {};
}, [css]);
useEffect(() => {
const sharedLabelColor = getSharedLabelColor();
sharedLabelColor.source = SharedLabelColorSource.Dashboard;
return () => {
// clean up label color
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
metadata?.color_namespace,
);
categoricalNamespace.resetColors();
sharedLabelColor.clear();
};
}, [metadata?.color_namespace]);
useEffect(() => {
if (datasetsApiError) {
addDangerToast(

View File

@ -43,8 +43,8 @@ export interface GetFormDataWithExtraFiltersArguments {
dataMask: DataMaskStateWithId;
nativeFilters: PartialFilters;
extraControls: Record<string, string | boolean | null>;
labelColors?: Record<string, string>;
sharedLabelColors?: Record<string, string>;
labelsColor?: Record<string, string>;
labelsColorMap?: Record<string, string>;
allSliceIds: number[];
}
@ -61,8 +61,8 @@ export default function getFormDataWithExtraFilters({
sliceId,
dataMask,
extraControls,
labelColors,
sharedLabelColors,
labelsColor,
labelsColorMap,
allSliceIds,
}: GetFormDataWithExtraFiltersArguments) {
// if dashboard metadata + filters have not changed, use cache if possible
@ -75,10 +75,10 @@ export default function getFormDataWithExtraFilters({
areObjectsEqual(cachedFormData?.color_namespace, colorNamespace, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.label_colors, labelColors, {
areObjectsEqual(cachedFormData?.label_colors, labelsColor, {
ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.shared_label_colors, sharedLabelColors, {
areObjectsEqual(cachedFormData?.shared_label_colors, labelsColorMap, {
ignoreUndefined: true,
}) &&
!!cachedFormData &&
@ -110,8 +110,8 @@ export default function getFormDataWithExtraFilters({
const formData = {
...chart.form_data,
label_colors: labelColors,
shared_label_colors: sharedLabelColors,
label_colors: labelsColor,
shared_label_colors: labelsColorMap,
...(colorScheme && { color_scheme: colorScheme }),
extra_filters: getEffectiveExtraFilters(filters),
...extraData,

View File

@ -21,14 +21,7 @@ import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { Tooltip } from 'src/components/Tooltip';
import {
CategoricalColorNamespace,
css,
logging,
SupersetClient,
t,
tn,
} from '@superset-ui/core';
import { css, logging, SupersetClient, t, tn } from '@superset-ui/core';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import AlteredSliceTag from 'src/components/AlteredSliceTag';
import Button from 'src/components/Button';
@ -38,6 +31,7 @@ import { sliceUpdated } from 'src/explore/actions/exploreActions';
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
import MetadataBar, { MetadataType } from 'src/components/MetadataBar';
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
import { applyColors, resetColors } from 'src/utils/colorScheme';
import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
const propTypes = {
@ -96,6 +90,13 @@ export const ExploreChartHeader = ({
const dashboard =
dashboardId && dashboards && dashboards.find(d => d.id === dashboardId);
if (!dashboard) {
// clean up color namespace and shared color maps
// to avoid colors spill outside of dashboard context
resetColors(metadata?.color_namespace);
return;
}
if (dashboard) {
try {
// Dashboards from metadata don't contain the json_metadata field
@ -106,23 +107,8 @@ export const ExploreChartHeader = ({
const result = response?.json?.result;
// setting the chart to use the dashboard custom label colors if any
const metadata = JSON.parse(result.json_metadata);
const sharedLabelColors = metadata.shared_label_colors || {};
const customLabelColors = metadata.label_colors || {};
const mergedLabelColors = {
...sharedLabelColors,
...customLabelColors,
};
const categoricalNamespace = CategoricalColorNamespace.getNamespace();
Object.keys(mergedLabelColors).forEach(label => {
categoricalNamespace.setColor(
label,
mergedLabelColors[label],
metadata.color_scheme,
);
});
const dashboardMetadata = JSON.parse(result.json_metadata);
applyColors(dashboardMetadata);
} catch (error) {
logging.info(t('Unable to retrieve dashboard colors'));
}
@ -130,7 +116,7 @@ export const ExploreChartHeader = ({
};
useEffect(() => {
if (dashboardId) updateCategoricalNamespace();
updateCategoricalNamespace();
}, []);
const openPropertiesModal = () => {

View File

@ -28,7 +28,7 @@ import { render, screen, waitFor } from 'spec/helpers/testing-library';
import ColorSchemeControl, { ColorSchemes } from '.';
const defaultProps = () => ({
hasCustomLabelColors: false,
hasCustomLabelsColor: false,
label: 'Color scheme',
labelMargin: 0,
name: 'color',
@ -58,7 +58,7 @@ test('should display a label', async () => {
expect(await screen.findByText('Color scheme')).toBeTruthy();
});
test('should not display an alert icon if hasCustomLabelColors=false', async () => {
test('should not display an alert icon if hasCustomLabelsColor=false', async () => {
setup();
await waitFor(() => {
expect(
@ -67,11 +67,12 @@ test('should not display an alert icon if hasCustomLabelColors=false', async ()
});
});
test('should display an alert icon if hasCustomLabelColors=true', async () => {
const hasCustomLabelColorsProps = {
hasCustomLabelColors: true,
test('should display an alert icon if hasCustomLabelsColor=true', async () => {
const hasCustomLabelsColorProps = {
...defaultProps,
hasCustomLabelsColor: true,
};
setup(hasCustomLabelColorsProps);
setup(hasCustomLabelsColorProps);
await waitFor(() => {
expect(
screen.getByRole('img', { name: 'alert-solid' }),

View File

@ -31,17 +31,17 @@ export default function ColorSchemeLabel(props: ColorSchemeLabelProps) {
const { id, label, colors } = props;
const [showTooltip, setShowTooltip] = useState<boolean>(false);
const labelNameRef = useRef<HTMLElement>(null);
const labelColorsRef = useRef<HTMLElement>(null);
const labelsColorRef = useRef<HTMLElement>(null);
const handleShowTooltip = () => {
const labelNameElement = labelNameRef.current;
const labelColorsElement = labelColorsRef.current;
const labelsColorElement = labelsColorRef.current;
if (
labelNameElement &&
labelColorsElement &&
labelsColorElement &&
(labelNameElement.scrollWidth > labelNameElement.offsetWidth ||
labelNameElement.scrollHeight > labelNameElement.offsetHeight ||
labelColorsElement.scrollWidth > labelColorsElement.offsetWidth ||
labelColorsElement.scrollHeight > labelColorsElement.offsetHeight)
labelsColorElement.scrollWidth > labelsColorElement.offsetWidth ||
labelsColorElement.scrollHeight > labelsColorElement.offsetHeight)
) {
setShowTooltip(true);
}
@ -109,7 +109,7 @@ export default function ColorSchemeLabel(props: ColorSchemeLabelProps) {
{label}
</span>
<span
ref={labelColorsRef}
ref={labelsColorRef}
css={(theme: SupersetTheme) => css`
flex: 100%;
text-overflow: ellipsis;

View File

@ -46,7 +46,7 @@ export interface ColorSchemes {
}
export interface ColorSchemeControlProps {
hasCustomLabelColors: boolean;
hasCustomLabelsColor: boolean;
dashboardId?: number;
label: string;
name: string;
@ -75,14 +75,14 @@ const DASHBOARD_ALERT = t(
const Label = ({
label,
hasCustomLabelColors,
hasCustomLabelsColor,
dashboardId,
}: Pick<
ColorSchemeControlProps,
'label' | 'hasCustomLabelColors' | 'dashboardId'
'label' | 'hasCustomLabelsColor' | 'dashboardId'
>) => {
if (hasCustomLabelColors || dashboardId) {
const alertTitle = hasCustomLabelColors
if (hasCustomLabelsColor || dashboardId) {
const alertTitle = hasCustomLabelsColor
? CUSTOM_LABEL_ALERT
: DASHBOARD_ALERT;
return (
@ -98,7 +98,7 @@ const Label = ({
};
const ColorSchemeControl = ({
hasCustomLabelColors = false,
hasCustomLabelsColor = false,
dashboardId,
label = t('Color scheme'),
onChange = () => {},
@ -231,7 +231,7 @@ const ColorSchemeControl = ({
label={
<Label
label={label}
hasCustomLabelColors={hasCustomLabelColors}
hasCustomLabelsColor={hasCustomLabelsColor}
dashboardId={dashboardId}
/>
}

View File

@ -20,11 +20,11 @@ import { useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import {
getSharedLabelColor,
getLabelsColorMap,
isDefined,
JsonObject,
makeApi,
SharedLabelColorSource,
LabelsColorMapSource,
t,
getClientErrorObject,
} from '@superset-ui/core';
@ -84,8 +84,8 @@ const getDashboardContextFormData = () => {
if (dashboardContext) {
const sliceId = getUrlParam(URL_PARAMS.sliceId) || 0;
const {
labelColors,
sharedLabelColors,
labelsColor,
labelsColorMap,
colorScheme,
chartConfiguration,
nativeFilters,
@ -100,8 +100,8 @@ const getDashboardContextFormData = () => {
chartConfiguration,
colorScheme,
dataMask,
labelColors,
sharedLabelColors,
labelsColor,
labelsColorMap,
sliceId,
allSliceIds: [sliceId],
extraControls: {},
@ -151,7 +151,7 @@ export default function ExplorePage() {
isExploreInitialized.current = true;
});
}
getSharedLabelColor().source = SharedLabelColorSource.Explore;
getLabelsColorMap().source = LabelsColorMapSource.Explore;
}, [dispatch, location]);
if (!isLoaded) {

View File

@ -24,8 +24,8 @@ import {
import { ChartConfiguration } from 'src/dashboard/types';
export interface DashboardContextForExplore {
labelColors: Record<string, string>;
sharedLabelColors: Record<string, string>;
labelsColor: Record<string, string>;
labelsColorMap: Record<string, string>;
colorScheme: string;
chartConfiguration: ChartConfiguration;
nativeFilters: PartialFilters;

View File

@ -0,0 +1,140 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
CategoricalColorNamespace,
getCategoricalSchemeRegistry,
getLabelsColorMap,
} from '@superset-ui/core';
/**
* Forces falsy namespace values to undefined to default to GLOBAL
*
* @param namespace
* @returns - namespace or default undefined
*/
export const getColorNamespace = (namespace?: string) => namespace || undefined;
/**
* Get the labels color map entries
*
* @returns Record<string, string>
*/
export const getLabelsColorMapEntries = (): Record<string, string> => {
const labelsColorMapInstance = getLabelsColorMap();
const updatedLabelsColorMapEntries = Object.fromEntries(
labelsColorMapInstance.getColorMap(),
);
return updatedLabelsColorMapEntries;
};
export const getColorSchemeDomain = (colorScheme: string) =>
getCategoricalSchemeRegistry().get(colorScheme)?.colors || [];
/**
* Compare the current labels color map with a fresh one
*
* @param currentLabelsColorMap - the current labels color map
* @returns true if the labels color map is the same as fresh
*/
export const isLabelsColorMapSynced = (
metadata: Record<string, any>,
): boolean => {
const currentLabelsColorMap = metadata?.shared_label_colors || {};
const customLabelColors = metadata?.label_colors || {};
const freshLabelsColorMap = getLabelsColorMap().getColorMap();
const isSynced = Array.from(freshLabelsColorMap.entries()).every(
([label, color]) =>
currentLabelsColorMap.hasOwnProperty(label) &&
(currentLabelsColorMap[label] === color ||
customLabelColors[label] !== undefined),
);
return isSynced;
};
/**
* Annihilate color maps
*
* @param color_namespace - the categorical namespace
*/
export const resetColors = (color_namespace?: string) => {
const labelsColorMapInstance = getLabelsColorMap();
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
getColorNamespace(color_namespace),
);
categoricalNamespace.resetColors();
labelsColorMapInstance.clear();
};
/**
* Update the labels color map based on current color scheme
* It will respect custom label colors if set via namespace
*
* @param namespace - the color namespace
* @param colorScheme - the current color scheme
*/
export const refreshLabelsColorMap = (
namespace?: string,
colorScheme?: string,
) => {
const colorNameSpace = getColorNamespace(namespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const labelsColorMapInstance = getLabelsColorMap();
labelsColorMapInstance.updateColorMap(categoricalNamespace, colorScheme);
};
/**
* Merge labels colors with custom labels colors
* Apply labels color based on chosen color scheme
*
* @param metadata - the dashboard metadata object
*/
export const applyColors = (metadata: Record<string, any>, fresh = false) => {
const colorNameSpace = getColorNamespace(metadata?.color_namespace);
const categoricalNamespace =
CategoricalColorNamespace.getNamespace(colorNameSpace);
const colorScheme = metadata?.color_scheme;
const customLabelColors = metadata?.label_colors || {};
// when scheme unset, update only custom label colors
const labelsColorMap = metadata?.shared_label_colors || {};
// reset forced colors (custom labels + labels color map)
categoricalNamespace.resetColors();
// apply custom label colors first
Object.keys(customLabelColors).forEach(label => {
categoricalNamespace.setColor(label, customLabelColors[label]);
});
// re-instantiate a fresh labels color map based on current scheme
// will consider also just applied custom label colors
refreshLabelsColorMap(metadata?.color_namespace, colorScheme);
// get the fresh map that was just updated or existing
const labelsColorMapEntries = fresh
? getLabelsColorMapEntries()
: labelsColorMap;
// apply the final color map
Object.keys(labelsColorMapEntries).forEach(label => {
categoricalNamespace.setColor(label, labelsColorMapEntries[label]);
});
};

View File

@ -75,6 +75,7 @@ FEATURE_FLAGS = {
"SHARE_QUERIES_VIA_KV_STORE": True,
"ENABLE_TEMPLATE_PROCESSING": True,
"ALERT_REPORTS": True,
"AVOID_COLORS_COLLISION": True,
"DRILL_TO_DETAIL": True,
"DRILL_BY": True,
"HORIZONTAL_FILTER_BAR": True,