From 90c9daea08cd59ba7261c13e1ce4e80a72f84b48 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Fri, 1 Apr 2022 23:42:30 +0800 Subject: [PATCH] feat(color): support analogous colors to prevent color conflict (#19325) * feat(color): support analogous colors * fix test * fix range * add some comment --- .../src/color/CategoricalColorScale.ts | 16 ++++++++++ .../src/color/SharedLabelColorSingleton.ts | 19 +++--------- .../superset-ui-core/src/color/utils.ts | 22 ++++++++++++++ .../test/color/CategoricalColorScale.test.ts | 29 +++++-------------- 4 files changed, 50 insertions(+), 36 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts index d34960dac097..c6f37e4ff771 100644 --- a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts +++ b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts @@ -23,6 +23,7 @@ import { ExtensibleFunction } from '../models'; import { ColorsLookup } from './types'; import stringifyAndTrim from './stringifyAndTrim'; import getSharedLabelColor from './SharedLabelColorSingleton'; +import { getAnalogousColors } from './utils'; // Use type augmentation to correct the fact that // an instance of CategoricalScale is also a function @@ -31,6 +32,8 @@ interface CategoricalColorScale { } class CategoricalColorScale extends ExtensibleFunction { + originColors: string[]; + colors: string[]; scale: ScaleOrdinal<{ toString(): string }, string>; @@ -39,6 +42,8 @@ class CategoricalColorScale extends ExtensibleFunction { forcedColors: ColorsLookup; + multiple: number; + /** * Constructor * @param {*} colors an array of colors @@ -48,11 +53,13 @@ class CategoricalColorScale extends ExtensibleFunction { constructor(colors: string[], parentForcedColors?: ColorsLookup) { super((value: string, sliceId?: number) => this.getColor(value, sliceId)); + this.originColors = colors; this.colors = colors; this.scale = scaleOrdinal<{ toString(): string }, string>(); this.scale.range(colors); this.parentForcedColors = parentForcedColors; this.forcedColors = {}; + this.multiple = 0; } getColor(value?: string, sliceId?: number) { @@ -72,6 +79,15 @@ class CategoricalColorScale extends ExtensibleFunction { return forcedColor; } + 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)); + } + const color = this.scale(cleanedValue); sharedLabelColor.addSlice(cleanedValue, color, sliceId); diff --git a/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts b/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts index 227b565276a9..d2a59ac7c292 100644 --- a/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts +++ b/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts @@ -17,9 +17,9 @@ * under the License. */ -import tinycolor from 'tinycolor2'; import { CategoricalColorNamespace } from '.'; import makeSingleton from '../utils/makeSingleton'; +import { getAnalogousColors } from './utils'; export class SharedLabelColor { sliceLabelColorMap: Record>; @@ -39,27 +39,16 @@ export class SharedLabelColor { CategoricalColorNamespace.getNamespace(colorNamespace); const colors = categoricalNamespace.getScale(colorScheme).range(); const sharedLabels = this.getSharedLabels(); - const generatedColors: tinycolor.Instance[] = []; + let generatedColors: string[] = []; let sharedLabelMap; if (sharedLabels.length) { const multiple = Math.ceil(sharedLabels.length / colors.length); - const ext = 5; - const analogousColors = colors.map(color => { - const result = tinycolor(color).analogous(multiple + ext); - return result.slice(ext); - }); - - // [[A, AA, AAA], [B, BB, BBB]] => [A, B, AA, BB, AAA, BBB] - while (analogousColors[analogousColors.length - 1]?.length) { - analogousColors.forEach(colors => - generatedColors.push(colors.shift() as tinycolor.Instance), - ); - } + generatedColors = getAnalogousColors(colors, multiple); sharedLabelMap = sharedLabels.reduce( (res, label, index) => ({ ...res, - [label.toString()]: generatedColors[index]?.toHexString(), + [label.toString()]: generatedColors[index], }), {}, ); diff --git a/superset-frontend/packages/superset-ui-core/src/color/utils.ts b/superset-frontend/packages/superset-ui-core/src/color/utils.ts index 47a936aaa618..0ce64d049012 100644 --- a/superset-frontend/packages/superset-ui-core/src/color/utils.ts +++ b/superset-frontend/packages/superset-ui-core/src/color/utils.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import tinycolor from 'tinycolor2'; const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/; export function getContrastingColor(color: string, thresholds = 186) { @@ -51,3 +52,24 @@ export function getContrastingColor(color: string, thresholds = 186) { return r * 0.299 + g * 0.587 + b * 0.114 > thresholds ? '#000' : '#FFF'; } + +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 => { + const result = tinycolor(color).analogous(results + ext); + return result.slice(ext); + }); + + // [[A, AA, AAA], [B, BB, BBB]] => [A, B, AA, BB, AAA, BBB] + while (analogousColors[analogousColors.length - 1]?.length) { + analogousColors.forEach(colors => { + const color = colors.shift() as tinycolor.Instance; + generatedColors.push(color.toHexString()); + }); + } + + return generatedColors; +} diff --git a/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts b/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts index f080b6fc84e5..1d47cf760e32 100644 --- a/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts @@ -62,28 +62,15 @@ describe('CategoricalColorScale', () => { expect(c2).not.toBe(c3); expect(c3).not.toBe(c1); }); - it('recycles colors when number of items exceed available colors', () => { - const colorSet: { [key: string]: number } = {}; + it('get analogous colors when number of items exceed available colors', () => { const scale = new CategoricalColorScale(['blue', 'red', 'green']); - const colors = [ - scale.getColor('pig'), - scale.getColor('horse'), - scale.getColor('cat'), - scale.getColor('cow'), - scale.getColor('donkey'), - scale.getColor('goat'), - ]; - colors.forEach(color => { - if (colorSet[color]) { - colorSet[color] += 1; - } else { - colorSet[color] = 1; - } - }); - expect(Object.keys(colorSet)).toHaveLength(3); - ['blue', 'red', 'green'].forEach(color => { - expect(colorSet[color]).toBe(2); - }); + scale.getColor('pig'); + scale.getColor('horse'); + scale.getColor('cat'); + scale.getColor('cow'); + scale.getColor('donkey'); + scale.getColor('goat'); + expect(scale.range()).toHaveLength(6); }); }); describe('.setColor(value, forcedColor)', () => {