diff --git a/api/charts.api.md b/api/charts.api.md index 9b269e9649..efd696ec70 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -435,6 +435,7 @@ export type CurveType = $Values; export type CustomAnnotationTooltip = ComponentType<{ header?: string; details?: string; + datum: LineAnnotationDatum | RectAnnotationDatum; }> | null; // @public diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-lines-y-domain-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-lines-y-domain-visually-looks-correct-1-snap.png index 5a7bbd9ce1..efa443c72b 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-lines-y-domain-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-lines-y-domain-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-heterogeneous-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-heterogeneous-visually-looks-correct-1-snap.png index c4a9efdc8a..3b4504d2f0 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-heterogeneous-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-heterogeneous-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-horizontal-bars-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-horizontal-bars-visually-looks-correct-1-snap.png index b24d2d81ab..1f20f901a6 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-horizontal-bars-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-small-multiples-alpha-horizontal-bars-visually-looks-correct-1-snap.png differ diff --git a/package.json b/package.json index 216bf7947f..95c2eae3a0 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "d3-scale": "^1.0.7", "d3-shape": "^1.3.4", "newtype-ts": "^0.2.4", - "path2d-polyfill": "^0.4.2", "prop-types": "^15.7.2", "re-reselect": "^3.4.0", "react-redux": "^7.1.0", diff --git a/scripts/setup_enzyme.ts b/scripts/setup_enzyme.ts index abb50b14aa..ab109f86f4 100644 --- a/scripts/setup_enzyme.ts +++ b/scripts/setup_enzyme.ts @@ -60,3 +60,8 @@ class ResizeObserverMock { } window.ResizeObserver = ResizeObserverMock; + +// Some tests will fail due to undefined Path2D, this mock doesn't create issues on test env +class Path2D {} +// @ts-ignore +window.Path2D = Path2D; diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts b/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts index 6a3eac94d4..4788392268 100644 --- a/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts +++ b/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts @@ -53,7 +53,10 @@ function expectAnnotationAtPosition( const annotations = computeAnnotationDimensionsSelector(store.getState()); expect(annotations.get(annotation.id)).toEqual([ MockAnnotationLineProps.default({ - details: { detailsText: undefined, headerText: `${indexPosition}` }, + specId: 'line_annotation_1', + datum: { + dataValue: indexPosition, + }, linePathPoints: { x1: expectedLinePosition, y1: 0, @@ -147,13 +150,14 @@ describe('Render vertical line annotation within', () => { const annotations = computeAnnotationDimensionsSelector(store.getState()); expect(annotations.get(annotation.id)).toEqual([ MockAnnotationLineProps.default({ + specId: 'line_annotation_1', linePathPoints: { x1: 95, y1: 0, x2: 95, y2: 100, }, - details: { detailsText: 'foo', headerText: '9.5' }, + datum: { dataValue: 9.5, details: 'foo' }, }), ]); }); diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.test.ts b/src/chart_types/xy_chart/annotations/line/dimensions.test.ts index 1310bb3811..3734eeae50 100644 --- a/src/chart_types/xy_chart/annotations/line/dimensions.test.ts +++ b/src/chart_types/xy_chart/annotations/line/dimensions.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { MockAnnotationLineProps } from '../../../../mocks/annotations/annotations'; +import { MockAnnotationLineProps, MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; import { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs'; import { MockStore } from '../../../../mocks/store'; import { ScaleType } from '../../../../scales/constants'; @@ -88,21 +88,22 @@ describe('Annotation utils', () => { const expectedDimensions = new Map(); expectedDimensions.set('foo', [ MockAnnotationLineProps.default({ + specId: 'foo', linePathPoints: { x1: 0, y1: 80, x2: 100, y2: 80, }, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]); expectedDimensions.set('rect', [ - { - details: undefined, + MockAnnotationRectProps.default({ rect: { x: 0, y: 50, width: 50, height: 20 }, panel: { top: 0, left: 0, width: 100, height: 100 }, - }, + datum: { coordinates: { x0: 'a', x1: 'b', y0: 3, y1: 5 } }, + }), ]); expect(dimensions).toEqual(expectedDimensions); @@ -156,6 +157,7 @@ describe('Annotation utils', () => { const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 0, y1: 80, @@ -163,7 +165,7 @@ describe('Annotation utils', () => { y2: 80, }, panel, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -199,6 +201,7 @@ describe('Annotation utils', () => { const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 0, y1: 80, @@ -206,7 +209,7 @@ describe('Annotation utils', () => { y2: 80, }, panel: { width: 10, height: 100, top: 0, left: 0 }, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -241,13 +244,14 @@ describe('Annotation utils', () => { const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 0, y1: 80, x2: 100, y2: 80, }, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -313,13 +317,14 @@ describe('Annotation utils', () => { const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 12.5, y1: 0, x2: 12.5, y2: 100, }, - details: { detailsText: 'foo', headerText: 'a' }, + datum: { dataValue: 'a', details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -354,13 +359,14 @@ describe('Annotation utils', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 25, y1: 0, x2: 25, y2: 100, }, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -395,13 +401,14 @@ describe('Annotation utils', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 25, y1: 0, x2: 25, y2: 100, }, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -436,13 +443,14 @@ describe('Annotation utils', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 12.5, y1: 0, x2: 12.5, y2: 100, }, - details: { detailsText: 'foo', headerText: 'a' }, + datum: { dataValue: 'a', details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -478,6 +486,7 @@ describe('Annotation utils', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 12.5, y1: 0, @@ -485,7 +494,7 @@ describe('Annotation utils', () => { y2: 100, }, panel, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -521,6 +530,7 @@ describe('Annotation utils', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 12.5, y1: 0, @@ -528,7 +538,7 @@ describe('Annotation utils', () => { y2: 100, }, panel, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -563,13 +573,14 @@ describe('Annotation utils', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 25, y1: 0, x2: 25, y2: 100, }, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); @@ -605,6 +616,7 @@ describe('Annotation utils', () => { const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ + specId: 'foo-line', linePathPoints: { x1: 25, y1: 0, @@ -612,7 +624,7 @@ describe('Annotation utils', () => { y2: 50, }, panel, - details: { detailsText: 'foo', headerText: '2' }, + datum: { dataValue: 2, details: 'foo' }, }), ]; expect(dimensions.get('foo-line')).toEqual(expectedDimensions); diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.ts b/src/chart_types/xy_chart/annotations/line/dimensions.ts index 6b0008abd0..f5b6db6dbc 100644 --- a/src/chart_types/xy_chart/annotations/line/dimensions.ts +++ b/src/chart_types/xy_chart/annotations/line/dimensions.ts @@ -23,36 +23,36 @@ import { isContinuousScale, isBandScale } from '../../../../scales/types'; import { isNil, Position, Rotation } from '../../../../utils/common'; import { Dimensions, Size } from '../../../../utils/dimensions'; import { GroupId } from '../../../../utils/ids'; +import { mergeWithDefaultAnnotationLine } from '../../../../utils/themes/theme'; import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; import { isHorizontalRotation } from '../../state/utils/common'; import { computeXScaleOffset } from '../../state/utils/utils'; import { getPanelSize } from '../../utils/panel'; import { AnnotationDomainTypes, LineAnnotationSpec, LineAnnotationDatum } from '../../utils/specs'; -import { AnnotationMarker } from '../types'; import { AnnotationLineProps } from './types'; -/** @internal */ -export const DEFAULT_LINE_OVERFLOW = 0; - function computeYDomainLineAnnotationDimensions( annotationSpec: LineAnnotationSpec, yScale: Scale, { vertical, horizontal }: SmallMultipleScales, chartRotation: Rotation, - lineColor: string, axisPosition?: Position, ): AnnotationLineProps[] { const { + id: specId, dataValues, - marker, - markerDimensions = { width: 0, height: 0 }, + marker: icon, + markerDimensions: dimension, markerPosition: specMarkerPosition, + style, } = annotationSpec; + const lineStyle = mergeWithDefaultAnnotationLine(style); + const color = lineStyle?.line?.stroke ?? 'red'; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); // let's use a default Bottom-X/Left-Y axis orientation if we are not showing an axis // but we are displaying a line annotation - const anchorPosition = getAnchorPosition(false, isHorizontalChartRotation, specMarkerPosition, axisPosition); + const alignment = getAnchorPosition(false, isHorizontalChartRotation, specMarkerPosition, axisPosition); const lineProps: AnnotationLineProps[] = []; const [domainStart, domainEnd] = yScale.domain; @@ -79,43 +79,42 @@ function computeYDomainLineAnnotationDimensions( vertical.domain.forEach((verticalValue) => { horizontal.domain.forEach((horizontalValue) => { - const topPos = vertical.scaleOrThrow(verticalValue); - const leftPos = horizontal.scaleOrThrow(horizontalValue); + const top = vertical.scaleOrThrow(verticalValue); + const left = horizontal.scaleOrThrow(horizontalValue); const width = isHorizontalChartRotation ? horizontal.bandwidth : vertical.bandwidth; const height = isHorizontalChartRotation ? vertical.bandwidth : horizontal.bandwidth; - const markerPosition = getMarkerPositionForYAnnotation( + const position = getMarkerPositionForYAnnotation( panelSize, chartRotation, - markerDimensions, - anchorPosition, + alignment, annotationValueYPosition, + dimension, ); + const linePathPoints = getYLinePath({ width, height }, annotationValueYPosition); - const annotationMarker: AnnotationMarker | undefined = marker - ? { - icon: marker, - color: lineColor, - dimension: { ...markerDimensions }, - position: { - top: markerPosition.top, - left: markerPosition.left, - }, - } - : undefined; const lineProp: AnnotationLineProps = { + specId, + id: getAnnotationLinePropsId(specId, datum, verticalValue, horizontalValue), + datum, linePathPoints, - marker: annotationMarker, + markers: icon + ? [ + { + icon, + color, + dimension, + position, + alignment, + }, + ] + : [], panel: { ...panelSize, - top: topPos, - left: leftPos, - }, - details: { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), + top, + left, }, }; @@ -132,20 +131,23 @@ function computeXDomainLineAnnotationDimensions( xScale: Scale, { vertical, horizontal }: SmallMultipleScales, chartRotation: Rotation, - lineColor: string, isHistogramMode: boolean, axisPosition?: Position, ): AnnotationLineProps[] { const { + id: specId, dataValues, - marker, - markerDimensions = { width: 0, height: 0 }, + marker: icon, + markerDimensions: dimension, markerPosition: specMarkerPosition, + style, } = annotationSpec; + const lineStyle = mergeWithDefaultAnnotationLine(style); + const color = lineStyle?.line?.stroke ?? 'red'; const lineProps: AnnotationLineProps[] = []; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); - const anchorPosition = getAnchorPosition(true, isHorizontalChartRotation, specMarkerPosition, axisPosition); + const alignment = getAnchorPosition(true, isHorizontalChartRotation, specMarkerPosition, axisPosition); const panelSize = getPanelSize({ vertical, horizontal }); dataValues.forEach((datum: LineAnnotationDatum) => { @@ -190,43 +192,41 @@ function computeXDomainLineAnnotationDimensions( return; } - const topPos = vertical.scaleOrThrow(verticalValue); - const leftPos = horizontal.scaleOrThrow(horizontalValue); + const top = vertical.scaleOrThrow(verticalValue); + const left = horizontal.scaleOrThrow(horizontalValue); const width = isHorizontalChartRotation ? horizontal.bandwidth : vertical.bandwidth; const height = isHorizontalChartRotation ? vertical.bandwidth : horizontal.bandwidth; - const markerPosition = getMarkerPositionForXAnnotation( + const position = getMarkerPositionForXAnnotation( panelSize, chartRotation, - markerDimensions, - anchorPosition, + alignment, annotationValueXPosition, + dimension, ); const linePathPoints = getXLinePath({ width, height }, annotationValueXPosition); - const annotationMarker: AnnotationMarker | undefined = marker - ? { - icon: marker, - color: lineColor, - dimension: { ...markerDimensions }, - position: { - top: markerPosition.top, - left: markerPosition.left, - }, - } - : undefined; const lineProp: AnnotationLineProps = { + specId, + id: getAnnotationLinePropsId(specId, datum, verticalValue, horizontalValue), + datum, linePathPoints, - details: { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), - }, - marker: annotationMarker, + markers: icon + ? [ + { + icon, + color, + dimension, + position, + alignment, + }, + ] + : [], panel: { ...panelSize, - top: topPos, - left: leftPos, + top, + left, }, }; lineProps.push(lineProp); @@ -253,17 +253,12 @@ export function computeLineAnnotationDimensions( return null; } - // this type is guaranteed as this has been merged with default - const lineStyle = annotationSpec.style; - const lineColor = lineStyle?.line?.stroke ?? 'red'; - if (domainType === AnnotationDomainTypes.XDomain) { return computeXDomainLineAnnotationDimensions( annotationSpec, xScale, smallMultipleScales, chartRotation, - lineColor, isHistogramMode, axisPosition, ); @@ -280,7 +275,6 @@ export function computeLineAnnotationDimensions( yScale, smallMultipleScales, chartRotation, - lineColor, axisPosition, ); } @@ -343,9 +337,9 @@ function getYLinePath({ width }: Size, value: number): Line { export function getMarkerPositionForXAnnotation( { width, height }: Size, rotation: Rotation, - { width: mWidth, height: mHeight }: Size, position: Position, value: number, + { width: mWidth, height: mHeight }: Size = { width: 0, height: 0 }, ): Pick { switch (position) { case Position.Right: @@ -375,13 +369,10 @@ export function getMarkerPositionForXAnnotation( function getMarkerPositionForYAnnotation( { width, height }: Size, rotation: Rotation, - { width: mWidth, height: mHeight }: Size, position: Position, value: number, -): { - top: number; - left: number; -} { + { width: mWidth, height: mHeight }: Size = { width: 0, height: 0 }, +): Pick { switch (position) { case Position.Right: return { @@ -406,3 +397,15 @@ function getMarkerPositionForYAnnotation( }; } } + +/** + * @internal + */ +export function getAnnotationLinePropsId( + specId: string, + datum: LineAnnotationDatum, + verticalValue?: any, + horizontalValue?: any, +) { + return [specId, verticalValue, horizontalValue, datum.header, datum.details].join('__'); +} diff --git a/src/chart_types/xy_chart/annotations/line/line.test.tsx b/src/chart_types/xy_chart/annotations/line/line.test.tsx index bec61bbd83..996d289ac7 100644 --- a/src/chart_types/xy_chart/annotations/line/line.test.tsx +++ b/src/chart_types/xy_chart/annotations/line/line.test.tsx @@ -83,14 +83,16 @@ describe('annotation marker', () => { x2: 100, y2: 80, }, - details: { detailsText: 'foo', headerText: '2' }, - - marker: { - icon:
, - color: '#777', - dimension: { width: 0, height: 0 }, - position: { left: -0, top: 80 }, - }, + specId: 'foo-line', + datum: { dataValue: 2, details: 'foo' }, + markers: [ + { + icon:
, + color: '#777', + position: { left: -0, top: 80 }, + alignment: 'left', + }, + ], }), ]; expect(dimensions.get(id)).toEqual(expectedDimensions); @@ -121,13 +123,16 @@ describe('annotation marker', () => { x2: 100, y2: 80, }, - details: { detailsText: 'foo', headerText: '2' }, - marker: { - icon:
, - color: '#777', - dimension: { width: 0, height: 0 }, - position: { left: -0, top: 20 }, - }, + specId: 'foo-line', + datum: { dataValue: 2, details: 'foo' }, + markers: [ + { + icon:
, + color: '#777', + position: { left: -0, top: 20 }, + alignment: 'left', + }, + ], }), ]; expect(dimensions.get(id)).toEqual(expectedDimensions); @@ -148,19 +153,22 @@ describe('annotation marker', () => { const expectedDimensions: AnnotationLineProps[] = [ MockAnnotationLineProps.default({ - details: { detailsText: 'foo', headerText: '2' }, + specId: 'foo-line', + datum: { dataValue: 2, details: 'foo' }, linePathPoints: { x1: 20, y1: 0, x2: 20, y2: 100, }, - marker: { - icon:
, - color: '#777', - dimension: { width: 0, height: 0 }, - position: { top: 100, left: 20 }, - }, + markers: [ + { + icon:
, + color: '#777', + position: { top: 100, left: 20 }, + alignment: 'bottom', + }, + ], }), ]; expect(dimensions.get(id)).toEqual(expectedDimensions); diff --git a/src/chart_types/xy_chart/annotations/line/tooltip.test.ts b/src/chart_types/xy_chart/annotations/line/tooltip.test.ts deleted file mode 100644 index cc0d2c2200..0000000000 --- a/src/chart_types/xy_chart/annotations/line/tooltip.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -/* - * 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 React from 'react'; - -import { ChartTypes } from '../../..'; -import { MockAnnotationLineProps, MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; -import { MockGlobalSpec } from '../../../../mocks/specs/specs'; -import { SpecTypes } from '../../../../specs/constants'; -import { Position, Rotation } from '../../../../utils/common'; -import { Dimensions } from '../../../../utils/dimensions'; -import { AnnotationId } from '../../../../utils/ids'; -import { Point } from '../../../../utils/point'; -import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../../utils/themes/theme'; -import { - AnnotationDomainTypes, - AnnotationSpec, - AnnotationTypes, - AxisSpec, - LineAnnotationSpec, - RectAnnotationSpec, -} from '../../utils/specs'; -import { computeAnnotationTooltipState } from '../tooltip'; -import { AnnotationDimensions, AnnotationTooltipState } from '../types'; -import { computeLineAnnotationTooltipState } from './tooltip'; -import { AnnotationLineProps } from './types'; - -describe('Annotation tooltips', () => { - const groupId = 'foo-group'; - const chartDimensions: Dimensions = { - width: 10, - height: 20, - top: 5, - left: 15, - }; - const horizontalAxisSpec = MockGlobalSpec.axis({ - groupId, - position: Position.Bottom, - }); - const verticalAxisSpec = MockGlobalSpec.axis({ - groupId, - position: Position.Left, - }); - test('should compute the tooltip state for an annotation line', () => { - const cursorPosition: Point = { x: 16, y: 7 }; - const annotationLines: AnnotationLineProps[] = [ - MockAnnotationLineProps.default({ - linePathPoints: { - x1: 1, - y1: 2, - x2: 3, - y2: 4, - }, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 10, height: 10 }, - position: { top: 0, left: 0 }, - }, - }), - MockAnnotationLineProps.default({ - linePathPoints: { - x1: 0, - y1: 10, - x2: 20, - y2: 10, - }, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 20, height: 20 }, - position: { top: 0, left: 0 }, - }, - }), - ]; - const localAxesSpecs: AxisSpec[] = []; - // missing annotation axis (xDomain) - const missingTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - - expect(missingTooltipState).toBeNull(); - - // add axis for xDomain annotation - localAxesSpecs.push(horizontalAxisSpec); - - const xDomainTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedXDomainTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Line, - anchor: { - height: 10, - left: 15, - top: 5, - width: 10, - }, - }; - expect(xDomainTooltipState).toMatchObject(expectedXDomainTooltipState); - - // rotated xDomain - const xDomainRotatedTooltipState = computeLineAnnotationTooltipState( - { x: 24, y: 23 }, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedXDomainRotatedTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(xDomainRotatedTooltipState).toMatchObject(expectedXDomainRotatedTooltipState); - - // add axis for yDomain annotation - localAxesSpecs.push(verticalAxisSpec); - - const yDomainTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(yDomainTooltipState).toMatchObject(expectedYDomainTooltipState); - - const flippedYDomainTooltipState = computeLineAnnotationTooltipState( - { x: 24, y: 23 }, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedFlippedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(flippedYDomainTooltipState).toMatchObject(expectedFlippedYDomainTooltipState); - - const rotatedYDomainTooltipState = computeLineAnnotationTooltipState( - { x: 25, y: 15 }, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedRotatedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(rotatedYDomainTooltipState).toMatchObject(expectedRotatedYDomainTooltipState); - }); - - test('should compute the tooltip state for an annotation', () => { - const annotations: AnnotationSpec[] = []; - const annotationId = 'foo'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const cursorPosition: Point = { x: 16, y: 7 }; - - const annotationLines: AnnotationLineProps[] = [ - MockAnnotationLineProps.default({ - linePathPoints: { - x1: 1, - y1: 2, - x2: 3, - y2: 4, - }, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 10, height: 10 }, - position: { top: 0, left: 0 }, - }, - }), - ]; - const chartRotation: Rotation = 0; - const localAxesSpecs: AxisSpec[] = []; - - const annotationDimensions = new Map(); - annotationDimensions.set(annotationId, annotationLines); - - // missing annotations - const missingSpecTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(missingSpecTooltipState).toBe(null); - - // add valid annotation axis - annotations.push(lineAnnotation); - localAxesSpecs.push(verticalAxisSpec); - - // hide tooltipState - lineAnnotation.hideTooltips = true; - - const hideTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(hideTooltipState).toBe(null); - - // show tooltipState, hide lines - lineAnnotation.hideTooltips = false; - lineAnnotation.hideLines = true; - - const hideLinesTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(hideLinesTooltipState).toBe(null); - - // show tooltipState & lines - lineAnnotation.hideTooltips = false; - lineAnnotation.hideLines = false; - - const tooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - const expectedTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Line, - anchor: { - height: 10, - left: 15, - top: 5, - width: 10, - }, - }; - - expect(tooltipState).toMatchObject(expectedTooltipState); - - // rect annotation tooltip - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], - }; - - const rectAnnotations: RectAnnotationSpec[] = []; - rectAnnotations.push(annotationRectangle); - - annotationDimensions.set(annotationRectangle.id, [ - MockAnnotationRectProps.default({ rect: { x: 2, y: 3, width: 3, height: 5 } }), - ]); - - const rectTooltipState = computeAnnotationTooltipState( - { x: 18, y: 9 }, - annotationDimensions, - rectAnnotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(rectTooltipState).toMatchObject({ - isVisible: true, - annotationType: AnnotationTypes.Rectangle, - anchor: { - left: 18, - top: 9, - }, - }); - annotationRectangle.hideTooltips = true; - - const rectHideTooltipState = computeAnnotationTooltipState( - { x: 3, y: 4 }, - annotationDimensions, - rectAnnotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(rectHideTooltipState).toBe(null); - }); -}); diff --git a/src/chart_types/xy_chart/annotations/line/tooltip.test.tsx b/src/chart_types/xy_chart/annotations/line/tooltip.test.tsx new file mode 100644 index 0000000000..e40f550b30 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line/tooltip.test.tsx @@ -0,0 +1,192 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { ChartTypes } from '../../..'; +import { Chart } from '../../../../components/chart'; +import { MockAnnotationLineProps, MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; +import { ScaleType } from '../../../../scales/constants'; +import { SpecTypes } from '../../../../specs/constants'; +import { Settings } from '../../../../specs/settings'; +import { Rotation } from '../../../../utils/common'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AnnotationId } from '../../../../utils/ids'; +import { LineAnnotation } from '../../specs/line_annotation'; +import { LineSeries } from '../../specs/line_series'; +import { AnnotationDomainTypes, AnnotationTypes, AxisSpec, RectAnnotationSpec } from '../../utils/specs'; +import { computeRectAnnotationTooltipState } from '../tooltip'; +import { AnnotationDimensions } from '../types'; +import { AnnotationLineProps } from './types'; + +describe('Annotation tooltips', () => { + describe('Line annotation tooltips', () => { + test('should show tooltip on mouseenter', () => { + const wrapper = mount( + + + + } + /> + , + ); + const annotation = wrapper.find('.echAnnotation'); + expect(annotation).toHaveLength(1); + expect(wrapper.find('.echAnnotation__tooltip')).toHaveLength(0); + annotation.simulate('mouseenter'); + const header = wrapper.find('.echAnnotation__header'); + expect(header).toHaveLength(1); + expect(header.text()).toEqual('2'); + expect(wrapper.find('.echAnnotation__details').text()).toEqual('foo'); + annotation.simulate('mouseleave'); + expect(wrapper.find('.echAnnotation__header')).toHaveLength(0); + }); + + test('should now show tooltip if hidden', () => { + const wrapper = mount( + + + + } + hideTooltips + /> + , + ); + const annotation = wrapper.find('.echAnnotation'); + expect(wrapper.find('.echAnnotation__tooltip')).toHaveLength(0); + annotation.simulate('mouseenter'); + expect(wrapper.find('.echAnnotation__header')).toHaveLength(0); + }); + }); + + test('should compute the tooltip state for rect annotation', () => { + const groupId = 'foo-group'; + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + const annotationLines: AnnotationLineProps[] = [ + MockAnnotationLineProps.default({ + specId: 'foo', + linePathPoints: { + x1: 1, + y1: 2, + x2: 3, + y2: 4, + }, + markers: [ + { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 10, height: 10 }, + position: { top: 0, left: 0 }, + }, + ], + }), + ]; + const chartRotation: Rotation = 0; + const localAxesSpecs: AxisSpec[] = []; + + const annotationDimensions = new Map(); + annotationDimensions.set('foo', annotationLines); + + // rect annotation tooltip + const annotationRectangle: RectAnnotationSpec = { + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + id: 'rect', + groupId, + annotationType: AnnotationTypes.Rectangle, + dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], + }; + + const rectAnnotations: RectAnnotationSpec[] = []; + rectAnnotations.push(annotationRectangle); + + annotationDimensions.set(annotationRectangle.id, [ + MockAnnotationRectProps.default({ rect: { x: 2, y: 3, width: 3, height: 5 } }), + ]); + + const rectTooltipState = computeRectAnnotationTooltipState( + { x: 18, y: 9 }, + annotationDimensions, + rectAnnotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(rectTooltipState).toMatchObject({ + isVisible: true, + annotationType: AnnotationTypes.Rectangle, + anchor: { + left: 18, + top: 9, + }, + }); + annotationRectangle.hideTooltips = true; + + const rectHideTooltipState = computeRectAnnotationTooltipState( + { x: 3, y: 4 }, + annotationDimensions, + rectAnnotations, + chartRotation, + localAxesSpecs, + chartDimensions, + ); + + expect(rectHideTooltipState).toBe(null); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/line/tooltip.ts b/src/chart_types/xy_chart/annotations/line/tooltip.ts deleted file mode 100644 index d4112a8119..0000000000 --- a/src/chart_types/xy_chart/annotations/line/tooltip.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 { Dimensions } from '../../../../utils/dimensions'; -import { GroupId } from '../../../../utils/ids'; -import { Point } from '../../../../utils/point'; -import { getAxesSpecForSpecId } from '../../state/utils/spec'; -import { AnnotationDomainType, AnnotationTypes, AxisSpec } from '../../utils/specs'; -import { isWithinRectBounds } from '../rect/dimensions'; -import { AnnotationTooltipState, AnnotationMarker, Bounds } from '../types'; -import { isXDomain, getTransformedCursor, invertTranformedCursor } from '../utils'; -import { AnnotationLineProps } from './types'; - -/** @internal */ -export function computeLineAnnotationTooltipState( - cursorPosition: Point, - annotationLines: AnnotationLineProps[], - groupId: GroupId, - domainType: AnnotationDomainType, - axesSpecs: AxisSpec[], - chartDimensions: Dimensions, -): AnnotationTooltipState | null { - const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); - const isXDomainAnnotation = isXDomain(domainType); - const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; - - if (!annotationAxis) { - return null; - } - // get cursor point relative to the rendering area (within the chartDimension margins) - const projectedPointer = getTransformedCursor(cursorPosition, chartDimensions, null, true); - const totalAnnotationLines = annotationLines.length; - for (let i = 0; i < totalAnnotationLines; i++) { - const line = annotationLines[i]; - - if (isWithinLineMarkerBounds(projectedPointer, line.panel, line.marker)) { - const position = invertTranformedCursor( - { - x: line.marker.position.left, - y: line.marker.position.top, - }, - chartDimensions, - null, - true, - ); - return { - annotationType: AnnotationTypes.Line, - isVisible: true, - anchor: { - top: position.y + line.panel.top, - left: position.x + line.panel.left, - ...line.marker.dimension, - }, - ...(line.details && { header: line.details.headerText }), - ...(line.details && { details: line.details.detailsText }), - }; - } - } - - return null; -} - -/** - * Checks if the cursorPosition is within the line annotation marker - * @param cursorPosition the cursor position relative to the projected area - * @param panel - * @param marker the line annotation marker - */ -function isWithinLineMarkerBounds( - cursorPosition: Point, - panel: Dimensions, - marker?: AnnotationMarker, -): marker is AnnotationMarker { - if (!marker) { - return false; - } - const { - position: { top, left }, - dimension: { width, height }, - } = marker; - const markerRect: Bounds = { - startX: left + panel.left, - startY: top + panel.top, - endX: left + panel.left + width, - endY: top + panel.top + height, - }; - return isWithinRectBounds(cursorPosition, markerRect); -} diff --git a/src/chart_types/xy_chart/annotations/line/types.ts b/src/chart_types/xy_chart/annotations/line/types.ts index 5ad78b9cfb..f736198cd8 100644 --- a/src/chart_types/xy_chart/annotations/line/types.ts +++ b/src/chart_types/xy_chart/annotations/line/types.ts @@ -19,15 +19,18 @@ import { Line } from '../../../../geoms/types'; import { Dimensions } from '../../../../utils/dimensions'; -import { AnnotationDetails, AnnotationMarker } from '../types'; +import { LineAnnotationDatum } from '../../utils/specs'; +import { AnnotationMarker } from '../types'; /** @internal */ export interface AnnotationLineProps { + specId: string; + id: string; + datum: LineAnnotationDatum; /** * The path points of a line annotation */ linePathPoints: Line; - details: AnnotationDetails; - marker?: AnnotationMarker; + markers: Array; panel: Dimensions; } diff --git a/src/chart_types/xy_chart/annotations/rect/dimensions.ts b/src/chart_types/xy_chart/annotations/rect/dimensions.ts index f5e83273d6..a312412d29 100644 --- a/src/chart_types/xy_chart/annotations/rect/dimensions.ts +++ b/src/chart_types/xy_chart/annotations/rect/dimensions.ts @@ -45,14 +45,13 @@ export function computeRectAnnotationDimensions( smallMultiplesScales: SmallMultipleScales, isHistogram: boolean = false, ): AnnotationRectProps[] | null { - const { dataValues } = annotationSpec; - const { groupId } = annotationSpec; + const { dataValues, groupId } = annotationSpec; const yScale = yScales.get(groupId); const rectsProps: Omit[] = []; const panelSize = getPanelSize(smallMultiplesScales); - dataValues.forEach((dataValue: RectAnnotationDatum) => { - const { x0: initialX0, x1: initialX1, y0: initialY0, y1: initialY1 } = dataValue.coordinates; + dataValues.forEach((datum: RectAnnotationDatum) => { + const { x0: initialX0, x1: initialX1, y0: initialY0, y1: initialY1 } = datum.coordinates; // if everything is null, return; otherwise we coerce the other coordinates if (initialX0 == null && initialX1 == null && initialY0 == null && initialY1 == null) { @@ -87,7 +86,7 @@ export function computeRectAnnotationDimensions( rectsProps.push({ rect: rectDimensions, - details: dataValue.details, + datum, }); } return; @@ -120,7 +119,7 @@ export function computeRectAnnotationDimensions( rectsProps.push({ rect: rectDimensions, - details: dataValue.details, + datum, }); }); diff --git a/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts b/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts index f4012ea97b..38b805e3b4 100644 --- a/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts +++ b/src/chart_types/xy_chart/annotations/rect/tooltip.test.ts @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ +import { MockAnnotationRectProps } from '../../../../mocks/annotations/annotations'; import { Dimensions } from '../../../../utils/dimensions'; import { AnnotationTypes } from '../../utils/specs'; import { AnnotationTooltipState } from '../types'; -import { computeRectAnnotationTooltipState } from './tooltip'; +import { getRectAnnotationTooltipState } from './tooltip'; +import { AnnotationRectProps } from './types'; describe('Rect annotation tooltip', () => { test('should compute tooltip state for rect annotation', () => { @@ -30,11 +32,15 @@ describe('Rect annotation tooltip', () => { left: 15, }; const cursorPosition = { x: 18, y: 9 }; - const annotationRects = [ - { rect: { x: 2, y: 3, width: 3, height: 5 }, panel: { top: 0, left: 0, width: 10, height: 20 } }, + const annotationRects: AnnotationRectProps[] = [ + MockAnnotationRectProps.default({ + rect: { x: 2, y: 3, width: 3, height: 5 }, + panel: { top: 0, left: 0, width: 10, height: 20 }, + datum: { coordinates: { x0: 0, x1: 10, y0: 0, y1: 10 } }, + }), ]; - const visibleTooltip = computeRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); + const visibleTooltip = getRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); const expectedVisibleTooltipState: AnnotationTooltipState = { isVisible: true, annotationType: AnnotationTypes.Rectangle, @@ -42,6 +48,7 @@ describe('Rect annotation tooltip', () => { top: cursorPosition.y, left: cursorPosition.x, }, + datum: { coordinates: { x0: 0, x1: 10, y0: 0, y1: 10 } }, }; expect(visibleTooltip).toEqual(expectedVisibleTooltipState); diff --git a/src/chart_types/xy_chart/annotations/rect/tooltip.ts b/src/chart_types/xy_chart/annotations/rect/tooltip.ts index d6fbdf55bd..094fe4e16e 100644 --- a/src/chart_types/xy_chart/annotations/rect/tooltip.ts +++ b/src/chart_types/xy_chart/annotations/rect/tooltip.ts @@ -28,7 +28,7 @@ import { isWithinRectBounds } from './dimensions'; import { AnnotationRectProps } from './types'; /** @internal */ -export function computeRectAnnotationTooltipState( +export function getRectAnnotationTooltipState( cursorPosition: Point, annotationRects: AnnotationRectProps[], rotation: Rotation, @@ -38,7 +38,7 @@ export function computeRectAnnotationTooltipState( for (let i = 0; i < totalAnnotationRect; i++) { const rectProps = annotationRects[i]; - const { details, panel } = rectProps; + const { panel, datum } = rectProps; const rect = transformRotateRect(rectProps.rect, rotation, panel); @@ -56,7 +56,7 @@ export function computeRectAnnotationTooltipState( left: cursorPosition.x, top: cursorPosition.y, }, - ...(details && { details }), + datum, }; } } diff --git a/src/chart_types/xy_chart/annotations/rect/types.ts b/src/chart_types/xy_chart/annotations/rect/types.ts index 0102654f6a..3a4f5729ed 100644 --- a/src/chart_types/xy_chart/annotations/rect/types.ts +++ b/src/chart_types/xy_chart/annotations/rect/types.ts @@ -17,8 +17,13 @@ * under the License. */ import { Dimensions } from '../../../../utils/dimensions'; +import { RectAnnotationDatum } from '../../utils/specs'; +/** + * @internal + */ export interface AnnotationRectProps { + datum: RectAnnotationDatum; rect: { x: number; y: number; @@ -26,5 +31,4 @@ export interface AnnotationRectProps { height: number; }; panel: Dimensions; - details?: string; } diff --git a/src/chart_types/xy_chart/annotations/tooltip.ts b/src/chart_types/xy_chart/annotations/tooltip.ts index 7adeeac007..aea89dfc8d 100644 --- a/src/chart_types/xy_chart/annotations/tooltip.ts +++ b/src/chart_types/xy_chart/annotations/tooltip.ts @@ -22,15 +22,13 @@ import { Rotation } from '../../../utils/common'; import { Dimensions } from '../../../utils/dimensions'; import { AnnotationId } from '../../../utils/ids'; import { Point } from '../../../utils/point'; -import { AnnotationSpec, AxisSpec, isLineAnnotation, isRectAnnotation } from '../utils/specs'; -import { computeLineAnnotationTooltipState } from './line/tooltip'; -import { AnnotationLineProps } from './line/types'; -import { computeRectAnnotationTooltipState } from './rect/tooltip'; +import { AnnotationSpec, AxisSpec, isRectAnnotation } from '../utils/specs'; +import { getRectAnnotationTooltipState } from './rect/tooltip'; import { AnnotationRectProps } from './rect/types'; import { AnnotationDimensions, AnnotationTooltipState } from './types'; /** @internal */ -export function computeAnnotationTooltipState( +export function computeRectAnnotationTooltipState( cursorPosition: Point, annotationDimensions: Map, annotationSpecs: AnnotationSpec[], @@ -40,7 +38,7 @@ export function computeAnnotationTooltipState( ): AnnotationTooltipState | null { // allow picking up the last spec added as the top most or use it's zIndex value const sortedAnnotationSpecs = annotationSpecs - .slice() + .filter(isRectAnnotation) .reverse() .sort(({ zIndex: a = Number.MIN_SAFE_INTEGER }, { zIndex: b = Number.MIN_SAFE_INTEGER }) => b - a); @@ -50,47 +48,24 @@ export function computeAnnotationTooltipState( if (spec.hideTooltips || !annotationDimension) { continue; } - const { groupId, customTooltip, customTooltipDetails } = spec; + const { customTooltip, customTooltipDetails } = spec; const tooltipSettings = getTooltipSettings(spec); - if (isLineAnnotation(spec)) { - if (spec.hideLines) { - continue; - } - const lineAnnotationTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationDimension as AnnotationLineProps[], - groupId, - spec.domainType, - axesSpecs, - chartDimensions, - ); + const rectAnnotationTooltipState = getRectAnnotationTooltipState( + cursorPosition, + annotationDimension as AnnotationRectProps[], + chartRotation, + chartDimensions, + ); - if (lineAnnotationTooltipState) { - return { - ...lineAnnotationTooltipState, - tooltipSettings, - customTooltip, - customTooltipDetails, - }; - } - } else if (isRectAnnotation(spec)) { - const rectAnnotationTooltipState = computeRectAnnotationTooltipState( - cursorPosition, - annotationDimension as AnnotationRectProps[], - chartRotation, - chartDimensions, - ); - - if (rectAnnotationTooltipState) { - return { - ...rectAnnotationTooltipState, - tooltipSettings, - customTooltip, - customTooltipDetails: customTooltipDetails ?? spec.renderTooltip, - }; - } + if (rectAnnotationTooltipState) { + return { + ...rectAnnotationTooltipState, + tooltipSettings, + customTooltip, + customTooltipDetails: customTooltipDetails ?? spec.renderTooltip, + }; } } diff --git a/src/chart_types/xy_chart/annotations/types.ts b/src/chart_types/xy_chart/annotations/types.ts index 5cabdba4e6..c289a10df2 100644 --- a/src/chart_types/xy_chart/annotations/types.ts +++ b/src/chart_types/xy_chart/annotations/types.ts @@ -21,7 +21,7 @@ import { ComponentType } from 'react'; import { TooltipPortalSettings } from '../../../components/portal'; import { Position, Color } from '../../../utils/common'; -import { AnnotationType } from '../utils/specs'; +import { AnnotationType, LineAnnotationDatum, RectAnnotationDatum } from '../utils/specs'; import { AnnotationLineProps } from './line/types'; import { AnnotationRectProps } from './rect/types'; @@ -32,6 +32,7 @@ export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | nul export type CustomAnnotationTooltip = ComponentType<{ header?: string; details?: string; + datum: LineAnnotationDatum | RectAnnotationDatum; }> | null; /** @@ -53,10 +54,11 @@ export interface AnnotationMarker { top: number; left: number; }; - dimension: { + dimension?: { width: number; height: number; }; + alignment: Position; color: Color; } @@ -64,10 +66,8 @@ export interface AnnotationMarker { export interface AnnotationTooltipState { isVisible: true; annotationType: AnnotationType; - header?: string; - details?: string; + datum: LineAnnotationDatum | RectAnnotationDatum; anchor: { - position?: Position; top: number; left: number; }; @@ -77,7 +77,7 @@ export interface AnnotationTooltipState { } /** @internal */ -export type AnnotationDimensions = AnnotationLineProps[] | AnnotationRectProps[]; +export type AnnotationDimensions = Array; /** @internal */ export type Bounds = { diff --git a/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss b/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss index 227e50981d..dfd7f6cba1 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss +++ b/src/chart_types/xy_chart/renderer/dom/annotations/_annotations.scss @@ -1,5 +1,4 @@ .echAnnotation { - pointer-events: none; position: absolute; user-select: none; font-size: $euiFontSizeXS; diff --git a/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx b/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx index 7ea0173af3..cceb482e43 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx +++ b/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx @@ -65,10 +65,10 @@ export const AnnotationTooltip = ({ state, chartRef, chartId, onScroll, zIndex } return { ...rest, - placement: placement ?? state?.anchor?.position ?? Placement.Right, + placement: placement ?? Placement.Right, boundary: boundary === 'chart' && chartRef.current ? chartRef.current : undefined, }; - }, [state?.tooltipSettings, state?.anchor?.position, chartRef]); + }, [state?.tooltipSettings, chartRef]); const position = useMemo(() => state?.anchor ?? null, [state?.anchor]); if (!state?.isVisible) { diff --git a/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx b/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx index 000ef68835..d0482e40c9 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx +++ b/src/chart_types/xy_chart/renderer/dom/annotations/annotations.tsx @@ -21,12 +21,18 @@ import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; +import { + DOMElementType, + onDOMElementEnter as onDOMElementEnterAction, + onDOMElementLeave as onDOMElementLeaveAction, +} from '../../../../../state/actions/dom_element'; import { onPointerMove as onPointerMoveAction } from '../../../../../state/actions/mouse'; import { GlobalChartState, BackwardRef } from '../../../../../state/chart_state'; import { getInternalIsInitializedSelector, InitStatus, } from '../../../../../state/selectors/get_internal_is_intialized'; +import { Position } from '../../../../../utils/common'; import { Dimensions } from '../../../../../utils/dimensions'; import { AnnotationId } from '../../../../../utils/ids'; import { AnnotationLineProps } from '../../../annotations/line/types'; @@ -42,6 +48,8 @@ import { AnnotationTooltip } from './annotation_tooltip'; interface AnnotationsDispatchProps { onPointerMove: typeof onPointerMoveAction; + onDOMElementEnter: typeof onDOMElementEnterAction; + onDOMElementLeave: typeof onDOMElementLeaveAction; } interface AnnotationsStateProps { @@ -60,30 +68,58 @@ interface AnnotationsOwnProps { type AnnotationsProps = AnnotationsDispatchProps & AnnotationsStateProps & AnnotationsOwnProps; +const MARKER_TRANSFORMS = { + [Position.Right]: 'translate(-50%, 0%)', + [Position.Left]: 'translate(-100%, -50%)', + [Position.Top]: 'translate(-50%, -100%)', + [Position.Bottom]: 'translate(-50%, 0%)', +}; + +function getMarkerCentredTransform(alignment: Position, hasMarkerDimensions: boolean): string | undefined { + if (hasMarkerDimensions) { + return undefined; + } + return MARKER_TRANSFORMS[alignment]; +} + function renderAnnotationLineMarkers( chartDimensions: Dimensions, annotationLines: AnnotationLineProps[], - id: AnnotationId, + onDOMElementEnter: typeof onDOMElementEnterAction, + onDOMElementLeave: typeof onDOMElementLeaveAction, ) { - return annotationLines.reduce((markers, { marker, panel }: AnnotationLineProps, index: number) => { - if (!marker) { - return markers; + return annotationLines.reduce((acc, { id, specId, datum, markers, panel }: AnnotationLineProps) => { + if (markers.length === 0) { + return acc; } - const { icon, color, position } = marker; + const { icon, color, position, alignment, dimension } = markers[0]; const style = { color, top: chartDimensions.top + position.top + panel.top, left: chartDimensions.left + position.left + panel.left, }; - markers.push( - // eslint-disable-next-line react/no-array-index-key -
+ const transform = { transform: getMarkerCentredTransform(alignment, Boolean(dimension)) }; + acc.push( +
{ + onDOMElementEnter({ + createdBySpecId: specId, + id, + type: DOMElementType.LineAnnotationMarker, + datum, + }); + }} + onMouseLeave={onDOMElementLeave} + style={{ ...style, ...transform }} + > {icon}
, ); - return markers; + return acc; }, []); } const AnnotationsComponent = ({ @@ -96,6 +132,8 @@ const AnnotationsComponent = ({ chartId, zIndex, onPointerMove, + onDOMElementEnter, + onDOMElementLeave, }: AnnotationsProps) => { const renderAnnotationMarkers = useCallback((): JSX.Element[] => { const markers: JSX.Element[] = []; @@ -108,13 +146,18 @@ const AnnotationsComponent = ({ if (isLineAnnotation(annotationSpec)) { const annotationLines = dimensions as AnnotationLineProps[]; - const lineMarkers = renderAnnotationLineMarkers(chartDimensions, annotationLines, id); + const lineMarkers = renderAnnotationLineMarkers( + chartDimensions, + annotationLines, + onDOMElementEnter, + onDOMElementLeave, + ); markers.push(...lineMarkers); } }); return markers; - }, [chartDimensions, annotationDimensions, annotationSpecs]); + }, [onDOMElementEnter, onDOMElementLeave, chartDimensions, annotationDimensions, annotationSpecs]); const onScroll = useCallback(() => { onPointerMove({ x: -1, y: -1 }, Date.now()); @@ -141,7 +184,14 @@ const AnnotationsComponent = ({ AnnotationsComponent.displayName = 'Annotations'; const mapDispatchToProps = (dispatch: Dispatch): AnnotationsDispatchProps => - bindActionCreators({ onPointerMove: onPointerMoveAction }, dispatch); + bindActionCreators( + { + onPointerMove: onPointerMoveAction, + onDOMElementLeave: onDOMElementLeaveAction, + onDOMElementEnter: onDOMElementEnterAction, + }, + dispatch, + ); const mapStateToProps = (state: GlobalChartState): AnnotationsStateProps => { const { zIndex, chartId } = state; diff --git a/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx b/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx index 023fd1d7fe..23139c7668 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx +++ b/src/chart_types/xy_chart/renderer/dom/annotations/tooltip_content.tsx @@ -19,28 +19,28 @@ import React, { useCallback } from 'react'; -import { AnnotationTypes } from '../../../../specs'; +import { AnnotationTypes, LineAnnotationDatum, RectAnnotationDatum } from '../../../../specs'; import { AnnotationTooltipState } from '../../../annotations/types'; /** @internal */ export const TooltipContent = ({ annotationType, - header, - details, + datum, customTooltip: CustomTooltip, customTooltipDetails, }: AnnotationTooltipState) => { - const renderLine = useCallback( - () => ( + const renderLine = useCallback(() => { + const { details, dataValue, header = dataValue.toString() } = datum as LineAnnotationDatum; + return (

{header}

{customTooltipDetails ? customTooltipDetails(details) : details}
- ), - [header, details, customTooltipDetails], - ); + ); + }, [datum, customTooltipDetails]); const renderRect = useCallback(() => { + const { details } = datum as RectAnnotationDatum; const tooltipContent = customTooltipDetails ? customTooltipDetails(details) : details; if (!tooltipContent) { return null; @@ -53,10 +53,14 @@ export const TooltipContent = ({
); - }, [details, customTooltipDetails]); + }, [datum, customTooltipDetails]); if (CustomTooltip) { - return ; + const { details } = datum; + if ('header' in datum) { + return ; + } + return ; } switch (annotationType) { diff --git a/src/chart_types/xy_chart/specs/line_annotation.tsx b/src/chart_types/xy_chart/specs/line_annotation.tsx index 6d71d3169d..940b3d98d6 100644 --- a/src/chart_types/xy_chart/specs/line_annotation.tsx +++ b/src/chart_types/xy_chart/specs/line_annotation.tsx @@ -17,108 +17,25 @@ * under the License. */ -import React, { createRef, CSSProperties, Component } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; +import React from 'react'; import { ChartTypes } from '../..'; -import { Spec } from '../../../specs'; import { SpecTypes } from '../../../specs/constants'; -import { upsertSpec as upsertSpecAction, removeSpec as removeSpecAction } from '../../../state/actions/specs'; -import { DEFAULT_ANNOTATION_LINE_STYLE, mergeWithDefaultAnnotationLine } from '../../../utils/themes/theme'; +import { getConnect, specComponentFactory } from '../../../state/spec_factory'; +import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/theme'; import { LineAnnotationSpec, DEFAULT_GLOBAL_ID, AnnotationTypes } from '../utils/specs'; -type InjectedProps = LineAnnotationSpec & - DispatchProps & - Readonly<{ - children?: React.ReactNode; - }>; - -/** @internal */ -export class LineAnnotationSpecComponent extends Component { - static defaultProps: Partial = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - groupId: DEFAULT_GLOBAL_ID, - annotationType: AnnotationTypes.Line, - style: DEFAULT_ANNOTATION_LINE_STYLE, - hideLines: false, - hideTooltips: false, - hideLinesTooltips: true, - zIndex: 1, - }; - - private markerRef = createRef(); - - componentDidMount() { - const { children, upsertSpec, removeSpec, ...config } = this.props as InjectedProps; - if (this.markerRef.current) { - const { offsetWidth, offsetHeight } = this.markerRef.current; - config.markerDimensions = { - width: offsetWidth, - height: offsetHeight, - }; - } - const style = mergeWithDefaultAnnotationLine(config.style); - const spec = { ...config, style }; - upsertSpec(spec); - } - - componentDidUpdate() { - const { upsertSpec, removeSpec, children, ...config } = this.props as InjectedProps; - if (this.markerRef.current) { - const { offsetWidth, offsetHeight } = this.markerRef.current; - config.markerDimensions = { - width: offsetWidth, - height: offsetHeight, - }; - } - const style = mergeWithDefaultAnnotationLine(config.style); - const spec = { ...config, style }; - upsertSpec(spec); - } - - componentWillUnmount() { - const { removeSpec, id } = this.props as InjectedProps; - removeSpec(id); - } - - render() { - if (!this.props.marker) { - return null; - } - - /* - * We need to get the width & height of the marker passed into the spec - * so we render the marker offscreen if one has been defined & update the config - * with the width & height. - */ - const offscreenStyle: CSSProperties = { - position: 'absolute', - left: -9999, - opacity: 0, - }; - - return ( -
- {this.props.marker} -
- ); - } -} - -interface DispatchProps { - upsertSpec: (spec: Spec) => void; - removeSpec: (id: string) => void; -} -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => - bindActionCreators( - { - upsertSpec: upsertSpecAction, - removeSpec: removeSpecAction, - }, - dispatch, - ); +const defaultProps = { + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + groupId: DEFAULT_GLOBAL_ID, + annotationType: AnnotationTypes.Line, + style: DEFAULT_ANNOTATION_LINE_STYLE, + hideLines: false, + hideTooltips: false, + hideLinesTooltips: true, + zIndex: 1, +}; type SpecRequiredProps = Pick; type SpecOptionalProps = Partial< @@ -128,11 +45,6 @@ type SpecOptionalProps = Partial< > >; -export const LineAnnotation: React.FunctionComponent = connect< - null, - DispatchProps, - LineAnnotationSpec ->( - null, - mapDispatchToProps, -)(LineAnnotationSpecComponent); +export const LineAnnotation: React.FunctionComponent = getConnect()( + specComponentFactory(defaultProps), +); diff --git a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts index 34bf05acaa..7d3d48491e 100644 --- a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts +++ b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts @@ -19,7 +19,9 @@ import createCachedSelector from 're-reselect'; +import { TooltipPortalSettings } from '../../../../components/portal/types'; import { TooltipInfo } from '../../../../components/tooltip/types'; +import { DOMElement } from '../../../../state/actions/dom_element'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; @@ -27,7 +29,9 @@ import { Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { AnnotationId } from '../../../../utils/ids'; import { Point } from '../../../../utils/point'; -import { computeAnnotationTooltipState } from '../../annotations/tooltip'; +import { AnnotationLineProps } from '../../annotations/line/types'; +import { AnnotationRectProps } from '../../annotations/rect/types'; +import { computeRectAnnotationTooltipState } from '../../annotations/tooltip'; import { AnnotationTooltipState, AnnotationDimensions } from '../../annotations/types'; import { AxisSpec, AnnotationSpec, AnnotationTypes } from '../../utils/specs'; import { ComputedGeometries } from '../utils/types'; @@ -38,6 +42,7 @@ import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; import { getTooltipInfoSelector } from './get_tooltip_values_highlighted_geoms'; const getCurrentPointerPosition = (state: GlobalChartState) => state.interactions.pointer.current.position; +const getHoveredDOMElement = (state: GlobalChartState) => state.interactions.hoveredDOMElement; /** @internal */ export const getAnnotationTooltipStateSelector = createCachedSelector( @@ -50,6 +55,7 @@ export const getAnnotationTooltipStateSelector = createCachedSelector( getAxisSpecsSelector, computeAnnotationDimensionsSelector, getTooltipInfoSelector, + getHoveredDOMElement, ], getAnnotationTooltipState, )(getChartIdSelector); @@ -67,7 +73,17 @@ function getAnnotationTooltipState( axesSpecs: AxisSpec[], annotationDimensions: Map, tooltip: TooltipInfo, + hoveredDOMElement: DOMElement | null, ): AnnotationTooltipState | null { + const hoveredTooltip = getTooltipStateForDOMElements( + chartDimensions, + annotationSpecs, + annotationDimensions, + hoveredDOMElement, + ); + if (hoveredTooltip) { + return hoveredTooltip; + } // get positions relative to chart if (cursorPosition.x < 0 || cursorPosition.y < 0) { return null; @@ -77,7 +93,7 @@ function getAnnotationTooltipState( if (!xScale || !yScales) { return null; } - const tooltipState = computeAnnotationTooltipState( + const tooltipState = computeRectAnnotationTooltipState( cursorPosition, annotationDimensions, annotationSpecs, @@ -99,3 +115,59 @@ function getAnnotationTooltipState( return tooltipState; } + +function getTooltipStateForDOMElements( + chartDimensions: Dimensions, + annotationSpecs: AnnotationSpec[], + annotationDimensions: Map, + hoveredDOMElement: DOMElement | null, +): AnnotationTooltipState | null { + if (!hoveredDOMElement) { + return null; + } + // current type for hoveredDOMElement is only used for line annotation markers + // and we can safety cast the union types to the respective Line types + const spec = annotationSpecs.find(({ id }) => id === hoveredDOMElement.createdBySpecId); + if (!spec || spec.hideTooltips) { + return null; + } + const dimension = (annotationDimensions.get(hoveredDOMElement.createdBySpecId) ?? []) + .filter(isAnnotationLineProps) + .find((d) => { + return d.id === hoveredDOMElement.id && d.datum === hoveredDOMElement.datum; + }); + + if (!dimension) { + return null; + } + + return { + isVisible: true, + annotationType: AnnotationTypes.Line, + datum: dimension.datum, + anchor: { + top: (dimension.markers[0]?.position.top ?? 0) + dimension.panel.top + chartDimensions.top, + left: (dimension.markers[0]?.position.left ?? 0) + dimension.panel.left + chartDimensions.left, + }, + customTooltipDetails: spec.customTooltipDetails, + customTooltip: spec.customTooltip, + tooltipSettings: getTooltipSettings(spec), + }; +} +function isAnnotationLineProps(prop: AnnotationLineProps | AnnotationRectProps): prop is AnnotationLineProps { + return 'linePathPoints' in prop; +} + +function getTooltipSettings({ + placement, + fallbackPlacements, + boundary, + offset, +}: AnnotationSpec): TooltipPortalSettings<'chart'> { + return { + placement, + fallbackPlacements, + boundary, + offset, + }; +} diff --git a/src/index.ts b/src/index.ts index 7c7f2604c4..6415db1dcd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import 'path2d-polyfill'; - export * from './components'; export { ChartTypes } from './chart_types'; export { ChartSize, ChartSizeArray, ChartSizeObject } from './utils/chart_size'; diff --git a/src/mocks/annotations/annotations.ts b/src/mocks/annotations/annotations.ts index b1786050fc..4730bcec22 100644 --- a/src/mocks/annotations/annotations.ts +++ b/src/mocks/annotations/annotations.ts @@ -17,6 +17,7 @@ * under the License. */ +import { getAnnotationLinePropsId } from '../../chart_types/xy_chart/annotations/line/dimensions'; import { AnnotationLineProps } from '../../chart_types/xy_chart/annotations/line/types'; import { AnnotationRectProps } from '../../chart_types/xy_chart/annotations/rect/types'; import { mergePartial, RecursivePartial } from '../../utils/common'; @@ -24,6 +25,8 @@ import { mergePartial, RecursivePartial } from '../../utils/common'; /** @internal */ export class MockAnnotationLineProps { private static readonly base: AnnotationLineProps = { + id: getAnnotationLinePropsId('spec1', { dataValue: 0 }), + specId: 'spec1', linePathPoints: { x1: 0, y1: 0, @@ -31,13 +34,27 @@ export class MockAnnotationLineProps { y2: 0, }, panel: { top: 0, left: 0, width: 100, height: 100 }, - details: {}, + datum: { dataValue: 0 }, + markers: [], }; - static default(partial?: RecursivePartial) { - return mergePartial(MockAnnotationLineProps.base, partial, { - mergeOptionalPartialValues: true, - }); + static default(partial?: RecursivePartial, smVerticalValue?: any, smHorizontalValue?: any) { + const id = getAnnotationLinePropsId( + partial?.specId ?? MockAnnotationLineProps.base.specId, + { + ...MockAnnotationLineProps.base.datum, + ...partial?.datum, + }, + smVerticalValue, + smHorizontalValue, + ); + return mergePartial( + MockAnnotationLineProps.base, + { id, ...partial }, + { + mergeOptionalPartialValues: true, + }, + ); } static fromPoints(x1 = 0, y1 = 0, x2 = 0, y2 = 0): AnnotationLineProps { @@ -50,11 +67,18 @@ export class MockAnnotationLineProps { }, }); } + + static fromPartialAndId(partial?: RecursivePartial) { + return mergePartial(MockAnnotationLineProps.base, partial, { + mergeOptionalPartialValues: true, + }); + } } /** @internal */ export class MockAnnotationRectProps { private static readonly base: AnnotationRectProps = { + datum: { coordinates: { x0: 0, x1: 1, y0: 0, y1: 1 } }, rect: { x: 0, y: 0, diff --git a/src/state/actions/dom_element.ts b/src/state/actions/dom_element.ts new file mode 100644 index 0000000000..f4dcbdb600 --- /dev/null +++ b/src/state/actions/dom_element.ts @@ -0,0 +1,55 @@ +/* + * 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 { $Values } from 'utility-types'; + +export const ON_DOM_ELEMENT_ENTER = 'ON_DOM_ELEMENT_ENTER'; +export const ON_DOM_ELEMENT_LEAVE = 'ON_DOM_ELEMENT_LEAVE'; + +export const DOMElementType = Object.freeze({ + LineAnnotationMarker: 'LineAnnotationMarker' as const, +}); +export type DOMElementType = $Values; + +export interface DOMElement { + type: DOMElementType; + id: string; + createdBySpecId: string; // TODO is that + datum enough to identify the elements? + datum: unknown; +} +interface DOMElementEnterAction { + type: typeof ON_DOM_ELEMENT_ENTER; + element: DOMElement; +} + +interface DOMElementLeaveAction { + type: typeof ON_DOM_ELEMENT_LEAVE; +} + +/** @internal */ +export function onDOMElementLeave(): DOMElementLeaveAction { + return { type: ON_DOM_ELEMENT_LEAVE }; +} + +/** @internal */ +export function onDOMElementEnter(element: DOMElement): DOMElementEnterAction { + return { type: ON_DOM_ELEMENT_ENTER, element }; +} + +/** @internal */ +export type DOMElementActions = DOMElementEnterAction | DOMElementLeaveAction; diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 58bfd76546..e2fe1200ef 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -36,7 +36,8 @@ import { Point } from '../utils/point'; import { StateActions } from './actions'; import { CHART_RENDERED } from './actions/chart'; import { UPDATE_PARENT_DIMENSION } from './actions/chart_settings'; -import { CLEAR_TEMPORARY_COLORS, SET_PERSISTED_COLOR, SET_TEMPORARY_COLOR } from './actions/colors'; +import { SET_PERSISTED_COLOR, SET_TEMPORARY_COLOR, CLEAR_TEMPORARY_COLORS } from './actions/colors'; +import { DOMElement } from './actions/dom_element'; import { EXTERNAL_POINTER_EVENT } from './actions/events'; import { LegendPath } from './actions/legend'; import { REMOVE_SPEC, SPEC_PARSED, SPEC_UNMOUNTED, UPSERT_SPEC } from './actions/specs'; @@ -184,6 +185,7 @@ export interface InteractionsState { highlightedLegendItemKey: PrimitiveValue; highlightedLegendPath: LegendPath; deselectedDataSeries: SeriesIdentifier[]; + hoveredDOMElement: DOMElement | null; } /** @internal */ @@ -272,6 +274,7 @@ export const getInitialState = (chartId: string): GlobalChartState => ({ highlightedLegendItemKey: null, highlightedLegendPath: [], deselectedDataSeries: [], + hoveredDOMElement: null, }, externalEvents: { pointer: null, diff --git a/src/state/reducers/interactions.ts b/src/state/reducers/interactions.ts index 1c80924342..5f26b693c9 100644 --- a/src/state/reducers/interactions.ts +++ b/src/state/reducers/interactions.ts @@ -21,15 +21,16 @@ import { getSeriesIndex } from '../../chart_types/xy_chart/utils/series'; import { LegendItem } from '../../common/legend'; import { SeriesIdentifier } from '../../common/series_id'; import { getDelta } from '../../utils/point'; -import { ON_KEY_UP, KeyActions } from '../actions/key'; +import { DOMElementActions, ON_DOM_ELEMENT_ENTER, ON_DOM_ELEMENT_LEAVE } from '../actions/dom_element'; +import { KeyActions, ON_KEY_UP } from '../actions/key'; import { + LegendActions, ON_LEGEND_ITEM_OUT, ON_LEGEND_ITEM_OVER, ON_TOGGLE_DESELECT_SERIES, - LegendActions, ToggleDeselectSeriesAction, } from '../actions/legend'; -import { ON_MOUSE_DOWN, ON_MOUSE_UP, ON_POINTER_MOVE, MouseActions } from '../actions/mouse'; +import { MouseActions, ON_MOUSE_DOWN, ON_MOUSE_UP, ON_POINTER_MOVE } from '../actions/mouse'; import { InteractionsState } from '../chart_state'; import { getInitialPointerState } from '../utils'; @@ -46,7 +47,7 @@ const DRAG_DETECTION_PIXEL_DELTA = 4; /** @internal */ export function interactionsReducer( state: InteractionsState, - action: LegendActions | MouseActions | KeyActions, + action: LegendActions | MouseActions | KeyActions | DOMElementActions, legendItems: LegendItem[], ): InteractionsState { switch (action.type) { @@ -152,6 +153,17 @@ export function interactionsReducer( ...state, deselectedDataSeries: toggleDeselectedDataSeries(action, state.deselectedDataSeries, legendItems), }; + + case ON_DOM_ELEMENT_ENTER: + return { + ...state, + hoveredDOMElement: action.element, + }; + case ON_DOM_ELEMENT_LEAVE: + return { + ...state, + hoveredDOMElement: null, + }; default: return state; } diff --git a/stories/small_multiples/6_heterogeneous_cartesians.tsx b/stories/small_multiples/6_heterogeneous_cartesians.tsx index bc70c85827..e78facbd01 100644 --- a/stories/small_multiples/6_heterogeneous_cartesians.tsx +++ b/stories/small_multiples/6_heterogeneous_cartesians.tsx @@ -66,6 +66,7 @@ export const Example = () => { margin: 'auto', fontSize: 8, borderRadius: 2, + lineHeight: 8, }} > MIN diff --git a/yarn.lock b/yarn.lock index 66676ea6b1..43270992a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18043,11 +18043,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -path2d-polyfill@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-0.4.2.tgz#594d3103838ef6b9dd4a7fd498fe9a88f1f28531" - integrity sha512-JSeAnUfkFjl+Ml/EZL898ivMSbGHrOH63Mirx5EQ1ycJiryHDmj1Q7Are+uEPvenVGCUN9YbolfGfyUewJfJEg== - pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"