diff --git a/.eslintignore b/.eslintignore index 13ca9d8266..f7292e1a66 100644 --- a/.eslintignore +++ b/.eslintignore @@ -16,3 +16,6 @@ license_header.js # Compiled source src/utils/d3-delaunay/* **/dist + +# auto generated directories +integration/tmp diff --git a/.eslintrc.js b/.eslintrc.js index 70fc8a2cce..dbb10ed19b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, }, }, { diff --git a/api/charts.api.md b/api/charts.api.md index 180b6c91b9..2d6a0e5c96 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -120,6 +120,7 @@ export interface AreaSeriesStyle { export interface AreaStyle { fill?: Color | ColorVariant; opacity: number; + texture?: TexturedStyles; visible: boolean; } @@ -1585,6 +1586,7 @@ export interface RectBorderStyle { export interface RectStyle { fill?: Color | ColorVariant; opacity: number; + texture?: TexturedStyles; widthPixel?: Pixels; widthRatio?: Ratio; } @@ -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 & { + 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 | 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; + // @public (undocumented) export interface Theme { // (undocumented) diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-visually-looks-correct-1-snap.png index 55fc9a5a59..c717e48655 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-with-base-theme-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-with-base-theme-visually-looks-correct-1-snap.png index 55fc9a5a59..c717e48655 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-with-base-theme-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partial-custom-theme-with-base-theme-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-texture-multiple-series-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-texture-multiple-series-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..47ae395e5e Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-texture-multiple-series-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-with-texture-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-with-texture-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..112c888545 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-with-texture-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-allow-any-random-texture-customization-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-allow-any-random-texture-customization-1-snap.png new file mode 100644 index 0000000000..93892c5d73 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-allow-any-random-texture-customization-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-render-texture-with-lines-as-shape-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-render-texture-with-lines-as-shape-1-snap.png new file mode 100644 index 0000000000..14101b0df7 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-render-texture-with-lines-as-shape-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-use-custom-path-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-use-custom-path-1-snap.png new file mode 100644 index 0000000000..27bb43bf4a Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-use-custom-path-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-use-hover-opacity-for-texture-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-use-hover-opacity-for-texture-1-snap.png new file mode 100644 index 0000000000..13835c9f12 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-area-should-use-hover-opacity-for-texture-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-allow-any-random-texture-customization-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-allow-any-random-texture-customization-1-snap.png new file mode 100644 index 0000000000..24e6da9392 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-allow-any-random-texture-customization-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-render-texture-with-lines-as-shape-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-render-texture-with-lines-as-shape-1-snap.png new file mode 100644 index 0000000000..57b8eda6a0 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-render-texture-with-lines-as-shape-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-use-custom-path-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-use-custom-path-1-snap.png new file mode 100644 index 0000000000..4fb4380f26 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-use-custom-path-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-use-hover-opacity-for-texture-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-use-hover-opacity-for-texture-1-snap.png new file mode 100644 index 0000000000..a83be725a4 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-texture-bar-should-use-hover-opacity-for-texture-1-snap.png differ diff --git a/integration/tests/stylings_stories.test.ts b/integration/tests/stylings_stories.test.ts new file mode 100644 index 0000000000..f9fb769311 --- /dev/null +++ b/integration/tests/stylings_stories.test.ts @@ -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 }, + ); + }); + }); + }); +}); diff --git a/package.json b/package.json index 3dc2a5cec7..aaf44d10b3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/chart_types/xy_chart/renderer/canvas/areas.ts b/src/chart_types/xy_chart/renderer/canvas/areas.ts index 591e89f71c..ede7395189 100644 --- a/src/chart_types/xy_chart/renderer/canvas/areas.ts +++ b/src/chart_types/xy_chart/renderer/canvas/areas.ts @@ -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) => { @@ -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 }, ); @@ -96,6 +96,7 @@ export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometries function renderArea( ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, glyph: AreaGeometry, sharedStyle: SharedGeometryStateStyle, clippings: Rect, @@ -103,8 +104,9 @@ function renderArea( ) { 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( @@ -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); } diff --git a/src/chart_types/xy_chart/renderer/canvas/bars.ts b/src/chart_types/xy_chart/renderer/canvas/bars.ts index e1f4e114e1..1d32fe7488 100644 --- a/src/chart_types/xy_chart/renderer/canvas/bars.ts +++ b/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -32,6 +32,7 @@ import { withPanelTransform } from './utils/panel_transform'; /** @internal */ export function renderBars( ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, barGeometries: Array>, sharedStyle: SharedGeometryStateStyle, clippings: Rect, @@ -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, @@ -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); diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts index 92287e70cb..0e82caad7c 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts @@ -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'; @@ -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); + } } diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts index 08576e4c5a..0f9caf757a 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts @@ -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( @@ -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) { @@ -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'; - } - } -} diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts index 7a0ef6a967..88cc2e1305 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/shapes.ts @@ -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'; @@ -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)); diff --git a/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/src/chart_types/xy_chart/renderer/canvas/renderers.ts index 16bd5cb86c..ed6d233b14 100644 --- a/src/chart_types/xy_chart/renderer/canvas/renderers.ts +++ b/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -39,6 +39,8 @@ export function renderXYChartCanvas2d( clippings: Rect, props: ReactiveChartStateProps, ) { + const imgCanvas = document.createElement('canvas'); + withContext(ctx, (ctx) => { // let's set the devicePixelRatio once and for all; then we'll never worry about it again ctx.scale(dpr, dpr); @@ -114,13 +116,22 @@ export function renderXYChartCanvas2d( // rendering bars (ctx: CanvasRenderingContext2D) => { withContext(ctx, (ctx) => { - renderBars(ctx, geometries.bars, sharedStyle, clippings, renderingArea, highlightedLegendItem, rotation); + renderBars( + ctx, + imgCanvas, + geometries.bars, + sharedStyle, + clippings, + renderingArea, + highlightedLegendItem, + rotation, + ); }); }, // rendering areas (ctx: CanvasRenderingContext2D) => { withContext(ctx, (ctx) => { - renderAreas(ctx, { + renderAreas(ctx, imgCanvas, { areas: geometries.areas, clippings, renderingArea, diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts b/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts index b2558a49ac..75367f1e3b 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts @@ -19,16 +19,28 @@ import { stringToRGB } from '../../../../../common/color_library_wrappers'; import { Fill } from '../../../../../geoms/types'; -import { MockStyles } from '../../../../../mocks'; +import { getMockCanvas, getMockCanvasContext2D, MockStyles } from '../../../../../mocks'; import * as common from '../../../../../utils/common'; +import { getTextureStyles } from '../../../utils/texture'; import { buildAreaStyles } from './area'; +import 'jest-canvas-mock'; + jest.mock('../../../../../common/color_library_wrappers'); +jest.mock('../../../utils/texture'); jest.spyOn(common, 'getColorFromVariant'); const COLOR = 'aquamarine'; describe('Area styles', () => { + let ctx: CanvasRenderingContext2D; + let imgCanvas: HTMLCanvasElement; + + beforeEach(() => { + ctx = getMockCanvasContext2D(); + imgCanvas = getMockCanvas(); + }); + describe('#buildAreaStyles', () => { let result: Fill; let baseColor = COLOR; @@ -42,7 +54,7 @@ describe('Area styles', () => { } beforeEach(() => { - result = buildAreaStyles(baseColor, themeAreaStyle, geometryStateStyle); + result = buildAreaStyles(ctx, imgCanvas, baseColor, themeAreaStyle, geometryStateStyle); }); it('should call getColorFromVariant with correct args for fill', () => { @@ -84,5 +96,25 @@ describe('Area styles', () => { expect(result.color.opacity).toEqual(expected); }); }); + + describe('Texture', () => { + const texture = {}; + const mockTexture = {}; + + beforeAll(() => { + setDefaults(); + themeAreaStyle = MockStyles.area({ texture }); + (getTextureStyles as jest.Mock).mockReturnValue(mockTexture); + }); + + it('should return correct texture', () => { + expect(result.texture).toEqual(mockTexture); + }); + + it('should call getTextureStyles with params', () => { + expect(getTextureStyles).toBeCalledTimes(1); + expect(getTextureStyles).toBeCalledWith(ctx, imgCanvas, baseColor, expect.anything(), texture); + }); + }); }); }); diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/area.ts b/src/chart_types/xy_chart/renderer/canvas/styles/area.ts index edc296b4e3..66319baa06 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/area.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/area.ts @@ -19,8 +19,9 @@ import { OpacityFn, stringToRGB } from '../../../../../common/color_library_wrappers'; import { Fill } from '../../../../../geoms/types'; -import { getColorFromVariant } from '../../../../../utils/common'; +import { Color, ColorVariant, getColorFromVariant } from '../../../../../utils/common'; import { GeometryStateStyle, AreaStyle } from '../../../../../utils/themes/theme'; +import { getTextureStyles } from '../../../utils/texture'; /** * Return the rendering props for an area. The color of the area will be overwritten @@ -31,13 +32,19 @@ import { GeometryStateStyle, AreaStyle } from '../../../../../utils/themes/theme * @internal */ export function buildAreaStyles( - baseColor: string, + ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, + baseColor: Color | ColorVariant, themeAreaStyle: AreaStyle, geometryStateStyle: GeometryStateStyle, ): Fill { - const fillOpacity: OpacityFn = (opacity) => opacity * themeAreaStyle.opacity * geometryStateStyle.opacity; - const fillColor = stringToRGB(getColorFromVariant(baseColor, themeAreaStyle.fill), fillOpacity); + const fillOpacity: OpacityFn = (opacity, seriesOpacity = themeAreaStyle.opacity) => + opacity * seriesOpacity * geometryStateStyle.opacity; + const texture = getTextureStyles(ctx, imgCanvas, baseColor, fillOpacity, themeAreaStyle.texture); + const color = stringToRGB(getColorFromVariant(baseColor, themeAreaStyle.fill), fillOpacity); + return { - color: fillColor, + color, + texture, }; } diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts b/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts index fcef6922b3..07336c6606 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts @@ -19,16 +19,28 @@ import { stringToRGB } from '../../../../../common/color_library_wrappers'; import { Fill, Stroke } from '../../../../../geoms/types'; -import { MockStyles } from '../../../../../mocks'; +import { getMockCanvas, getMockCanvasContext2D, MockStyles } from '../../../../../mocks'; import * as common from '../../../../../utils/common'; +import { getTextureStyles } from '../../../utils/texture'; import { buildBarStyles } from './bar'; +import 'jest-canvas-mock'; + jest.mock('../../../../../common/color_library_wrappers'); +jest.mock('../../../utils/texture'); jest.spyOn(common, 'getColorFromVariant'); const COLOR = 'aquamarine'; describe('Bar styles', () => { + let ctx: CanvasRenderingContext2D; + let imgCanvas: HTMLCanvasElement; + + beforeEach(() => { + ctx = getMockCanvasContext2D(); + imgCanvas = getMockCanvas(); + }); + describe('#buildBarStyles', () => { let result: { fill: Fill; stroke: Stroke }; let baseColor = COLOR; @@ -44,7 +56,7 @@ describe('Bar styles', () => { } beforeEach(() => { - result = buildBarStyles(baseColor, themeRectStyle, themeRectBorderStyle, geometryStateStyle); + result = buildBarStyles(ctx, imgCanvas, baseColor, themeRectStyle, themeRectBorderStyle, geometryStateStyle); }); it('should call getColorFromVariant with correct args for fill', () => { @@ -149,5 +161,25 @@ describe('Bar styles', () => { }); }); }); + + describe('Texture', () => { + const texture = {}; + const mockTexture = {}; + + beforeAll(() => { + setDefaults(); + themeRectStyle = MockStyles.rect({ texture }); + (getTextureStyles as jest.Mock).mockReturnValue(mockTexture); + }); + + it('should return correct texture', () => { + expect(result.fill.texture).toEqual(mockTexture); + }); + + it('should call getTextureStyles with params', () => { + expect(getTextureStyles).toBeCalledTimes(1); + expect(getTextureStyles).toBeCalledWith(ctx, imgCanvas, baseColor, expect.anything(), texture); + }); + }); }); }); diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts b/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts index 7226cd92fc..e4e697d4c2 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts @@ -21,6 +21,7 @@ import { stringToRGB, OpacityFn } from '../../../../../common/color_library_wrap import { Stroke, Fill } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/common'; import { GeometryStateStyle, RectStyle, RectBorderStyle } from '../../../../../utils/themes/theme'; +import { getTextureStyles } from '../../../utils/texture'; /** * Return the rendering styles (stroke and fill) for a bar. @@ -35,15 +36,20 @@ import { GeometryStateStyle, RectStyle, RectBorderStyle } from '../../../../../u * @internal */ export function buildBarStyles( + ctx: CanvasRenderingContext2D, + imgCanvas: HTMLCanvasElement, baseColor: string, themeRectStyle: RectStyle, themeRectBorderStyle: RectBorderStyle, geometryStateStyle: GeometryStateStyle, ): { fill: Fill; stroke: Stroke } { - const fillOpacity: OpacityFn = (opacity) => opacity * themeRectStyle.opacity * geometryStateStyle.opacity; + const fillOpacity: OpacityFn = (opacity, seriesOpacity = themeRectStyle.opacity) => + opacity * seriesOpacity * geometryStateStyle.opacity; + const texture = getTextureStyles(ctx, imgCanvas, baseColor, fillOpacity, themeRectStyle.texture); const fillColor = stringToRGB(getColorFromVariant(baseColor, themeRectStyle.fill), fillOpacity); const fill: Fill = { color: fillColor, + texture, }; const defaultStrokeOpacity = themeRectBorderStyle.strokeOpacity === undefined ? themeRectStyle.opacity : themeRectBorderStyle.strokeOpacity; diff --git a/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts b/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts index d7706a0d08..6e2224b959 100644 --- a/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts +++ b/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts @@ -19,6 +19,7 @@ import { Fill, Stroke, Rect } from '../../../../../geoms/types'; import { withContext } from '../../../../../renderers/canvas'; +import { getRadians } from '../../../../../utils/common'; import { Point } from '../../../../../utils/point'; import { renderRect } from '../primitives/rect'; @@ -50,7 +51,7 @@ export function renderDebugRect( ) { withContext(ctx, (ctx) => { ctx.translate(rect.x, rect.y); - ctx.rotate((rotation * Math.PI) / 180); + ctx.rotate(getRadians(rotation)); renderRect( ctx, { @@ -78,7 +79,7 @@ export function renderDebugRectCenterRotated( withContext(ctx, (ctx) => { ctx.translate(x, y); - ctx.rotate((rotation * Math.PI) / 180); + ctx.rotate(getRadians(rotation)); ctx.translate(-x, -y); renderRect( ctx, diff --git a/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts b/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts index e41d6bbdbd..71f594f146 100644 --- a/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts +++ b/src/chart_types/xy_chart/renderer/canvas/utils/panel_transform.ts @@ -19,7 +19,7 @@ import { Rect } from '../../../../../geoms/types'; import { withContext } from '../../../../../renderers/canvas'; -import { Rotation } from '../../../../../utils/common'; +import { getRadians, Rotation } from '../../../../../utils/common'; import { Dimensions } from '../../../../../utils/dimensions'; import { computeChartTransform } from '../../../state/utils/utils'; @@ -40,7 +40,7 @@ export function withPanelTransform( const top = renderingArea.top + panel.top + transform.y; withContext(context, (ctx) => { ctx.translate(left, top); - ctx.rotate((rotation * Math.PI) / 180); + ctx.rotate(getRadians(rotation)); if (clippings?.shouldClip) { const { x, y, width, height } = clippings.area; diff --git a/src/chart_types/xy_chart/renderer/shapes_paths.ts b/src/chart_types/xy_chart/renderer/shapes_paths.ts index a3a8648c29..fbd58036fb 100644 --- a/src/chart_types/xy_chart/renderer/shapes_paths.ts +++ b/src/chart_types/xy_chart/renderer/shapes_paths.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PointShape } from '../../../utils/themes/theme'; +import { PointShape, TextureShape } from '../../../utils/themes/theme'; /** @internal */ export type SVGPath = string; @@ -44,7 +44,12 @@ export const square: SVGPathFn = (r: number) => { /** @internal */ export const circle: SVGPathFn = (r: number) => { - return `M ${-r} ${0} a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 ${-r * 2},0`; + return `M ${-r} 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 ${-r * 2},0`; +}; + +/** @internal */ +export const line: SVGPathFn = (r: number) => { + return `M 0 ${-r} l 0 ${r * 2}`; }; /** @internal */ @@ -56,3 +61,9 @@ export const ShapeRendererFn: Record = { [PointShape.Square]: [square, 0], [PointShape.Triangle]: [triangle, 0], }; + +/** @internal */ +export const TextureRendererFn: Record = { + ...ShapeRendererFn, + [TextureShape.Line]: [line, 0], +}; diff --git a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts index 1f2e37e32d..630a89928e 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts @@ -353,7 +353,7 @@ describe('Rendering bands - areas', () => { opacity: 1, }, rectBorder: { - strokeWidth: 0, + strokeWidth: 1, visible: false, }, }, @@ -389,7 +389,7 @@ describe('Rendering bands - areas', () => { opacity: 1, }, rectBorder: { - strokeWidth: 0, + strokeWidth: 1, visible: false, }, }, @@ -425,7 +425,7 @@ describe('Rendering bands - areas', () => { opacity: 1, }, rectBorder: { - strokeWidth: 0, + strokeWidth: 1, visible: false, }, }, diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index 19c86316cd..8f83de2223 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -27,6 +27,7 @@ import { VerticalAlignment, HorizontalAlignment, getPercentageValue, + getRadians, } from '../../../utils/common'; import { Dimensions, Margins, getSimplePadding, Size } from '../../../utils/dimensions'; import { Range } from '../../../utils/domain'; @@ -197,9 +198,7 @@ export function getScaleForAxisSpec( /** @internal */ export function computeRotatedLabelDimensions(unrotatedDims: BBox, degreesRotation: number): BBox { const { width, height } = unrotatedDims; - - const radians = (degreesRotation * Math.PI) / 180; - + const radians = getRadians(degreesRotation); const rotatedHeight = Math.abs(width * Math.sin(radians)) + Math.abs(height * Math.cos(radians)); const rotatedWidth = Math.abs(width * Math.cos(radians)) + Math.abs(height * Math.sin(radians)); diff --git a/src/chart_types/xy_chart/utils/texture.ts b/src/chart_types/xy_chart/utils/texture.ts new file mode 100644 index 0000000000..a06e842a42 --- /dev/null +++ b/src/chart_types/xy_chart/utils/texture.ts @@ -0,0 +1,115 @@ +/* + * 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 { OpacityFn } from '../../../common/color_library_wrappers'; +import { Texture } from '../../../geoms/types'; +import { Color, ColorVariant, getColorFromVariant, getRadians } from '../../../utils/common'; +import { Point } from '../../../utils/point'; +import { TexturedStyles, TextureShape } from '../../../utils/themes/theme'; +import { TextureRendererFn } from '../renderer/shapes_paths'; + +const getSpacing = ({ spacing }: TexturedStyles): Point => ({ + x: typeof spacing === 'number' ? spacing : spacing?.x ?? 0, + y: typeof spacing === 'number' ? spacing : spacing?.y ?? 0, +}); + +const getPath = (textureStyle: TexturedStyles, size: number, stokeWith: number): [path: Path2D, rotation: number] => { + if ('path' in textureStyle) { + const path = typeof textureStyle.path === 'string' ? new Path2D(textureStyle.path) : textureStyle.path; + + return [path, 0]; + } + const [pathFn, rotation] = TextureRendererFn[textureStyle.shape]; + // Prevents clipping shapes near edge + const stokeWidthPadding = [TextureShape.Circle, TextureShape.Square].includes(textureStyle.shape as any) + ? stokeWith + : 0; + + return [new Path2D(pathFn((size - stokeWidthPadding) / 2)), rotation]; +}; + +/** @internal */ +function createPattern( + ctx: CanvasRenderingContext2D, + dpi: number, + patternCanvas: HTMLCanvasElement, + baseColor: Color | ColorVariant, + fillOpacity: OpacityFn, + textureStyle?: TexturedStyles, +): CanvasPattern | undefined { + const pCtx = patternCanvas.getContext('2d'); + if (!textureStyle || !pCtx) return; + + const { size = 10, stroke, strokeWidth = 1, opacity, shapeRotation, fill, dash } = textureStyle; + + const spacing = getSpacing(textureStyle); + const cssWidth = size + spacing.x; + const cssHeight = size + spacing.y; + patternCanvas.width = dpi * cssWidth; + patternCanvas.height = dpi * cssHeight; + + pCtx.globalAlpha = opacity ? fillOpacity(opacity, 1) : fillOpacity(1); + pCtx.lineWidth = strokeWidth; + + pCtx.strokeStyle = getColorFromVariant(baseColor, stroke ?? ColorVariant.Series); + if (dash) pCtx.setLineDash(dash); + + if (fill) pCtx.fillStyle = getColorFromVariant(baseColor, fill); + + const [path, pathRotation] = getPath(textureStyle, size, strokeWidth); + const rotation = (shapeRotation ?? 0) + pathRotation; + + pCtx.scale(dpi, dpi); + pCtx.translate(cssWidth / 2, cssHeight / 2); + + if (rotation) pCtx.rotate(getRadians(rotation)); + + pCtx.beginPath(); + + if (path) { + pCtx.stroke(path); + if (fill) pCtx.fill(path); + } + + return ctx.createPattern(patternCanvas, 'repeat') ?? undefined; +} + +/** @internal */ +export const getTextureStyles = ( + ctx: CanvasRenderingContext2D, + patternCanvas: HTMLCanvasElement, + baseColor: Color | ColorVariant, + fillOpacity: OpacityFn, + texture?: TexturedStyles, +): Texture | undefined => { + const dpi = window.devicePixelRatio; + const pattern = createPattern(ctx, dpi, patternCanvas, baseColor, fillOpacity, texture); + + if (!pattern || !texture) return; + + const scale = 1 / dpi; + pattern.setTransform(new DOMMatrix([scale, 0, 0, scale, 0, 0])); + const { rotation, offset } = texture; + + return { + pattern, + rotation, + offset, + }; +}; diff --git a/src/common/color_library_wrappers.ts b/src/common/color_library_wrappers.ts index ca3989be5d..92c5434446 100644 --- a/src/common/color_library_wrappers.ts +++ b/src/common/color_library_wrappers.ts @@ -40,7 +40,7 @@ export const transparentColor: RgbObject = { r: 0, g: 0, b: 0, opacity: 0 }; export const defaultD3Color: D3RGBColor = d3Rgb(defaultColor.r, defaultColor.g, defaultColor.b, defaultColor.opacity); /** @internal */ -export type OpacityFn = (colorOpacity: number) => number; +export type OpacityFn = (opacity: number, seriesOpacity?: number) => number; /** @internal */ export function stringToRGB(cssColorSpecifier?: string, opacity?: number | OpacityFn): RgbObject { diff --git a/src/geoms/types.ts b/src/geoms/types.ts index 1536d938c3..fd4d7a8f0b 100644 --- a/src/geoms/types.ts +++ b/src/geoms/types.ts @@ -19,10 +19,9 @@ import { RgbObject } from '../common/color_library_wrappers'; import { Radian } from '../common/geometry'; +import { TexturedStyles } from '../utils/themes/theme'; -/** - * @internal - */ +/** @internal */ export interface Text { text: string; x: number; @@ -60,6 +59,17 @@ export interface Circle { radius: number; } +/** + * render options for texture + * @public + */ +export interface Texture extends Pick { + /** + * patern to apply to canvas fill + */ + pattern: CanvasPattern; +} + /** * Fill style for every geometry * @public @@ -69,6 +79,7 @@ export interface Fill { * fill color in rgba */ color: RgbObject; + texture?: Texture; } /** diff --git a/src/mocks/canvas.ts b/src/mocks/canvas.ts new file mode 100644 index 0000000000..2f57d2b4e5 --- /dev/null +++ b/src/mocks/canvas.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +/** @internal */ +export const getMockCanvasContext2D = (): CanvasRenderingContext2D => { + const ctx = document.createElement('canvas').getContext('2d'); + if (ctx) return ctx; + + throw new Error('Unable to create mock context'); +}; + +/** @internal */ +export const getMockCanvas = (): HTMLCanvasElement => { + return document.createElement('canvas'); +}; diff --git a/src/mocks/index.ts b/src/mocks/index.ts index 7f81239f45..df6d39a673 100644 --- a/src/mocks/index.ts +++ b/src/mocks/index.ts @@ -20,3 +20,4 @@ export * from './series'; export * from './geometries'; export * from './theme'; +export * from './canvas'; diff --git a/src/mocks/series/series_identifiers.ts b/src/mocks/series/series_identifiers.ts index bf86d83ba0..d8103aa8d3 100644 --- a/src/mocks/series/series_identifiers.ts +++ b/src/mocks/series/series_identifiers.ts @@ -29,6 +29,8 @@ export class MockSeriesIdentifier { seriesKeys: ['a'], splitAccessors: new Map().set('g', 'a'), key: 'spec{bars}yAccessor{y}splitAccessors{g-a}', + smHorizontalAccessorValue: undefined, + smVerticalAccessorValue: undefined, }; static default(partial?: Partial) { diff --git a/src/mocks/theme.ts b/src/mocks/theme.ts index a35c6ca3f0..56be07cbba 100644 --- a/src/mocks/theme.ts +++ b/src/mocks/theme.ts @@ -55,6 +55,7 @@ export class MockStyles { opacity: 1, }, partial, + { mergeOptionalPartialValues: true }, ); } @@ -67,6 +68,7 @@ export class MockStyles { strokeOpacity: 1, }, partial, + { mergeOptionalPartialValues: true }, ); } @@ -78,6 +80,7 @@ export class MockStyles { opacity: 1, }, partial, + { mergeOptionalPartialValues: true }, ); } @@ -91,6 +94,7 @@ export class MockStyles { dash: [1, 2, 1], }, partial, + { mergeOptionalPartialValues: true }, ); } @@ -105,6 +109,7 @@ export class MockStyles { radius: 10, }, partial, + { mergeOptionalPartialValues: true }, ); } @@ -114,6 +119,7 @@ export class MockStyles { opacity: 1, }, partial, + { mergeOptionalPartialValues: true }, ); } } diff --git a/src/mocks/utils.ts b/src/mocks/utils.ts index 2d9701eb12..e1e36cae22 100644 --- a/src/mocks/utils.ts +++ b/src/mocks/utils.ts @@ -71,3 +71,25 @@ export class SeededDataGenerator extends DataGenerator { super(frequency, getRandomNumberGenerator()); } } + +/** + * Returns random array or object value + * @internal + */ +export const getRandomEntryFn = (seed = getRNGSeed()) => { + const rng = seedrandom(seed); + + return function getRandomEntryClosure(entries: T[] | Record) { + if (Array.isArray(entries)) { + const index = Math.floor(rng() * entries.length); + + return entries[index]; + } + + const keys = Object.keys(entries); + const index = Math.floor(rng() * keys.length); + const key = keys[index]; + + return entries[key]; + }; +}; diff --git a/src/renderers/canvas/index.ts b/src/renderers/canvas/index.ts index f3866e7efe..25e56cb1b5 100644 --- a/src/renderers/canvas/index.ts +++ b/src/renderers/canvas/index.ts @@ -19,6 +19,7 @@ import { Coordinate } from '../../common/geometry'; import { Rect } from '../../geoms/types'; +import { getRadians } from '../../utils/common'; import { ClippedRanges } from '../../utils/geometry'; import { Point } from '../../utils/point'; @@ -119,7 +120,7 @@ export function withRotatedOrigin( withContext(ctx, (ctx) => { const { x, y } = origin; ctx.translate(x, y); - ctx.rotate((rotation * Math.PI) / 180); + ctx.rotate(getRadians(rotation)); ctx.translate(-x, -y); fn(ctx); }); diff --git a/src/utils/common.tsx b/src/utils/common.tsx index 88f50fa23d..b67573dc14 100644 --- a/src/utils/common.tsx +++ b/src/utils/common.tsx @@ -189,6 +189,13 @@ export function getColorFromVariant(seriesColor: Color, color?: Color | ColorVar return color || seriesColor; } +/** + * Converts degree to radians + * @param angle - in degrees + * @public + */ +export const getRadians = (angle: number) => (angle * Math.PI) / 180; + /** * This function returns a function to generate ids. * This can be used to generate unique, but predictable ids to pair labels diff --git a/src/utils/point.ts b/src/utils/point.ts index d27cc1c500..0772b7a200 100644 --- a/src/utils/point.ts +++ b/src/utils/point.ts @@ -17,11 +17,12 @@ * under the License. */ -/** @internal */ +/** @public */ export interface Point { x: number; y: number; } + /** @internal * */ export function getDelta(start: Point, end: Point) { return Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); diff --git a/src/utils/themes/dark_theme.ts b/src/utils/themes/dark_theme.ts index a209620d3b..79bb60b4b2 100644 --- a/src/utils/themes/dark_theme.ts +++ b/src/utils/themes/dark_theme.ts @@ -80,7 +80,7 @@ export const DARK_THEME: Theme = { }, rectBorder: { visible: false, - strokeWidth: 0, + strokeWidth: 1, }, displayValue: { fontSize: 8, diff --git a/src/utils/themes/light_theme.ts b/src/utils/themes/light_theme.ts index e1e85c5b97..a3a2f73929 100644 --- a/src/utils/themes/light_theme.ts +++ b/src/utils/themes/light_theme.ts @@ -80,7 +80,7 @@ export const LIGHT_THEME: Theme = { }, rectBorder: { visible: false, - strokeWidth: 0, + strokeWidth: 1, }, displayValue: { fontSize: 8, diff --git a/src/utils/themes/theme.ts b/src/utils/themes/theme.ts index 7574ec4749..5cb83434cf 100644 --- a/src/utils/themes/theme.ts +++ b/src/utils/themes/theme.ts @@ -22,6 +22,7 @@ import { $Values } from 'utility-types'; import { Pixels, Ratio } from '../../common/geometry'; import { Color, ColorVariant, HorizontalAlignment, RecursivePartial, VerticalAlignment } from '../common'; import { Margins, SimplePadding } from '../dimensions'; +import { Point } from '../point'; /** @public */ export interface Visible { @@ -369,8 +370,70 @@ export interface LineStyle { dash?: number[]; } +/** @public */ +export const TextureShape = Object.freeze({ + ...PointShape, + Line: 'line' as const, +}); +/** @public */ +export type TextureShape = $Values; + +/** @public */ +export interface TexturedStylesBase { + /** polygon fill color for texture */ + fill?: Color | ColorVariant; + /** polygon stroke color for texture */ + stroke?: Color | ColorVariant; + /** polygon stroke width for texture */ + strokeWidth?: number; + /** polygon opacity for texture */ + opacity?: number; + /** polygon opacity for texture */ + dash?: number[]; + /** polygon opacity for texture */ + size?: number; + /** + * The angle of rotation for entire texture + * in degrees + */ + rotation?: number; + /** + * The angle of rotation for polygons + * in degrees + */ + shapeRotation?: number; + /** texture spacing between polygons */ + spacing?: Partial | number; + /** overall origin offset of pattern */ + offset?: Partial & { + /** apply offset along global coordinate axes */ + global?: boolean; + }; +} + +/** @public */ +export interface TexturedShapeStyles extends TexturedStylesBase { + /** typed of texture designs currently supported */ + shape: TextureShape; +} + +/** @public */ +export interface TexturedPathStyles extends TexturedStylesBase { + /** path for polygon texture */ + path: string | Path2D; +} + +/** + * @public + * + * Texture style config for area spec + */ +export type TexturedStyles = TexturedPathStyles | TexturedShapeStyles; + /** @public */ export interface AreaStyle { + /** applying textures to the area on the theme/series */ + texture?: TexturedStyles; /** is the area is visible or hidden ? */ visible: boolean; /** a static fill color if defined, if not it will use the color of the series */ @@ -405,6 +468,8 @@ export interface RectStyle { /** The ratio of the width limited to [0,1]. If expressed together with `widthPixel` then the `widthRatio` * will express the max available size, where the `widthPixel` express the derived/min width. */ widthRatio?: Ratio; + /** applying textures to the bar on the theme/series */ + texture?: TexturedStyles; } /** @public */ diff --git a/stories/bar/32_scale_to_extent.tsx b/stories/bar/32_scale_to_extent.tsx index 0366f4ae75..0465df7942 100644 --- a/stories/bar/32_scale_to_extent.tsx +++ b/stories/bar/32_scale_to_extent.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { Axis, Chart, DomainPaddingUnit, Position, ScaleType } from '../../src'; import { computeContinuousDataDomain } from '../../src/utils/domain'; -import { getKnobsFromEnum, getXYSeriesTypeKnob } from '../utils/knobs'; +import { getKnobsFromEnum, getXYSeriesKnob } from '../utils/knobs'; import { SB_SOURCE_PANEL } from '../utils/storybook'; const logDomains = (data: any[], customDomain: any) => { @@ -65,7 +65,7 @@ export const Example = () => { }, 'all negative', ); - const SeriesType = getXYSeriesTypeKnob(); + const [SeriesType] = getXYSeriesKnob(); const shouldLogDomains = boolean('console log domains', true); let data; diff --git a/stories/stylings/23_with_texture.tsx b/stories/stylings/23_with_texture.tsx new file mode 100644 index 0000000000..f0e5679403 --- /dev/null +++ b/stories/stylings/23_with_texture.tsx @@ -0,0 +1,136 @@ +/* + * 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 { array, boolean, color, number, text } from '@storybook/addon-knobs'; +import React from 'react'; + +import { + Axis, + Chart, + CurveType, + Position, + ScaleType, + TexturedStyles, + Settings, + TextureShape, + LIGHT_THEME, +} from '../../src'; +import { SeededDataGenerator } from '../../src/mocks/utils'; +import { getKnobsFromEnum, getXYSeriesKnob } from '../utils/knobs'; +import { SB_KNOBS_PANEL } from '../utils/storybook'; + +const dg = new SeededDataGenerator(); +const barData = dg.generateBasicSeries(4); +const areaData = dg.generateBasicSeries(20, 10); + +const group = { + texture: 'Texture', + pattern: 'Pattern', + series: 'Series', +}; +const STAR = + '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'; +const DEFAULT_COLOR = LIGHT_THEME.colors.vizColors[0]; + +const getTextureKnobs = (useCustomPath: boolean): TexturedStyles => ({ + ...(useCustomPath + ? { path: text('Custom path', STAR, group.texture) } + : { + shape: + getKnobsFromEnum('Shape', TextureShape, TextureShape.Line as TextureShape, { + group: group.texture, + }) ?? TextureShape.Line, + }), + stroke: boolean('Use stroke color', true, group.texture) + ? color('Stoke color', DEFAULT_COLOR, group.texture) + : undefined, + strokeWidth: number('Stroke width', 1, { min: 0, max: 10, step: 0.5 }, group.texture), + dash: array('Stroke dash', [], ',', group.texture).map((n) => parseInt(n, 10)), + fill: boolean('Use fill color', true, group.texture) ? color('Fill color', DEFAULT_COLOR, group.texture) : undefined, + rotation: number('Rotation (degrees)', 45, { min: -365, max: 365 }, group.pattern), + opacity: number('Opacity', 1, { min: 0, max: 1, step: 0.1 }, group.texture), + shapeRotation: number('Shape rotation (degrees)', 0, { min: -365, max: 365 }, group.texture), + size: useCustomPath + ? number('Shape size - custom path', 20, { min: 0 }, group.texture) + : number('Shape size', 20, { min: 0 }, group.texture), + spacing: { + x: number('Shape spacing - x', 10, { min: 0 }, group.pattern), + y: number('Shape spacing - y', 0, { min: 0 }, group.pattern), + }, + offset: { + x: number('Pattern offset - x', 0, {}, group.pattern), + y: number('Pattern offset - y', 0, {}, group.pattern), + global: boolean('Apply offset along global coordinate axes', true, group.pattern), + }, +}); + +export const Example = () => { + const useCustomPath = boolean('Use custom path', false, group.texture); + const texture: TexturedStyles = getTextureKnobs(useCustomPath); + const opacity = number('Series opacity', 1, { min: 0, max: 1, step: 0.1 }, group.series); + const showFill = boolean('Show series fill', false, group.series); + const seriesColor = color('Series color', DEFAULT_COLOR, group.series); + const [SeriesType, seriesType] = getXYSeriesKnob('Series type', 'area', group.series, { ignore: ['bubble', 'line'] }); + + return ( + + + + + Number(d).toFixed(2)} /> + + + + ); +}; + +// storybook configuration +Example.story = { + parameters: { + options: { selectedPanel: SB_KNOBS_PANEL }, + }, +}; diff --git a/stories/stylings/24_texture_multiple_series.tsx b/stories/stylings/24_texture_multiple_series.tsx new file mode 100644 index 0000000000..b22e3fe75a --- /dev/null +++ b/stories/stylings/24_texture_multiple_series.tsx @@ -0,0 +1,171 @@ +/* + * 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 { boolean, color, number, button } from '@storybook/addon-knobs'; +import React, { useState } from 'react'; + +import { Axis, Chart, CurveType, Position, TexturedStyles, Settings, TextureShape } from '../../src'; +import { getRandomNumberGenerator, SeededDataGenerator, getRandomEntryFn } from '../../src/mocks/utils'; +import { getKnobsFromEnum, getXYSeriesKnob } from '../utils/knobs'; +import { SB_KNOBS_PANEL } from '../utils/storybook'; + +const group = { + random: 'Randomized parameters', + default: 'Default parameters', +}; +const dg = new SeededDataGenerator(); +const rng = getRandomNumberGenerator(); +const getRandomEntry = getRandomEntryFn(); + +interface Random { + shape: boolean; + rotation: boolean; + shapeRotation: boolean; + size: boolean; + spacing: { + x: boolean; + y: boolean; + }; + offset: { + x: boolean; + y: boolean; + }; +} + +const getDefaultTextureKnobs = (): TexturedStyles => ({ + shape: + getKnobsFromEnum('Shape', TextureShape, TextureShape.Circle as TextureShape, { + group: group.default, + }) ?? TextureShape.Circle, + strokeWidth: number('Stroke width', 1, { min: 0, max: 10, step: 0.5 }, group.default), + rotation: number('Rotation (degrees)', 45, { min: -365, max: 365 }, group.default), + shapeRotation: number('Shape rotation (degrees)', 0, { min: -365, max: 365 }, group.default), + size: number('Shape size', 20, { min: 0 }, group.default), + opacity: number('Opacity', 1, { min: 0, max: 1, step: 0.1 }, group.default), + spacing: { + x: number('Shape spacing - x', 10, { min: 0 }, group.default), + y: number('Shape spacing - y', 10, { min: 0 }, group.default), + }, + offset: { + x: number('Pattern offset - x', 0, {}, group.default), + y: number('Pattern offset - y', 0, {}, group.default), + global: true, + }, +}); + +const getRandomKnobs = (): Random => ({ + shape: boolean('Shape', true, group.random), + rotation: boolean('Rotation', false, group.random), + shapeRotation: boolean('Shape rotation', false, group.random), + size: boolean('Size', true, group.random), + spacing: { + x: boolean('X spacing', false, group.random), + y: boolean('Y spacing', false, group.random), + }, + offset: { + x: boolean('X offset', false, group.random), + y: boolean('Y offset', false, group.random), + }, +}); + +const getTexture = (randomize: Random): Partial => ({ + shape: randomize.shape ? getRandomEntry(TextureShape) : undefined, + rotation: randomize.rotation ? rng(0, 365) : undefined, + shapeRotation: randomize.shapeRotation ? rng(0, 365) : undefined, + size: randomize.size ? rng(5, 30) : undefined, + spacing: { + x: randomize.spacing.x ? rng(0, 30) : undefined, + y: randomize.spacing.y ? rng(0, 30) : undefined, + }, + offset: { + x: randomize.offset.x ? rng(0, 30) : undefined, + y: randomize.offset.y ? rng(0, 30) : undefined, + }, +}); + +const data = new Array(10).fill(0).map(() => dg.generateBasicSeries(10, 10)); + +export const Example = () => { + const [count, setCount] = useState(0); + button('Randomize', () => setCount((i) => i + 1), group.random); + const n = number('Total series', 4, { min: 0, max: 10, step: 1 }) ?? 2; + const showLegend = boolean('Show legend', false); + const showFill = boolean('Show series fill', false); + const chartColor = color('Chart color', 'rgba(0,0,0,1)'); + const random = getRandomKnobs(); + const [SeriesType] = getXYSeriesKnob('Series type', 'area', undefined, { ignore: ['bubble', 'line'] }); + const texture = getDefaultTextureKnobs(); + + return ( + + + + + + + {new Array(n).fill(0).map((v, i) => ( + + ))} + + ); +}; + +// storybook configuration +Example.story = { + parameters: { + options: { selectedPanel: SB_KNOBS_PANEL }, + }, +}; diff --git a/stories/stylings/stylings.stories.tsx b/stories/stylings/stylings.stories.tsx index de0415db85..9942a0c7e6 100644 --- a/stories/stylings/stylings.stories.tsx +++ b/stories/stylings/stylings.stories.tsx @@ -49,3 +49,5 @@ export { Example as areaSeriesColorVariant } from './19_area_series_color_varian export { Example as partitionBackground } from './20_partition_background'; export { Example as partitionLabels } from './21_partition_labels'; export { Example as darkTheme } from './22_dark_theme'; +export { Example as withTexture } from './23_with_texture'; +export { Example as textureMultipleSeries } from './24_texture_multiple_series'; diff --git a/stories/utils/knobs.ts b/stories/utils/knobs.ts index 8618c5e66b..0e625da7ec 100644 --- a/stories/utils/knobs.ts +++ b/stories/utils/knobs.ts @@ -21,6 +21,7 @@ import { PopoverAnchorPosition } from '@elastic/eui'; import { select, array, number, optionsKnob } from '@storybook/addon-knobs'; import { SelectTypeKnobValue } from '@storybook/addon-knobs/dist/components/types'; import { startCase, kebabCase } from 'lodash'; +import { $Values } from 'utility-types'; import { Rotation, @@ -283,19 +284,34 @@ export const getHorizontalTextAlignmentKnob = (group?: string) => group, ) || undefined; -const seriesTypeMap = { +export const XYSeriesTypeMap = { [SeriesType.Bar]: BarSeries, [SeriesType.Line]: LineSeries, [SeriesType.Area]: AreaSeries, [SeriesType.Bubble]: BubbleSeries, }; -export const getXYSeriesTypeKnob = (group?: string, ignore: SeriesType[] = []) => { - const spectType = select( - 'SeriesType', - Object.fromEntries(Object.entries(SeriesType).filter(([, type]) => !ignore.includes(type))), - SeriesType.Bar, - group, + +export const getXYSeriesTypeKnob = ( + name = 'SeriesType', + value: SeriesType = SeriesType.Bar, + groupId?: string, + options?: { ignore: SeriesType[] }, +) => { + return select( + name, + Object.fromEntries(Object.entries(SeriesType).filter(([, type]) => !(options?.ignore ?? []).includes(type))), + value, + groupId, ); +}; + +export const getXYSeriesKnob = ( + name = 'SeriesType', + value: SeriesType = SeriesType.Bar, + groupId?: string, + options?: { ignore: SeriesType[] }, +): [$Values, SeriesType] => { + const spectType = getXYSeriesTypeKnob(name, value, groupId, options); - return seriesTypeMap[spectType]; + return [XYSeriesTypeMap[spectType], spectType]; }; diff --git a/yarn.lock b/yarn.lock index 41f99e66f1..12f9f710fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9417,7 +9417,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -10156,6 +10156,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" + integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M= + csso@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" @@ -11519,7 +11524,8 @@ eslint-module-utils@^2.6.0: pkg-dir "^2.0.0" "eslint-plugin-elastic-charts@link:./packages/eslint-plugin-elastic-charts": - version "1.0.0" + version "0.0.0" + uid "" eslint-plugin-eslint-comments@^3.2.0: version "3.2.0" @@ -14967,6 +14973,14 @@ java-properties@^1.0.0: resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ== +jest-canvas-mock@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.3.1.tgz#9535d14bc18ccf1493be36ac37dd349928387826" + integrity sha512-5FnSZPrX3Q2ZfsbYNE3wqKR3+XorN8qFzDzB5o0golWgt6EOX1+emBnpOc9IAQ+NXFj8Nzm3h7ZdE/9H0ylBcg== + dependencies: + cssfontparser "^1.2.1" + moo-color "^1.0.2" + jest-changed-files@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" @@ -16150,13 +16164,8 @@ lines-and-columns@^1.1.6: integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= "link-kibana@link:./packages/link_kibana": - version "1.0.0" - dependencies: - chalk "^4.1.1" - change-case "^4.1.2" - glob "^7.1.7" - inquirer "^8.0.0" - ora "^5.4.0" + version "0.0.0" + uid "" lint-staged@^10.5.3: version "10.5.3" @@ -17398,6 +17407,13 @@ monocle-ts@^1.0.0: resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.2.tgz#d9825ae18846ab63f915cb6f2194a78a40025610" integrity sha512-F08hPUzQ14vOtac2vOagnvXPr0R0MRKWXF6Bwd3gQ4XnV2qfU0MzPL+L18kX4dXBkat74pxbL88V1BjAj3YOWg== +moo-color@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.2.tgz#837c40758d2d58763825d1359a84e330531eca64" + integrity sha512-5iXz5n9LWQzx/C2WesGFfpE6RLamzdHwsn3KpfzShwbfIqs7stnoEpaNErf/7+3mbxwZ4s8Foq7I0tPxw7BWHg== + dependencies: + color-name "^1.1.4" + moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"