mirror of https://github.com/apache/superset.git
Merge 36795f2061
into f5843fe588
This commit is contained in:
commit
1d9c3e9e00
|
@ -88,6 +88,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
|
||||
|
|
|
@ -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(51, 61, 71)');
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
|
||||
// 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(51, 61, 71)');
|
||||
.should('have.css', 'fill', 'rgb(234, 11, 140)');
|
||||
});
|
||||
|
||||
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);
|
||||
|
|
|
@ -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)');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-dupe-class-members */
|
||||
import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
|
||||
import { ExtensibleFunction } from '../models';
|
||||
import { ColorsInitLookup, ColorsLookup } from './types';
|
||||
|
@ -39,99 +38,172 @@ class CategoricalColorScale extends ExtensibleFunction {
|
|||
|
||||
scale: ScaleOrdinal<{ toString(): string }, string>;
|
||||
|
||||
parentForcedColors: ColorsLookup;
|
||||
|
||||
forcedColors: ColorsLookup;
|
||||
|
||||
sharedColorMapInstance: ReturnType<typeof getSharedLabelColor>;
|
||||
|
||||
sliceMap: 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 = {}) {
|
||||
constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
|
||||
super((value: string, sliceId?: number) => this.getColor(value, sliceId));
|
||||
|
||||
// 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.sliceMap = new Map();
|
||||
// shared color map instance (when context is shared, i.e. dashboard)
|
||||
this.sharedColorMapInstance = getSharedLabelColor();
|
||||
// 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);
|
||||
/**
|
||||
* Increments 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
|
||||
* @param sliceId
|
||||
* @returns the color for the given value
|
||||
*/
|
||||
getColor(value?: string, sliceId?: number): string {
|
||||
const cleanedValue = stringifyAndTrim(value);
|
||||
const sharedLabelColor = getSharedLabelColor();
|
||||
const sharedColorMap = sharedLabelColor.getColorMap();
|
||||
const sharedColorMap = this.sharedColorMapInstance.getColorMap();
|
||||
const sharedColor = sharedColorMap.get(cleanedValue);
|
||||
// priority: forced color (i.e. custom label colors) > shared color > scale color
|
||||
const forcedColor = this.forcedColors?.[cleanedValue] || sharedColor;
|
||||
const isExistingLabel = this.sliceMap.has(cleanedValue);
|
||||
let color = forcedColor || this.scale(cleanedValue);
|
||||
|
||||
// priority: parentForcedColors > forcedColors > labelColors
|
||||
let color =
|
||||
this.parentForcedColors?.[cleanedValue] ||
|
||||
this.forcedColors?.[cleanedValue] ||
|
||||
sharedColor;
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
|
||||
const multiple = Math.floor(
|
||||
this.domain().length / this.originColors.length,
|
||||
);
|
||||
if (multiple > this.multiple) {
|
||||
this.multiple = multiple;
|
||||
const newRange = getAnalogousColors(this.originColors, multiple);
|
||||
this.range(this.originColors.concat(newRange));
|
||||
// a forced color will always be used independently of the usage count
|
||||
if (!forcedColor && !isExistingLabel) {
|
||||
if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
|
||||
this.incrementColorRange();
|
||||
}
|
||||
}
|
||||
const newColor = this.scale(cleanedValue);
|
||||
if (!color) {
|
||||
color = newColor;
|
||||
if (isFeatureEnabled(FeatureFlag.AvoidColorsCollision)) {
|
||||
this.removeSharedLabelColorFromRange(sharedColorMap, cleanedValue);
|
||||
color = this.scale(cleanedValue);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
sharedLabelColor.addSlice(cleanedValue, color, sliceId);
|
||||
// keep track of values in this slice
|
||||
this.sliceMap.set(cleanedValue, color);
|
||||
|
||||
// store the value+color in the shared context
|
||||
if (sliceId) {
|
||||
this.sharedColorMapInstance.addSlice(cleanedValue, color, sliceId);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce specific color for given value
|
||||
*
|
||||
* @param color
|
||||
* @returns whether 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
|
||||
* @param color
|
||||
* @returns the count of the color usage in this slice
|
||||
*/
|
||||
getColorUsageCount(currentColor: string): number {
|
||||
let count = 0;
|
||||
this.sliceMap.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 SharedLabelColorSingleton
|
||||
*
|
||||
* @param currentColor
|
||||
* @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
|
||||
*/
|
||||
|
@ -153,7 +225,6 @@ class CategoricalColorScale extends ExtensibleFunction {
|
|||
return {
|
||||
...colorMap,
|
||||
...this.forcedColors,
|
||||
...this.parentForcedColors,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -163,12 +234,11 @@ class CategoricalColorScale extends ExtensibleFunction {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,10 +43,10 @@ export class SharedLabelColor {
|
|||
CategoricalColorNamespace.getNamespace(colorNamespace);
|
||||
const newColorMap = new Map();
|
||||
this.colorMap.clear();
|
||||
this.sliceLabelMap.forEach(labels => {
|
||||
this.sliceLabelMap.forEach((labels, sliceId) => {
|
||||
const colorScale = categoricalNamespace.getScale(colorScheme);
|
||||
labels.forEach(label => {
|
||||
const newColor = colorScale(label);
|
||||
const newColor = colorScale.getColor(label, sliceId);
|
||||
newColorMap.set(label, newColor);
|
||||
});
|
||||
});
|
||||
|
@ -57,12 +57,8 @@ export class SharedLabelColor {
|
|||
return this.colorMap;
|
||||
}
|
||||
|
||||
addSlice(label: string, color: string, sliceId?: number) {
|
||||
if (
|
||||
this.source !== SharedLabelColorSource.Dashboard ||
|
||||
sliceId === undefined
|
||||
)
|
||||
return;
|
||||
addSlice(label: string, color: string, sliceId: number) {
|
||||
if (this.source !== SharedLabelColorSource.Dashboard) return;
|
||||
const labels = this.sliceLabelMap.get(sliceId) || [];
|
||||
if (!labels.includes(label)) {
|
||||
labels.push(label);
|
||||
|
@ -83,13 +79,6 @@ 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.colorMap.clear();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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]
|
||||
>;
|
||||
let getNextAvailableColorSpy: jest.SpyInstance<
|
||||
string,
|
||||
[currentColor: string]
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||
// Spy on the addSlice method of sharedColorMapInstance
|
||||
addSliceSpy = jest.spyOn(scale.sharedColorMapInstance, '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,68 @@ 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 sliceMap and calls addSlice', () => {
|
||||
const value = 'testValue';
|
||||
const sliceId = 123;
|
||||
|
||||
it('should remove shared color from range if avoid colors collision enabled', () => {
|
||||
expect(scale.sliceMap.has(value)).toBe(false);
|
||||
|
||||
scale.getColor(value, sliceId);
|
||||
|
||||
expect(scale.sliceMap.has(value)).toBe(true);
|
||||
expect(scale.sliceMap.get(value)).toBeDefined();
|
||||
|
||||
expect(addSliceSpy).toHaveBeenCalledWith(
|
||||
value,
|
||||
expect.any(String),
|
||||
sliceId,
|
||||
);
|
||||
|
||||
const expectedColor = scale.sliceMap.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 +211,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 +225,7 @@ describe('CategoricalColorScale', () => {
|
|||
expect(scale2.getColorMap()).toEqual({
|
||||
cow: 'black',
|
||||
pig: 'pink',
|
||||
horse: 'green',
|
||||
horse: 'blue', // least used color
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -230,10 +278,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 +396,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -94,13 +94,7 @@ describe('SharedLabelColor', () => {
|
|||
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');
|
||||
sharedLabelColor.addSlice('a', 'red', 1);
|
||||
expect(Object.fromEntries(sharedLabelColor.sliceLabelMap)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
@ -142,8 +136,8 @@ describe('SharedLabelColor', () => {
|
|||
const colorMap = sharedLabelColor.getColorMap();
|
||||
expect(Object.fromEntries(colorMap)).toEqual({
|
||||
a: 'yellow',
|
||||
b: 'yellow',
|
||||
c: 'green',
|
||||
b: 'green',
|
||||
c: 'yellow',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -193,9 +187,9 @@ describe('SharedLabelColor', () => {
|
|||
const sharedLabelColor = getSharedLabelColor();
|
||||
sharedLabelColor.addSlice('a', 'red', 1);
|
||||
sharedLabelColor.addSlice('b', 'blue', 2);
|
||||
sharedLabelColor.reset();
|
||||
sharedLabelColor.clear();
|
||||
const colorMap = sharedLabelColor.getColorMap();
|
||||
expect(Object.fromEntries(colorMap)).toEqual({ a: '', b: '' });
|
||||
expect(Object.fromEntries(colorMap)).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,16 +58,15 @@ export function dashboardInfoChanged(newInfo: { metadata: any }) {
|
|||
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
|
||||
metadata?.color_namespace,
|
||||
);
|
||||
|
||||
// reset forced colors
|
||||
categoricalNamespace.resetColors();
|
||||
|
||||
if (metadata?.shared_label_colors) {
|
||||
updateColorSchema(metadata, metadata?.shared_label_colors);
|
||||
}
|
||||
const mergedLabelColors = {
|
||||
...(metadata?.label_colors || {}),
|
||||
...(metadata?.shared_label_colors || {}),
|
||||
};
|
||||
|
||||
if (metadata?.label_colors) {
|
||||
updateColorSchema(metadata, metadata?.label_colors);
|
||||
}
|
||||
updateColorSchema(metadata, mergedLabelColors);
|
||||
|
||||
return { type: DASHBOARD_INFO_UPDATED, newInfo };
|
||||
}
|
||||
|
|
|
@ -372,13 +372,17 @@ class Header extends React.PureComponent {
|
|||
? currentRefreshFrequency
|
||||
: dashboardInfo.metadata?.refresh_frequency;
|
||||
|
||||
const currentColorScheme =
|
||||
dashboardInfo?.metadata?.color_scheme || colorScheme;
|
||||
const currentColorNamespace =
|
||||
dashboardInfo?.metadata?.color_namespace || colorNamespace;
|
||||
const currentColorScheme =
|
||||
dashboardInfo?.metadata?.color_scheme || colorScheme;
|
||||
const currentSharedLabelColors = Object.fromEntries(
|
||||
getSharedLabelColor().getColorMap(),
|
||||
);
|
||||
// an empty color scheme should not bring shared label colors forward
|
||||
const sharedLabelColors = currentColorScheme
|
||||
? currentSharedLabelColors
|
||||
: {};
|
||||
|
||||
const data = {
|
||||
certified_by: dashboardInfo.certified_by,
|
||||
|
@ -395,7 +399,7 @@ class Header extends React.PureComponent {
|
|||
color_scheme: currentColorScheme,
|
||||
positions,
|
||||
refresh_frequency: refreshFrequency,
|
||||
shared_label_colors: currentSharedLabelColors,
|
||||
shared_label_colors: sharedLabelColors,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -381,16 +381,21 @@ const PropertiesModal = ({
|
|||
const sharedLabelColor = getSharedLabelColor();
|
||||
const categoricalNamespace =
|
||||
CategoricalColorNamespace.getNamespace(colorNamespace);
|
||||
|
||||
// reset forced colors
|
||||
categoricalNamespace.resetColors();
|
||||
|
||||
if (currentColorScheme) {
|
||||
// reset the shared label color map based on the current color scheme
|
||||
sharedLabelColor.updateColorMap(colorNamespace, currentColorScheme);
|
||||
// store the shared label color map and domain in the metadata
|
||||
metadata.shared_label_colors = Object.fromEntries(
|
||||
sharedLabelColor.getColorMap(),
|
||||
);
|
||||
metadata.color_scheme_domain =
|
||||
categoricalSchemeRegistry.get(colorScheme)?.colors || [];
|
||||
} else {
|
||||
sharedLabelColor.reset();
|
||||
sharedLabelColor.clear();
|
||||
metadata.shared_label_colors = {};
|
||||
metadata.color_scheme_domain = [];
|
||||
}
|
||||
|
|
|
@ -18,11 +18,19 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import {
|
||||
getSharedLabelColor,
|
||||
styled,
|
||||
SupersetClient,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { connect } from 'react-redux';
|
||||
import { LineEditableTabs } from 'src/components/Tabs';
|
||||
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
|
||||
import { AntdModal } from 'src/components';
|
||||
import { isEqual } from 'lodash';
|
||||
import jsonStringify from 'json-stringify-pretty-compact';
|
||||
import { dashboardInfoChanged } from 'src/dashboard/actions/dashboardInfo';
|
||||
import { Draggable } from '../dnd/DragDroppable';
|
||||
import DragHandle from '../dnd/DragHandle';
|
||||
import DashboardComponent from '../../containers/DashboardComponent';
|
||||
|
@ -256,10 +264,51 @@ export class Tabs extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
// the initial color map is generated on save and catches all rendered charts
|
||||
// charts hidden in nested tabs are not rendered, thus not included
|
||||
// must update the label color map when switching tabs to include these charts
|
||||
updateLabelColorsMap() {
|
||||
const currentLabelColorsMap = Object.fromEntries(
|
||||
getSharedLabelColor().getColorMap(),
|
||||
);
|
||||
const { metadata } = this.props.dashboardInfo;
|
||||
const labelColorsMap = metadata?.shared_label_colors || {};
|
||||
// color consistency is currently only supported when a color scheme is set
|
||||
if (
|
||||
metadata?.color_scheme &&
|
||||
!isEqual(labelColorsMap, currentLabelColorsMap)
|
||||
) {
|
||||
const updatedMetadata = {
|
||||
...metadata,
|
||||
shared_label_colors: currentLabelColorsMap,
|
||||
};
|
||||
SupersetClient.put({
|
||||
endpoint: `/api/v1/dashboard/${this.props.dashboardId}`,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
json_metadata: jsonStringify(updatedMetadata),
|
||||
}),
|
||||
})
|
||||
.then(() =>
|
||||
this.props.dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: updatedMetadata,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch(e => console.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
handleClickTab(tabIndex) {
|
||||
const { component } = this.props;
|
||||
const { children: tabIds } = component;
|
||||
|
||||
// the charts need to render first
|
||||
setTimeout(() => {
|
||||
this.updateLabelColorsMap();
|
||||
}, 500);
|
||||
|
||||
if (tabIndex !== this.state.tabIndex) {
|
||||
const pathToTabIndex = getDirectPathToTabIndex(component, tabIndex);
|
||||
const targetTabId = pathToTabIndex[pathToTabIndex.length - 1];
|
||||
|
|
|
@ -78,6 +78,7 @@ function mapStateToProps(
|
|||
editMode: dashboardState.editMode,
|
||||
filters: getActiveFilters(),
|
||||
dashboardId: dashboardInfo.id,
|
||||
dashboardInfo,
|
||||
fullSizeChartId: dashboardState.fullSizeChartId,
|
||||
};
|
||||
|
||||
|
|
|
@ -190,6 +190,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
|||
useEffect(() => {
|
||||
const sharedLabelColor = getSharedLabelColor();
|
||||
sharedLabelColor.source = SharedLabelColorSource.Dashboard;
|
||||
|
||||
return () => {
|
||||
// clean up label color
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
|
||||
|
|
|
@ -24,6 +24,7 @@ import { Tooltip } from 'src/components/Tooltip';
|
|||
import {
|
||||
CategoricalColorNamespace,
|
||||
css,
|
||||
getSharedLabelColor,
|
||||
logging,
|
||||
SupersetClient,
|
||||
t,
|
||||
|
@ -95,6 +96,16 @@ export const ExploreChartHeader = ({
|
|||
const { dashboards } = metadata || {};
|
||||
const dashboard =
|
||||
dashboardId && dashboards && dashboards.find(d => d.id === dashboardId);
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace();
|
||||
const sharedLabelColor = getSharedLabelColor();
|
||||
|
||||
if (!dashboard) {
|
||||
// clean up color namespace and shared color maps
|
||||
// to avoid colors spill outside of dashboard context
|
||||
categoricalNamespace.resetColors();
|
||||
sharedLabelColor.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dashboard) {
|
||||
try {
|
||||
|
@ -106,21 +117,19 @@ 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 dashboardMetadata = JSON.parse(result.json_metadata);
|
||||
const sharedLabelColors = dashboardMetadata.shared_label_colors || {};
|
||||
const customLabelColors = dashboardMetadata.label_colors || {};
|
||||
const mergedLabelColors = {
|
||||
...sharedLabelColors,
|
||||
...customLabelColors,
|
||||
};
|
||||
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace();
|
||||
|
||||
Object.keys(mergedLabelColors).forEach(label => {
|
||||
categoricalNamespace.setColor(
|
||||
label,
|
||||
mergedLabelColors[label],
|
||||
metadata.color_scheme,
|
||||
dashboardMetadata.color_scheme,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -130,7 +139,7 @@ export const ExploreChartHeader = ({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboardId) updateCategoricalNamespace();
|
||||
updateCategoricalNamespace();
|
||||
}, []);
|
||||
|
||||
const openPropertiesModal = () => {
|
||||
|
|
|
@ -71,6 +71,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,
|
||||
|
|
Loading…
Reference in New Issue