mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
feat(color): support analogous colors to prevent color conflict (#19325)
* feat(color): support analogous colors * fix test * fix range * add some comment
This commit is contained in:
parent
08aca83f6c
commit
90c9daea08
@ -23,6 +23,7 @@ import { ExtensibleFunction } from '../models';
|
|||||||
import { ColorsLookup } from './types';
|
import { ColorsLookup } from './types';
|
||||||
import stringifyAndTrim from './stringifyAndTrim';
|
import stringifyAndTrim from './stringifyAndTrim';
|
||||||
import getSharedLabelColor from './SharedLabelColorSingleton';
|
import getSharedLabelColor from './SharedLabelColorSingleton';
|
||||||
|
import { getAnalogousColors } from './utils';
|
||||||
|
|
||||||
// Use type augmentation to correct the fact that
|
// Use type augmentation to correct the fact that
|
||||||
// an instance of CategoricalScale is also a function
|
// an instance of CategoricalScale is also a function
|
||||||
@ -31,6 +32,8 @@ interface CategoricalColorScale {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CategoricalColorScale extends ExtensibleFunction {
|
class CategoricalColorScale extends ExtensibleFunction {
|
||||||
|
originColors: string[];
|
||||||
|
|
||||||
colors: string[];
|
colors: string[];
|
||||||
|
|
||||||
scale: ScaleOrdinal<{ toString(): string }, string>;
|
scale: ScaleOrdinal<{ toString(): string }, string>;
|
||||||
@ -39,6 +42,8 @@ class CategoricalColorScale extends ExtensibleFunction {
|
|||||||
|
|
||||||
forcedColors: ColorsLookup;
|
forcedColors: ColorsLookup;
|
||||||
|
|
||||||
|
multiple: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {*} colors an array of colors
|
* @param {*} colors an array of colors
|
||||||
@ -48,11 +53,13 @@ class CategoricalColorScale extends ExtensibleFunction {
|
|||||||
constructor(colors: string[], parentForcedColors?: ColorsLookup) {
|
constructor(colors: string[], parentForcedColors?: ColorsLookup) {
|
||||||
super((value: string, sliceId?: number) => this.getColor(value, sliceId));
|
super((value: string, sliceId?: number) => this.getColor(value, sliceId));
|
||||||
|
|
||||||
|
this.originColors = colors;
|
||||||
this.colors = colors;
|
this.colors = colors;
|
||||||
this.scale = scaleOrdinal<{ toString(): string }, string>();
|
this.scale = scaleOrdinal<{ toString(): string }, string>();
|
||||||
this.scale.range(colors);
|
this.scale.range(colors);
|
||||||
this.parentForcedColors = parentForcedColors;
|
this.parentForcedColors = parentForcedColors;
|
||||||
this.forcedColors = {};
|
this.forcedColors = {};
|
||||||
|
this.multiple = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getColor(value?: string, sliceId?: number) {
|
getColor(value?: string, sliceId?: number) {
|
||||||
@ -72,6 +79,15 @@ class CategoricalColorScale extends ExtensibleFunction {
|
|||||||
return forcedColor;
|
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);
|
const color = this.scale(cleanedValue);
|
||||||
sharedLabelColor.addSlice(cleanedValue, color, sliceId);
|
sharedLabelColor.addSlice(cleanedValue, color, sliceId);
|
||||||
|
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
import { CategoricalColorNamespace } from '.';
|
import { CategoricalColorNamespace } from '.';
|
||||||
import makeSingleton from '../utils/makeSingleton';
|
import makeSingleton from '../utils/makeSingleton';
|
||||||
|
import { getAnalogousColors } from './utils';
|
||||||
|
|
||||||
export class SharedLabelColor {
|
export class SharedLabelColor {
|
||||||
sliceLabelColorMap: Record<number, Record<string, string | undefined>>;
|
sliceLabelColorMap: Record<number, Record<string, string | undefined>>;
|
||||||
@ -39,27 +39,16 @@ export class SharedLabelColor {
|
|||||||
CategoricalColorNamespace.getNamespace(colorNamespace);
|
CategoricalColorNamespace.getNamespace(colorNamespace);
|
||||||
const colors = categoricalNamespace.getScale(colorScheme).range();
|
const colors = categoricalNamespace.getScale(colorScheme).range();
|
||||||
const sharedLabels = this.getSharedLabels();
|
const sharedLabels = this.getSharedLabels();
|
||||||
const generatedColors: tinycolor.Instance[] = [];
|
let generatedColors: string[] = [];
|
||||||
let sharedLabelMap;
|
let sharedLabelMap;
|
||||||
|
|
||||||
if (sharedLabels.length) {
|
if (sharedLabels.length) {
|
||||||
const multiple = Math.ceil(sharedLabels.length / colors.length);
|
const multiple = Math.ceil(sharedLabels.length / colors.length);
|
||||||
const ext = 5;
|
generatedColors = getAnalogousColors(colors, multiple);
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sharedLabelMap = sharedLabels.reduce(
|
sharedLabelMap = sharedLabels.reduce(
|
||||||
(res, label, index) => ({
|
(res, label, index) => ({
|
||||||
...res,
|
...res,
|
||||||
[label.toString()]: generatedColors[index]?.toHexString(),
|
[label.toString()]: generatedColors[index],
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
|
const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
|
||||||
export function getContrastingColor(color: string, thresholds = 186) {
|
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';
|
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;
|
||||||
|
}
|
||||||
|
@ -62,28 +62,15 @@ describe('CategoricalColorScale', () => {
|
|||||||
expect(c2).not.toBe(c3);
|
expect(c2).not.toBe(c3);
|
||||||
expect(c3).not.toBe(c1);
|
expect(c3).not.toBe(c1);
|
||||||
});
|
});
|
||||||
it('recycles colors when number of items exceed available colors', () => {
|
it('get analogous colors when number of items exceed available colors', () => {
|
||||||
const colorSet: { [key: string]: number } = {};
|
|
||||||
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
const scale = new CategoricalColorScale(['blue', 'red', 'green']);
|
||||||
const colors = [
|
scale.getColor('pig');
|
||||||
scale.getColor('pig'),
|
scale.getColor('horse');
|
||||||
scale.getColor('horse'),
|
scale.getColor('cat');
|
||||||
scale.getColor('cat'),
|
scale.getColor('cow');
|
||||||
scale.getColor('cow'),
|
scale.getColor('donkey');
|
||||||
scale.getColor('donkey'),
|
scale.getColor('goat');
|
||||||
scale.getColor('goat'),
|
expect(scale.range()).toHaveLength(6);
|
||||||
];
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('.setColor(value, forcedColor)', () => {
|
describe('.setColor(value, forcedColor)', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user