Skip to content

Commit

Permalink
feat(a11y): add textures to fill options (#1138)
Browse files Browse the repository at this point in the history
Allow optional textured fill as `TexturedStylesBase` for area and bar series fill via `AreaStyle.texture` and `RectStyles.texture` props.

Co-authored-by: Debajyoti Halder <rondebajyoti@gmail.com>
  • Loading branch information
ron-debajyoti authored Jun 8, 2021
1 parent 7baa88c commit fd0479f
Show file tree
Hide file tree
Showing 52 changed files with 929 additions and 97 deletions.
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ license_header.js
# Compiled source
src/utils/d3-delaunay/*
**/dist

# auto generated directories
integration/tmp
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ module.exports = {
files: ['stories/**/*.ts?(x)', 'docs/**/*.ts?(x)'],
rules: {
'@typescript-eslint/no-unsafe-call': 0,
'@typescript-eslint/no-unnecessary-type-assertion': 0,
},
},
{
Expand Down
46 changes: 46 additions & 0 deletions api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export interface AreaSeriesStyle {
export interface AreaStyle {
fill?: Color | ColorVariant;
opacity: number;
texture?: TexturedStyles;
visible: boolean;
}

Expand Down Expand Up @@ -1585,6 +1586,7 @@ export interface RectBorderStyle {
export interface RectStyle {
fill?: Color | ColorVariant;
opacity: number;
texture?: TexturedStyles;
widthPixel?: Pixels;
widthRatio?: Ratio;
}
Expand Down Expand Up @@ -1962,6 +1964,50 @@ export interface TextStyle {
padding: number | SimplePadding;
}

// @public (undocumented)
export interface TexturedPathStyles extends TexturedStylesBase {
path: string | Path2D;
}

// @public (undocumented)
export interface TexturedShapeStyles extends TexturedStylesBase {
shape: TextureShape;
}

// @public
export type TexturedStyles = TexturedPathStyles | TexturedShapeStyles;

// @public (undocumented)
export interface TexturedStylesBase {
dash?: number[];
fill?: Color | ColorVariant;
offset?: Partial<Point> & {
global?: boolean;
};
opacity?: number;
rotation?: number;
shapeRotation?: number;
size?: number;
// Warning: (ae-forgotten-export) The symbol "Point" needs to be exported by the entry point index.d.ts
spacing?: Partial<Point> | number;
stroke?: Color | ColorVariant;
strokeWidth?: number;
}

// @public (undocumented)
export const TextureShape: Readonly<{
Line: "line";
Circle: "circle";
Square: "square";
Diamond: "diamond";
Plus: "plus";
X: "x";
Triangle: "triangle";
}>;

// @public (undocumented)
export type TextureShape = $Values<typeof TextureShape>;

// @public (undocumented)
export interface Theme {
// (undocumented)
Expand Down
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.
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.
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.
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.
52 changes: 52 additions & 0 deletions integration/tests/stylings_stories.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { SeriesType } from '../../src';
import { common } from '../page_objects';

describe('Stylings stories', () => {
describe('Texture', () => {
describe.each([SeriesType.Bar, SeriesType.Area])('%s', (seriesType) => {
it(`should use custom path`, async () => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/stylings--with-texture&knob-Use custom path_Texture=true&knob-Custom path_Texture=M -7.75 -2.5 l 5.9 0 l 1.85 -6.1 l 1.85 6.1 l 5.9 0 l -4.8 3.8 l 1.85 6.1 l -4.8 -3.8 l -4.8 3.8 l 1.85 -6.1 l -4.8 -3.8 z&knob-Use stroke color_Texture=true&knob-Stoke color_Texture=&knob-Stroke width_Texture=1&knob-Use fill color_Texture=true&knob-Fill color_Texture=&knob-Rotation (degrees)_Pattern=45&knob-Opacity_Texture=1&knob-Shape rotation (degrees)_Texture=0&knob-Shape size - custom path_Texture=20&knob-Shape spacing - x_Pattern=10&knob-Shape spacing - y_Pattern=0&knob-Pattern offset - x_Pattern=0&knob-Pattern offset - y_Pattern=0&knob-Apply offset along global coordinate axes_Pattern=true&knob-Series opacity_Series=1&knob-Show series fill_Series=&knob-Series color_Series=rgba(0,0,0,1)&knob-Series type_Series=${seriesType}`,
);
});

it(`should render texture with lines as shape`, async () => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/stylings--with-texture&knob-Use custom path_Texture=&knob-Shape_Texture=line&knob-Use stroke color_Texture=true&knob-Stoke color_Texture=rgba(0,0,0,1)&knob-Stroke width_Texture=1&knob-Stroke dash_Texture[0]=10&knob-Stroke dash_Texture[1]= 5&knob-Use fill color_Texture=&knob-Fill color_Texture=rgba(30,165,147,0.28)&knob-Rotation (degrees)_Pattern=-45&knob-Opacity_Texture=1&knob-Shape rotation (degrees)_Texture=0&knob-Shape size_Texture=20&knob-Shape spacing - x_Pattern=0&knob-Shape spacing - y_Pattern=0&knob-Pattern offset - x_Pattern=0&knob-Pattern offset - y_Pattern=0&knob-Apply offset along global coordinate axes_Pattern=true&knob-Series opacity_Series=1&knob-Show series fill_Series=&knob-Series color_Series=rgba(0,0,0,1)&knob-Series type_Series=${seriesType}`,
);
});

it(`should allow any random texture customization`, async () => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/stylings--texture-multiple-series&knob-Total series=4&knob-Show legend=&knob-Show series fill=&knob-Chart color=rgba(0,0,0,1)&knob-Shape_Randomized parameters=true&knob-Rotation_Randomized parameters=true&knob-Shape rotation_Randomized parameters=true&knob-Size_Randomized parameters=true&knob-X spacing_Randomized parameters=true&knob-Y spacing_Randomized parameters=true&knob-X offset_Randomized parameters=true&knob-Y offset_Randomized parameters=true&knob-Series type=${seriesType}&knob-Shape_Default parameters=circle&knob-Stroke width_Default parameters=1&knob-Rotation (degrees)_Default parameters=45&knob-Shape rotation (degrees)_Default parameters=0&knob-Shape size_Default parameters=20&knob-Opacity_Default parameters=1&knob-Shape spacing - x_Default parameters=10&knob-Shape spacing - y_Default parameters=10&knob-Pattern offset - x_Default parameters=0&knob-Pattern offset - y_Default parameters=0`,
);
});

it(`should use hover opacity for texture`, async () => {
await common.expectChartWithMouseAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/stylings--texture-multiple-series&knob-Total series=4&knob-Show legend=true&knob-Show series fill=&knob-Chart color=rgba(0,0,0,1)&knob-Shape_Randomized parameters=true&knob-Rotation_Randomized parameters=&knob-Shape rotation_Randomized parameters=&knob-Size_Randomized parameters=true&knob-X spacing_Randomized parameters=&knob-Y spacing_Randomized parameters=&knob-X offset_Randomized parameters=&knob-Y offset_Randomized parameters=&knob-Series type=${seriesType}&knob-Shape_Default parameters=circle&knob-Stroke width_Default parameters=1&knob-Rotation (degrees)_Default parameters=45&knob-Shape rotation (degrees)_Default parameters=0&knob-Shape size_Default parameters=20&knob-Opacity_Default parameters=1&knob-Shape spacing - x_Default parameters=10&knob-Shape spacing - y_Default parameters=10&knob-Pattern offset - x_Default parameters=0&knob-Pattern offset - y_Default parameters=0`,
{ top: 45, right: 40 },
);
});
});
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
"html-webpack-plugin": "^4.5.2",
"husky": "^4.3.6",
"jest": "^26.6.3",
"jest-canvas-mock": "^2.3.1",
"jest-extended": "^0.11.5",
"jest-image-snapshot": "^4.3.0",
"jest-matcher-utils": "^26.6.2",
Expand Down
14 changes: 8 additions & 6 deletions src/chart_types/xy_chart/renderer/canvas/areas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ interface AreaGeometriesProps {
}

/** @internal */
export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometriesProps) {
export function renderAreas(ctx: CanvasRenderingContext2D, imgCanvas: HTMLCanvasElement, props: AreaGeometriesProps) {
const { sharedStyle, highlightedLegendItem, areas, rotation, clippings, renderingArea } = props;

withContext(ctx, (ctx) => {
Expand All @@ -54,7 +54,7 @@ export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometries
rotation,
renderingArea,
(ctx) => {
renderArea(ctx, area, sharedStyle, clippings, highlightedLegendItem);
renderArea(ctx, imgCanvas, area, sharedStyle, clippings, highlightedLegendItem);
},
{ area: clippings, shouldClip: true },
);
Expand Down Expand Up @@ -96,15 +96,17 @@ export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometries

function renderArea(
ctx: CanvasRenderingContext2D,
imgCanvas: HTMLCanvasElement,
glyph: AreaGeometry,
sharedStyle: SharedGeometryStateStyle,
clippings: Rect,
highlightedLegendItem?: LegendItem,
) {
const { area, color, transform, seriesIdentifier, seriesAreaStyle, clippedRanges, hideClippedRanges } = glyph;
const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem);
const fill = buildAreaStyles(color, seriesAreaStyle, geometryStateStyle);
renderAreaPath(ctx, transform, area, fill, clippedRanges, clippings, hideClippedRanges);
const styles = buildAreaStyles(ctx, imgCanvas, color, seriesAreaStyle, geometryStateStyle);

renderAreaPath(ctx, transform, area, styles, clippedRanges, clippings, hideClippedRanges);
}

function renderAreaLines(
Expand All @@ -116,7 +118,7 @@ function renderAreaLines(
) {
const { lines, color, seriesIdentifier, transform, seriesAreaLineStyle, clippedRanges, hideClippedRanges } = glyph;
const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem);
const stroke = buildLineStyles(color, seriesAreaLineStyle, geometryStateStyle);
const styles = buildLineStyles(color, seriesAreaLineStyle, geometryStateStyle);

renderLinePaths(ctx, transform, lines, stroke, clippedRanges, clippings, hideClippedRanges);
renderLinePaths(ctx, transform, lines, styles, clippedRanges, clippings, hideClippedRanges);
}
21 changes: 19 additions & 2 deletions src/chart_types/xy_chart/renderer/canvas/bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { withPanelTransform } from './utils/panel_transform';
/** @internal */
export function renderBars(
ctx: CanvasRenderingContext2D,
imgCanvas: HTMLCanvasElement,
barGeometries: Array<PerPanel<BarGeometry[]>>,
sharedStyle: SharedGeometryStateStyle,
clippings: Rect,
Expand All @@ -40,13 +41,22 @@ export function renderBars(
rotation?: Rotation,
) {
withContext(ctx, (ctx) => {
const barRenderer = renderPerPanelBars(ctx, clippings, sharedStyle, renderingArea, highlightedLegendItem, rotation);
const barRenderer = renderPerPanelBars(
ctx,
imgCanvas,
clippings,
sharedStyle,
renderingArea,
highlightedLegendItem,
rotation,
);
barGeometries.forEach(barRenderer);
});
}

function renderPerPanelBars(
ctx: CanvasRenderingContext2D,
imgCanvas: HTMLCanvasElement,
clippings: Rect,
sharedStyle: SharedGeometryStateStyle,
renderingArea: Dimensions,
Expand All @@ -66,7 +76,14 @@ function renderPerPanelBars(
bars.forEach((barGeometry) => {
const { x, y, width, height, color, seriesStyle, seriesIdentifier } = barGeometry;
const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem);
const { fill, stroke } = buildBarStyles(color, seriesStyle.rect, seriesStyle.rectBorder, geometryStateStyle);
const { fill, stroke } = buildBarStyles(
ctx,
imgCanvas,
color,
seriesStyle.rect,
seriesStyle.rectBorder,
geometryStateStyle,
);
const rect = { x, y, width, height };
withContext(ctx, (ctx) => {
renderRect(ctx, rect, fill, stroke);
Expand Down
20 changes: 19 additions & 1 deletion src/chart_types/xy_chart/renderer/canvas/primitives/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { RGBtoString } from '../../../../../common/color_library_wrappers';
import { Rect, Stroke, Fill } from '../../../../../geoms/types';
import { withContext, withClipRanges } from '../../../../../renderers/canvas';
import { getRadians } from '../../../../../utils/common';
import { ClippedRanges } from '../../../../../utils/geometry';
import { Point } from '../../../../../utils/point';
import { renderMultiLine } from './line';
Expand Down Expand Up @@ -93,6 +94,23 @@ export function renderAreaPath(
function renderPathFill(ctx: CanvasRenderingContext2D, path: string, fill: Fill) {
const path2d = new Path2D(path);
ctx.fillStyle = RGBtoString(fill.color);
ctx.beginPath();
ctx.fill(path2d);

if (fill.texture) {
ctx.clip(path2d);

const rotation = getRadians(fill.texture.rotation ?? 0);
const { offset } = fill.texture;

if (offset && offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0);
if (rotation) ctx.rotate(rotation);
if (offset && !offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0);

ctx.fillStyle = fill.texture.pattern;

// Use oversized rect to fill rotation/offset beyond path
const rotationRectFillSize = ctx.canvas.clientWidth * ctx.canvas.clientHeight;
ctx.translate(-rotationRectFillSize / 2, -rotationRectFillSize / 2);
ctx.fillRect(0, 0, rotationRectFillSize, rotationRectFillSize);
}
}
61 changes: 25 additions & 36 deletions src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import { RGBtoString } from '../../../../../common/color_library_wrappers';
import { Rect, Fill, Stroke } from '../../../../../geoms/types';
import { withContext } from '../../../../../renderers/canvas';
import { getRadians } from '../../../../../utils/common';

/** @internal */
export function renderRect(
Expand All @@ -38,9 +40,32 @@ export function renderRect(
const y = rect.y + borderOffset;
const width = rect.width - borderOffset * 2;
const height = rect.height - borderOffset * 2;

drawRect(ctx, { x, y, width, height });
ctx.fillStyle = RGBtoString(fill.color);
ctx.fill();

if (fill.texture) {
const { texture } = fill;
withContext(ctx, (ctx) => {
drawRect(ctx, { x, y, width, height });
ctx.clip();

const rotation = getRadians(texture.rotation ?? 0);
const { offset } = texture;

if (offset && offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0);
if (rotation) ctx.rotate(rotation);
if (offset && !offset.global) ctx.translate(offset?.x ?? 0, offset?.y ?? 0);

ctx.fillStyle = texture.pattern;

// Use oversized rect to fill rotation/offset beyond path
const rotationRectFillSize = ctx.canvas.clientWidth * ctx.canvas.clientHeight;
ctx.translate(-rotationRectFillSize / 2, -rotationRectFillSize / 2);
ctx.fillRect(0, 0, rotationRectFillSize, rotationRectFillSize);
});
}
}

if (stroke && stroke.width > 0.001) {
Expand Down Expand Up @@ -74,39 +99,3 @@ function drawRect(ctx: CanvasRenderingContext2D, rect: Rect) {
ctx.lineTo(x, y + height);
ctx.lineTo(x, y);
}

/** @internal */
export function renderMultiRect(ctx: CanvasRenderingContext2D, rects: Rect[], fill?: Fill, stroke?: Stroke) {
if (!fill && !stroke && rects.length > 0) {
return;
}

const rectsLength = rects.length;
ctx.beginPath();
for (let i = 0; i < rectsLength; i++) {
const { width, height, x, y } = rects[i];
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.lineTo(x, y);
}

if (fill) {
ctx.fillStyle = RGBtoString(fill.color);
ctx.fill();
}
if (stroke && stroke.width > 0.001) {
// Canvas2d stroke ignores an exact zero line width
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';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { Circle, Fill, Stroke } from '../../../../../geoms/types';
import { withContext } from '../../../../../renderers/canvas';
import { getRadians } from '../../../../../utils/common';
import { PointShape } from '../../../../../utils/themes/theme';
import { ShapeRendererFn } from '../../shapes_paths';
import { fillAndStroke } from './utils';
Expand All @@ -38,7 +39,7 @@ export function renderShape(
const [pathFn, rotation] = ShapeRendererFn[shape];
const { x, y, radius } = coordinates;
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.rotate(getRadians(rotation));
ctx.beginPath();

const path = new Path2D(pathFn(radius));
Expand Down
Loading

0 comments on commit fd0479f

Please sign in to comment.