Skip to content

Commit e5a206d

Browse files
authored
feat: allow colorVariant option for series specific color styles (#630)
- allows the use of `ColorVariant.Series` to set a series color style to the computed series color - allows the use of `ColorVariant.None` to set color to transparent.
1 parent 2c1d224 commit e5a206d

26 files changed

+1295
-44
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License. */
18+
19+
const module = jest.requireActual('../d3_utils.ts');
20+
21+
export const stringToRGB = jest.fn(module.stringToRGB);
22+
export const validateColor = jest.fn(module.validateColor);
23+
export const argsToRGB = jest.fn(module.argsToRGB);
24+
export const argsToRGBString = jest.fn(module.argsToRGBString);
25+
export const RGBtoString = jest.fn(module.RGBtoString);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License. */
18+
19+
import {
20+
stringToRGB,
21+
validateColor,
22+
defaultD3Color,
23+
argsToRGB,
24+
RgbObject,
25+
argsToRGBString,
26+
RGBtoString,
27+
} from './d3_utils';
28+
29+
describe('d3 Utils', () => {
30+
describe('stringToRGB', () => {
31+
describe('bad colors or undefined', () => {
32+
it('should return default color for undefined color string', () => {
33+
expect(stringToRGB()).toMatchObject({
34+
r: 255,
35+
g: 0,
36+
b: 0,
37+
opacity: 1,
38+
});
39+
});
40+
41+
it('should return default RgbObject', () => {
42+
expect(stringToRGB('not a color')).toMatchObject({
43+
r: 255,
44+
g: 0,
45+
b: 0,
46+
opacity: 1,
47+
});
48+
});
49+
50+
it('should return default color if bad opacity', () => {
51+
expect(stringToRGB('rgba(50,50,50,x)')).toMatchObject({
52+
r: 255,
53+
g: 0,
54+
b: 0,
55+
opacity: 1,
56+
});
57+
});
58+
});
59+
60+
describe('hex colors', () => {
61+
it('should return RgbObject', () => {
62+
expect(stringToRGB('#ef713d')).toMatchObject({
63+
r: 239,
64+
g: 113,
65+
b: 61,
66+
});
67+
});
68+
69+
it('should return RgbObject from shorthand', () => {
70+
expect(stringToRGB('#ccc')).toMatchObject({
71+
r: 204,
72+
g: 204,
73+
b: 204,
74+
});
75+
});
76+
77+
it('should return RgbObject with correct opacity', () => {
78+
// https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4
79+
expect(stringToRGB('#ef713d80').opacity).toBeCloseTo(0.5, 1);
80+
});
81+
82+
it('should return correct RgbObject for alpha value of 0', () => {
83+
expect(stringToRGB('#00000000')).toMatchObject({
84+
r: 0,
85+
g: 0,
86+
b: 0,
87+
opacity: 0,
88+
});
89+
});
90+
});
91+
92+
describe('rgb colors', () => {
93+
it('should return RgbObject', () => {
94+
expect(stringToRGB('rgb(50,50,50)')).toMatchObject({
95+
r: 50,
96+
g: 50,
97+
b: 50,
98+
});
99+
});
100+
101+
it('should return RgbObject with correct opacity', () => {
102+
expect(stringToRGB('rgba(50,50,50,0.25)').opacity).toBe(0.25);
103+
});
104+
105+
it('should return correct RgbObject for alpha value of 0', () => {
106+
expect(stringToRGB('rgba(50,50,50,0)')).toMatchObject({
107+
r: 50,
108+
g: 50,
109+
b: 50,
110+
opacity: 0,
111+
});
112+
});
113+
});
114+
115+
describe('hsl colors', () => {
116+
it('should return RgbObject', () => {
117+
expect(stringToRGB('hsl(0,0%,50%)')).toMatchObject({
118+
r: 127.5,
119+
g: 127.5,
120+
b: 127.5,
121+
});
122+
});
123+
124+
it('should return RgbObject with correct opacity', () => {
125+
expect(stringToRGB('hsla(0,0%,50%,0.25)').opacity).toBe(0.25);
126+
});
127+
128+
it('should return correct RgbObject for alpha value of 0', () => {
129+
expect(stringToRGB('hsla(0,0%,50%,0)')).toEqual({
130+
r: 127.5,
131+
g: 127.5,
132+
b: 127.5,
133+
opacity: 0,
134+
});
135+
});
136+
});
137+
138+
describe('named colors', () => {
139+
it('should return RgbObject', () => {
140+
expect(stringToRGB('aquamarine')).toMatchObject({
141+
r: 127,
142+
g: 255,
143+
b: 212,
144+
});
145+
});
146+
147+
it('should return default RgbObject with 0 opacity', () => {
148+
expect(stringToRGB('transparent')).toMatchObject({
149+
r: 0,
150+
g: 0,
151+
b: 0,
152+
opacity: 0,
153+
});
154+
});
155+
156+
it('should return default RgbObject with 0 opacity even with override', () => {
157+
expect(stringToRGB('transparent', 0.5)).toMatchObject({
158+
r: 0,
159+
g: 0,
160+
b: 0,
161+
opacity: 0,
162+
});
163+
});
164+
});
165+
166+
describe('Optional opactiy override', () => {
167+
it('should override opacity from color', () => {
168+
expect(stringToRGB('rgba(50,50,50,0.25)', 0.75).opacity).toBe(0.75);
169+
});
170+
171+
it('should use OpacityFn to compute opacity override', () => {
172+
expect(stringToRGB('rgba(50,50,50,0.25)', (o) => o * 2).opacity).toBe(0.5);
173+
});
174+
});
175+
});
176+
177+
describe('validateColor', () => {
178+
it.each<string>(['r', 'g', 'b', 'opacity'])('should return null if %s is NaN', (value) => {
179+
expect(
180+
validateColor({
181+
...defaultD3Color,
182+
[value]: NaN,
183+
}),
184+
).toBeNull();
185+
});
186+
187+
it('should return valid colors', () => {
188+
expect(validateColor(defaultD3Color)).toBe(defaultD3Color);
189+
});
190+
});
191+
192+
describe('argsToRGB', () => {
193+
it.each<keyof RgbObject>(['r', 'g', 'b', 'opacity'])('should return defaultD3Color if %s is NaN', (value) => {
194+
const { r, g, b, opacity }: RgbObject = {
195+
...defaultD3Color,
196+
[value]: NaN,
197+
};
198+
expect(argsToRGB(r, g, b, opacity)).toEqual(defaultD3Color);
199+
});
200+
201+
it('should return valid colors', () => {
202+
const { r, g, b, opacity } = defaultD3Color;
203+
expect(argsToRGB(r, g, b, opacity)).toEqual(defaultD3Color);
204+
});
205+
});
206+
207+
describe('argsToRGBString', () => {
208+
it('should return valid colors', () => {
209+
const { r, g, b, opacity } = defaultD3Color;
210+
expect(argsToRGBString(r, g, b, opacity)).toBe('rgb(255, 0, 0)');
211+
});
212+
});
213+
214+
describe('RGBtoString', () => {
215+
it('should return valid colors', () => {
216+
expect(RGBtoString(defaultD3Color)).toBe('rgb(255, 0, 0)');
217+
});
218+
});
219+
});

src/chart_types/partition_chart/layout/utils/d3_utils.ts

+66-6
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,76 @@ type A = number;
2323
export type RgbTuple = [RGB, RGB, RGB, RGB?];
2424
export type RgbObject = { r: RGB; g: RGB; b: RGB; opacity: A };
2525

26-
const defaultColor: RgbObject = { r: 255, g: 0, b: 0, opacity: 1 };
27-
const defaultD3Color: D3RGBColor = d3Rgb(defaultColor.r, defaultColor.g, defaultColor.b, defaultColor.opacity);
26+
/** @internal */
27+
export const defaultColor: RgbObject = { r: 255, g: 0, b: 0, opacity: 1 };
28+
/** @internal */
29+
export const transparentColor: RgbObject = { r: 0, g: 0, b: 0, opacity: 0 };
30+
/** @internal */
31+
export const defaultD3Color: D3RGBColor = d3Rgb(defaultColor.r, defaultColor.g, defaultColor.b, defaultColor.opacity);
32+
33+
/** @internal */
34+
export type OpacityFn = (colorOpacity: number) => number;
35+
36+
/** @internal */
37+
export function stringToRGB(cssColorSpecifier?: string, opacity?: number | OpacityFn): RgbObject {
38+
if (cssColorSpecifier === 'transparent') {
39+
return transparentColor;
40+
}
41+
const color = getColor(cssColorSpecifier);
42+
43+
if (opacity === undefined) {
44+
return color;
45+
}
46+
47+
const opacityOverride = typeof opacity === 'number' ? opacity : opacity(color.opacity);
48+
49+
if (isNaN(opacityOverride)) {
50+
return color;
51+
}
52+
53+
return {
54+
...color,
55+
opacity: opacityOverride,
56+
};
57+
}
58+
59+
/**
60+
* Returns color as RgbObject or default fallback.
61+
*
62+
* Handles issue in d3-color for hsla and rgba colors with alpha value of `0`
63+
*
64+
* @param cssColorSpecifier
65+
*/
66+
function getColor(cssColorSpecifier: string = ''): RgbObject {
67+
let color: D3RGBColor;
68+
const endRegEx = /,\s*0+(\.0*)?\s*\)$/;
69+
// TODO: make this check more robust
70+
if (/^(rgba|hsla)\(/i.test(cssColorSpecifier) && endRegEx.test(cssColorSpecifier)) {
71+
color = {
72+
...d3Rgb(cssColorSpecifier.replace(endRegEx, ',1)')),
73+
opacity: 0,
74+
};
75+
} else {
76+
color = d3Rgb(cssColorSpecifier);
77+
}
78+
79+
return validateColor(color) ?? defaultColor;
80+
}
2881

2982
/** @internal */
30-
export function stringToRGB(cssColorSpecifier: string): RgbObject {
31-
return d3Rgb(cssColorSpecifier) || defaultColor;
83+
export function validateColor(color: D3RGBColor): D3RGBColor | null {
84+
const { r, g, b, opacity } = color;
85+
86+
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(opacity)) {
87+
return null;
88+
}
89+
90+
return color;
3291
}
3392

34-
function argsToRGB(r: number, g: number, b: number, opacity: number): D3RGBColor {
35-
return d3Rgb(r, g, b, opacity) || defaultD3Color;
93+
/** @internal */
94+
export function argsToRGB(r: number, g: number, b: number, opacity: number): D3RGBColor {
95+
return validateColor(d3Rgb(r, g, b, opacity)) ?? defaultD3Color;
3696
}
3797

3898
/** @internal */

src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ export function renderRect(
3131
return;
3232
}
3333

34-
// fill
35-
3634
if (fill) {
3735
const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width : 0;
3836
// console.log(stroke, borderOffset);
@@ -58,12 +56,16 @@ export function renderRect(
5856
drawRect(ctx, { x, y, width, height });
5957
if (stroke.dash) {
6058
ctx.setLineDash(stroke.dash);
59+
} else {
60+
// Setting linecap with dash causes solid line
61+
ctx.lineCap = 'square';
6162
}
6263

6364
ctx.stroke();
6465
}
6566
}
6667

68+
/** @internal */
6769
function drawRect(ctx: CanvasRenderingContext2D, rect: Rect) {
6870
const { x, y, width, height } = rect;
6971
ctx.beginPath();
@@ -100,5 +102,12 @@ export function renderMultiRect(ctx: CanvasRenderingContext2D, rects: Rect[], fi
100102
ctx.strokeStyle = RGBtoString(stroke.color);
101103
ctx.lineWidth = stroke.width;
102104
ctx.stroke();
105+
106+
if (stroke.dash) {
107+
ctx.setLineDash(stroke.dash);
108+
} else {
109+
// Setting linecap with dash causes solid line
110+
ctx.lineCap = 'square';
111+
}
103112
}
104113
}

0 commit comments

Comments
 (0)