diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-can-style-the-area-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-can-style-the-area-1-snap.png new file mode 100644 index 0000000000..19ecdaa254 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-can-style-the-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-can-style-the-line-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-can-style-the-line-1-snap.png new file mode 100644 index 0000000000..6f7fc5a8ba Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-can-style-the-line-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-apply-opacity-on-styled-fit-series-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-apply-opacity-on-styled-fit-series-1-snap.png new file mode 100644 index 0000000000..97def0099f Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-apply-opacity-on-styled-fit-series-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-interpolated-area-as-non-interpolated-areas-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-interpolated-area-as-non-interpolated-areas-1-snap.png new file mode 100644 index 0000000000..787ef2d450 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-interpolated-area-as-non-interpolated-areas-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-interpolated-line-as-non-interpolated-lines-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-interpolated-line-as-non-interpolated-lines-1-snap.png new file mode 100644 index 0000000000..6a12fa01ef Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-interpolated-line-as-non-interpolated-lines-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-style-on-stacked-areas-1-snap.png b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-style-on-stacked-areas-1-snap.png new file mode 100644 index 0000000000..be85d35d14 Binary files /dev/null and b/integration/tests/__image_snapshots__/stylings-stories-test-ts-stylings-stories-fit-function-styling-should-render-style-on-stacked-areas-1-snap.png differ diff --git a/integration/tests/stylings_stories.test.ts b/integration/tests/stylings_stories.test.ts index b49796e7c8..09a2755dd8 100644 --- a/integration/tests/stylings_stories.test.ts +++ b/integration/tests/stylings_stories.test.ts @@ -38,4 +38,45 @@ describe('Stylings stories', () => { }); }); }); + describe('Fit function styling', () => { + it('can style the line', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&globals=theme:light&knob-Curve=0&knob-End value=none&knob-Explicit value (using Fit.Explicit)=5&knob-dataset=all&knob-fit area color_fit style=rgba(0,0,0,1)&knob-fit area opacity_fit style=0.15&knob-fit line color_fit style=rgba(5, 5, 5, 1)&knob-fit line dash array_fit style=5,10,1,10&knob-fit line opacity_fit style=0.4&knob-fitting function=average&knob-seriesType=line&knob-use series color for area_fit style=true&knob-use series color for line_fit style=&knob-use texture on area_fit style=', + ); + }); + it('can style the area', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&globals=theme:light&knob-Curve=0&knob-End value=none&knob-Explicit value (using Fit.Explicit)=5&knob-dataset=all&knob-fit area color_fit style=rgba(0,0,0,1)&knob-fit area opacity_fit style=0.08&knob-fit line color_fit style=rgba(5, 5, 5, 1)&knob-fit line dash array_fit style=5,10,1,10&knob-fit line opacity_fit style=0&knob-fitting function=average&knob-seriesType=area&knob-use series color for area_fit style=&knob-use series color for line_fit style=&knob-use texture on area_fit style=true', + ); + }); + + it('should render interpolated line as non-interpolated lines', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&globals=theme:light&knob-Curve=0&knob-End value=nearest&knob-Explicit value (using Fit.Explicit)=5&knob-dataset=all&knob-fit area color_fit style=rgba(0,0,0,1)&knob-fit area opacity_fit style=0.3&knob-fit line color_fit style=rgba(5, 5, 5, 1)&knob-fit line dash array_fit style=&knob-fit line opacity_fit style=1&knob-fitting function=linear&knob-seriesType=line&knob-use series color for area_fit style=true&knob-use series color for line_fit style=true&knob-use texture on area_fit style=', + ); + }); + it('should render interpolated area as non-interpolated areas', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&globals=theme:light&knob-Curve=0&knob-End value=nearest&knob-Explicit value (using Fit.Explicit)=5&knob-dataset=all&knob-fit area color_fit style=rgba(0,0,0,1)&knob-fit area opacity_fit style=0.3&knob-fit line color_fit style=rgba(5, 5, 5, 1)&knob-fit line dash array_fit style=&knob-fit line opacity_fit style=1&knob-fitting function=linear&knob-seriesType=area&knob-use series color for area_fit style=true&knob-use series color for line_fit style=true&knob-use texture on area_fit style=', + ); + }); + it('should render style on stacked areas', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/mixed-charts--fitting-functions-stacked-series&globals=theme:light&knob-stackMode=none&knob-dataset=all&knob-fitting function=linear&knob-Curve=0&knob-End value=none&knob-Explicit value (using Fit.Explicit)=5&knob-apply custom fit style=true', + ); + }); + + it('should apply opacity on styled fit series', async () => { + const action = async () => { + await common.moveMouseRelativeToDOMElement({ left: 0, top: 0 }, '.echLegendItem'); + }; + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/mixed-charts--fitting-functions-stacked-series&globals=theme:light&knob-stackMode=none&knob-dataset=all&knob-fitting function=linear&knob-Curve=0&knob-End value=none&knob-Explicit value (using Fit.Explicit)=5&knob-apply custom fit style=true', + { + action, + waitSelector: common.chartWaitSelector, + }, + ); + }); + }); }); diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index d6ac5eeb84..faa5d42d3a 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -11,6 +11,9 @@ import { default as React_2 } from 'react'; import { ReactChild } from 'react'; import { ReactNode } from 'react'; +// @public (undocumented) +export type A = number; + // @public export type Accessor = AccessorObjectKey | AccessorArrayIndex; @@ -111,6 +114,12 @@ export interface ArcStyle { visible: boolean; } +// @public (undocumented) +export type AreaFitStyle = Visible & Opacity & { + fill: Color | typeof ColorVariant.Series; + texture?: TexturedStyles; +}; + // Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts // @@ -132,6 +141,11 @@ export interface AreaSeriesStyle { // (undocumented) area: AreaStyle; // (undocumented) + fit: { + line: LineFitStyle; + area: AreaFitStyle; + }; + // (undocumented) line: LineStyle; // (undocumented) point: PointStyle; @@ -1445,6 +1459,11 @@ export interface LinearScale { type: typeof ScaleType.Linear; } +// @public (undocumented) +export type LineFitStyle = Visible & Opacity & StrokeDashArray & { + stroke: Color | typeof ColorVariant.Series; +}; + // Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts // @@ -1462,6 +1481,10 @@ export type LineSeriesSpec = BasicSeriesSpec & HistogramConfig & { // @public (undocumented) export interface LineSeriesStyle { + // (undocumented) + fit: { + line: LineFitStyle; + }; // (undocumented) line: LineStyle; // (undocumented) @@ -1853,6 +1876,12 @@ export type RenderChangeListener = (isRendered: boolean) => void; // @public (undocumented) export type Rendering = 'canvas' | 'svg'; +// @public (undocumented) +export type RGB = number; + +// @public (undocumented) +export type RgbaTuple = [r: RGB, g: RGB, b: RGB, alpha: A]; + // @public (undocumented) export type Rotation = 0 | 90 | -90 | 180; diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/areas.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/areas.ts index 389ccb731e..026c53fd5f 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/areas.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/areas.ts @@ -6,14 +6,16 @@ * Side Public License, v 1. */ +import { colorToRgba, overrideOpacity } from '../../../../common/color_library_wrappers'; import { LegendItem } from '../../../../common/legend'; -import { Rect } from '../../../../geoms/types'; +import { Fill, Rect, Stroke } from '../../../../geoms/types'; import { withContext } from '../../../../renderers/canvas'; -import { Rotation } from '../../../../utils/common'; +import { ColorVariant, Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { AreaGeometry, PerPanel } from '../../../../utils/geometry'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { getGeometryStateStyle } from '../../rendering/utils'; +import { getTextureStyles } from '../../utils/texture'; import { renderPoints } from './points'; import { renderLinePaths, renderAreaPath } from './primitives/path'; import { buildAreaStyles } from './styles/area'; @@ -35,8 +37,8 @@ export function renderAreas(ctx: CanvasRenderingContext2D, imgCanvas: HTMLCanvas withContext(ctx, () => { areas.forEach(({ panel, value: area }) => { - const { seriesAreaLineStyle, seriesAreaStyle } = area; - if (seriesAreaStyle.visible) { + const { style } = area; + if (style.area.visible) { withPanelTransform( ctx, panel, @@ -46,7 +48,7 @@ export function renderAreas(ctx: CanvasRenderingContext2D, imgCanvas: HTMLCanvas { area: clippings, shouldClip: true }, ); } - if (seriesAreaLineStyle.visible) { + if (style.line.visible) { withPanelTransform( ctx, panel, @@ -59,8 +61,8 @@ export function renderAreas(ctx: CanvasRenderingContext2D, imgCanvas: HTMLCanvas }); areas.forEach(({ panel, value: area }) => { - const { seriesPointStyle, seriesIdentifier, points } = area; - const visiblePoints = seriesPointStyle.visible ? points : points.filter(({ orphan }) => orphan); + const { style, seriesIdentifier, points } = area; + const visiblePoints = style.point.visible ? points : points.filter(({ orphan }) => orphan); if (visiblePoints.length === 0) { return; } @@ -80,28 +82,64 @@ export function renderAreas(ctx: CanvasRenderingContext2D, imgCanvas: HTMLCanvas function renderArea( ctx: CanvasRenderingContext2D, imgCanvas: HTMLCanvasElement, - glyph: AreaGeometry, + geometry: AreaGeometry, sharedStyle: SharedGeometryStateStyle, clippings: Rect, highlightedLegendItem?: LegendItem, ) { - const { area, color, transform, seriesIdentifier, seriesAreaStyle, clippedRanges, hideClippedRanges } = glyph; + const { area, color, transform, seriesIdentifier, style, clippedRanges, shouldClip } = geometry; const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); - const styles = buildAreaStyles(ctx, imgCanvas, color, seriesAreaStyle, geometryStateStyle); + const areaFill = buildAreaStyles(ctx, imgCanvas, color, style.area, geometryStateStyle); - renderAreaPath(ctx, transform, area, styles, clippedRanges, clippings, hideClippedRanges); + const fitAreaFillColor = style.fit.area.fill === ColorVariant.Series ? color : style.fit.area.fill; + const fitAreaFill: Fill = { + texture: getTextureStyles(ctx, imgCanvas, fitAreaFillColor, geometryStateStyle.opacity, style.fit.area.texture), + color: overrideOpacity( + colorToRgba(fitAreaFillColor), + (opacity) => opacity * geometryStateStyle.opacity * style.fit.area.opacity, + ), + }; + + renderAreaPath( + ctx, + transform, + area, + areaFill, + fitAreaFill, + clippedRanges, + clippings, + shouldClip && style.fit.area.visible, + ); } function renderAreaLines( ctx: CanvasRenderingContext2D, - glyph: AreaGeometry, + geometry: AreaGeometry, sharedStyle: SharedGeometryStateStyle, clippings: Rect, highlightedLegendItem?: LegendItem, ) { - const { lines, color, seriesIdentifier, transform, seriesAreaLineStyle, clippedRanges, hideClippedRanges } = glyph; + const { lines, color, seriesIdentifier, transform, style, clippedRanges, shouldClip } = geometry; const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); - const styles = buildLineStyles(color, seriesAreaLineStyle, geometryStateStyle); + const lineStyle = buildLineStyles(color, style.line, geometryStateStyle); + + const fitLineStroke: Stroke = { + dash: style.fit.line.dash, + width: style.line.strokeWidth, + color: overrideOpacity( + colorToRgba(style.fit.line.stroke === ColorVariant.Series ? color : style.fit.line.stroke), + (opacity) => opacity * geometryStateStyle.opacity * style.fit.line.opacity, + ), + }; - renderLinePaths(ctx, transform, lines, styles, clippedRanges, clippings, hideClippedRanges); + renderLinePaths( + ctx, + transform, + lines, + lineStyle, + fitLineStroke, + clippedRanges, + clippings, + shouldClip && style.fit.line.visible, + ); } diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts index df2a0f1ec4..1ef3af8734 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts @@ -6,10 +6,11 @@ * Side Public License, v 1. */ +import { colorToRgba, overrideOpacity } from '../../../../common/color_library_wrappers'; import { LegendItem } from '../../../../common/legend'; -import { Rect } from '../../../../geoms/types'; +import { Rect, Stroke } from '../../../../geoms/types'; import { withContext } from '../../../../renderers/canvas'; -import { Rotation } from '../../../../utils/common'; +import { ColorVariant, Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { LineGeometry, PerPanel } from '../../../../utils/geometry'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; @@ -35,9 +36,9 @@ export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometries const { lines, sharedStyle, highlightedLegendItem, clippings, renderingArea, rotation } = props; lines.forEach(({ panel, value: line }) => { - const { seriesLineStyle, seriesPointStyle, points } = line; + const { style, points } = line; - if (seriesLineStyle.visible) { + if (style.line.visible) { withPanelTransform( ctx, panel, @@ -48,7 +49,7 @@ export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometries ); } - const visiblePoints = seriesPointStyle.visible ? points : points.filter(({ orphan }) => orphan); + const visiblePoints = style.point.visible ? points : points.filter(({ orphan }) => orphan); if (visiblePoints.length === 0) { return; } @@ -73,8 +74,28 @@ function renderLine( clippings: Rect, highlightedLegendItem?: LegendItem, ) { - const { color, transform, seriesIdentifier, seriesLineStyle, clippedRanges, hideClippedRanges } = line; + const { color, transform, seriesIdentifier, style, clippedRanges, shouldClip } = line; const geometryStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); - const stroke = buildLineStyles(color, seriesLineStyle, geometryStyle); - renderLinePaths(ctx, transform, [line.line], stroke, clippedRanges, clippings, hideClippedRanges); + + const lineStroke = buildLineStyles(color, style.line, geometryStyle); + const fitLineStrokeColor = style.fit.line.stroke === ColorVariant.Series ? color : style.fit.line.stroke; + const fitLineStroke: Stroke = { + dash: style.fit.line.dash, + width: style.line.strokeWidth, + color: overrideOpacity( + colorToRgba(fitLineStrokeColor), + (opacity) => opacity * geometryStyle.opacity * style.fit.line.opacity, + ), + }; + + renderLinePaths( + ctx, + transform, + [line.line], + lineStroke, + fitLineStroke, + clippedRanges, + clippings, + shouldClip && style.fit.line.visible, + ); } diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts index 465c2e206b..762a48fb82 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { overrideOpacity, RGBATupleToString } from '../../../../../common/color_library_wrappers'; +import { RGBATupleToString } from '../../../../../common/color_library_wrappers'; import { Fill, Rect, Stroke } from '../../../../../geoms/types'; import { withClipRanges } from '../../../../../renderers/canvas'; import { ClippedRanges } from '../../../../../utils/geometry'; @@ -19,18 +19,19 @@ export function renderLinePaths( transform: Point, linePaths: Array, stroke: Stroke, + fitStroke: Stroke, clippedRanges: ClippedRanges, clippings: Rect, - hideClippedRanges = false, + shouldClip: boolean, ) { withClipRanges(ctx, clippedRanges, clippings, false, () => { ctx.translate(transform.x, transform.y); renderMultiLine(ctx, linePaths, stroke); }); - if (clippedRanges.length > 0 && !hideClippedRanges) { + if (clippedRanges.length > 0 && shouldClip) { withClipRanges(ctx, clippedRanges, clippings, true, () => { ctx.translate(transform.x, transform.y); - renderMultiLine(ctx, linePaths, { ...stroke, dash: [5, 5] }); + renderMultiLine(ctx, linePaths, fitStroke); }); } } @@ -41,15 +42,14 @@ export function renderAreaPath( transform: Point, area: string, fill: Fill, + fitFill: Fill, clippedRanges: ClippedRanges, clippings: Rect, - hideClippedRanges = false, + shouldClip: boolean, ) { withClipRanges(ctx, clippedRanges, clippings, false, () => renderPathFill(ctx, area, fill, transform)); - if (clippedRanges.length > 0 && !hideClippedRanges) { - withClipRanges(ctx, clippedRanges, clippings, true, () => - renderPathFill(ctx, area, { ...fill, color: overrideOpacity(fill.color, fill.color[3] / 2) }, transform), - ); + if (clippedRanges.length > 0 && shouldClip) { + withClipRanges(ctx, clippedRanges, clippings, true, () => renderPathFill(ctx, area, fitFill, transform)); } } diff --git a/packages/charts/src/chart_types/xy_chart/rendering/area.ts b/packages/charts/src/chart_types/xy_chart/rendering/area.ts index 969e6dfe18..9d430ff7b4 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/area.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/area.ts @@ -38,11 +38,11 @@ export function renderArea( curve: CurveType, hasY0Accessors: boolean, xScaleOffset: number, - seriesStyle: AreaSeriesStyle, + style: AreaSeriesStyle, markSizeOptions: MarkSizeOptions, - isStacked = false, + isStacked: boolean, + hasFit: boolean, pointStyleAccessor?: PointStyleAccessor, - hasFit?: boolean, ): { areaGeometry: AreaGeometry; indexedGeometryMap: IndexedGeometryMap; @@ -61,6 +61,7 @@ export function renderArea( }) .curve(getCurveFactory(curve)); + // TODO we can probably avoid this function call if no fit function is applied. const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset); const lines: string[] = []; @@ -76,11 +77,11 @@ export function renderArea( yScale, panel, color, - seriesStyle.point, + style.point, hasY0Accessors, markSizeOptions, - pointStyleAccessor, false, + pointStyleAccessor, ); const areaGeometry: AreaGeometry = { @@ -101,12 +102,10 @@ export function renderArea( smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, }, - seriesAreaStyle: seriesStyle.area, - seriesAreaLineStyle: seriesStyle.line, - seriesPointStyle: seriesStyle.point, + style, isStacked, clippedRanges, - hideClippedRanges: !hasFit, + shouldClip: hasFit, }; return { areaGeometry, diff --git a/packages/charts/src/chart_types/xy_chart/rendering/bubble.ts b/packages/charts/src/chart_types/xy_chart/rendering/bubble.ts index 9a946f8911..3088daf10a 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/bubble.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/bubble.ts @@ -45,8 +45,8 @@ export function renderBubble( seriesStyle.point, hasY0Accessors, markSizeOptions, - pointStyleAccessor, !isMixedChart, + pointStyleAccessor, ); const bubbleGeometry = { diff --git a/packages/charts/src/chart_types/xy_chart/rendering/line.ts b/packages/charts/src/chart_types/xy_chart/rendering/line.ts index 27fa929779..c6d22f88e3 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/line.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/line.ts @@ -33,8 +33,8 @@ export function renderLine( xScaleOffset: number, seriesStyle: LineSeriesStyle, markSizeOptions: MarkSizeOptions, + hasFit: boolean, pointStyleAccessor?: PointStyleAccessor, - hasFit?: boolean, ): { lineGeometry: LineGeometry; indexedGeometryMap: IndexedGeometryMap; @@ -59,9 +59,11 @@ export function renderLine( seriesStyle.point, hasY0Accessors, markSizeOptions, + false, pointStyleAccessor, ); + // TODO we can probably avoid computing the clipped ranges if no fit function is applied. const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset); const lineGeometry = { @@ -81,10 +83,9 @@ export function renderLine( smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, smVerticalAccessorValue: dataSeries.smVerticalAccessorValue, }, - seriesLineStyle: seriesStyle.line, - seriesPointStyle: seriesStyle.point, + style: seriesStyle, clippedRanges, - hideClippedRanges: !hasFit, + shouldClip: hasFit, }; return { lineGeometry, diff --git a/packages/charts/src/chart_types/xy_chart/rendering/points.ts b/packages/charts/src/chart_types/xy_chart/rendering/points.ts index 9f1ab85fae..1c4f5facaa 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/points.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/points.ts @@ -37,8 +37,8 @@ export function renderPoints( pointStyle: PointStyle, hasY0Accessors: boolean, markSizeOptions: MarkSizeOptions, + useSpatialIndex: boolean, styleAccessor?: PointStyleAccessor, - spatial = false, ): { pointGeometries: PointGeometry[]; indexedGeometryMap: IndexedGeometryMap; @@ -47,7 +47,7 @@ export function renderPoints( const getRadius = markSizeOptions.enabled ? getRadiusFn(dataSeries.data, pointStyle.strokeWidth, markSizeOptions.ratio) : () => 0; - const geometryType = spatial ? GeometryType.spatial : GeometryType.linear; + const geometryType = useSpatialIndex ? GeometryType.spatial : GeometryType.linear; const y1Fn = getY1ScaledValueFn(yScale); const y0Fn = getY0ScaledValueFn(yScale); diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts index a14ab16461..6cd2648de9 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts @@ -118,8 +118,8 @@ function getBarsState( barGeometries: Array>, ): DebugStateBar[] { const buckets = new Map(); - const bars = barGeometries.reduce((acc, bars) => { - return [...acc, ...bars.value]; + const bars = barGeometries.reduce((acc, { value }) => { + return [...acc, ...value]; }, []); bars.forEach( ({ @@ -162,8 +162,7 @@ function getLineState(seriesNameMap: Map) { points, color, seriesIdentifier: { key }, - seriesLineStyle, - seriesPointStyle, + style, }, }: PerPanel): DebugStateLine => { const name = seriesNameMap.get(key) ?? ''; @@ -173,8 +172,8 @@ function getLineState(seriesNameMap: Map) { color, key, name, - visible: hasVisibleStyle(seriesLineStyle), - visiblePoints: hasVisibleStyle(seriesPointStyle), + visible: hasVisibleStyle(style.line), + visiblePoints: hasVisibleStyle(style.point), points: points.map(({ value: { x, y, mark } }) => ({ x, y, mark })), }; }; @@ -188,9 +187,7 @@ function getAreaState(seriesNameMap: Map) { points, color, seriesIdentifier: { key }, - seriesAreaStyle, - seriesPointStyle, - seriesAreaLineStyle, + style, }, }: PerPanel): DebugStateArea => { const [y1Path, y0Path] = lines; @@ -212,8 +209,8 @@ function getAreaState(seriesNameMap: Map) { y1: [], }, ); - const lineVisible = hasVisibleStyle(seriesAreaLineStyle); - const visiblePoints = hasVisibleStyle(seriesPointStyle); + const lineVisible = hasVisibleStyle(style.line); + const visiblePoints = hasVisibleStyle(style.point); const name = seriesNameMap.get(key) ?? ''; return { @@ -221,7 +218,7 @@ function getAreaState(seriesNameMap: Map) { color, key, name, - visible: hasVisibleStyle(seriesAreaStyle), + visible: hasVisibleStyle(style.area), lines: { y0: y0Path ? { diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts b/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts index 555998c70d..fa052047bb 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts @@ -549,12 +549,12 @@ describe('Chart State utils', () => { const geometries = getGeometriesFromSpecs([line1, line2, line3]); expect(geometries.geometries.lines[0].value.color).toBe('violet'); - expect(geometries.geometries.lines[0].value.seriesLineStyle).toEqual({ + expect(geometries.geometries.lines[0].value.style.line).toEqual({ visible: true, strokeWidth: 100, // the override strokeWidth opacity: 1, }); - expect(geometries.geometries.lines[0].value.seriesPointStyle).toEqual({ + expect(geometries.geometries.lines[0].value.style.point).toEqual({ visible: true, fill: 'green', // the override strokeWidth opacity: 1, @@ -609,17 +609,17 @@ describe('Chart State utils', () => { const geometries = getGeometriesFromSpecs([area1, area2, area3]); expect(geometries.geometries.areas[0].value.color).toBe('violet'); - expect(geometries.geometries.areas[0].value.seriesAreaStyle).toEqual({ + expect(geometries.geometries.areas[0].value.style.area).toEqual({ visible: true, fill: 'area-fill-custom-color', opacity: 0.2, }); - expect(geometries.geometries.areas[0].value.seriesAreaLineStyle).toEqual({ + expect(geometries.geometries.areas[0].value.style.line).toEqual({ visible: true, strokeWidth: 100, opacity: 1, }); - expect(geometries.geometries.areas[0].value.seriesPointStyle).toEqual({ + expect(geometries.geometries.areas[0].value.style.point).toEqual({ visible: false, fill: 'point-fill-custom-color', // the override strokeWidth opacity: 1, diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts index 2d2fed96cd..e03c1e319a 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts @@ -425,8 +425,8 @@ function renderGeometries( enabled: spec.markSizeAccessor !== undefined && lineSeriesStyle.point.visible, ratio: chartTheme.markSizeRatio, }, - spec.pointStyleAccessor, hasFitFnConfigured(spec.fit), + spec.pointStyleAccessor, ); geometriesIndex.merge(renderedLines.indexedGeometryMap); @@ -459,8 +459,8 @@ function renderGeometries( ratio: chartTheme.markSizeRatio, }, spec.stackAccessors ? spec.stackAccessors.length > 0 : false, - spec.pointStyleAccessor, hasFitFnConfigured(spec.fit), + spec.pointStyleAccessor, ); geometriesIndex.merge(renderedAreas.indexedGeometryMap); areas.push({ diff --git a/packages/charts/src/common/color_library_wrappers.ts b/packages/charts/src/common/color_library_wrappers.ts index a45d042c8f..cd15a1f268 100644 --- a/packages/charts/src/common/color_library_wrappers.ts +++ b/packages/charts/src/common/color_library_wrappers.ts @@ -13,8 +13,11 @@ import { Logger } from '../utils/logger'; import { Color, Colors } from './colors'; import { LRUCache } from './data_structures'; -type RGB = number; -type A = number; +/** @public */ +export type RGB = number; + +/** @public */ +export type A = number; /** @internal */ export type RgbTuple = [RGB, RGB, RGB, A?]; diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index 4c4c982910..439979ac41 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -112,6 +112,7 @@ export { Pixels, Ratio } from './common/geometry'; export { AdditiveNumber } from './utils/accessor'; export { FontStyle, FONT_STYLES } from './common/text_utils'; export { Color } from './common/colors'; +export { RGB, A, RgbaTuple } from './common/color_library_wrappers'; export { ESCalendarInterval, diff --git a/packages/charts/src/mocks/geometries.ts b/packages/charts/src/mocks/geometries.ts index 811ee39df3..ff0eea82df 100644 --- a/packages/charts/src/mocks/geometries.ts +++ b/packages/charts/src/mocks/geometries.ts @@ -71,9 +71,9 @@ export class MockLineGeometry { color: Colors.Red.keyword, transform: { x: 0, y: 0 }, seriesIdentifier: MockSeriesIdentifier.default(), - seriesLineStyle: lineSeriesStyle.line, - seriesPointStyle: lineSeriesStyle.point, + style: lineSeriesStyle, clippedRanges: [], + shouldClip: false, }; static default(partial?: RecursivePartial) { @@ -90,11 +90,10 @@ export class MockAreaGeometry { color: Colors.Red.keyword, transform: { x: 0, y: 0 }, seriesIdentifier: MockSeriesIdentifier.default(), - seriesAreaStyle: areaSeriesStyle.area, - seriesAreaLineStyle: areaSeriesStyle.line, - seriesPointStyle: areaSeriesStyle.point, + style: areaSeriesStyle, isStacked: false, clippedRanges: [], + shouldClip: false, }; static default(partial?: RecursivePartial) { diff --git a/packages/charts/src/utils/geometry.ts b/packages/charts/src/utils/geometry.ts index 2b3fdd9e88..0bed5f1eeb 100644 --- a/packages/charts/src/utils/geometry.ts +++ b/packages/charts/src/utils/geometry.ts @@ -13,7 +13,7 @@ import { LabelOverflowConstraint } from '../chart_types/xy_chart/utils/specs'; import { Color } from '../common/colors'; import { Fill, Stroke } from '../geoms/types'; import { Dimensions } from './dimensions'; -import { BarSeriesStyle, PointStyle, AreaStyle, LineStyle, PointShape } from './themes/theme'; +import { BarSeriesStyle, PointStyle, PointShape, LineSeriesStyle, AreaSeriesStyle } from './themes/theme'; /** * The accessor type @@ -116,13 +116,12 @@ export interface LineGeometry { y: number; }; seriesIdentifier: XYChartSeriesIdentifier; - seriesLineStyle: LineStyle; - seriesPointStyle: PointStyle; + style: LineSeriesStyle; /** * Ranges of `[x0, x1]` pairs to clip from series */ clippedRanges: ClippedRanges; - hideClippedRanges?: boolean; + shouldClip: boolean; } /** @internal */ @@ -136,15 +135,13 @@ export interface AreaGeometry { y: number; }; seriesIdentifier: XYChartSeriesIdentifier; - seriesAreaStyle: AreaStyle; - seriesAreaLineStyle: LineStyle; - seriesPointStyle: PointStyle; + style: AreaSeriesStyle; isStacked: boolean; /** * Ranges of `[x0, x1]` pairs to clip from series */ clippedRanges: ClippedRanges; - hideClippedRanges?: boolean; + shouldClip: boolean; } /** @internal */ diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts index 7debec741d..fb4e257632 100644 --- a/packages/charts/src/utils/themes/dark_theme.ts +++ b/packages/charts/src/utils/themes/dark_theme.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { ColorVariant } from '../common'; import { palettes } from './colors'; import { Theme } from './theme'; import { @@ -32,6 +33,14 @@ export const DARK_THEME: Theme = { radius: 2, opacity: 1, }, + fit: { + line: { + visible: true, + dash: [5, 5], + stroke: ColorVariant.Series, + opacity: 1, + }, + }, }, bubbleSeriesStyle: { point: { @@ -59,6 +68,19 @@ export const DARK_THEME: Theme = { radius: 1, opacity: 1, }, + fit: { + line: { + visible: true, + dash: [5, 5], + stroke: ColorVariant.Series, + opacity: 1, + }, + area: { + visible: true, + opacity: 0.15, + fill: ColorVariant.Series, + }, + }, }, barSeriesStyle: { rect: { diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts index 2ca93e9b4a..04d395fa39 100644 --- a/packages/charts/src/utils/themes/light_theme.ts +++ b/packages/charts/src/utils/themes/light_theme.ts @@ -7,6 +7,7 @@ */ import { Colors } from '../../common/colors'; +import { ColorVariant } from '../common'; import { palettes } from './colors'; import { Theme } from './theme'; import { @@ -33,6 +34,14 @@ export const LIGHT_THEME: Theme = { radius: 2, opacity: 1, }, + fit: { + line: { + opacity: 1, + visible: true, + dash: [5, 5], + stroke: ColorVariant.Series, + }, + }, }, bubbleSeriesStyle: { point: { @@ -60,6 +69,19 @@ export const LIGHT_THEME: Theme = { radius: 2, opacity: 1, }, + fit: { + line: { + visible: true, + dash: [5, 5], + stroke: ColorVariant.Series, + opacity: 1, + }, + area: { + visible: true, + opacity: 0.15, + fill: ColorVariant.Series, + }, + }, }, barSeriesStyle: { rect: { diff --git a/packages/charts/src/utils/themes/theme.test.ts b/packages/charts/src/utils/themes/theme.test.ts index f2c3103dd6..cf2d08b925 100644 --- a/packages/charts/src/utils/themes/theme.test.ts +++ b/packages/charts/src/utils/themes/theme.test.ts @@ -158,6 +158,14 @@ describe('Theme', () => { visible: true, opacity: 314571, }, + fit: { + line: { + opacity: 314571, + visible: false, + dash: [1, 2, 3, 4], + stroke: 'rgba(1, 2, 3, 4)', + }, + }, }; const customTheme = mergeWithDefaultTheme({ lineSeriesStyle, @@ -203,6 +211,19 @@ describe('Theme', () => { strokeWidth: 314571, opacity: 314571, }, + fit: { + area: { + opacity: 314571, + visible: false, + fill: 'rgba(1, 2, 3, 4)', + }, + line: { + opacity: 314571, + visible: false, + dash: [1, 2, 3, 4], + stroke: 'rgba(1, 2, 3, 4)', + }, + }, }; const customTheme = mergeWithDefaultTheme({ areaSeriesStyle, diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts index 1ff002e732..4f129c8b2a 100644 --- a/packages/charts/src/utils/themes/theme.ts +++ b/packages/charts/src/utils/themes/theme.ts @@ -532,6 +532,9 @@ export interface BubbleSeriesStyle { export interface LineSeriesStyle { line: LineStyle; point: PointStyle; + fit: { + line: LineFitStyle; + }; } /** @public */ @@ -539,8 +542,26 @@ export interface AreaSeriesStyle { area: AreaStyle; line: LineStyle; point: PointStyle; + fit: { + line: LineFitStyle; + area: AreaFitStyle; + }; } +/** @public */ +export type AreaFitStyle = Visible & + Opacity & { + fill: Color | typeof ColorVariant.Series; + texture?: TexturedStyles; + }; + +/** @public */ +export type LineFitStyle = Visible & + Opacity & + StrokeDashArray & { + stroke: Color | typeof ColorVariant.Series; + }; + /** @public */ export interface ArcSeriesStyle { arc: ArcStyle; diff --git a/storybook/stories/mixed/6_fitting.story.tsx b/storybook/stories/mixed/6_fitting.story.tsx index 09c2321c9f..234e10c99e 100644 --- a/storybook/stories/mixed/6_fitting.story.tsx +++ b/storybook/stories/mixed/6_fitting.story.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { select, number } from '@storybook/addon-knobs'; +import { select, boolean, color, number, text } from '@storybook/addon-knobs'; import React from 'react'; import { @@ -20,8 +20,11 @@ import { Settings, Fit, SeriesType, + RecursivePartial, } from '@elastic/charts'; +import { ColorVariant } from '../../../packages/charts/src/utils/common'; +import { AreaFitStyle, LineFitStyle, TextureShape } from '../../../packages/charts/src/utils/themes/theme'; import { useBaseTheme } from '../../use_base_theme'; export const Example = () => { @@ -153,6 +156,57 @@ export const Example = () => { const parsedEndValue: number | 'nearest' = Number.isNaN(Number(endValue)) ? 'nearest' : Number(endValue); const value = number('Explicit value (using Fit.Explicit)', 5); const xScaleType = dataKey === 'ordinal' ? ScaleType.Ordinal : ScaleType.Linear; + const baseTheme = useBaseTheme(); + + const useSeriesColorLine = boolean('use series color for line', true, 'fit style'); + const customLineColor = color('fit line color', 'rgba(0,0,0,1)', 'fit style'); + + const fitLineStyle: RecursivePartial = { + opacity: number( + 'fit line opacity', + seriesType === SeriesType.Area + ? baseTheme.areaSeriesStyle.fit.line.opacity + : baseTheme.lineSeriesStyle.fit.line.opacity, + { + range: true, + min: 0, + max: 1, + step: 0.05, + }, + 'fit style', + ), + stroke: useSeriesColorLine ? ColorVariant.Series : customLineColor, + dash: text( + 'fit line dash array', + (seriesType === SeriesType.Area + ? baseTheme.areaSeriesStyle.fit.line.dash + : baseTheme.lineSeriesStyle.fit.line.dash + ).join(','), + 'fit style', + ) + .split(',') + .map(Number), + }; + const useSeriesColor = boolean('use series color for area', true, 'fit style'); + const fitAreaCustomColor = color('fit area color', 'rgba(0,0,0,1)', 'fit style'); + const fitAreaOpacity = number( + 'fit area opacity', + baseTheme.areaSeriesStyle.fit.area.opacity, + { + range: true, + min: 0, + max: baseTheme.areaSeriesStyle.area.opacity, + step: 0.01, + }, + 'fit style', + ); + const fitAreaStyle: RecursivePartial = { + opacity: fitAreaOpacity, + fill: useSeriesColor ? ColorVariant.Series : fitAreaCustomColor, + texture: boolean('use texture on area', false, 'fit style') + ? { shape: TextureShape.Line, rotation: -45, opacity: fitAreaOpacity } + : undefined, + }; return ( @@ -166,7 +220,7 @@ export const Example = () => { }, }, }} - baseTheme={useBaseTheme()} + baseTheme={baseTheme} /> @@ -183,6 +237,12 @@ export const Example = () => { value: fit === Fit.Explicit ? value : undefined, endValue: endValue === 'none' ? undefined : parsedEndValue, }} + areaSeriesStyle={{ + fit: { + line: fitLineStyle, + area: fitAreaStyle, + }, + }} data={dataset} /> ) : ( @@ -198,6 +258,11 @@ export const Example = () => { value: fit === Fit.Explicit ? value : undefined, endValue: endValue === 'none' ? undefined : parsedEndValue, }} + lineSeriesStyle={{ + fit: { + line: fitLineStyle, + }, + }} data={dataset} /> )} diff --git a/storybook/stories/mixed/6_fitting_stacked.story.tsx b/storybook/stories/mixed/6_fitting_stacked.story.tsx index 61c1476eec..490b86fcd2 100644 --- a/storybook/stories/mixed/6_fitting_stacked.story.tsx +++ b/storybook/stories/mixed/6_fitting_stacked.story.tsx @@ -13,6 +13,7 @@ import React from 'react'; import { AreaSeries, Axis, Chart, CurveType, Position, ScaleType, Settings, Fit, StackMode } from '@elastic/charts'; import { getRandomNumberGenerator, getRNGSeed } from '@elastic/charts/src/mocks/utils'; +import { TextureShape } from '../../../packages/charts/src/utils/themes/theme'; import { useBaseTheme } from '../../use_base_theme'; export const Example = () => { @@ -201,6 +202,26 @@ export const Example = () => { endValue: endValue === 'none' ? undefined : parsedEndValue, }} data={dataset} + areaSeriesStyle={ + boolean('apply custom fit style', false) + ? { + fit: { + line: { + stroke: 'gray', + opacity: 0.5, + }, + area: { + fill: 'gray', + texture: { + shape: TextureShape.Line, + rotation: -45, + opacity: 0.2, + }, + }, + }, + } + : undefined + } /> { return { line: { stroke: lineStroke ? color(`line.stroke (${tag})`, lineStroke, groupName) : undefined, diff --git a/storybook/stories/stylings/12_custom_area.story.tsx b/storybook/stories/stylings/12_custom_area.story.tsx index 37d60235b9..a22b65c2bd 100644 --- a/storybook/stories/stylings/12_custom_area.story.tsx +++ b/storybook/stories/stylings/12_custom_area.story.tsx @@ -9,7 +9,17 @@ import { boolean, color, number } from '@storybook/addon-knobs'; import React from 'react'; -import { AreaSeries, Axis, Chart, Position, ScaleType, Settings, LineSeriesStyle, PartialTheme } from '@elastic/charts'; +import { + AreaSeries, + Axis, + Chart, + Position, + ScaleType, + Settings, + LineSeriesStyle, + PartialTheme, + RecursivePartial, +} from '@elastic/charts'; import { useBaseTheme } from '../../use_base_theme'; @@ -36,7 +46,7 @@ function generateLineAndPointSeriesStyleKnobs( pointRadius?: number, lineStrokeWidth?: number, lineStroke?: string, -): LineSeriesStyle { +): RecursivePartial { return { line: { stroke: lineStroke ? color(`line.stroke (${tag})`, lineStroke, groupName) : undefined,