diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-log-y-axis-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-log-y-axis-visually-looks-correct-1-snap.png index 0d3c9e2b39..232535d160 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-log-y-axis-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-log-y-axis-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-line-chart-test-orphan-data-points-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-line-chart-test-orphan-data-points-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..41795fc0f9 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-line-chart-test-orphan-data-points-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-area-with-orphan-data-points-render-correctly-fit-function-1-snap.png b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-area-with-orphan-data-points-render-correctly-fit-function-1-snap.png new file mode 100644 index 0000000000..29eece38ba Binary files /dev/null and b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-area-with-orphan-data-points-render-correctly-fit-function-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-non-stacked-linear-area-with-discontinuous-data-points-no-fit-function-1-snap.png b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-non-stacked-linear-area-with-discontinuous-data-points-no-fit-function-1-snap.png index 7f9d12fbd8..517b18356b 100644 Binary files a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-non-stacked-linear-area-with-discontinuous-data-points-no-fit-function-1-snap.png and b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-non-stacked-linear-area-with-discontinuous-data-points-no-fit-function-1-snap.png differ diff --git a/integration/tests/area_stories.test.ts b/integration/tests/area_stories.test.ts index c3ba29873e..330a93a847 100644 --- a/integration/tests/area_stories.test.ts +++ b/integration/tests/area_stories.test.ts @@ -91,4 +91,11 @@ describe('Area series stories', () => { ); }); }); + describe('Area with orphan data points', () => { + it('render correctly fit function', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/line-chart--test-orphan-data-points&knob-enable fit function=&knob-switch to area=true', + ); + }); + }); }); diff --git a/src/chart_types/xy_chart/renderer/canvas/areas.ts b/src/chart_types/xy_chart/renderer/canvas/areas.ts index ce60777095..3d472df934 100644 --- a/src/chart_types/xy_chart/renderer/canvas/areas.ts +++ b/src/chart_types/xy_chart/renderer/canvas/areas.ts @@ -74,8 +74,9 @@ export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometries }); areas.forEach(({ panel, value: area }) => { - const { seriesPointStyle, seriesIdentifier } = area; - if (!seriesPointStyle.visible) { + const { seriesPointStyle, seriesIdentifier, points } = area; + const visiblePoints = seriesPointStyle.visible ? points : points.filter(({ orphan }) => orphan); + if (visiblePoints.length === 0) { return; } const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); @@ -85,9 +86,9 @@ export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometries rotation, renderingArea, (ctx) => { - renderPoints(ctx, area.points, seriesPointStyle, geometryStateStyle); + renderPoints(ctx, visiblePoints, seriesPointStyle, geometryStateStyle); }, - { area: clippings, shouldClip: area.points[0]?.value.mark !== null }, + { area: clippings, shouldClip: points[0]?.value.mark !== null }, ); }); }); diff --git a/src/chart_types/xy_chart/renderer/canvas/bars.ts b/src/chart_types/xy_chart/renderer/canvas/bars.ts index 488ddcb8f5..98128190ef 100644 --- a/src/chart_types/xy_chart/renderer/canvas/bars.ts +++ b/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -54,6 +54,9 @@ function renderPerPanelBars( rotation: Rotation = 0, ) { return ({ panel, value: bars }: PerPanel) => { + if (bars.length === 0) { + return; + } withPanelTransform( ctx, panel, diff --git a/src/chart_types/xy_chart/renderer/canvas/bubbles.ts b/src/chart_types/xy_chart/renderer/canvas/bubbles.ts index e00d30238b..6206903275 100644 --- a/src/chart_types/xy_chart/renderer/canvas/bubbles.ts +++ b/src/chart_types/xy_chart/renderer/canvas/bubbles.ts @@ -59,6 +59,9 @@ export function renderBubbles(ctx: CanvasRenderingContext2D, props: BubbleGeomet }, [], ); + if (allPoints.length === 0) { + return; + } renderPointGroup( ctx, diff --git a/src/chart_types/xy_chart/renderer/canvas/lines.ts b/src/chart_types/xy_chart/renderer/canvas/lines.ts index 7a95809a50..d00f9712f9 100644 --- a/src/chart_types/xy_chart/renderer/canvas/lines.ts +++ b/src/chart_types/xy_chart/renderer/canvas/lines.ts @@ -46,7 +46,7 @@ export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometries const { lines, sharedStyle, highlightedLegendItem, clippings, renderingArea, rotation } = props; lines.forEach(({ panel, value: line }) => { - const { seriesLineStyle, seriesPointStyle } = line; + const { seriesLineStyle, seriesPointStyle, points } = line; if (seriesLineStyle.visible) { withPanelTransform(ctx, panel, rotation, renderingArea, (ctx) => { @@ -54,20 +54,22 @@ export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometries }); } - if (seriesPointStyle.visible) { - withPanelTransform( - ctx, - panel, - rotation, - renderingArea, - (ctx) => { - const geometryStyle = getGeometryStateStyle(line.seriesIdentifier, sharedStyle, highlightedLegendItem); - renderPoints(ctx, line.points, line.seriesPointStyle, geometryStyle); - }, - // TODO: add padding over clipping - { area: clippings, shouldClip: line.points[0]?.value.mark !== null }, - ); + const visiblePoints = seriesPointStyle.visible ? points : points.filter(({ orphan }) => orphan); + if (visiblePoints.length === 0) { + return; } + const geometryStyle = getGeometryStateStyle(line.seriesIdentifier, sharedStyle, highlightedLegendItem); + withPanelTransform( + ctx, + panel, + rotation, + renderingArea, + (ctx) => { + renderPoints(ctx, visiblePoints, line.seriesPointStyle, geometryStyle); + }, + // TODO: add padding over clipping + { area: clippings, shouldClip: line.points[0]?.value.mark !== null }, + ); }); }); } diff --git a/src/chart_types/xy_chart/rendering/area.ts b/src/chart_types/xy_chart/rendering/area.ts index f87d9d68ff..301c4a7971 100644 --- a/src/chart_types/xy_chart/rendering/area.ts +++ b/src/chart_types/xy_chart/rendering/area.ts @@ -30,9 +30,10 @@ import { PointStyleAccessor } from '../utils/specs'; import { renderPoints } from './points'; import { getClippedRanges, - getY0ScaledValueOrThrow, - getY1ScaledValueOrThrow, - isYValueDefined, + getY0ScaledValueOrThrowFn, + getY1ScaledValueOrThrowFn, + getYDatumValueFn, + isYValueDefinedFn, MarkSizeOptions, } from './utils'; @@ -56,15 +57,17 @@ export function renderArea( areaGeometry: AreaGeometry; indexedGeometryMap: IndexedGeometryMap; } { - const y1Fn = getY1ScaledValueOrThrow(yScale); - const y0Fn = getY0ScaledValueOrThrow(yScale); - const definedFn = isYValueDefined(yScale, xScale); + const y1Fn = getY1ScaledValueOrThrowFn(yScale); + const y0Fn = getY0ScaledValueOrThrowFn(yScale); + const definedFn = isYValueDefinedFn(yScale, xScale); + const y1DatumAccessor = getYDatumValueFn(); + const y0DatumAccessor = getYDatumValueFn('y0'); const pathGenerator = area() .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) .y1(y1Fn) .y0(y0Fn) .defined((datum) => { - return definedFn(datum) && (hasY0Accessors ? definedFn(datum, 'y0') : true); + return definedFn(datum, y1DatumAccessor) && (hasY0Accessors ? definedFn(datum, y0DatumAccessor) : true); }) .curve(getCurveFactory(curve)); diff --git a/src/chart_types/xy_chart/rendering/line.ts b/src/chart_types/xy_chart/rendering/line.ts index 5e186c7f98..6fffa2e52f 100644 --- a/src/chart_types/xy_chart/rendering/line.ts +++ b/src/chart_types/xy_chart/rendering/line.ts @@ -28,7 +28,13 @@ import { IndexedGeometryMap } from '../utils/indexed_geometry_map'; import { DataSeries, DataSeriesDatum } from '../utils/series'; import { PointStyleAccessor } from '../utils/specs'; import { renderPoints } from './points'; -import { getClippedRanges, getY1ScaledValueOrThrow, isYValueDefined, MarkSizeOptions } from './utils'; +import { + getClippedRanges, + getY1ScaledValueOrThrowFn, + getYDatumValueFn, + isYValueDefinedFn, + MarkSizeOptions, +} from './utils'; /** @internal */ export function renderLine( @@ -49,14 +55,15 @@ export function renderLine( lineGeometry: LineGeometry; indexedGeometryMap: IndexedGeometryMap; } { - const y1Fn = getY1ScaledValueOrThrow(yScale); - const definedFn = isYValueDefined(yScale, xScale); + const y1Fn = getY1ScaledValueOrThrowFn(yScale); + const definedFn = isYValueDefinedFn(yScale, xScale); + const y1Accessor = getYDatumValueFn(); const pathGenerator = line() .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) .y(y1Fn) .defined((datum) => { - return definedFn(datum); + return definedFn(datum, y1Accessor); }) .curve(getCurveFactory(curve)); diff --git a/src/chart_types/xy_chart/rendering/points.ts b/src/chart_types/xy_chart/rendering/points.ts index 7f156931d2..4e5248eae3 100644 --- a/src/chart_types/xy_chart/rendering/points.ts +++ b/src/chart_types/xy_chart/rendering/points.ts @@ -17,7 +17,7 @@ * under the License. */ import { Scale } from '../../../scales'; -import { Color } from '../../../utils/commons'; +import { Color, isNil } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { BandedAccessorType, PointGeometry } from '../../../utils/geometry'; import { LineStyle, PointStyle } from '../../../utils/themes/theme'; @@ -25,11 +25,13 @@ import { GeometryType, IndexedGeometryMap } from '../utils/indexed_geometry_map' import { DataSeries, DataSeriesDatum, FilledValues, XYChartSeriesIdentifier } from '../utils/series'; import { PointStyleAccessor, StackMode } from '../utils/specs'; import { - getY0ScaledValueOrThrow, - getY1ScaledValueOrThrow, + getY0ScaledValueOrThrowFn, + getY1ScaledValueOrThrowFn, + getYDatumValueFn, isDatumFilled, - isYValueDefined, + isYValueDefinedFn, MarkSizeOptions, + YDefinedFn, } from './utils'; /** @internal */ @@ -55,12 +57,14 @@ export function renderPoints( : () => 0; const geometryType = spatial ? GeometryType.spatial : GeometryType.linear; - const y1Fn = getY1ScaledValueOrThrow(yScale); - const y0Fn = getY0ScaledValueOrThrow(yScale); - const yDefined = isYValueDefined(yScale, xScale); + const y1Fn = getY1ScaledValueOrThrowFn(yScale); + const y0Fn = getY0ScaledValueOrThrowFn(yScale); + const yDefined = isYValueDefinedFn(yScale, xScale); - const pointGeometries = dataSeries.data.reduce((acc, datum) => { + const pointGeometries = dataSeries.data.reduce((acc, datum, dataIndex) => { const { x: xValue, mark } = datum; + const prev = dataSeries.data[dataIndex - 1]; + const next = dataSeries.data[dataIndex + 1]; // don't create the point if not within the xScale domain if (!xScale.isValueInDomain(xValue)) { return acc; @@ -78,7 +82,8 @@ export function renderPoints( const points: PointGeometry[] = []; const yDatumKeyNames: Array> = hasY0Accessors ? ['y0', 'y1'] : ['y1']; - yDatumKeyNames.forEach((yDatumKeyName, index) => { + yDatumKeyNames.forEach((yDatumKeyName, keyIndex) => { + const valueAccessor = getYDatumValueFn(yDatumKeyName); // skip rendering point if y1 is null const radius = getRadius(mark); let y: number | null; @@ -91,7 +96,7 @@ export function renderPoints( return; } - const originalY = getDatumYValue(datum, index === 0, hasY0Accessors, dataSeries.stackMode); + const originalY = getDatumYValue(datum, keyIndex === 0, hasY0Accessors, dataSeries.stackMode); const seriesIdentifier: XYChartSeriesIdentifier = { key: dataSeries.key, specId: dataSeries.specId, @@ -102,6 +107,7 @@ export function renderPoints( smHorizontalAccessorValue: dataSeries.smHorizontalAccessorValue, }; const styleOverrides = getPointStyleOverrides(datum, seriesIdentifier, styleAccessor); + const orphan = isOrphanDataPoint(dataIndex, dataSeries.data.length, yDefined, prev, next); const pointGeometry: PointGeometry = { radius, x, @@ -111,7 +117,7 @@ export function renderPoints( x: xValue, y: originalY, mark, - accessor: hasY0Accessors && index === 0 ? BandedAccessorType.Y0 : BandedAccessorType.Y1, + accessor: hasY0Accessors && keyIndex === 0 ? BandedAccessorType.Y0 : BandedAccessorType.Y1, datum: datum.datum, }, transform: { @@ -121,10 +127,11 @@ export function renderPoints( seriesIdentifier, styleOverrides, panel, + orphan, }; indexedGeometryMap.set(pointGeometry, geometryType); // use the geometry only if the yDatum in contained in the current yScale domain - if (yDefined(datum, yDatumKeyName)) { + if (yDefined(datum, valueAccessor)) { points.push(pointGeometry); } }); @@ -228,3 +235,26 @@ export function getRadiusFn( return circleRadius ? Math.sqrt(circleRadius + baseMagicNumber) + lineWidth : lineWidth; }; } + +function yAccessorForOrphanCheck(datum: DataSeriesDatum): number | null { + return datum.filled?.y1 ? null : datum.y1; +} + +function isOrphanDataPoint( + index: number, + length: number, + yDefined: YDefinedFn, + prev?: DataSeriesDatum, + next?: DataSeriesDatum, +): boolean { + if (index === 0 && (isNil(next) || !yDefined(next, yAccessorForOrphanCheck))) { + return true; + } + if (index === length - 1 && (isNil(prev) || !yDefined(prev, yAccessorForOrphanCheck))) { + return true; + } + return ( + (isNil(prev) || !yDefined(prev, yAccessorForOrphanCheck)) && + (isNil(next) || !yDefined(next, yAccessorForOrphanCheck)) + ); +} diff --git a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts index 8014e71c6e..a7a053bc8e 100644 --- a/src/chart_types/xy_chart/rendering/rendering.areas.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.areas.test.ts @@ -106,74 +106,78 @@ describe('Rendering points - areas', () => { test('Can render two points', () => { const { points } = areaGeometry; - expect(points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: - 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', - smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', + expect(points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 25, y: 0, - }, - panel: { - width: 100, - height: 100, - top: 0, - left: 0, - }, - } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ - x: 50, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: SPEC_ID, - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: - 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', - smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { - x: 25, - y: 0, - }, - panel: { - width: 100, - height: 100, - top: 0, - left: 0, - }, - } as unknown) as PointGeometry); + radius: 0, + color: 'red', + seriesIdentifier: { + specId: SPEC_ID, + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + }, + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + expect(points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: { + specId: SPEC_ID, + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + }, + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); expect(geometriesIndex.size).toEqual(points.length); }); }); @@ -233,147 +237,155 @@ describe('Rendering points - areas', () => { const { areas } = geometries.geometries; const [{ value: firstArea }] = areas; expect(firstArea.points.length).toEqual(2); - expect(firstArea.points[0]).toEqual(({ - x: 0, - y: 50, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: 'spec_1', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: - 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', - smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', + expect(firstArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - transform: { - x: 25, - y: 0, - }, - panel: { - width: 100, - height: 100, - top: 0, - left: 0, - }, - } as unknown) as PointGeometry); - expect(firstArea.points[1]).toEqual(({ - x: 50, - y: 75, - radius: 0, - color: 'red', - seriesIdentifier: { - specId: 'spec_1', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: - 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', - smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1, - y: 5, - mark: null, - datum: [1, 5], - }, - transform: { - x: 25, - y: 0, - }, - panel: { - width: 100, - height: 100, - top: 0, - left: 0, - }, - } as unknown) as PointGeometry); + y: 50, + radius: 0, + color: 'red', + seriesIdentifier: { + specId: 'spec_1', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + }, + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 0, + y: 10, + mark: null, + datum: [0, 10], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + expect(firstArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 75, + radius: 0, + color: 'red', + seriesIdentifier: { + specId: 'spec_1', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: + 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + }, + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 1, + y: 5, + mark: null, + datum: [1, 5], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); }); test('can render second spec points', () => { const { areas } = geometries.geometries; const [, { value: secondArea }] = areas; expect(secondArea.points.length).toEqual(2); - expect(secondArea.points[0]).toEqual(({ - x: 0, - y: 0, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: 'spec_2', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: - 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', - smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', + expect(secondArea.points[0]).toEqual( + MockPointGeometry.default({ x: 0, - y: 20, - mark: null, - datum: [0, 20], - }, - transform: { - x: 25, - y: 0, - }, - panel: { - width: 100, - height: 100, - top: 0, - left: 0, - }, - } as unknown) as PointGeometry); - expect(secondArea.points[1]).toEqual(({ - x: 50, - y: 50, - radius: 0, - color: 'blue', - seriesIdentifier: { - specId: 'spec_2', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - key: - 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', - smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', - }, - styleOverrides: undefined, - value: { - accessor: 'y1', - x: 1, - y: 10, - mark: null, - datum: [1, 10], - }, - transform: { - x: 25, y: 0, - }, - panel: { - width: 100, - height: 100, - top: 0, - left: 0, - }, - } as unknown) as PointGeometry); + radius: 0, + color: 'blue', + seriesIdentifier: { + specId: 'spec_2', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: + 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + }, + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 0, + y: 20, + mark: null, + datum: [0, 20], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); + expect(secondArea.points[1]).toEqual( + MockPointGeometry.default({ + x: 50, + y: 50, + radius: 0, + color: 'blue', + seriesIdentifier: { + specId: 'spec_2', + yAccessor: 1, + splitAccessors: new Map(), + seriesKeys: [1], + key: + 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}smV{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}smH{__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__}', + smHorizontalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + smVerticalAccessorValue: '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__', + }, + styleOverrides: undefined, + value: { + accessor: 'y1', + x: 1, + y: 10, + mark: null, + datum: [1, 10], + }, + transform: { + x: 25, + y: 0, + }, + panel: { + width: 100, + height: 100, + top: 0, + left: 0, + }, + }), + ); }); test('has the right number of geometry in the indexes', () => { const { areas } = geometries.geometries; 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 7263c1b3d2..ed2a8b7194 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bands.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bands.test.ts @@ -235,6 +235,8 @@ describe('Rendering bands - areas', () => { mark: null, datum: [0, 2, 10], }, + // the first point is also an orphan because the next one is null + orphan: true, }), ); expect(points[1]).toMatchObject( @@ -246,6 +248,7 @@ describe('Rendering bands - areas', () => { mark: null, datum: [0, 2, 10], }, + orphan: true, }), ); expect(points[2]).toMatchObject( diff --git a/src/chart_types/xy_chart/rendering/utils.ts b/src/chart_types/xy_chart/rendering/utils.ts index 24f190fc3f..d55be7629b 100644 --- a/src/chart_types/xy_chart/rendering/utils.ts +++ b/src/chart_types/xy_chart/rendering/utils.ts @@ -39,17 +39,15 @@ export interface MarkSizeOptions { * Passing a filled key (x, y1, y0) it will return that value or the filled one * @internal */ -export const getYDatumValue = ( - datum: DataSeriesDatum, - valueName: keyof Omit = 'y1', - returnFilled = true, -): number | null => { - const value = datum[valueName]; - if (value !== null || !returnFilled) { - return value; - } - return (datum.filled && datum.filled[valueName]) ?? null; -}; +export function getYDatumValueFn(valueName: keyof Omit = 'y1') { + return (datum: DataSeriesDatum, returnFilled = true): number | null => { + const value = datum[valueName]; + if (value !== null || !returnFilled) { + return value; + } + return datum.filled?.[valueName] ?? null; + }; +} /** * @@ -170,14 +168,17 @@ const DEFAULT_ZERO_BASELINE = 0; const DEFAULT_LOG_ZERO_BASELINE = LOG_MIN_ABS_DOMAIN; /** @internal */ -export function isYValueDefined( - yScale: Scale, - xScale: Scale, -): (datum: DataSeriesDatum, valueName?: keyof Omit) => boolean { +export type YDefinedFn = ( + datum: DataSeriesDatum, + getValueAccessor: (datum: DataSeriesDatum) => number | null, +) => boolean; + +/** @internal */ +export function isYValueDefinedFn(yScale: Scale, xScale: Scale): YDefinedFn { const isLogScale = isLogarithmicScale(yScale); const domainPolarity = getDomainPolarity(yScale.domain); - return (datum, valueName = 'y1') => { - const yValue = getYDatumValue(datum, valueName); + return (datum, getValueAccessor) => { + const yValue = getValueAccessor(datum); return ( yValue !== null && !((isLogScale && domainPolarity >= 0 && yValue <= 0) || (domainPolarity < 0 && yValue >= 0)) && @@ -188,15 +189,16 @@ export function isYValueDefined( } /** @internal */ -export function getY1ScaledValueOrThrow(yScale: Scale): (datum: DataSeriesDatum) => number { +export function getY1ScaledValueOrThrowFn(yScale: Scale): (datum: DataSeriesDatum) => number { + const datumAccessor = getYDatumValueFn(); return (datum) => { - const yValue = getYDatumValue(datum); + const yValue = datumAccessor(datum); return yScale.scaleOrThrow(yValue); }; } /** @internal */ -export function getY0ScaledValueOrThrow(yScale: Scale): (datum: DataSeriesDatum) => number { +export function getY0ScaledValueOrThrowFn(yScale: Scale): (datum: DataSeriesDatum) => number { const isLogScale = isLogarithmicScale(yScale); const domainPolarity = getDomainPolarity(yScale.domain); diff --git a/src/mocks/geometries.ts b/src/mocks/geometries.ts index 92df270a2a..47551aa032 100644 --- a/src/mocks/geometries.ts +++ b/src/mocks/geometries.ts @@ -52,6 +52,7 @@ export class MockPointGeometry { left: 0, top: 0, }, + orphan: false, }; static default(partial?: RecursivePartial) { diff --git a/src/mocks/series/utils.ts b/src/mocks/series/utils.ts index 36ac2808ff..1300d548e6 100644 --- a/src/mocks/series/utils.ts +++ b/src/mocks/series/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getYDatumValue } from '../../chart_types/xy_chart/rendering/utils'; +import { getYDatumValueFn } from '../../chart_types/xy_chart/rendering/utils'; import { DataSeriesDatum } from '../../chart_types/xy_chart/utils/series'; /** @@ -44,7 +44,9 @@ export const getXValueData = (data: DataSeriesDatum[]): (number | string)[] => d * Returns value of `y1` or `filled.y1` or null * @internal */ -export const getYResolvedData = (data: DataSeriesDatum[]): (number | null)[] => - data.map((d) => { - return getYDatumValue(d); +export const getYResolvedData = (data: DataSeriesDatum[]): (number | null)[] => { + const datumAccessor = getYDatumValueFn(); + return data.map((d) => { + return datumAccessor(d); }); +}; diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index 46720c5dd6..2adbbda570 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -67,6 +67,7 @@ export interface PointGeometry { value: GeometryValue; styleOverrides?: Partial; panel: Dimensions; + orphan: boolean; } export interface PerPanel { diff --git a/stories/line/12_orphan_data_points.tsx b/stories/line/12_orphan_data_points.tsx new file mode 100644 index 0000000000..c725dfc6cc --- /dev/null +++ b/stories/line/12_orphan_data_points.tsx @@ -0,0 +1,76 @@ +/* + * 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 } from '@storybook/addon-knobs'; +import React from 'react'; + +import { Axis, Chart, CurveType, LineSeries, Position, ScaleType, Settings, Fit, AreaSeries } from '../../src'; + +export const Example = () => { + const fitEnabled = boolean('enable fit function', false); + const isArea = boolean('switch to area', false); + const LineOrAreaSeries = isArea ? AreaSeries : LineSeries; + return ( + + + + + + + + ); +}; diff --git a/stories/line/line.stories.tsx b/stories/line/line.stories.tsx index 87fd629976..813bd5a73a 100644 --- a/stories/line/line.stories.tsx +++ b/stories/line/line.stories.tsx @@ -36,4 +36,5 @@ export { Example as multipleWithAxisAndLegend } from './7_multiple'; export { Example as stackedWithAxisAndLegend } from './8_stacked'; export { Example as multiSeriesWithLogValues } from './9_multi_series'; export { Example as discontinuousDataPoints } from './11_discontinuous_data_points'; +export { Example as testOrphanDataPoints } from './12_orphan_data_points'; export { Example as testPathOrdering } from './10_test_path_ordering';