From 023dda1946f2a3170877dd1c2a1b2d2fee3f8f9c Mon Sep 17 00:00:00 2001 From: Quentin Bullock Date: Wed, 15 Nov 2023 13:45:24 -0500 Subject: [PATCH 1/4] feat(line chart/point renderer): expose canvas context during point rendering This commit aims to allow users to specify a custom point rendering function while iterating a line. An example use case is adding the value label to the point in a custom manner --- .../xy_chart/renderer/canvas/points.ts | 7 +- .../xy_chart/renderer/dom/highlighter.tsx | 10 +-- packages/charts/src/utils/geometry.ts | 3 +- .../line/16_custom_point_shapes.story.tsx | 72 +++++++++++++++++++ storybook/stories/line/line.stories.tsx | 1 + 5 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 storybook/stories/line/16_custom_point_shapes.story.tsx diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts index 754e524659..93e51c6d09 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts @@ -15,7 +15,7 @@ import { Circle, Fill, Stroke } from '../../../../geoms/types'; import { Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { PointGeometry } from '../../../../utils/geometry'; -import { GeometryStateStyle } from '../../../../utils/themes/theme'; +import { GeometryStateStyle, PointShape } from '../../../../utils/themes/theme'; /** * Renders points from single series @@ -33,6 +33,9 @@ export function renderPoints(ctx: CanvasRenderingContext2D, points: PointGeometr ...style.stroke, color: overrideOpacity(style.stroke.color, (fillOpacity) => fillOpacity * opacity), }; + if (typeof style.shape === 'function') { + return style.shape(ctx, coordinates); + } renderShape(ctx, style.shape, coordinates, fill, stroke); }); } @@ -61,7 +64,7 @@ export function renderPointGroup( color: overrideOpacity(style.stroke.color, (fillOpacity) => fillOpacity * opacity), }; const coordinates: Circle = { x: x + transform.x, y, radius }; - const renderer = () => renderShape(ctx, style.shape, coordinates, fill, stroke); + const renderer = () => renderShape(ctx, style.shape as PointShape, coordinates, fill, stroke); const clippings = { area: getPanelClipping(panel, rotation), shouldClip }; withPanelTransform(ctx, panel, rotation, renderingArea, renderer, clippings); }); diff --git a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index afcc41c7d3..a0cd1e6904 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -19,7 +19,7 @@ import { getColorFromVariant, Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { isPointGeometry, IndexedGeometry, PointGeometry } from '../../../../utils/geometry'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { HighlighterStyle } from '../../../../utils/themes/theme'; +import { HighlighterStyle, PointShape } from '../../../../utils/themes/theme'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; import { getHighlightedGeomsSelector } from '../../state/selectors/get_tooltip_values_highlighted_geoms'; @@ -46,7 +46,7 @@ function getTransformForPanel(panel: Dimensions, rotation: Rotation, { left, top function renderPath(geom: PointGeometry, radius: number) { // keep the highlighter radius to a minimum - const [shapeFn, rotate] = ShapeRendererFn[geom.style.shape]; + const [shapeFn, rotate] = ShapeRendererFn[geom.style.shape as PointShape]; return { d: shapeFn(radius), rotate, @@ -75,8 +75,10 @@ class HighlighterComponent extends React.Component { const x = geom.x + geom.transform.x; const y = geom.y + geom.transform.y; const geomTransform = getTransformForPanel(panel, chartRotation, chartDimensions); - - if (isPointGeometry(geom)) { + if (typeof geom.style?.shape === 'function') { + return; + } + if (isPointGeometry(geom) ) { // using the stroke because the fill is always white on points const fillColor = getColorFromVariant(RGBATupleToString(geom.style.stroke.color), style.point.fill); const strokeColor = getColorFromVariant(RGBATupleToString(geom.style.stroke.color), style.point.stroke); diff --git a/packages/charts/src/utils/geometry.ts b/packages/charts/src/utils/geometry.ts index f99372e874..5eb94457d0 100644 --- a/packages/charts/src/utils/geometry.ts +++ b/packages/charts/src/utils/geometry.ts @@ -14,6 +14,7 @@ import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; import { LabelOverflowConstraint } from '../chart_types/xy_chart/utils/specs'; import { Color } from '../common/colors'; import { Fill, Stroke } from '../geoms/types'; +import { Coordinate } from '../common/geometry'; /** * The accessor type @@ -70,7 +71,7 @@ export interface PointGeometry { export interface PointGeometryStyle { fill: Fill; stroke: Stroke; - shape: PointShape; + shape: PointShape | ((ctx: CanvasRenderingContext2D, coordinates: { x: number; y: number }) => void); } /** @internal */ diff --git a/storybook/stories/line/16_custom_point_shapes.story.tsx b/storybook/stories/line/16_custom_point_shapes.story.tsx new file mode 100644 index 0000000000..516db17919 --- /dev/null +++ b/storybook/stories/line/16_custom_point_shapes.story.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { boolean } from '@storybook/addon-knobs'; +import React from 'react'; + +import { + Axis, + Chart, + LineSeries, + niceTimeFormatByDay, + Position, + ScaleType, + Settings, + timeFormatter, +} from '@elastic/charts'; +import { KIBANA_METRICS } from '@elastic/charts/src/utils/data_samples/test_dataset_kibana'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { getColorPicker } from '../utils/components/get_color_picker'; + +const dateFormatter = timeFormatter(niceTimeFormatByDay(1)); +const data = KIBANA_METRICS.metrics.kibana_os_load.v1.data.slice(10, 150); + +export const Example: ChartsStory = (_, { title, description }) => { + const showColorPicker = boolean('Show color picker', false); + + return ( + + + + `${Number(d).toFixed(0)}%`} + /> + { + return { + shape: (ctx, coordinates) => { + ctx.beginPath(); + ctx.ellipse(coordinates.x, coordinates.y, 5, 5, Math.PI / 4, 0, 2 * Math.PI); + ctx.fill(); + ctx.fillText(Math.floor(datum.y1), coordinates.x - 5, coordinates.y - 10); + }, + visible: true + }; + }} + data={data.map(([x, y], i) => [x, y + 60, i])} + /> + + ); +}; diff --git a/storybook/stories/line/line.stories.tsx b/storybook/stories/line/line.stories.tsx index 988b7d9e00..7d36761d70 100644 --- a/storybook/stories/line/line.stories.tsx +++ b/storybook/stories/line/line.stories.tsx @@ -25,3 +25,4 @@ export { Example as testPathOrdering } from './10_test_path_ordering.story'; export { Example as lineWithMarkAccessor } from './13_line_mark_accessor.story'; export { Example as pointShapes } from './14_point_shapes.story'; export { Example as testNegativePoints } from './15_test_negative_points.story'; +export { Example as customPointShapes } from './16_custom_point_shapes.story'; From 8bac653222605b91f08f69fd421be65fb29655d1 Mon Sep 17 00:00:00 2001 From: Quentin Bullock Date: Wed, 15 Nov 2023 14:09:47 -0500 Subject: [PATCH 2/4] Update type casting in highlighter.tsx --- .../src/chart_types/xy_chart/renderer/dom/highlighter.tsx | 3 ++- storybook/stories/line/16_custom_point_shapes.story.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index a0cd1e6904..ec32132852 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -71,11 +71,12 @@ class HighlighterComponent extends React.Component { {highlightedGeometries.map((geom, i) => { + console.log({geom}) const { panel } = geom; const x = geom.x + geom.transform.x; const y = geom.y + geom.transform.y; const geomTransform = getTransformForPanel(panel, chartRotation, chartDimensions); - if (typeof geom.style?.shape === 'function') { + if (typeof (geom as PointGeometry).style?.shape === 'function') { return; } if (isPointGeometry(geom) ) { diff --git a/storybook/stories/line/16_custom_point_shapes.story.tsx b/storybook/stories/line/16_custom_point_shapes.story.tsx index 516db17919..6ab0f5d203 100644 --- a/storybook/stories/line/16_custom_point_shapes.story.tsx +++ b/storybook/stories/line/16_custom_point_shapes.story.tsx @@ -62,7 +62,7 @@ export const Example: ChartsStory = (_, { title, description }) => { ctx.fill(); ctx.fillText(Math.floor(datum.y1), coordinates.x - 5, coordinates.y - 10); }, - visible: true + visible: true; }; }} data={data.map(([x, y], i) => [x, y + 60, i])} From 752cc28b1303750dc7d8b163650261d339e911ce Mon Sep 17 00:00:00 2001 From: Quentin Bullock Date: Wed, 15 Nov 2023 14:16:03 -0500 Subject: [PATCH 3/4] general clean up --- .../charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index ec32132852..7c8fd796ae 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -71,7 +71,6 @@ class HighlighterComponent extends React.Component { {highlightedGeometries.map((geom, i) => { - console.log({geom}) const { panel } = geom; const x = geom.x + geom.transform.x; const y = geom.y + geom.transform.y; From 92a1daa437b27767c2b95e5969dacb0e2ea3625d Mon Sep 17 00:00:00 2001 From: Quentin Bullock Date: Wed, 15 Nov 2023 14:17:20 -0500 Subject: [PATCH 4/4] Update point geometry checking --- .../src/chart_types/xy_chart/renderer/dom/highlighter.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index 7c8fd796ae..d4a7aa23e2 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -75,10 +75,10 @@ class HighlighterComponent extends React.Component { const x = geom.x + geom.transform.x; const y = geom.y + geom.transform.y; const geomTransform = getTransformForPanel(panel, chartRotation, chartDimensions); - if (typeof (geom as PointGeometry).style?.shape === 'function') { - return; - } - if (isPointGeometry(geom) ) { + if (isPointGeometry(geom)) { + if (typeof geom.style.shape === 'function') { + return; + } // using the stroke because the fill is always white on points const fillColor = getColorFromVariant(RGBATupleToString(geom.style.stroke.color), style.point.fill); const strokeColor = getColorFromVariant(RGBATupleToString(geom.style.stroke.color), style.point.stroke);