Skip to content

Commit

Permalink
feat: allow colorVariant option for series specific color styles (#630)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
nickofthyme authored Apr 21, 2020
1 parent 2c1d224 commit e5a206d
Show file tree
Hide file tree
Showing 26 changed files with 1,295 additions and 44 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/chart_types/partition_chart/layout/utils/__mocks__/d3_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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. */

const module = jest.requireActual('../d3_utils.ts');

export const stringToRGB = jest.fn(module.stringToRGB);
export const validateColor = jest.fn(module.validateColor);
export const argsToRGB = jest.fn(module.argsToRGB);
export const argsToRGBString = jest.fn(module.argsToRGBString);
export const RGBtoString = jest.fn(module.RGBtoString);
219 changes: 219 additions & 0 deletions src/chart_types/partition_chart/layout/utils/d3_utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 {
stringToRGB,
validateColor,
defaultD3Color,
argsToRGB,
RgbObject,
argsToRGBString,
RGBtoString,
} from './d3_utils';

describe('d3 Utils', () => {
describe('stringToRGB', () => {
describe('bad colors or undefined', () => {
it('should return default color for undefined color string', () => {
expect(stringToRGB()).toMatchObject({
r: 255,
g: 0,
b: 0,
opacity: 1,
});
});

it('should return default RgbObject', () => {
expect(stringToRGB('not a color')).toMatchObject({
r: 255,
g: 0,
b: 0,
opacity: 1,
});
});

it('should return default color if bad opacity', () => {
expect(stringToRGB('rgba(50,50,50,x)')).toMatchObject({
r: 255,
g: 0,
b: 0,
opacity: 1,
});
});
});

describe('hex colors', () => {
it('should return RgbObject', () => {
expect(stringToRGB('#ef713d')).toMatchObject({
r: 239,
g: 113,
b: 61,
});
});

it('should return RgbObject from shorthand', () => {
expect(stringToRGB('#ccc')).toMatchObject({
r: 204,
g: 204,
b: 204,
});
});

it('should return RgbObject with correct opacity', () => {
// https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4
expect(stringToRGB('#ef713d80').opacity).toBeCloseTo(0.5, 1);
});

it('should return correct RgbObject for alpha value of 0', () => {
expect(stringToRGB('#00000000')).toMatchObject({
r: 0,
g: 0,
b: 0,
opacity: 0,
});
});
});

describe('rgb colors', () => {
it('should return RgbObject', () => {
expect(stringToRGB('rgb(50,50,50)')).toMatchObject({
r: 50,
g: 50,
b: 50,
});
});

it('should return RgbObject with correct opacity', () => {
expect(stringToRGB('rgba(50,50,50,0.25)').opacity).toBe(0.25);
});

it('should return correct RgbObject for alpha value of 0', () => {
expect(stringToRGB('rgba(50,50,50,0)')).toMatchObject({
r: 50,
g: 50,
b: 50,
opacity: 0,
});
});
});

describe('hsl colors', () => {
it('should return RgbObject', () => {
expect(stringToRGB('hsl(0,0%,50%)')).toMatchObject({
r: 127.5,
g: 127.5,
b: 127.5,
});
});

it('should return RgbObject with correct opacity', () => {
expect(stringToRGB('hsla(0,0%,50%,0.25)').opacity).toBe(0.25);
});

it('should return correct RgbObject for alpha value of 0', () => {
expect(stringToRGB('hsla(0,0%,50%,0)')).toEqual({
r: 127.5,
g: 127.5,
b: 127.5,
opacity: 0,
});
});
});

describe('named colors', () => {
it('should return RgbObject', () => {
expect(stringToRGB('aquamarine')).toMatchObject({
r: 127,
g: 255,
b: 212,
});
});

it('should return default RgbObject with 0 opacity', () => {
expect(stringToRGB('transparent')).toMatchObject({
r: 0,
g: 0,
b: 0,
opacity: 0,
});
});

it('should return default RgbObject with 0 opacity even with override', () => {
expect(stringToRGB('transparent', 0.5)).toMatchObject({
r: 0,
g: 0,
b: 0,
opacity: 0,
});
});
});

describe('Optional opactiy override', () => {
it('should override opacity from color', () => {
expect(stringToRGB('rgba(50,50,50,0.25)', 0.75).opacity).toBe(0.75);
});

it('should use OpacityFn to compute opacity override', () => {
expect(stringToRGB('rgba(50,50,50,0.25)', (o) => o * 2).opacity).toBe(0.5);
});
});
});

describe('validateColor', () => {
it.each<string>(['r', 'g', 'b', 'opacity'])('should return null if %s is NaN', (value) => {
expect(
validateColor({
...defaultD3Color,
[value]: NaN,
}),
).toBeNull();
});

it('should return valid colors', () => {
expect(validateColor(defaultD3Color)).toBe(defaultD3Color);
});
});

describe('argsToRGB', () => {
it.each<keyof RgbObject>(['r', 'g', 'b', 'opacity'])('should return defaultD3Color if %s is NaN', (value) => {
const { r, g, b, opacity }: RgbObject = {
...defaultD3Color,
[value]: NaN,
};
expect(argsToRGB(r, g, b, opacity)).toEqual(defaultD3Color);
});

it('should return valid colors', () => {
const { r, g, b, opacity } = defaultD3Color;
expect(argsToRGB(r, g, b, opacity)).toEqual(defaultD3Color);
});
});

describe('argsToRGBString', () => {
it('should return valid colors', () => {
const { r, g, b, opacity } = defaultD3Color;
expect(argsToRGBString(r, g, b, opacity)).toBe('rgb(255, 0, 0)');
});
});

describe('RGBtoString', () => {
it('should return valid colors', () => {
expect(RGBtoString(defaultD3Color)).toBe('rgb(255, 0, 0)');
});
});
});
72 changes: 66 additions & 6 deletions src/chart_types/partition_chart/layout/utils/d3_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,76 @@ type A = number;
export type RgbTuple = [RGB, RGB, RGB, RGB?];
export type RgbObject = { r: RGB; g: RGB; b: RGB; opacity: A };

const defaultColor: RgbObject = { r: 255, g: 0, b: 0, opacity: 1 };
const defaultD3Color: D3RGBColor = d3Rgb(defaultColor.r, defaultColor.g, defaultColor.b, defaultColor.opacity);
/** @internal */
export const defaultColor: RgbObject = { r: 255, g: 0, b: 0, opacity: 1 };
/** @internal */
export const transparentColor: RgbObject = { r: 0, g: 0, b: 0, opacity: 0 };
/** @internal */
export const defaultD3Color: D3RGBColor = d3Rgb(defaultColor.r, defaultColor.g, defaultColor.b, defaultColor.opacity);

/** @internal */
export type OpacityFn = (colorOpacity: number) => number;

/** @internal */
export function stringToRGB(cssColorSpecifier?: string, opacity?: number | OpacityFn): RgbObject {
if (cssColorSpecifier === 'transparent') {
return transparentColor;
}
const color = getColor(cssColorSpecifier);

if (opacity === undefined) {
return color;
}

const opacityOverride = typeof opacity === 'number' ? opacity : opacity(color.opacity);

if (isNaN(opacityOverride)) {
return color;
}

return {
...color,
opacity: opacityOverride,
};
}

/**
* Returns color as RgbObject or default fallback.
*
* Handles issue in d3-color for hsla and rgba colors with alpha value of `0`
*
* @param cssColorSpecifier
*/
function getColor(cssColorSpecifier: string = ''): RgbObject {
let color: D3RGBColor;
const endRegEx = /,\s*0+(\.0*)?\s*\)$/;
// TODO: make this check more robust
if (/^(rgba|hsla)\(/i.test(cssColorSpecifier) && endRegEx.test(cssColorSpecifier)) {
color = {
...d3Rgb(cssColorSpecifier.replace(endRegEx, ',1)')),
opacity: 0,
};
} else {
color = d3Rgb(cssColorSpecifier);
}

return validateColor(color) ?? defaultColor;
}

/** @internal */
export function stringToRGB(cssColorSpecifier: string): RgbObject {
return d3Rgb(cssColorSpecifier) || defaultColor;
export function validateColor(color: D3RGBColor): D3RGBColor | null {
const { r, g, b, opacity } = color;

if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(opacity)) {
return null;
}

return color;
}

function argsToRGB(r: number, g: number, b: number, opacity: number): D3RGBColor {
return d3Rgb(r, g, b, opacity) || defaultD3Color;
/** @internal */
export function argsToRGB(r: number, g: number, b: number, opacity: number): D3RGBColor {
return validateColor(d3Rgb(r, g, b, opacity)) ?? defaultD3Color;
}

/** @internal */
Expand Down
13 changes: 11 additions & 2 deletions src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ export function renderRect(
return;
}

// fill

if (fill) {
const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width : 0;
// console.log(stroke, borderOffset);
Expand All @@ -58,12 +56,16 @@ export function renderRect(
drawRect(ctx, { x, y, width, height });
if (stroke.dash) {
ctx.setLineDash(stroke.dash);
} else {
// Setting linecap with dash causes solid line
ctx.lineCap = 'square';
}

ctx.stroke();
}
}

/** @internal */
function drawRect(ctx: CanvasRenderingContext2D, rect: Rect) {
const { x, y, width, height } = rect;
ctx.beginPath();
Expand Down Expand Up @@ -100,5 +102,12 @@ export function renderMultiRect(ctx: CanvasRenderingContext2D, rects: Rect[], fi
ctx.strokeStyle = RGBtoString(stroke.color);
ctx.lineWidth = stroke.width;
ctx.stroke();

if (stroke.dash) {
ctx.setLineDash(stroke.dash);
} else {
// Setting linecap with dash causes solid line
ctx.lineCap = 'square';
}
}
}
Loading

0 comments on commit e5a206d

Please sign in to comment.